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
@@ -392,4 +392,38 @@ describe('gnubok_update_account', () => {
|
||||
expect(result.preview.current.account_name).toBe('Förbrukningsinventarier')
|
||||
expect(result.preview.changes).toEqual({ account_name: 'Verktyg', is_active: false })
|
||||
})
|
||||
|
||||
it('preserves an existing booking rate when only treatment changes', async () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({
|
||||
data: {
|
||||
account_number: '4056', account_name: 'EU-varor', default_vat_rate: 0.12,
|
||||
default_vat_treatment: null, is_active: true,
|
||||
},
|
||||
})
|
||||
const result = (await updateAccount.execute(
|
||||
{ account_number: '4056', default_vat_treatment: 'reverse_charge_eu_goods', dry_run: true },
|
||||
'company-1', 'user-1', supabase as never,
|
||||
)) as { preview: { changes: Record<string, unknown> } }
|
||||
|
||||
expect(result.preview.changes).toEqual({
|
||||
default_vat_treatment: 'reverse_charge_eu_goods',
|
||||
})
|
||||
})
|
||||
|
||||
it('can clear a treatment to restore BAS fallback', async () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({
|
||||
data: {
|
||||
account_number: '3041', account_name: 'Försäljning', default_vat_rate: 0.25,
|
||||
default_vat_treatment: 'standard_25', is_active: true,
|
||||
},
|
||||
})
|
||||
const result = (await updateAccount.execute(
|
||||
{ account_number: '3041', default_vat_treatment: null, dry_run: true },
|
||||
'company-1', 'user-1', supabase as never,
|
||||
)) as { preview: { changes: Record<string, unknown> } }
|
||||
|
||||
expect(result.preview.changes).toEqual({ default_vat_treatment: null })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -168,9 +168,12 @@ describe('tools/list payload size guard', () => {
|
||||
// Both property descriptions trimmed to one sentence first; headroom
|
||||
// before the change was under 20 tokens, so even the trimmed wire
|
||||
// contract crossed.
|
||||
// * 59.5K to 59.7K with account VAT treatments: create_account and
|
||||
// update_account both expose the 12-value treatment vocabulary. The
|
||||
// descriptions are minimal; the enum values are the wire contract.
|
||||
// Long-term answer to growth is leaning harder on gnubok_search_tools: if this
|
||||
// fires again, prefer trimming descriptions or making a tool opt-in via search
|
||||
// before bumping further.
|
||||
expect(approxTokens).toBeLessThan(59_500)
|
||||
expect(approxTokens).toBeLessThan(59_700)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,7 +41,9 @@ interface MockLine {
|
||||
interface MockChartAccount {
|
||||
account_number: string
|
||||
account_name: string
|
||||
account_class: number
|
||||
default_vat_rate: number | null
|
||||
default_vat_treatment: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +129,9 @@ describe('gnubok_vat_close_check: declaration completeness', () => {
|
||||
[{
|
||||
account_number: '3011',
|
||||
account_name: 'Försäljning tjänster inom Sverige, 25 % moms',
|
||||
account_class: 3,
|
||||
default_vat_rate: null,
|
||||
default_vat_treatment: null,
|
||||
}],
|
||||
),
|
||||
)
|
||||
@@ -139,6 +143,54 @@ describe('gnubok_vat_close_check: declaration completeness', () => {
|
||||
expect(result.ready_to_close).toBe(true)
|
||||
})
|
||||
|
||||
it('honors an explicit override of a standard BAS revenue account', async () => {
|
||||
const result = await computeVatCloseCheck(
|
||||
PERIOD,
|
||||
'company-1',
|
||||
mockSupabase(
|
||||
[
|
||||
{ entry: 'e1', account_number: '3011', credit_amount: 1000 },
|
||||
{ entry: 'e1', account_number: '1510', debit_amount: 1000 },
|
||||
],
|
||||
[{
|
||||
account_number: '3011',
|
||||
account_name: 'Momsfri försäljning',
|
||||
account_class: 3,
|
||||
default_vat_rate: 0,
|
||||
default_vat_treatment: 'exempt',
|
||||
}],
|
||||
),
|
||||
)
|
||||
|
||||
expect(result.rutor.ruta05).toBe(0)
|
||||
})
|
||||
|
||||
it('accepts a custom EU goods basis account in the MCP close check', async () => {
|
||||
const result = await computeVatCloseCheck(
|
||||
PERIOD,
|
||||
'company-1',
|
||||
mockSupabase(
|
||||
[
|
||||
{ entry: 'e1', account_number: '4056', debit_amount: 1000 },
|
||||
{ entry: 'e1', account_number: '2440', credit_amount: 1000 },
|
||||
{ entry: 'e1', account_number: '2614', credit_amount: 250 },
|
||||
{ entry: 'e1', account_number: '2645', debit_amount: 250 },
|
||||
],
|
||||
[{
|
||||
account_number: '4056',
|
||||
account_name: 'Inköp varor EU 25 %',
|
||||
account_class: 4,
|
||||
default_vat_rate: 0.25,
|
||||
default_vat_treatment: 'reverse_charge_eu_goods',
|
||||
}],
|
||||
),
|
||||
)
|
||||
|
||||
expect(result.declaration_checks.map((finding) => finding.code))
|
||||
.not.toContain('RC_BASIS_MISSING')
|
||||
expect(result.ready_to_close).toBe(true)
|
||||
})
|
||||
|
||||
it('refuses the #1164 declaration: fiktiv moms on 2614/2645 with no basbelopp on 44xx/45xx', async () => {
|
||||
// Both VAT legs of a reverse-charge purchase booked, but the cost went
|
||||
// straight to 6540 instead of the 4535 basis account, so rutor 20-24 stay
|
||||
|
||||
+23
-3
@@ -17,6 +17,9 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import type { VatDeclarationRutor } from '@/types'
|
||||
|
||||
const { mockFindRcBasisGaps } = vi.hoisted(() => ({
|
||||
mockFindRcBasisGaps: vi.fn(),
|
||||
}))
|
||||
const mockSkvRequest = vi.fn()
|
||||
vi.mock('@/extensions/general/skatteverket/lib/api-client', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>
|
||||
@@ -47,10 +50,8 @@ vi.mock('@/lib/reports/vat-declaration', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
// The per-verifikat FK004 scan has its own coverage in lib/reports/__tests__;
|
||||
// here it must simply not add findings of its own.
|
||||
vi.mock('@/lib/reports/rc-basis-gaps', () => ({
|
||||
findRcBasisGaps: vi.fn(async () => []),
|
||||
findRcBasisGaps: (...args: unknown[]) => mockFindRcBasisGaps(...args),
|
||||
}))
|
||||
|
||||
import { tools } from '../server'
|
||||
@@ -87,10 +88,12 @@ function makeRutor(partial: Partial<VatDeclarationRutor> = {}): VatDeclarationRu
|
||||
function setDeclaration(
|
||||
rutor: VatDeclarationRutor,
|
||||
rcInput?: Record<string, { debit: number; credit: number }>,
|
||||
rcBasisByRate?: { r25: number; r12: number; r6: number },
|
||||
) {
|
||||
mockCalculateVatDeclaration.mockResolvedValue({
|
||||
rutor,
|
||||
...(rcInput ? { rcInputAccountTotals: rcInput } : {}),
|
||||
...(rcBasisByRate ? { rcBasisByRate } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ function skvOk(body: unknown = { kontrollResultat: { status: 'OK', resultat: []
|
||||
let prevEnv: string | undefined
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockFindRcBasisGaps.mockResolvedValue([])
|
||||
prevEnv = process.env.SKATTEVERKET_ENABLED
|
||||
process.env.SKATTEVERKET_ENABLED = 'true'
|
||||
})
|
||||
@@ -215,6 +219,22 @@ describe('gnubok_vat_declaration_validate', () => {
|
||||
expect(result.completeness_ok).toBe(true)
|
||||
})
|
||||
|
||||
it('uses declaration rate evidence when a correction gap is covered in aggregate', async () => {
|
||||
mockFindRcBasisGaps.mockResolvedValue([{}])
|
||||
setDeclaration(
|
||||
makeRutor({ ruta20: 5000, ruta30: 1250, ruta48: 1250 }),
|
||||
rcInput({ '2645': 1250 }),
|
||||
{ r25: 5000, r12: 0, r6: 0 },
|
||||
)
|
||||
skvOk()
|
||||
|
||||
const result = await run()
|
||||
|
||||
expect(result.completeness_checks.find((c) => c.code === 'RC_BASIS_MISSING')?.status)
|
||||
.toBe('WARNING')
|
||||
expect(result.completeness_ok).toBe(true)
|
||||
})
|
||||
|
||||
it('arithmetic_ok is false when Skatteverket returns an ERROR, independently of completeness', async () => {
|
||||
setDeclaration(CLEAN)
|
||||
skvOk({
|
||||
|
||||
@@ -9,6 +9,8 @@ interface AccountSummary {
|
||||
normal_balance: string
|
||||
is_active: boolean
|
||||
default_vat_code: string | null
|
||||
default_vat_rate: number | null
|
||||
default_vat_treatment: string | null
|
||||
}
|
||||
|
||||
export const chartOfAccountsResource: McpResource = {
|
||||
@@ -25,7 +27,7 @@ export const chartOfAccountsResource: McpResource = {
|
||||
accounts = await fetchAllRows<AccountSummary>(({ from, to }) =>
|
||||
supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('account_number, account_name, account_class, account_type, normal_balance, is_active, default_vat_code')
|
||||
.select('account_number, account_name, account_class, account_type, normal_balance, is_active, default_vat_code, default_vat_rate, default_vat_treatment')
|
||||
.eq('company_id', companyId)
|
||||
.order('account_number', { ascending: true })
|
||||
.range(from, to)
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
rcInputTotalsFromDeclaration,
|
||||
calculateVatDeclaration,
|
||||
} from '@/lib/reports/vat-declaration'
|
||||
import { fetchDynamicRuta05Accounts } from '@/lib/reports/vat-revenue-accounts'
|
||||
import { fetchDynamicVatAccounts } from '@/lib/reports/vat-revenue-accounts'
|
||||
// The momsdeklaration completeness checks live in core (lib/reports) and are
|
||||
// shared with the web UI's "Kontroll av underlaget" gate. The MCP surface
|
||||
// imports them instead of mirroring them: a hand-rolled copy here is exactly
|
||||
@@ -88,6 +88,12 @@ import {
|
||||
} from './tool-namespace'
|
||||
import { getRiskLevel } from '@/lib/pending-operations/risk-tiers'
|
||||
import { normalizeVatRateToDecimal } from '@/lib/vat/supplier-invoice-line-checks'
|
||||
import {
|
||||
ACCOUNT_VAT_TREATMENTS,
|
||||
defaultRateForVatTreatment,
|
||||
isAccountVatTreatment,
|
||||
isVatTreatmentAllowedForAccountClass,
|
||||
} from '@/lib/vat/account-vat-treatment'
|
||||
import { CreateSupplierParamsSchema } from '@/lib/pending-operations/schemas/create-supplier'
|
||||
import { accountClassTypeConflict } from '@/lib/pending-operations/schemas/account'
|
||||
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
||||
@@ -1359,33 +1365,15 @@ const SKV_AGI_STATUS_OUTPUT_SCHEMA = {
|
||||
// (the original sale's VAT silently disappears) and over-credit Period N+M
|
||||
// (a reversal with no original), incorrect per ML 2023:200.
|
||||
|
||||
/** Common BAS taxable-revenue accounts that contribute to ruta 05.
|
||||
*
|
||||
* Conservative expansion beyond 3001/3002/3003. Excludes 3004 (momsfri,
|
||||
* exempt) and 3108/3305/3308 (handled by ruta35/40/39). 3106 covers the
|
||||
* rare case of taxable EU goods (momspliktig EU-leverans, e.g. when the
|
||||
* buyer's VAT number is invalid).
|
||||
*
|
||||
* This hand-maintained widening predates #1261 and is kept so no company
|
||||
* loses a figure it already saw. It is no longer the only path: a company's
|
||||
* own class 3 konto marked with a moms-sats is resolved at runtime by
|
||||
* fetchDynamicRuta05Accounts and unioned in below, which is what actually
|
||||
* covers non-standard charts (Accounted's BAS chart ships no varugrupp
|
||||
* accounts at all). */
|
||||
const RUTA_05_ACCOUNTS = [
|
||||
// The 30xx gruppkonto. ACCOUNT_RUTA maps it to ruta05, so leaving it out here
|
||||
// made a balance on 3000 appear in the filed projection but not in
|
||||
// report.rutor.ruta05.
|
||||
// Keep the MCP report's historical ruta 05 widening for accounts that do not
|
||||
// have an explicit treatment. The effective resolver adds custom ruta 05
|
||||
// accounts and removes any of these when the company overrides their treatment.
|
||||
const RUTA_05_COMPATIBILITY_ACCOUNTS = [
|
||||
'3000',
|
||||
// Domestic sales by VAT rate (canonical BAS)
|
||||
'3001', '3002', '3003', '3005', '3006', '3007', '3008',
|
||||
// Taxable EU goods (momspliktig, buyer's VAT number invalid or buyer is private)
|
||||
'3106',
|
||||
// Domestic services (alternative numbering some companies use)
|
||||
'3041', '3042', '3043', '3044', '3045', '3046', '3047', '3048',
|
||||
// Domestic goods (alternative numbering)
|
||||
'3051', '3052', '3053', '3054', '3055', '3056', '3057', '3058',
|
||||
// Other domestic taxable
|
||||
'3071', '3072', '3073', '3074', '3075', '3076', '3077', '3078',
|
||||
] as const
|
||||
|
||||
@@ -1408,6 +1396,7 @@ export interface VatReportResult {
|
||||
|
||||
export interface VatReportWithRutor {
|
||||
report: VatReportResult
|
||||
dynamicVatAccounts: Awaited<ReturnType<typeof fetchDynamicVatAccounts>>
|
||||
/**
|
||||
* The FULL SKV 4700 projection of the same ledger aggregate, via core's
|
||||
* `rutorFromTotals`. `report.rutor` is the trimmed agent-facing view: it has
|
||||
@@ -1546,43 +1535,36 @@ export async function computeVatReportWithRutor(
|
||||
accountTotals.set(acc, existing)
|
||||
}
|
||||
|
||||
function creditBalance(acc: string): number {
|
||||
const t = accountTotals.get(acc)
|
||||
return t ? Math.round((t.credit - t.debit) * 100) / 100 : 0
|
||||
}
|
||||
|
||||
function debitBalance(acc: string): number {
|
||||
const t = accountTotals.get(acc)
|
||||
return t ? Math.round((t.debit - t.credit) * 100) / 100 : 0
|
||||
}
|
||||
|
||||
// The company's own momspliktiga intäktskonton join the hand-maintained list.
|
||||
// Deduped: an account can appear in both (e.g. 3041 with a moms-sats set),
|
||||
// and counting it twice would inflate ruta 05.
|
||||
const dynamicRuta05 = await fetchDynamicRuta05Accounts(supabase, companyId)
|
||||
const ruta05Accounts = [...new Set([...RUTA_05_ACCOUNTS, ...dynamicRuta05.accounts])]
|
||||
const ruta05 = ruta05Accounts.reduce((sum, acc) => sum + creditBalance(acc), 0)
|
||||
const ruta10 = creditBalance('2611')
|
||||
const ruta11 = creditBalance('2621')
|
||||
const ruta12 = creditBalance('2631')
|
||||
const ruta30 = creditBalance('2614')
|
||||
const ruta31 = creditBalance('2624')
|
||||
const ruta32 = creditBalance('2634')
|
||||
const ruta35 = creditBalance('3108') // EU intra-community goods supplies (momsfri leverans till EU)
|
||||
const ruta39 = creditBalance('3308')
|
||||
const ruta40 = creditBalance('3305')
|
||||
function creditBalance(acc: string): number {
|
||||
return -debitBalance(acc)
|
||||
}
|
||||
|
||||
const dynamicVatAccounts = await fetchDynamicVatAccounts(supabase, companyId)
|
||||
const declarationRutor = rutorFromTotals(accountTotals, dynamicVatAccounts)
|
||||
const {
|
||||
ruta10, ruta11, ruta12, ruta30, ruta31, ruta32,
|
||||
ruta35, ruta39, ruta40, ruta48, ruta49, ruta60, ruta61, ruta62,
|
||||
} = declarationRutor
|
||||
const reportRuta05Accounts = new Set<string>(
|
||||
RUTA_05_COMPATIBILITY_ACCOUNTS.filter(
|
||||
(account) => !dynamicVatAccounts.explicitAccounts.has(account),
|
||||
),
|
||||
)
|
||||
for (const [account, mapping] of dynamicVatAccounts.mappingByAccount) {
|
||||
if (mapping.box === 'ruta05') reportRuta05Accounts.add(account)
|
||||
}
|
||||
const ruta05 = [...reportRuta05Accounts]
|
||||
.reduce((sum, account) => sum + creditBalance(account), 0)
|
||||
// Import VAT (since 2015 declared via momsdeklaration, not Tullverket): the
|
||||
// importer books output VAT to 2615/2625/2635 (ruta 60/61/62) and the
|
||||
// matching deductible input to 2645 (rolls into ruta 48 below).
|
||||
const ruta60 = creditBalance('2615')
|
||||
const ruta61 = creditBalance('2625')
|
||||
const ruta62 = creditBalance('2635')
|
||||
const calculatedInput2645 = debitBalance('2645')
|
||||
const calculatedInput2647 = debitBalance('2647')
|
||||
const ruta48 = debitBalance('2641') + calculatedInput2645 + calculatedInput2647
|
||||
const ruta49 = Math.round(
|
||||
(ruta10 + ruta11 + ruta12 + ruta30 + ruta31 + ruta32 + ruta60 + ruta61 + ruta62 - ruta48) * 100
|
||||
) / 100
|
||||
|
||||
const monthNames = ['Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni',
|
||||
'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December']
|
||||
@@ -1642,7 +1624,8 @@ export async function computeVatReportWithRutor(
|
||||
// (incl. rutor 20-24 and 50) instead of the trimmed report view.
|
||||
return {
|
||||
report,
|
||||
declarationRutor: rutorFromTotals(accountTotals, dynamicRuta05.accounts),
|
||||
declarationRutor,
|
||||
dynamicVatAccounts,
|
||||
accountTotals,
|
||||
}
|
||||
}
|
||||
@@ -1676,6 +1659,8 @@ async function runVatCompletenessChecks(
|
||||
year: number,
|
||||
period: number,
|
||||
accountTotals?: VatCheckAccountTotals,
|
||||
dynamicVatAccounts?: Awaited<ReturnType<typeof fetchDynamicVatAccounts>>,
|
||||
rcBasisByRate?: { r25: number; r12: number; r6: number },
|
||||
): Promise<VatDeclarationCheck[]> {
|
||||
let scan: RcBasisGapScan
|
||||
try {
|
||||
@@ -1688,7 +1673,10 @@ async function runVatCompletenessChecks(
|
||||
// caller supplied the account totals; without them the per-voucher gaps
|
||||
// keep their blocking ERROR tier rather than guessing.
|
||||
const evidence = accountTotals
|
||||
? { rutor, rcBasisByRate: rcBasisTotalsByRate(accountTotals) }
|
||||
? {
|
||||
rutor,
|
||||
rcBasisByRate: rcBasisByRate ?? rcBasisTotalsByRate(accountTotals, dynamicVatAccounts),
|
||||
}
|
||||
: undefined
|
||||
return withRcBasisGapFindings(runVatDeclarationChecks(rutor, accountTotals), scan, evidence)
|
||||
}
|
||||
@@ -2073,7 +2061,7 @@ export async function computeVatCloseCheck(
|
||||
// step 4b: they need rutor 20-24 and 50, which the report view omits, plus
|
||||
// the per-account totals so the RC input comparison reads 2645/2647
|
||||
// instead of the ruta 48 aggregate.
|
||||
const { report: vatReport, declarationRutor, accountTotals } =
|
||||
const { report: vatReport, declarationRutor, dynamicVatAccounts, accountTotals } =
|
||||
await computeVatReportWithRutor(args, companyId, supabase)
|
||||
const { start, end, type: periodType, year, period } = vatReport.period
|
||||
|
||||
@@ -2184,6 +2172,7 @@ export async function computeVatCloseCheck(
|
||||
Number(year),
|
||||
Number(period),
|
||||
accountTotals,
|
||||
dynamicVatAccounts,
|
||||
)
|
||||
|
||||
// Zero deductible input VAT against self-assessed utgående moms is
|
||||
@@ -6409,6 +6398,11 @@ export const tools: McpTool[] = [
|
||||
description: { type: 'string' },
|
||||
default_vat_code: { type: 'string' },
|
||||
default_vat_rate: { type: 'number', enum: [0, 0.06, 0.12, 0.25], description: 'Fraction (0.25 = 25%). Livsmedel: 0.06 from 2026-04-01 (temporary cut from 0.12, reverts 2027-12-31).' },
|
||||
default_vat_treatment: {
|
||||
type: 'string',
|
||||
enum: [...ACCOUNT_VAT_TREATMENTS],
|
||||
description: 'VAT return treatment.',
|
||||
},
|
||||
sru_code: { type: 'string', description: 'Prefilled for BAS numbers.' },
|
||||
dry_run: { type: 'boolean', description: 'Validate and preview without staging.' },
|
||||
idempotency_key: { type: 'string', description: 'Per-operation UUID for safe retries (24h TTL).' },
|
||||
@@ -6470,6 +6464,17 @@ export const tools: McpTool[] = [
|
||||
if (vatRate !== undefined && ![0, 0.06, 0.12, 0.25].includes(vatRate)) {
|
||||
throw new Error('default_vat_rate must be one of 0, 0.06, 0.12, 0.25 (fraction, not percent)')
|
||||
}
|
||||
const vatTreatment = args.default_vat_treatment
|
||||
if (vatTreatment !== undefined && vatTreatment !== null && !isAccountVatTreatment(vatTreatment)) {
|
||||
throw new Error('default_vat_treatment is not supported')
|
||||
}
|
||||
const accountClass = Number(accountNumber[0])
|
||||
if (vatTreatment && !isVatTreatmentAllowedForAccountClass(vatTreatment, accountClass)) {
|
||||
throw new Error('default_vat_treatment is not valid for this account class')
|
||||
}
|
||||
const effectiveVatRate = vatTreatment && vatRate === undefined
|
||||
? defaultRateForVatTreatment(vatTreatment, accountClass)
|
||||
: vatRate
|
||||
|
||||
const params: Record<string, unknown> = {
|
||||
account_number: accountNumber,
|
||||
@@ -6479,7 +6484,8 @@ export const tools: McpTool[] = [
|
||||
plan_type: ref ? 'full_bas' : 'k1',
|
||||
description: String(args.description ?? '').trim() || ref?.description || undefined,
|
||||
default_vat_code: String(args.default_vat_code ?? '').trim() || undefined,
|
||||
default_vat_rate: vatRate,
|
||||
default_vat_rate: effectiveVatRate,
|
||||
default_vat_treatment: vatTreatment,
|
||||
sru_code: String(args.sru_code ?? '').trim() || ref?.sru_code || undefined,
|
||||
}
|
||||
|
||||
@@ -6514,6 +6520,11 @@ export const tools: McpTool[] = [
|
||||
description: { type: 'string' },
|
||||
default_vat_code: { type: 'string' },
|
||||
default_vat_rate: { type: 'number', enum: [0, 0.06, 0.12, 0.25], description: 'Default VAT rate as a fraction (0.25 = 25%). Livsmedel: 0.06 from 2026-04-01 (temporary cut from 0.12, reverts 2027-12-31).' },
|
||||
default_vat_treatment: {
|
||||
type: ['string', 'null'],
|
||||
enum: [...ACCOUNT_VAT_TREATMENTS, null],
|
||||
description: 'VAT return treatment.',
|
||||
},
|
||||
sru_code: { type: 'string' },
|
||||
is_active: { type: 'boolean', description: 'false deactivates (hides from pickers, keeps history); true (re)activates.' },
|
||||
dry_run: { type: 'boolean' },
|
||||
@@ -6535,7 +6546,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
const { data: current, error: fetchErr } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('account_number, account_name, description, default_vat_code, default_vat_rate, sru_code, is_active')
|
||||
.select('account_number, account_name, description, default_vat_code, default_vat_rate, default_vat_treatment, sru_code, is_active')
|
||||
.eq('company_id', companyId)
|
||||
.eq('account_number', accountNumber)
|
||||
.maybeSingle()
|
||||
@@ -6548,17 +6559,30 @@ export const tools: McpTool[] = [
|
||||
if (vatRate !== undefined && ![0, 0.06, 0.12, 0.25].includes(vatRate)) {
|
||||
throw new Error('default_vat_rate must be one of 0, 0.06, 0.12, 0.25 (fraction, not percent)')
|
||||
}
|
||||
const vatTreatment = args.default_vat_treatment
|
||||
if (vatTreatment !== undefined && vatTreatment !== null && !isAccountVatTreatment(vatTreatment)) {
|
||||
throw new Error('default_vat_treatment is not supported')
|
||||
}
|
||||
const accountClass = Number(accountNumber[0])
|
||||
if (vatTreatment && !isVatTreatmentAllowedForAccountClass(vatTreatment, accountClass)) {
|
||||
throw new Error('default_vat_treatment is not valid for this account class')
|
||||
}
|
||||
|
||||
const params: Record<string, unknown> = { account_number: accountNumber }
|
||||
const changes: Record<string, unknown> = {}
|
||||
for (const key of ['account_name', 'description', 'default_vat_code', 'default_vat_rate', 'sru_code', 'is_active']) {
|
||||
for (const key of ['account_name', 'description', 'default_vat_code', 'default_vat_rate', 'default_vat_treatment', 'sru_code', 'is_active']) {
|
||||
if (args[key] !== undefined) {
|
||||
params[key] = args[key]
|
||||
changes[key] = args[key]
|
||||
}
|
||||
}
|
||||
if (vatTreatment && vatRate === undefined && current.default_vat_rate == null) {
|
||||
const derivedRate = defaultRateForVatTreatment(vatTreatment, accountClass)
|
||||
params.default_vat_rate = derivedRate
|
||||
changes.default_vat_rate = derivedRate
|
||||
}
|
||||
if (Object.keys(changes).length === 0) {
|
||||
throw new Error('Nothing to update: pass at least one of account_name, description, default_vat_code, default_vat_rate, sru_code, is_active.')
|
||||
throw new Error('Nothing to update: pass at least one account field.')
|
||||
}
|
||||
|
||||
return stagePendingOperation(supabase, companyId, userId, 'update_account',
|
||||
@@ -11221,6 +11245,8 @@ export const tools: McpTool[] = [
|
||||
const completenessChecks = await runVatCompletenessChecks(
|
||||
supabase, companyId, declaration.rutor, periodType, year, period,
|
||||
rcInputTotalsFromDeclaration(declaration),
|
||||
undefined,
|
||||
declaration.rcBasisByRate,
|
||||
)
|
||||
const completenessOk = !isFilingBlocked(completenessChecks)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user