Wrap all 34 resource groups, then harden the request plumbing

Expands coverage to every `Route::resource` group in the v5 API plus
`users` (265 methods), and fixes several defects in the shared request
path that the new breadth made more exposed.

Coverage:
- 25 new models, each keeping the `extra` flatten map and the
  `null_to_default` convention so unmodeled fields round-trip.
- `crud_methods!` split out of `resource_methods!` for the three groups
  with no `/bulk` route, where a generated `bulk_*` would only 405.
- COVERAGE.md added as the detailed inventory, verified against
  `routes/api.php` on v5-stable.

Fixes, each confirmed against upstream source or an observed request:
- `list_all_*` silently truncated. `BaseController` caps page size at
  `min(abs(per_page), 5000)`, so the "short page means last page" rule
  stopped early and returned a partial list as if complete. Termination
  now prefers `meta.pagination.total_pages` (decoded but previously
  discarded), and `per_page` is clamped so the fallback stays sound.
- `per_page(0)` paged forever: the check `got < 0` has no solution. It
  now falls back to the default size, with a MAX_PAGES backstop that
  raises `Error::PaginationLimit` rather than quietly truncating.
- Ids were interpolated into paths raw, so a `/` or `?` in an id
  retargeted the request. All sites now use `seg(id)?`.
  Escaping alone is insufficient: the WHATWG rules `url` implements
  decode `%2E` to `.` before resolving dot segments, so an escaped `..`
  still collapses the path. Empty and dot-only ids are rejected.
- No request timeout. reqwest applies none, so a stalled connection hung
  the caller indefinitely. Adds a 60s default and `with_timeout`.
- Base URL is validated at construction; a missing scheme used to 404
  on every call instead of failing once.

Adds tests/client_requests.rs: 26 tests over the request plumbing
against a wiremock loopback server, including regressions for the
truncation and non-termination bugs. No network or credentials needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 18:08:38 -04:00
co-authored by Claude Opus 5
parent ff9a859d0b
commit 9976249210
44 changed files with 4003 additions and 175 deletions
+856 -23
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -30,6 +30,26 @@ pub enum Error {
#[error("invalid api token: {0}")]
InvalidToken(String),
/// An id or action could not be used as a single URL path segment.
///
/// Raised for values that would change which endpoint is addressed rather
/// than identifying a record — an empty id (which would hit the
/// collection route) or one made only of dots (which the URL parser
/// resolves away). Invoice Ninja ids are hashids, so this only rejects
/// input that was never a valid id.
#[error("`{0}` cannot be used as a url path segment")]
InvalidPathSegment(String),
/// A `list_all_*` call kept receiving full pages past the point where any
/// plausible result set would have ended.
///
/// This means the server is not paginating the way the client expects —
/// typically because it ignores the `page` parameter and keeps replaying
/// the same page. Raised instead of returning the partial result, so the
/// truncation cannot be mistaken for a complete answer.
#[error("pagination did not terminate for {path} after {pages} pages")]
PaginationLimit { path: String, pages: u64 },
}
pub type Result<T> = std::result::Result<T, Error>;
+1 -1
View File
@@ -18,5 +18,5 @@ mod client;
mod error;
pub mod models;
pub use client::InvoiceNinjaClient;
pub use client::{InvoiceNinjaClient, HOSTED_BASE_URL};
pub use error::{ApiErrorBody, Error, Result};
+39
View File
@@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A connected bank account feed.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BankIntegration {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub provider_bank_name: String,
/// Returned as an integer rather than a hashed id.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub bank_account_id: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub bank_account_name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub bank_account_number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub bank_account_status: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub bank_account_type: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub balance: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub currency: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+43
View File
@@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A transaction imported from a connected bank account.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BankTransaction {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// The upstream transaction identifier, returned as an integer rather
/// than a hashed id.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub transaction_id: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub bank_account_id: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub currency_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub account_type: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub description: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub category_id: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub category_type: String,
/// Either `CREDIT` or `DEBIT`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub base_type: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+40
View File
@@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A rule for auto-categorising imported bank transactions.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BankTransactionRule {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
/// Match clauses. Left untyped: the shape is a nested
/// `search_key`/`operator`/`value` object the spec does not define.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub rules: Vec<Value>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub auto_convert: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub matches_on_all: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub applies_to: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub category_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+22 -22
View File
@@ -5,37 +5,37 @@ use serde_json::{Map, Value};
pub struct Client {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub id_number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vat_number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub address1: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub address2: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub city: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub state: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub postal_code: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub country_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub phone: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub website: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub balance: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub paid_to_date: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub credit_balance: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
@@ -43,7 +43,7 @@ pub struct Client {
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub contacts: Vec<ClientContact>,
/// Any fields returned by the API that aren't modeled above are preserved here,
@@ -56,15 +56,15 @@ pub struct Client {
pub struct ClientContact {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub first_name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub email: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub phone: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_primary: bool,
#[serde(flatten)]
+30
View File
@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A stored payment method for a client.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientGatewayToken {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub token: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub routing_number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub company_gateway_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_default: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+72 -1
View File
@@ -1,7 +1,12 @@
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct ListResponse<T> {
/// Defaulted because not every list-shaped response carries a `data` key:
/// a bulk action whose `ids` match nothing returns HTTP 200 with just
/// `{"message": "No Invoices Found"}`, which should read as "no results"
/// rather than a decode failure.
#[serde(default = "Vec::new")]
pub data: Vec<T>,
#[serde(default)]
pub meta: Option<Meta>,
@@ -44,6 +49,19 @@ pub struct ListParams {
/// Set to 1 to include archived/deleted records.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
/// Extra query parameters appended verbatim.
///
/// The typed fields above are only the parameters common to every list
/// endpoint; individual resources accept many more (`client_id`,
/// `vendor_id`, `number`, `created_at`, `is_deleted`, per-resource
/// filters). Use [`param`](Self::param) to set those.
///
/// Skipped by serde and applied as a separate query pass, because
/// `serde_urlencoded` — which backs `reqwest`'s query serialization —
/// does not handle `#[serde(flatten)]`ed maps.
#[serde(skip)]
pub custom: Vec<(String, String)>,
}
impl ListParams {
@@ -80,4 +98,57 @@ impl ListParams {
self.status = Some(status.into());
self
}
/// Adds an arbitrary query parameter, for filters the typed setters do
/// not cover.
///
/// ```
/// use invoiceninjars::models::ListParams;
///
/// let params = ListParams::new()
/// .per_page(100)
/// .param("client_id", "abc123")
/// .param("is_deleted", "false");
/// ```
///
/// Can be called more than once with the same key; both pairs are sent.
pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.custom.push((key.into(), value.into()));
self
}
}
/// Deserializes `null` as `T::default()` instead of failing.
///
/// `#[serde(default)]` alone only covers a *missing* key — a key that is
/// present with an explicit `null` still errors for non-`Option` types.
/// Invoice Ninja returns `null` for plenty of nominally-string fields, so the
/// models pair `default` with this to tolerate both shapes.
pub(crate) fn null_to_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
/// Body for a bulk action (`POST /{resource}/bulk`).
///
/// The action set varies by resource; `archive`, `restore`, and `delete` work
/// everywhere, and are what the generated `bulk_*` methods are typed for.
/// See [`crate::InvoiceNinjaClient::bulk_invoices`] for the actions whose
/// responses are *not* a list of entities.
#[derive(Debug, Clone, Serialize)]
pub struct BulkRequest {
pub action: String,
pub ids: Vec<String>,
}
impl BulkRequest {
pub fn new(action: impl Into<String>, ids: &[impl AsRef<str>]) -> Self {
Self {
action: action.into(),
ids: ids.iter().map(|id| id.as_ref().to_string()).collect(),
}
}
}
+77
View File
@@ -0,0 +1,77 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A company (tenant).
///
/// The API returns well over a hundred fields here; this models the
/// identifying and structural ones and leaves the rest — including every
/// `enable_*`/`show_*` feature toggle — to [`extra`](Self::extra), which
/// still round-trips them on update.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Company {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub company_key: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub size_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub industry_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub portal_mode: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub subdomain: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub portal_domain: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub logo: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub db: String,
/// The company settings blob. Left untyped — `CompanySettings` is the
/// largest object in the API.
#[serde(default)]
pub settings: Value,
/// Custom field label definitions.
#[serde(default)]
pub custom_fields: Value,
/// Bitmask of enabled modules.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub enabled_modules: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub enabled_tax_rates: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub enabled_item_tax_rates: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub enabled_expense_tax_rates: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub first_day_of_week: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub first_month_of_year: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub slack_webhook_url: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub google_analytics_key: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub session_timeout: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub markdown_enabled: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_can_register: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_large: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_disabled: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub calculate_taxes: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+38
View File
@@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A configured payment gateway.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompanyGateway {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub gateway_key: String,
/// Bitmask of accepted card types.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub accepted_credit_cards: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub require_billing_address: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub require_shipping_address: bool,
/// An encrypted JSON *string*, not an object — decrypted server-side, so
/// it is opaque here.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub config: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub update_details: bool,
/// Per-card fee and limit rules; shape varies by gateway.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub fees_and_limits: Vec<Value>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+27
View File
@@ -0,0 +1,27 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// An API token. Served from `/tokens`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompanyToken {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
/// Only returned in full when the token is first created.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub token: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_system: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+32
View File
@@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A PDF template.
///
/// The `design` field itself (the HTML section bodies) is left to
/// [`extra`](Self::extra): the spec types it as a string, but the API
/// returns a nested object of `header`/`body`/`footer` sections.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Design {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_custom: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_active: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default)]
pub deleted_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+48
View File
@@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A document (file) attached to another entity.
///
/// Documents are created by uploading to the owning entity
/// (`PUT /{resource}/{id}/upload`, multipart) rather than by posting to
/// `/documents`, so this crate exposes read, update, delete, and download
/// but no `create_document`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Document {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub assigned_user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub project_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub url: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub preview: String,
/// File extension, e.g. `pdf`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub r#type: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub disk: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub hash: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_default: bool,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default)]
pub deleted_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+17 -17
View File
@@ -5,39 +5,39 @@ use serde_json::{Map, Value};
pub struct Expense {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub invoice_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub exchange_rate: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub public_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub payment_date: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub transaction_reference: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub category_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name1: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate1: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name2: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate2: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub should_be_invoiced: bool,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
+23
View File
@@ -0,0 +1,23 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExpenseCategory {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+28
View File
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A named settings group that clients can be assigned to.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GroupSetting {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
/// The settings override blob. Left untyped — Invoice Ninja's
/// `CompanySettings` object has well over a hundred fields.
#[serde(default)]
pub settings: Value,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+26 -26
View File
@@ -5,35 +5,35 @@ use serde_json::{Map, Value};
pub struct Invoice {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub due_date: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub po_number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub status_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub balance: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub discount: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_amount_discount: bool,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub public_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub terms: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub footer: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
@@ -41,7 +41,7 @@ pub struct Invoice {
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub line_items: Vec<InvoiceItem>,
#[serde(flatten)]
@@ -50,25 +50,25 @@ pub struct Invoice {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InvoiceItem {
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub product_key: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub cost: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub quantity: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub discount: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name1: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate1: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name2: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate2: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub line_total: f64,
#[serde(flatten)]
+44
View File
@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A physical address attached to a client or vendor.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Location {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub address1: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub address2: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub city: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub state: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub postal_code: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub country_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub assigned_user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_shipping_location: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+49 -1
View File
@@ -1,19 +1,67 @@
mod bank_integration;
mod bank_transaction;
mod bank_transaction_rule;
mod client;
mod client_gateway_token;
mod common;
mod company;
mod company_gateway;
mod company_token;
mod design;
mod document;
mod expense;
mod expense_category;
mod group_setting;
mod invoice;
mod location;
mod payment;
mod payment_term;
mod product;
mod project;
mod purchase_order;
mod recurring_expense;
mod recurring_invoice;
mod subscription;
mod system_log;
mod tag;
mod task;
mod task_scheduler;
mod task_status;
mod tax_rate;
mod user;
mod vendor;
mod webhook;
pub use bank_integration::BankIntegration;
pub use bank_transaction::BankTransaction;
pub use bank_transaction_rule::BankTransactionRule;
pub use client::{Client, ClientContact};
pub use common::{ItemResponse, ListParams, ListResponse, Meta, Pagination};
pub use client_gateway_token::ClientGatewayToken;
pub use common::{BulkRequest, ItemResponse, ListParams, ListResponse, Meta, Pagination};
pub use company::Company;
pub use company_gateway::CompanyGateway;
pub use company_token::CompanyToken;
pub use design::Design;
pub use document::Document;
pub use expense::Expense;
pub use expense_category::ExpenseCategory;
pub use group_setting::GroupSetting;
pub use invoice::{Credit, Invoice, InvoiceItem, Quote};
pub use location::Location;
pub use payment::{Payment, PaymentInvoice};
pub use payment_term::PaymentTerm;
pub use product::Product;
pub use project::Project;
pub use purchase_order::PurchaseOrder;
pub use recurring_expense::RecurringExpense;
pub use recurring_invoice::{RecurringInvoice, RecurringQuote};
pub use subscription::Subscription;
pub use system_log::SystemLog;
pub use tag::Tag;
pub use task::Task;
pub use task_scheduler::TaskScheduler;
pub use task_status::TaskStatus;
pub use tax_rate::TaxRate;
pub use user::User;
pub use vendor::{Vendor, VendorContact};
pub use webhook::Webhook;
+10 -10
View File
@@ -5,23 +5,23 @@ use serde_json::{Map, Value};
pub struct Payment {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub applied: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub refunded: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub transaction_reference: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub type_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
@@ -29,7 +29,7 @@ pub struct Payment {
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub invoices: Vec<PaymentInvoice>,
#[serde(flatten)]
+22
View File
@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PaymentTerm {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// Human-readable form of the term, e.g. `NET 30`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub num_days: i64,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+10 -10
View File
@@ -5,25 +5,25 @@ use serde_json::{Map, Value};
pub struct Product {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub product_key: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub cost: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub price: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub quantity: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name1: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate1: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name2: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate2: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
+10 -10
View File
@@ -5,25 +5,25 @@ use serde_json::{Map, Value};
pub struct Project {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub task_rate: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub due_date: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub budgeted_hours: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub color: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub public_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
+79
View File
@@ -0,0 +1,79 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use super::invoice::InvoiceItem;
/// A purchase order. Structurally close to an invoice, but addressed to a
/// vendor rather than a client.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PurchaseOrder {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub invoice_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub project_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub po_number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub quote_number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub status_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub due_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub partial_due_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_sent_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub balance: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub paid_to_date: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub discount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub partial: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub exchange_rate: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_amount_discount: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub uses_inclusive_taxes: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub public_notes: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub terms: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub footer: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub currency_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub design_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub last_viewed: Option<i64>,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub line_items: Vec<InvoiceItem>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+77
View File
@@ -0,0 +1,77 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A recurring expense template: an expense plus the recurrence fields
/// (`frequency_id`, `remaining_cycles`, `next_send_date`).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RecurringExpense {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub invoice_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub project_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub category_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub foreign_amount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub exchange_rate: f64,
/// Integer enum matching [`RecurringInvoice::frequency_id`].
///
/// [`RecurringInvoice::frequency_id`]: super::RecurringInvoice::frequency_id
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub frequency_id: i64,
/// Number of cycles left; `-1` means it recurs indefinitely.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub remaining_cycles: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub payment_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_sent_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub next_send_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub public_notes: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub transaction_reference: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name1: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate1: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name2: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate2: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_name3: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub tax_rate3: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub should_be_invoiced: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub uses_inclusive_taxes: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+92
View File
@@ -0,0 +1,92 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use super::invoice::InvoiceItem;
/// A recurring invoice template. Shares the invoices table with [`Invoice`],
/// plus the recurrence fields (`frequency_id`, `remaining_cycles`,
/// `next_send_date`, auto-billing).
///
/// [`Invoice`]: super::Invoice
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RecurringInvoice {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vendor_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub project_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub po_number: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub status_id: String,
/// How often the invoice recurs. Invoice Ninja returns this as an integer
/// enum (1 = daily, 2 = weekly, … ), not a human-readable string.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub frequency_id: i64,
/// Number of cycles left to send; `-1` means it recurs indefinitely.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub remaining_cycles: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub auto_bill: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub auto_bill_enabled: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub due_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub next_send_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_sent_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub amount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub balance: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub paid_to_date: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub discount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub partial: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub exchange_rate: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_amount_discount: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub uses_inclusive_taxes: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub public_notes: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub terms: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub footer: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub design_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub last_viewed: Option<i64>,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub line_items: Vec<InvoiceItem>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
/// Recurring quotes share the recurring-invoice schema (the API's
/// `RecurringQuote` fields are a strict subset of `RecurringInvoice`), so the
/// `/recurring_quotes` endpoint returns the same JSON shape.
pub type RecurringQuote = RecurringInvoice;
+57
View File
@@ -0,0 +1,57 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A subscription (billing plan) definition.
///
/// Note that unlike [`RecurringInvoice::frequency_id`], the API returns this
/// resource's `frequency_id` as a string.
///
/// [`RecurringInvoice::frequency_id`]: super::RecurringInvoice::frequency_id
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Subscription {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub product_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub recurring_invoice_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub frequency_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub auto_bill: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub promo_code: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub promo_discount: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_recurring: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_amount_discount: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub allow_cancellation: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub per_seat_enabled: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub max_seats_limit: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub trial_enabled: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub trial_duration: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub allow_query_overrides: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub allow_plan_changes: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub refund_period: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+32
View File
@@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A server-side log entry (gateway responses, mail failures, and so on).
///
/// Note that unlike every other resource, this one's `created_at` and
/// `updated_at` are returned as date **strings**, not integer timestamps.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SystemLog {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub event_id: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub category_id: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub type_id: i64,
/// The logged payload; shape depends on the event.
#[serde(default)]
pub log: Value,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub created_at: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub updated_at: String,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+26
View File
@@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Tag {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub entity_type: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
/// Hex colour value.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub color: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+9 -9
View File
@@ -5,24 +5,24 @@ use serde_json::{Map, Value};
pub struct Task {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub client_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub invoice_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub project_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub description: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub rate: f64,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub status_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_running: bool,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
/// JSON-encoded array of `[start_timestamp, end_timestamp]` pairs, as returned by the API.
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub time_log: String,
#[serde(default)]
pub archived_at: Option<i64>,
+40
View File
@@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A scheduled job (recurring report emails, and similar).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaskScheduler {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub paused: bool,
/// Frequency, e.g. `DAY`, `WEEK`, `MONTH`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub repeat_every: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub start_from: i64,
/// The job to run, e.g. `email_report`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub job: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date_range: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub date_key: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub start_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub end_date: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub report_keys: Vec<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+27
View File
@@ -0,0 +1,27 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaskStatus {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
/// Hex colour value, e.g. `#000000`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub color: String,
/// Position of this status in the kanban board ordering.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub task_status_order: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+24
View File
@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaxRate {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
/// The rate as a percentage, e.g. `10.0` for 10%.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub rate: f64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+55
View File
@@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// A user account.
///
/// Every `/users` endpoint except the index listing sits behind the API's
/// `password_protected` middleware, so reads of a single user and all writes
/// need an `X-API-PASSWORD` header — see
/// [`InvoiceNinjaClient::with_api_password`](crate::InvoiceNinjaClient::with_api_password).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct User {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub first_name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_name: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub email: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub phone: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub signature: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub avatar: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub language_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub accepted_terms_version: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub oauth_user_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub oauth_provider_id: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub verified_phone_number: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub has_password: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_confirmed_email_address: String,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub passkey_enabled: bool,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub passkey_count: i64,
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
#[serde(default)]
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
+19 -19
View File
@@ -5,31 +5,31 @@ use serde_json::{Map, Value};
pub struct Vendor {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub id_number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub vat_number: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub address1: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub address2: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub city: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub state: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub postal_code: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub country_id: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub phone: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub website: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub private_notes: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_deleted: bool,
#[serde(default)]
pub archived_at: Option<i64>,
@@ -37,7 +37,7 @@ pub struct Vendor {
pub created_at: Option<i64>,
#[serde(default)]
pub updated_at: Option<i64>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub contacts: Vec<VendorContact>,
/// Any fields returned by the API that aren't modeled above are preserved here,
@@ -50,15 +50,15 @@ pub struct Vendor {
pub struct VendorContact {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub first_name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub last_name: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub email: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub phone: String,
#[serde(default)]
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub is_primary: bool,
#[serde(flatten)]
+22
View File
@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
/// An outbound webhook registration (the API calls these "subscriptions"
/// internally, but they are served from `/webhooks`).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Webhook {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// The event this hook fires on, as an integer-enum string.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub event_id: String,
/// The URL Invoice Ninja posts to.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub target_url: String,
/// Payload format: `JSON` or `UBL`.
#[serde(default, deserialize_with = "super::common::null_to_default")]
pub format: String,
#[serde(flatten)]
pub extra: Map<String, Value>,
}