* fix(bookkeeping): negative item rows book on the opposite side, never as negative amounts
A supplier-invoice item with a negative line_total (an öresavrundning row on
3740, a rabatt row) was copied straight into debit_amount, producing a line
like "3740 debit -0.25". The entry balances arithmetically, so no trigger
fired, but the verifikat page renders only positive amounts: the row showed
empty and the visible debits (20 056,25) disagreed with the summa (20 056,00).
Prod holds 14 such lines: 12 supplier registrations in 3 companies, 1
customer invoice (3004 credit -0.50), 1 storno mirroring a bad original.
Why it occurred: the "one non-negative side per line" invariant lived
nowhere. Zod allows negative items (they are legitimate), the engine only
checked balance, and journal_entry_lines had no CHECK. Any producer that
aggregates user rows could repeat it.
What was removed or simplified: no new state. The privately-paid supplier
path already flipped negative buckets to credit; that rule is now one
helper (lib/bookkeeping/line-side.ts) shared by the supplier registration,
cash-method and privately-paid generators and by the customer-invoice
per-rate generator. The credit-note generator stops swapping sides and
takes |net|, since its inputs now arrive on the correct side.
Why this and not the proposed fix: patching only the supplier generator
leaves MCP, templates and future producers free to repeat the class, and
rejecting negative items at input would break real rabatt/avrundning rows.
So the sign is fixed at three levels: producers flip the side, the engine
refuses negative amounts before any write (JOURNAL_LINE_NEGATIVE_AMOUNT,
Swedish message), and a NOT VALID CHECK on journal_entry_lines rejects new
rows regardless of the writer. reverseEntry swaps on the net so a legacy
negative line stornos into a well-formed line before the data repair runs.
The 14 existing prod lines are repaired by a separate founder-approved SQL
(flip to the opposite column, net unchanged); VALIDATE CONSTRAINT follows
in a later migration once prod reports zero offending rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMJvTFitzKQYuv7ABUVFj9
* fix(bookkeeping): anchor foreign-currency 1510/1930 on the net of the revenue lines; flip salary buckets by side
Skeptic findings on ab119d6ed:
1. A non-SEK customer invoice with a negative row (rabatt, avrundning on a
separate revenue account) now lands that row on the debit side, but the
1510 (accrual) and 1930 (kontantmetod) anchors summed only credit_amount,
so the entry was overstated by the row and threw "Verifikationen
balanserar inte". Both anchors now use credit - debit. EUR test added for
both paths.
2. Salary: arbetsgivaravgifter, semesteravsättning, pension and SLP buckets
copied bucket.amount into debit_amount and the aggregated liability into
credit_amount. A negative month (unpaid leave beyond gross) produced
7510 D -628,40, which the engine now refuses. Buckets and liabilities go
through debitNatural/creditNatural so a negative month books 7510 K /
2731 D. Test added.
3. replaceOpeningBalanceEntry, the third engine write path, now runs the
same non-negative guard as createDraftEntry and updateDraftEntry.
4. The credit-note comment claimed |net| is side-correct for every original;
it is not for originals with a negative row (pre-existing, callers negate
items with -Math.abs). Comment now states the actual behaviour and the
known gap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(bookkeeping): map JOURNAL_LINE_NEGATIVE_AMOUNT to a structured 400; supplier anchors flip side when the invoice nets below zero
CodeRabbit on #2439:
- JournalLineNegativeAmountError was not registered in isBookkeepingError /
bookkeepingErrorResponse, so the journal-entry routes would have returned
a generic 500 instead of the structured 400 with code and details. Added,
with a test.
- The three supplier balance anchors (2440 on registration, the payment
account under kontantmetoden, the liability account for privately paid
invoices) were fixed-credit lines. An invoice whose rows net below zero
(a leverantörskreditfaktura keyed in as an invoice) produced a negative
credit there, which the engine now refuses. The anchors go through
creditNatural so such an invoice books 2440 D, as a supplier credit note
would. Tests for all three paths.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>