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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-06 11:20:46 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 8e7e7201d3
commit ac1529e413
5 changed files with 60 additions and 7 deletions
+23 -4
View File
@@ -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
</span>
</div>
<div className="flex items-center gap-2 pt-1 border-t text-sm">
<CheckCircle2 className="h-4 w-4 text-success" />
<span className="text-success font-medium">Balanserar</span>
</div>
{isBalanced ? (
<div className="flex items-center gap-2 pt-1 border-t text-sm">
<CheckCircle2 className="h-4 w-4 text-success" />
<span className="text-success font-medium">Balanserar</span>
</div>
) : (
<div className="flex items-center gap-2 pt-1 border-t text-sm">
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
<span className="text-destructive font-medium">
Balanserar inte (differens{' '}
<span className="tabular-nums">
{balanceDiff.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
</span>{' '}
SEK)
</span>
</div>
)}
</div>
{/* Error */}