fix(settings): scope cross-field VAT validations to saves that touch them (#2121)
* fix(settings): scope cross-field VAT validations to saves that touch them
The settings PUT validated the whole effective record on every partial
update, so companies stored as vat_registered without a vat_number were
blocked from saving anything through the endpoint, including the invoice
bank-details dialog, which has no VAT fields (reported by a user stuck on
"Momsregistreringsnummer kravs...").
Each cross-field check (VAT completeness, 40m-monthly, periodisk
sammanstallning) now runs only when the request body touches a field in
its group, so the invariant still holds whenever VAT config is edited.
Explicit null now counts as clearing a value during validation instead of
falling back to the stored one, closing a latent hole where
{ vat_number: null } passed validation but wrote null.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
* fix(invoices): gate issuance on the seller VAT number (skeptic finding)
The settings scoping in the previous commit removed what was accidentally
the only enforcement of "momsregistrerad implies momsregnr on file": with
bank details saveable again, a registered company without a stored VAT
number could issue a faktura charging moms with no seller VAT number in
the footer (mandatory element, ML (2023:200) 17 kap. 24 §).
Issuance is now gated the same way the payment account is, at all four
independent issuance points (issueAndBookInvoice, dashboard send, v1 send,
v1 mark-sent), with a structured error pointing at Installningar -> Skatt.
Credit notes, proformas, and delivery notes are exempt like the payment
gate exempts them.
Also, per the Swedish review and the secondary skeptic finding:
- PS/EU-trade edits join the VAT-completeness touch group, so enabling
periodisk sammanstallning on an incomplete registration keeps failing.
- The stale ML 11 kap. 8 citation is updated to ML 17 kap. 24.
The makeCompanySettings fixture now models a coherent registered company
(vat_number set); the missing-number tests override it explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
* fix(invoices): extend the seller-VAT-number gate to the headless issuance paths
Skeptic round 2 found three more issuance points beside the four gated in
the previous commit: the recurring auto-send service (cron, no human in
the loop), and the MCP staged-operation executors send_invoice and
mark_invoice_sent. Each carried the payment-account gate but not the VAT
gate; mark_invoice_sent additionally had a narrow settings select that
would have made a naive gate silently pass, now widened.
Recurring auto-send fails soft, matching its other guards: the invoice
stays a numbered draft with the standard schedule warning. The executors
return the structured Swedish message. Peppol send was verified
self-gating (BIS preflight requires the supplier VAT number).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
* test(email): refresh brand-mail snapshots for the coherent VAT fixture
The makeCompanySettings fixture now carries a VAT number, so the invoice
and reminder mail footers correctly render the VAT line; the snapshots
predate that. Also cites ML 17 kap. 22-23 (andringsfaktura content list)
in the seller-vat-number docstring per the Swedish review suggestion,
documenting why credit notes are exempt. No behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b5da51ea0a
commit
0406e628e1
@@ -182,14 +182,31 @@ export const PUT = withRouteContext(
|
||||
body.employer_seasonal = false
|
||||
}
|
||||
|
||||
// Validate: VAT-registered must have VAT number (ML 11 kap. 8§) and moms period (SFL 26 kap.)
|
||||
// Validate: VAT-registered must have VAT number (ML 17 kap. 24 §, the
|
||||
// invoice needs it) and moms period (SFL 26 kap.).
|
||||
// Each cross-field check runs only when the request touches a field in its
|
||||
// group: a partial save of unrelated settings (e.g. the invoice bank-details
|
||||
// dialog) must not be rejected for a pre-existing inconsistency it cannot
|
||||
// fix from that surface. Explicit null counts as touched, it clears a value,
|
||||
// so it must not fall back to the stored one during validation.
|
||||
// PS/EU-trade edits are in the completeness group: enabling the EU sales
|
||||
// list on an incomplete VAT registration must keep failing like it did
|
||||
// when the check ran on every save.
|
||||
const effectiveVatRegistered = body.vat_registered ?? oldSettings?.vat_registered
|
||||
const effectiveMomsPeriod = body.moms_period ?? oldSettings?.moms_period
|
||||
if (effectiveVatRegistered === true) {
|
||||
const effectiveVatNumber = body.vat_number ?? oldSettings?.vat_number
|
||||
const effectiveMomsPeriod =
|
||||
body.moms_period !== undefined ? body.moms_period : oldSettings?.moms_period
|
||||
const touchesVatCompleteness =
|
||||
body.vat_registered !== undefined ||
|
||||
body.vat_number !== undefined ||
|
||||
body.moms_period !== undefined ||
|
||||
body.vat_has_eu_trade !== undefined ||
|
||||
body.periodisk_sammanstallning_enabled !== undefined
|
||||
if (touchesVatCompleteness && effectiveVatRegistered === true) {
|
||||
const effectiveVatNumber =
|
||||
body.vat_number !== undefined ? body.vat_number : oldSettings?.vat_number
|
||||
if (!effectiveVatNumber) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Momsregistreringsnummer krävs när företaget är momsregistrerat (ML 11 kap. 8§)' },
|
||||
{ error: 'Momsregistreringsnummer krävs när företaget är momsregistrerat (ML 17 kap. 24 §)' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
@@ -201,21 +218,34 @@ export const PUT = withRouteContext(
|
||||
}
|
||||
}
|
||||
|
||||
const touchesVat40m =
|
||||
body.vat_registered !== undefined ||
|
||||
body.vat_taxable_base_over_40m !== undefined ||
|
||||
body.moms_period !== undefined
|
||||
const effectiveVatTaxableBaseOver40m =
|
||||
body.vat_taxable_base_over_40m ?? oldSettings?.vat_taxable_base_over_40m ?? false
|
||||
if (effectiveVatRegistered && effectiveVatTaxableBaseOver40m && effectiveMomsPeriod !== 'monthly') {
|
||||
if (
|
||||
touchesVat40m &&
|
||||
effectiveVatRegistered &&
|
||||
effectiveVatTaxableBaseOver40m &&
|
||||
effectiveMomsPeriod !== 'monthly'
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Företag med beskattningsunderlag över 40 miljoner kronor måste redovisa moms varje månad.' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const touchesPs =
|
||||
body.periodisk_sammanstallning_enabled !== undefined ||
|
||||
body.vat_registered !== undefined ||
|
||||
body.vat_has_eu_trade !== undefined
|
||||
const effectivePsEnabled =
|
||||
body.periodisk_sammanstallning_enabled ??
|
||||
oldSettings?.periodisk_sammanstallning_enabled ??
|
||||
false
|
||||
const effectiveEuTrade = body.vat_has_eu_trade ?? oldSettings?.vat_has_eu_trade ?? false
|
||||
if (effectivePsEnabled && (!effectiveVatRegistered || !effectiveEuTrade)) {
|
||||
if (touchesPs && effectivePsEnabled && (!effectiveVatRegistered || !effectiveEuTrade)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Periodisk sammanställning kräver momsregistrering och EU-handel.' },
|
||||
{ status: 400 },
|
||||
|
||||
Reference in New Issue
Block a user