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:
co-authored by
Claude Fable 5
parent
edfdbe2d2a
commit
86f0b70fdd
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +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'
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in New Issue
Block a user