feat(vat): add per-account declaration treatments (#1588)

Closes #1457
This commit is contained in:
Mattsson
2026-08-13 17:03:35 +02:00
committed by GitHub
parent 7881f757a9
commit d02fd82191
32 changed files with 1196 additions and 253 deletions
+25
View File
@@ -1996,6 +1996,23 @@ describe('CreateAccountSchema', () => {
})
expect(result.success).toBe(false)
})
it('rejects a VAT treatment that does not apply to the account class', () => {
expect(CreateAccountSchema.safeParse({
account_number: '5010',
account_name: 'Consulting costs',
account_type: 'expense',
normal_balance: 'debit',
default_vat_treatment: 'standard_25',
}).success).toBe(false)
expect(CreateAccountSchema.safeParse({
account_number: '5010',
account_name: 'EU services',
account_type: 'expense',
normal_balance: 'debit',
default_vat_treatment: 'reverse_charge_eu_services',
}).success).toBe(true)
})
})
// ============================================================
@@ -2305,6 +2322,14 @@ describe('UpdateAccountSchema', () => {
default_vat_rate: 0.5,
}).success).toBe(false)
})
it('accepts supported account VAT treatments and rejects unknown values', () => {
expect(UpdateAccountSchema.safeParse({
default_vat_treatment: 'reverse_charge_eu_goods',
}).success).toBe(true)
expect(UpdateAccountSchema.safeParse({ default_vat_treatment: null }).success).toBe(true)
expect(UpdateAccountSchema.safeParse({ default_vat_treatment: 'eu_purchase' }).success).toBe(false)
})
})
// ============================================================
+26
View File
@@ -16,6 +16,7 @@ import { INVOICE_POSTING_ACCOUNT_REGEX } from '@/lib/invoices/posting-account'
import { PERSONAL_NUMBER_INPUT_RE } from '@/lib/customers/mask-personal-number'
import type { AuditAction } from '@/types'
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
import { isVatTreatmentValidForAccountClass } from '@/lib/vat/account-vat-treatment'
// ============================================================
// Shared primitives
@@ -2110,6 +2111,15 @@ const defaultVatRate = z
.nullable()
.optional()
export const AccountVatTreatmentSchema = z.enum([
'standard_25', 'reduced_12', 'reduced_6', 'exempt',
'reverse_charge_domestic', 'reverse_charge_eu_goods',
'reverse_charge_eu_services', 'export_goods', 'export_services',
'vmb', 'rental_voluntary',
])
const defaultVatTreatment = AccountVatTreatmentSchema.nullable().optional()
export const CreateAccountSchema = z.object({
account_number: accountNumber,
account_name: z.string().min(1, 'Account name is required'),
@@ -2119,7 +2129,22 @@ export const CreateAccountSchema = z.object({
description: z.string().nullable().optional(),
default_vat_code: z.string().nullable().optional(),
default_vat_rate: defaultVatRate,
default_vat_treatment: defaultVatTreatment,
sru_code: z.string().nullable().optional(),
}).superRefine((value, ctx) => {
if (
value.default_vat_treatment &&
!isVatTreatmentValidForAccountClass(
value.default_vat_treatment,
Number(value.account_number.charAt(0)),
)
) {
ctx.addIssue({
code: 'custom',
path: ['default_vat_treatment'],
message: 'VAT treatment is not valid for the account class',
})
}
})
export const UpdateAccountSchema = z.object({
@@ -2128,6 +2153,7 @@ export const UpdateAccountSchema = z.object({
description: z.string().nullable().optional(),
default_vat_code: z.string().nullable().optional(),
default_vat_rate: defaultVatRate,
default_vat_treatment: defaultVatTreatment,
sru_code: z.string().nullable().optional(),
})