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
@@ -121,6 +121,22 @@ describe('syncMappedAccounts: create pass', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('derives the booking rate for a confirmed treatment when the rate is unset', async () => {
|
||||
const { supabase, inserts } = buildCapturingSupabase()
|
||||
const result = await run(supabase, [
|
||||
mapping({
|
||||
sourceAccount: '4056',
|
||||
targetAccount: '4056',
|
||||
sourceName: 'Inköp varor EU',
|
||||
defaultVatTreatment: 'reverse_charge_eu_goods',
|
||||
defaultVatRate: null,
|
||||
vatTreatmentReviewed: true,
|
||||
}),
|
||||
])
|
||||
expect(result.error).toBeNull()
|
||||
expect(inserts[0].default_vat_rate).toBe(0.25)
|
||||
})
|
||||
|
||||
it('creates a missing BAS account with the BAS default name when the file has no custom name', async () => {
|
||||
const { supabase, inserts } = buildCapturingSupabase()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
applyVatTreatmentReview,
|
||||
enrichChangedAccountMappingWithVat,
|
||||
enrichAccountMappingsWithVat,
|
||||
} from '../account-vat-treatment'
|
||||
import type { AccountMapping } from '../types'
|
||||
@@ -32,28 +33,6 @@ describe('enrichAccountMappingsWithVat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('requires review for suggested class 5 and 6 purchase treatments', () => {
|
||||
const results = enrichAccountMappingsWithVat(
|
||||
[
|
||||
mapping('5010', 'Inköp tjänst EU'),
|
||||
mapping('6010', 'Inköp tjänst utanför EU'),
|
||||
],
|
||||
[],
|
||||
)
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
defaultVatTreatment: 'reverse_charge_eu_services',
|
||||
requiresVatTreatmentReview: true,
|
||||
vatTreatmentReviewed: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
defaultVatTreatment: 'export_services',
|
||||
requiresVatTreatmentReview: true,
|
||||
vatTreatmentReviewed: false,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps an existing account treatment without asking again', () => {
|
||||
const [result] = enrichAccountMappingsWithVat(
|
||||
[mapping('3041', 'Försäljning tjänst 25% sv')],
|
||||
@@ -70,25 +49,99 @@ describe('enrichAccountMappingsWithVat', () => {
|
||||
requiresVatTreatmentReview: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves an existing booking rate when suggesting a missing treatment', () => {
|
||||
const [result] = enrichAccountMappingsWithVat(
|
||||
[mapping('3041', 'Försäljning tjänst 25% sv')],
|
||||
[{
|
||||
account_number: '3041',
|
||||
default_vat_treatment: null,
|
||||
default_vat_rate: 0.12,
|
||||
} as never],
|
||||
)
|
||||
expect(result.defaultVatTreatment).toBe('standard_25')
|
||||
expect(result.defaultVatRate).toBe(0.12)
|
||||
})
|
||||
|
||||
it('requires review for a suggested class 6 service treatment', () => {
|
||||
const [result] = enrichAccountMappingsWithVat(
|
||||
[mapping('6545', 'Inköp tjänster utanför EU 25%')],
|
||||
[],
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
defaultVatTreatment: 'reverse_charge_non_eu_services',
|
||||
requiresVatTreatmentReview: true,
|
||||
vatTreatmentReviewed: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyVatTreatmentReview', () => {
|
||||
it('marks only the selected row reviewed, including class 5 and 6 accounts', () => {
|
||||
const mappings = [
|
||||
mapping('5010', 'Inköp tjänst EU'),
|
||||
mapping('3041', 'Försäljning tjänst 25% sv'),
|
||||
]
|
||||
const result = applyVatTreatmentReview(
|
||||
it('persists a suggestion only after an explicit row confirmation', () => {
|
||||
const mappings = enrichAccountMappingsWithVat([
|
||||
mapping('4056', 'Inköp varor 25% EU'),
|
||||
], [])
|
||||
|
||||
expect(mappings[0].vatTreatmentReviewed).toBe(false)
|
||||
const reviewed = applyVatTreatmentReview(
|
||||
mappings,
|
||||
'5010',
|
||||
'reverse_charge_eu_services',
|
||||
0.25,
|
||||
'4056',
|
||||
mappings[0].defaultVatTreatment ?? null,
|
||||
mappings[0].defaultVatRate ?? null,
|
||||
)
|
||||
expect(result[0]).toMatchObject({
|
||||
defaultVatTreatment: 'reverse_charge_eu_services',
|
||||
defaultVatRate: 0.25,
|
||||
expect(reviewed[0]).toMatchObject({
|
||||
defaultVatTreatment: 'reverse_charge_eu_goods',
|
||||
vatTreatmentReviewed: true,
|
||||
vatTreatmentSuggested: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('clears hidden review state on remap and restores it on identity mapping', () => {
|
||||
const [suggested] = enrichAccountMappingsWithVat([
|
||||
mapping('4056', 'Inköp varor 25% EU'),
|
||||
], [])
|
||||
|
||||
const [remapped] = enrichAccountMappingsWithVat([{
|
||||
...suggested,
|
||||
targetAccount: '4010',
|
||||
targetName: 'Inköp material',
|
||||
}], [])
|
||||
expect(remapped).toMatchObject({
|
||||
defaultVatTreatment: null,
|
||||
requiresVatTreatmentReview: false,
|
||||
vatTreatmentReviewed: true,
|
||||
})
|
||||
|
||||
const [identity] = enrichAccountMappingsWithVat([{
|
||||
...remapped,
|
||||
targetAccount: '4056',
|
||||
targetName: 'Inköp varor 25% EU',
|
||||
}], [])
|
||||
expect(identity).toMatchObject({
|
||||
defaultVatTreatment: 'reverse_charge_eu_goods',
|
||||
requiresVatTreatmentReview: true,
|
||||
vatTreatmentReviewed: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves another row review when one mapping changes', () => {
|
||||
const initial = enrichAccountMappingsWithVat([
|
||||
mapping('3041', 'Försäljning tjänst 25% sv'),
|
||||
mapping('4056', 'Inköp varor 25% EU'),
|
||||
], [])
|
||||
const reviewed = applyVatTreatmentReview(initial, '3041', 'exempt', 0)
|
||||
const remapped = enrichChangedAccountMappingWithVat(
|
||||
reviewed.map((item) => item.sourceAccount === '4056'
|
||||
? { ...item, targetAccount: '4010', targetName: 'Inköp material' }
|
||||
: item),
|
||||
'4056',
|
||||
[],
|
||||
)
|
||||
|
||||
expect(remapped[0]).toMatchObject({
|
||||
defaultVatTreatment: 'exempt',
|
||||
defaultVatRate: 0,
|
||||
vatTreatmentReviewed: true,
|
||||
})
|
||||
expect(result[1].vatTreatmentReviewed).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,12 @@ import { classifyAccount } from '@/lib/bookkeeping/account-classifier'
|
||||
import { computeSRUCode } from '@/lib/bookkeeping/bas-data/sru-mapping'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import type { AccountMapping } from './types'
|
||||
import { isAccountVatTreatment, type AccountVatTreatment } from '@/lib/vat/account-vat-treatment'
|
||||
import {
|
||||
defaultRateForVatTreatment,
|
||||
isAccountVatTreatment,
|
||||
isVatTreatmentAllowedForAccountClass,
|
||||
type AccountVatTreatment,
|
||||
} from '@/lib/vat/account-vat-treatment'
|
||||
|
||||
export interface AccountSyncResult {
|
||||
/** Accounts inserted into chart_of_accounts */
|
||||
@@ -164,9 +169,19 @@ export async function syncMappedAccounts(
|
||||
result.error = `Invalid VAT treatment for account ${mapping.sourceAccount}`
|
||||
return result
|
||||
}
|
||||
const accountClass = Number(mapping.targetAccount.charAt(0))
|
||||
if (
|
||||
mapping.defaultVatTreatment &&
|
||||
!isVatTreatmentAllowedForAccountClass(mapping.defaultVatTreatment, accountClass)
|
||||
) {
|
||||
result.error = `VAT treatment is not valid for account ${mapping.sourceAccount}`
|
||||
return result
|
||||
}
|
||||
vatDefaults.set(mapping.targetAccount, {
|
||||
treatment: mapping.defaultVatTreatment ?? null,
|
||||
rate: mapping.defaultVatRate ?? null,
|
||||
rate: mapping.defaultVatTreatment && mapping.defaultVatRate == null
|
||||
? defaultRateForVatTreatment(mapping.defaultVatTreatment, accountClass)
|
||||
: mapping.defaultVatRate ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,16 @@ export function enrichAccountMappingsWithVat(
|
||||
)
|
||||
|
||||
return mappings.map((mapping) => {
|
||||
if (!mapping.targetAccount || mapping.sourceAccount !== mapping.targetAccount) return mapping
|
||||
if (!mapping.targetAccount || mapping.sourceAccount !== mapping.targetAccount) {
|
||||
return {
|
||||
...mapping,
|
||||
defaultVatTreatment: null,
|
||||
defaultVatRate: null,
|
||||
vatTreatmentSuggested: false,
|
||||
vatTreatmentReviewed: true,
|
||||
requiresVatTreatmentReview: false,
|
||||
}
|
||||
}
|
||||
const accountClass = Number(mapping.sourceAccount.charAt(0))
|
||||
if (accountClass < 3 || accountClass > 6) return mapping
|
||||
|
||||
@@ -39,10 +48,10 @@ export function enrichAccountMappingsWithVat(
|
||||
return {
|
||||
...mapping,
|
||||
defaultVatTreatment: suggestion?.treatment ?? null,
|
||||
defaultVatRate: suggestion?.rate ?? existing?.default_vat_rate ?? null,
|
||||
defaultVatRate: existing?.default_vat_rate ?? suggestion?.rate ?? null,
|
||||
vatTreatmentSuggested: Boolean(suggestion),
|
||||
vatTreatmentReviewed: false,
|
||||
requiresVatTreatmentReview: accountClass >= 3 && accountClass <= 6,
|
||||
requiresVatTreatmentReview: accountClass === 3 || accountClass === 4 || Boolean(suggestion),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -65,3 +74,15 @@ export function applyVatTreatmentReview(
|
||||
: mapping
|
||||
)
|
||||
}
|
||||
|
||||
export function enrichChangedAccountMappingWithVat(
|
||||
mappings: AccountMapping[],
|
||||
sourceAccount: string,
|
||||
existingAccounts: BASAccount[],
|
||||
): AccountMapping[] {
|
||||
return mappings.map((mapping) =>
|
||||
mapping.sourceAccount === sourceAccount
|
||||
? enrichAccountMappingsWithVat([mapping], existingAccounts)[0]
|
||||
: mapping
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user