Correct the models against what a real server actually sends
Every one of these was wrong in a way the mock tests could not see, because the mocks were written from the same upstream source the models were. - `SystemLog.created_at`/`updated_at` are integer timestamps, not the date strings the OpenAPI spec declares. - `frequency_id` arrives as a numeric string on recurring invoices and expenses, so both now decode through `int_or_numeric_string`. - `Location.country_id`, `PurchaseOrder.status_id` and `Webhook.format` are omitted when empty. The server runs `ConvertEmptyStringsToNull`, so an unset `String` sent as `""` becomes NULL and then fails a non-null column or an `integer` rule — which meant a create from a `Default` model could not succeed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,14 @@ pub struct Location {
|
||||
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")]
|
||||
/// Omitted when empty: the server validates it as an integer, so an unset
|
||||
/// `""` is rejected with a 422 and a default `Location` could not be
|
||||
/// created.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::common::null_to_default",
|
||||
skip_serializing_if = "String::is_empty"
|
||||
)]
|
||||
pub country_id: String,
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
pub user_id: String,
|
||||
|
||||
@@ -38,7 +38,9 @@ pub struct Payment {
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PaymentInvoice {
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
pub invoice_id: String,
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
pub amount: f64,
|
||||
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -23,7 +23,14 @@ pub struct PurchaseOrder {
|
||||
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")]
|
||||
/// Omitted when empty: the server turns `""` into `NULL` and then fails
|
||||
/// the insert on a non-null column with a 500, so a new purchase order
|
||||
/// must leave it out and take the server's default.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::common::null_to_default",
|
||||
skip_serializing_if = "String::is_empty"
|
||||
)]
|
||||
pub status_id: String,
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
pub date: String,
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct RecurringExpense {
|
||||
/// Integer enum matching [`RecurringInvoice::frequency_id`].
|
||||
///
|
||||
/// [`RecurringInvoice::frequency_id`]: super::RecurringInvoice::frequency_id
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
#[serde(default, deserialize_with = "super::common::int_or_numeric_string")]
|
||||
pub frequency_id: i64,
|
||||
/// Number of cycles left; `-1` means it recurs indefinitely.
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
|
||||
@@ -25,8 +25,9 @@ pub struct RecurringInvoice {
|
||||
#[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")]
|
||||
/// enum (1 = daily, 2 = weekly, … ), not a human-readable string. A real
|
||||
/// server sends it as a numeric string; both decode.
|
||||
#[serde(default, deserialize_with = "super::common::int_or_numeric_string")]
|
||||
pub frequency_id: i64,
|
||||
/// Number of cycles left to send; `-1` means it recurs indefinitely.
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
|
||||
@@ -3,8 +3,9 @@ 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.
|
||||
/// The OpenAPI spec declares `created_at` and `updated_at` as date strings,
|
||||
/// but a real server (5.13.50) sends integer timestamps like every other
|
||||
/// resource.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SystemLog {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -23,9 +24,9 @@ pub struct SystemLog {
|
||||
#[serde(default)]
|
||||
pub log: Value,
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
pub created_at: String,
|
||||
pub created_at: i64,
|
||||
#[serde(default, deserialize_with = "super::common::null_to_default")]
|
||||
pub updated_at: String,
|
||||
pub updated_at: i64,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
|
||||
@@ -13,8 +13,13 @@ pub struct Webhook {
|
||||
/// 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")]
|
||||
/// Payload format: `JSON` or `UBL`. Omitted when empty: the server turns
|
||||
/// `""` into `NULL` and fails the insert on a non-null column (a 500).
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::common::null_to_default",
|
||||
skip_serializing_if = "String::is_empty"
|
||||
)]
|
||||
pub format: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
|
||||
Reference in New Issue
Block a user