Files
accounted/lib/packs/__tests__/reverse-charge-basis.test.ts
T
MattssonandClaude Fable 5 e113e9c099 fix(vat,documents): EU reverse-charge packs feed ruta 20/21; daily reanchor cron for floating supplier-invoice underlag (#2095)
Two user reports (Anders, 2026-08-25 + 2026-08-29):

1. The seeded standardmallar "Inkop EU-varor/-tjanster, omvand moms 25%"
   booked the cost on 4010/6540, which no momsdeklaration ruta reads, so the
   fiktiv moms filled ruta 30/48 while ruta 20/21 (inkopsvarde) stayed 0;
   Skatteverket rejects that (FK004, ML 13 kap). The packs now book directly
   on the basis accounts 4515/4535 (ACCOUNT_RUTA -> ruta 20/21); the
   transaction-picker path already skips its own basis emission for basis
   debit accounts, so no double counting. Regression test pins every
   reverse-charge pack to a 44xx/45xx business debit. Prod rows update via
   the existing pack sync cron (upsert on pack_slug).

2. A kontantmetod payment verifikat stayed "Underlag saknas" although the
   invoice PDF was attached and eligible on every static condition: the
   inline anchorSupplierInvoiceDocument silently did nothing (prod case
   2026-08-28, verified in audit_log: no document_attachments update between
   the payment booking and the user's manual re-upload). The helper now
   verifies the guarded update actually matched a row instead of claiming
   success on zero rows, logs its silent bail branches, and a new daily cron
   (/api/documents/reanchor/cron) re-runs the anchor for any floating
   retained document with a posted verifikat, replacing the pattern of
   one-off repair migrations (20260727180000, 20260824150000). The sweep
   names the FK in its embed and is idempotent; locked/closed periods are
   skipped as before.


Claude-Session: https://claude.ai/code/session_01Jj6Rg1ViyFRej55gbxLVgj

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 10:16:07 +02:00

71 lines
2.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import path from 'node:path'
import { loadPacks } from '@/lib/packs/load'
/**
* A reverse-charge pack must book its cost on a basbelopp account.
*
* Momsdeklaration rutor 20-24 are driven purely by BAS account number
* (ACCOUNT_RUTA in lib/reports/vat-declaration.ts): the fiktiv-moms pair
* (2614/2624/2634 + 2645/2647) fills ruta 30-32 and 48, but the purchase
* value itself only reaches ruta 20-24 when the expense sits on the 44xx/45xx
* basis series. A reverse-charge pack that books its cost elsewhere (the
* pre-fix 4010/6540) produces a declaration with VAT but no inköpsvärde,
* which Skatteverket rejects (felkod FK004; ML 13 kap requires both sides).
* User report: Anders, 2026-08-25.
*/
const ROOT = path.resolve(__dirname, '../../..')
// Fictitious output-VAT accounts that mark a purchase-side reverse-charge pack.
const FIKTIV_OUTPUT_VAT = new Set(['2614', '2624', '2634'])
// Same range isBasisAccount() in lib/bookkeeping/booking-templates.ts guards:
// 44xx/45xx, the ruta 20-24 inputs.
const BASIS_ACCOUNT_RE = /^4[45]\d{2}$/
describe('reverse-charge packs feed momsdeklaration ruta 20-24', () => {
const { packs, errors } = loadPacks(ROOT)
it('catalogue loads', () => {
expect(errors).toEqual([])
})
const reverseChargePacks = packs.filter((p) =>
p.pack.lines.some((l) => FIKTIV_OUTPUT_VAT.has(l.account)),
)
it('the catalogue actually contains reverse-charge purchase packs', () => {
// Guards the filter above: if the fiktiv accounts are ever renumbered,
// this suite must be updated rather than silently asserting nothing.
expect(reverseChargePacks.length).toBeGreaterThanOrEqual(2)
})
it.each([
['inkop-eu-varor-omvand-moms-25', '4515'],
['inkop-eu-tjanster-omvand-moms-25', '4535'],
])('%s books its cost on basis account %s', (slug, account) => {
const pack = packs.find((p) => p.pack.meta.slug === slug)
expect(pack).toBeDefined()
const business = pack!.pack.lines.filter((l) => l.type === 'business')
expect(business).toHaveLength(1)
expect(business[0].account).toBe(account)
expect(business[0].side).toBe('debit')
})
it('every reverse-charge purchase pack books its business debit on a 44xx/45xx basis account', () => {
for (const p of reverseChargePacks) {
const businessDebits = p.pack.lines.filter(
(l) => l.type === 'business' && l.side === 'debit',
)
for (const line of businessDebits) {
expect(
BASIS_ACCOUNT_RE.test(line.account),
`${p.pack.meta.slug}: business debit on ${line.account} never reaches ruta 20-24; ` +
`book reverse-charge cost on the 44xx/45xx basis series`,
).toBe(true)
}
}
})
})