PATCH body — only the fields actually sent are applied (exclude_unset).
ZCT-265: the PATCH route used to bind :class:ArIssuerProfileIn and
model_dump() it whole, so every field the caller omitted was written at its
Pydantic default — not merely nulled. A partial update therefore cleared
bank_account_id (the creditor account printed on the Swiss QR-bill),
default_payment_terms_id and is_default, and reset country to
"CH" and template_key to "standard", silently. The house rule is
the one counterparty states outright — "unset fields are left untouched
(no reset-to-default)" — and the twelve sibling PATCH routes that dump with
exclude_unset (GlAccountUpdate, PayrollEmployeeUpdate, the payroll
rate/expense patches, accounting settings) all pair an all-optional schema
with that dump. This is that schema.
An explicit null still clears — that is what separates "omitted" from
"cleared", and the SPA relies on it (choosing "none" for the creditor bank
account sends bank_account_id: null). The five columns that are NOT NULL in
the database — label, legal_name, country, template_key,
is_default — are refused by name in ar_invoice.update_issuer_profile
rather than left to trip an opaque constraint violation 422 (the ZET-139
lesson, whose _NON_NULLABLE_ACCOUNT_FIELDS guard this mirrors).
Deliberately a standalone declaration rather than a shared _Fields base
(the PayrollEmployeeUpdate variant): extracting one would reorder
ArIssuerProfileIn's properties in the emitted OpenAPI and churn both
frontends' generated clients for no behavioural gain.
⚖️ ZCT-513 applies to updates too, as a no-clear rule rather than a required
field. The ruling is "enforce at the API/schema layer for creates and
updates", and the three address parts stay | None here on purpose: making them
required would undo ZCT-265 — every PATCH that means to change only
footer_text would have to resend an address, which is the wholesale-replace
shape this schema exists to end. What is refused is a write that empties them:
an explicit null or a blank string on address_line1/postal_code/
city. Omission stays inert, so a legacy address-less profile can still be
edited field by field, and no new address-less row can be produced from a complete
one. (The NOT NULL columns are refused a rung lower, by name, in
ar_invoice._NON_NULLABLE_ISSUER_FIELDS; these three are genuinely nullable in
the database and stay so — the ruling forbids a NOT NULL constraint.)