From ac1529e4130f250ef894197d79d0833b39e9d929 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:20:46 +0200 Subject: [PATCH] fix(ui): block illegal VAT rates, honest opening-balance state, surface account-save errors (#902) * fix(ui): block illegal VAT rates, honest opening-balance state, surface account-save errors - Supplier invoice form (#863 item 1): onSubmit now blocks any non-reverse-charge line whose VAT rate is outside the legal Swedish set {25, 12, 6, 0} with a destructive toast naming the line and the legal rates. Server-side schema tightening stays out of scope. - Opening balance import (#837): the summary derives isBalanced from the running totals (0.01 epsilon) and renders the AlertCircle destructive pattern with the differens amount when unbalanced; canExecute includes isBalanced so the commit button is disabled instead of funneling users into a server-side rejection. - EditAccountDialog (#838): a failed account save now shows a destructive toast with the server-provided message instead of being silently swallowed; the dialog stays open for retry. Co-Authored-By: Claude Fable 5 * fix(ui): compare opening-balance totals in whole ore, map account-save errors to Swedish Review findings on the first pass: the < 0.01 epsilon misclassified exact 1-ore imbalances as balanced (0.03 - 0.02 evaluates just under 0.01), and the save-failure toast surfaced raw English server text instead of routing through getErrorMessage like the sibling handlers in the same file. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- components/bookkeeping/EditAccountDialog.tsx | 16 ++++++++--- .../import/OpeningBalancePeriodStep.tsx | 27 ++++++++++++++++--- .../NewSupplierInvoiceForm.tsx | 20 ++++++++++++++ messages/en.json | 2 ++ messages/sv.json | 2 ++ 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/components/bookkeeping/EditAccountDialog.tsx b/components/bookkeeping/EditAccountDialog.tsx index 2c5b2159..47121c3f 100644 --- a/components/bookkeeping/EditAccountDialog.tsx +++ b/components/bookkeeping/EditAccountDialog.tsx @@ -257,14 +257,24 @@ export function EditAccountDialog({ open, onOpenChange, account, onSaved }: Edit }) if (!response.ok) { - const data = await response.json() - throw new Error(data.error || 'Kunde inte uppdatera kontot') + const data = await response.json().catch(() => null) + // Keep the dialog open so the user can correct and retry; map the + // server error to Swedish like the dimension-rule handlers above. + toast({ + title: 'Kunde inte uppdatera kontot', + description: getErrorMessage(data, { locale: 'sv' }), + variant: 'destructive', + }) + return } onSaved() onOpenChange(false) } catch { - // Error handled silently: toast is in parent + toast({ + title: 'Kunde inte uppdatera kontot', + variant: 'destructive', + }) } finally { setIsSaving(false) } diff --git a/components/import/OpeningBalancePeriodStep.tsx b/components/import/OpeningBalancePeriodStep.tsx index 5a93693f..4e61f55d 100644 --- a/components/import/OpeningBalancePeriodStep.tsx +++ b/components/import/OpeningBalancePeriodStep.tsx @@ -44,6 +44,11 @@ export default function OpeningBalancePeriodStep({ totalDebit = Math.round((totalDebit + row.debit_amount) * 100) / 100 totalCredit = Math.round((totalCredit + row.credit_amount) * 100) / 100 } + // Compare in whole öre, mirroring the engine's validateBalance: a float + // epsilon like (< 0.01) would misclassify exact 1-öre imbalances as + // balanced, since e.g. 0.03 - 0.02 evaluates to just under 0.01. + const balanceDiff = Math.round((totalDebit - totalCredit) * 100) / 100 + const isBalanced = Math.round((totalDebit - totalCredit) * 100) === 0 useEffect(() => { async function fetchPeriods() { @@ -83,6 +88,7 @@ export default function OpeningBalancePeriodStep({ !!selectedPeriodId && !periodIsClosed && !periodIsLocked && + isBalanced && !isLoading const handleExecute = useCallback(() => { @@ -160,10 +166,23 @@ export default function OpeningBalancePeriodStep({ {totalCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK -
- - Balanserar -
+ {isBalanced ? ( +
+ + Balanserar +
+ ) : ( +
+ + + Balanserar inte (differens{' '} + + {balanceDiff.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + {' '} + SEK) + +
+ )} {/* Error */} diff --git a/components/supplier-invoices/NewSupplierInvoiceForm.tsx b/components/supplier-invoices/NewSupplierInvoiceForm.tsx index d3118655..47cd91d3 100644 --- a/components/supplier-invoices/NewSupplierInvoiceForm.tsx +++ b/components/supplier-invoices/NewSupplierInvoiceForm.tsx @@ -1059,6 +1059,26 @@ export default function NewSupplierInvoiceForm({ }) return } + // Only 25/12/6/0 % are legal Swedish VAT rates (ML 2023:200). The free-text + // VatRateCell clamps to [0, 100] but accepts anything in between, so block + // illegal rates here. Reverse-charge invoices skip this: their line vat_rate + // is forced to 0 and RcRateSelect already restricts the self-assessed rate. + if (!data.reverse_charge) { + const rowWithIllegalRate = data.items.findIndex( + (item) => !VAT_RATE_PRESETS.includes(item.vat_rate), + ) + if (rowWithIllegalRate !== -1) { + toast({ + title: t('illegal_vat_rate_title'), + description: t('illegal_vat_rate_description', { + row: rowWithIllegalRate + 1, + rate: rateToPctString(data.items[rowWithIllegalRate].vat_rate), + }), + variant: 'destructive', + }) + return + } + } // A row with an open periodisering panel must carry a complete period of // at least two calendar months before the invoice can be booked. const invalidAccrual = canUseAccrual && data.items.some((item) => { diff --git a/messages/en.json b/messages/en.json index bf7bd21b..c78d6b85 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2889,6 +2889,8 @@ "invoice_number_missing_description": "Enter the supplier's invoice number.", "account_missing_title": "Account missing", "account_missing_description": "Select an expense account for line {row}.", + "illegal_vat_rate_title": "Invalid VAT rate", + "illegal_vat_rate_description": "Line {row} has VAT rate {rate} %. The legal Swedish VAT rates are 25, 12, 6 or 0 %.", "expense_registered_title": "Expense registered", "invoice_registered_title": "Invoice registered", "arrival_number_label": "Arrival number: {number}", diff --git a/messages/sv.json b/messages/sv.json index a498224e..c091b444 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -2889,6 +2889,8 @@ "invoice_number_missing_description": "Ange leverantörens fakturanummer.", "account_missing_title": "Konto saknas", "account_missing_description": "Välj ett bokföringskonto för rad {row}.", + "illegal_vat_rate_title": "Ogiltig momssats", + "illegal_vat_rate_description": "Rad {row} har momssats {rate} %. Tillåtna momssatser är 25, 12, 6 eller 0 %.", "expense_registered_title": "Utlägg registrerat", "invoice_registered_title": "Faktura registrerad", "arrival_number_label": "Ankomstnummer: {number}",