fix(vat): complete account treatment enforcement (#1593)

* fix(vat): complete account treatment enforcement

* docs(api): refresh account endpoint skill

* fix(mcp): preserve ruta 05 compatibility

* test(vat): seed migration constraint fixtures

* docs(vat): clarify treatment precedence

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-15 23:45:04 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent edfdbe2d2a
commit 86f0b70fdd
43 changed files with 1017 additions and 359 deletions
@@ -143,14 +143,14 @@ describe('rutorFromTotals: explicit account VAT treatments', () => {
expect(rutor.ruta20).toBe(1000)
})
it('keeps a static BAS mapping authoritative over an explicit treatment', () => {
it('lets an explicit treatment replace a static BAS mapping', () => {
const totals = new Map([['3001', { debit: 0, credit: 1000 }]])
const rutor = rutorFromTotals(totals, {
mappingByAccount: new Map([['3001', { box: 'ruta42', side: 'credit' }]]),
explicitAccounts: new Set(['3001']),
})
expect(rutor.ruta05).toBe(1000)
expect(rutor.ruta42).toBe(0)
expect(rutor.ruta05).toBe(0)
expect(rutor.ruta42).toBe(1000)
})
})
+3 -2
View File
@@ -150,8 +150,9 @@ export async function findRcBasisGaps(
const basisByEntryAndRate = new Map<string, number>()
for (const line of siblingLines) {
const rate = STATIC_RC_BASIS_RATE.get(line.account_number) ??
dynamicVatAccounts.rcBasisRateByAccount.get(line.account_number)
const rate = dynamicVatAccounts.explicitAccounts.has(line.account_number)
? dynamicVatAccounts.rcBasisRateByAccount.get(line.account_number)
: STATIC_RC_BASIS_RATE.get(line.account_number)
if (rate) {
const key = `${line.journal_entry_id}:${rate}`
const prev = basisByEntryAndRate.get(key) || 0
+2 -1
View File
@@ -436,6 +436,7 @@ export function rutorFromTotals(
}
for (const [account, mapping] of Object.entries(ACCOUNT_RUTA)) {
if (dynamic?.explicitAccounts.has(account)) continue
const t = totals.get(account)
if (!t) continue
const balance = mapping.side === 'credit'
@@ -445,7 +446,6 @@ export function rutorFromTotals(
}
for (const [account, mapping] of dynamic?.mappingByAccount ?? []) {
if (ACCOUNT_RUTA[account]) continue
const t = totals.get(account)
if (!t) continue
const balance = mapping.side === 'credit' ? t.credit - t.debit : t.debit - t.credit
@@ -561,6 +561,7 @@ export async function calculateVatDeclaration(
}
const RATE_BUCKET = { 0.25: 'base25', 0.12: 'base12', 0.06: 'base6' } as const
for (const [account, rate] of [['3001', 'base25'], ['3002', 'base12'], ['3003', 'base6']] as const) {
if (dynamicVatAccounts.explicitAccounts.has(account)) continue
const t = totals.get(account)
if (t) revenueByRate[rate] = round(t.credit - t.debit)
}
+2 -8
View File
@@ -2,8 +2,8 @@ import type {
VatDeclarationCheck,
VatCheckAccountTotals,
} from './vat-declaration-checks'
import type { VatDeclarationRutor } from '@/types'
import { roundOre } from '@/lib/money'
import type { VatDeclarationRutor } from '@/types'
/**
* The filing gate for the momsdeklaration: ONE derived value that the
@@ -94,12 +94,6 @@ export const RC_BASIS_ACCOUNTS_BY_RATE = {
r6: ['4517', '4537', '4533', '4417', '4427'],
} as const
const STATIC_RC_BASIS_ACCOUNTS = new Set<string>([
...RC_BASIS_ACCOUNTS_BY_RATE.r25,
...RC_BASIS_ACCOUNTS_BY_RATE.r12,
...RC_BASIS_ACCOUNTS_BY_RATE.r6,
])
/** Net debit balance of the RC basis accounts, one figure per momssats. */
export interface RcBasisTotalsByRate {
r25: number
@@ -120,6 +114,7 @@ export function rcBasisTotalsByRate(
const sumGroup = (accounts: readonly string[]): number => {
let sum = 0
for (const account of accounts) {
if (dynamic?.explicitAccounts.has(account)) continue
const t = totals.get(account)
if (t) sum += t.debit - t.credit
}
@@ -131,7 +126,6 @@ export function rcBasisTotalsByRate(
r6: sumGroup(RC_BASIS_ACCOUNTS_BY_RATE.r6),
}
for (const [account, rate] of dynamic?.rcBasisRateByAccount ?? []) {
if (STATIC_RC_BASIS_ACCOUNTS.has(account)) continue
const total = totals.get(account)
if (!total) continue
const key = rate === 0.25 ? 'r25' : rate === 0.12 ? 'r12' : 'r6'
+14 -17
View File
@@ -42,7 +42,7 @@ const emptyDynamicVatAccounts = (): DynamicVatAccounts => ({
rcBasisRateByAccount: new Map(),
})
/** Explicit treatments extend custom accounts; fixed BAS mappings stay authoritative. */
/** Explicit account treatments win; accounts without one keep BAS fallback. */
export async function fetchDynamicVatAccounts(
supabase: SupabaseClient,
companyId: string,
@@ -58,7 +58,6 @@ export async function fetchDynamicVatAccounts(
.select('account_number, account_name, account_class, default_vat_rate, default_vat_treatment')
.eq('company_id', companyId)
.in('account_class', [3, 4, 5, 6])
.not('is_active', 'is', false)
.order('account_number', { ascending: true })
.range(from, to),
)
@@ -66,27 +65,25 @@ export async function fetchDynamicVatAccounts(
const result = emptyDynamicVatAccounts()
for (const row of rows) {
const account = row.account_number
const accountClass = Number(row.account_class ?? account.charAt(0))
const configuredRate = row.default_vat_rate === null ? null : Number(row.default_vat_rate)
if (isAccountVatTreatment(row.default_vat_treatment)) {
if (ACCOUNT_TO_BOX[account]) {
if (
RUTA_05_STATIC_RATE_ACCOUNTS.has(account) &&
configuredRate !== null && TAXABLE_RATES.includes(configuredRate)
) {
result.staticRateByAccount.set(account, configuredRate)
}
continue
}
const mapping = resolveVatTreatmentRuta(row.default_vat_treatment, accountClass)
if (!mapping) continue
result.explicitAccounts.add(account)
const mapping = resolveVatTreatmentRuta(
row.default_vat_treatment,
row.account_class,
row.account_number,
)
if (!mapping) continue
result.mappingByAccount.set(account, mapping)
// Always include an explicit account in the ledger filter. The Set in
// fetchVatAccountTotals deduplicates static BAS accounts, while this also
// covers accounts that exist only in the separate moms-box mirror.
result.accounts.push(account)
const rate = configuredRate ?? defaultRateForVatTreatment(row.default_vat_treatment, accountClass)
const rate = configuredRate ?? defaultRateForVatTreatment(row.default_vat_treatment, row.account_class)
if (mapping.box === 'ruta05' && rate !== null && TAXABLE_RATES.includes(rate)) {
result.rateByAccount.set(account, rate)
const target = ACCOUNT_TO_BOX[account] ? result.staticRateByAccount : result.rateByAccount
target.set(account, rate)
}
if (
['ruta20', 'ruta21', 'ruta22', 'ruta23', 'ruta24'].includes(mapping.box) &&
@@ -97,7 +94,7 @@ export async function fetchDynamicVatAccounts(
continue
}
if (accountClass !== 3) continue
if (row.account_class !== 3) continue
const rate = configuredRate ?? inferDomesticSalesRate(account, row.account_name)
if (rate === null || !TAXABLE_RATES.includes(rate)) continue
if (ACCOUNT_TO_BOX[account]) {