@accounter/shaam6111-generator
Version:
Fully typed application that generates, parses, and validates SHAAM 6111 tax reports.
225 lines (211 loc) • 7.87 kB
text/typescript
import { z } from 'zod';
import {
ALLOWED_BALANCE_SHEET_CODES,
AllowedBalanceSheetCode,
BALANCE_SHEET_CODE_NAMES,
BalanceSheetSummaryCode,
CommonBalanceSheetSummaryCode,
NEGATIVE_ALLOWED_BALANCE_SHEET_CODES,
NegativeAllowedBalanceSheetCode,
SECTION_1_21,
SECTION_1_27,
SECTION_1_28,
SECTION_1_30,
SECTION_1_32,
SECTION_1_33,
SECTION_1_36,
SECTION_1_37,
SECTION_1_38,
SECTION_1_39,
SECTION_1_40,
SECTION_1_41,
SECTION_1_43,
SECTION_1_44,
SECTION_1_45,
SECTION_1_46,
SECTION_1_48,
SECTION_1_50,
SECTION_1_51,
} from '../types/index.js';
// Create a set for faster lookup of allowed codes
const allowedCodesSet = new Set<AllowedBalanceSheetCode>(ALLOWED_BALANCE_SHEET_CODES);
const negativeAllowedCodesSet = new Set<NegativeAllowedBalanceSheetCode>(
NEGATIVE_ALLOWED_BALANCE_SHEET_CODES,
);
function codeToName(code: AllowedBalanceSheetCode): string {
return `${BALANCE_SHEET_CODE_NAMES[code] ?? 'Unknown code'} (${code})`;
}
// Zod schema for a single balance sheet record
const balanceSheetRecordSchema = z
.object({
code: z
.number()
.int()
.superRefine((value, ctx) => {
if (!allowedCodesSet.has(value as AllowedBalanceSheetCode)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid balance sheet code: ${value}`,
path: ['code'],
});
}
}),
amount: z.number().int(),
})
.superRefine((record, ctx) => {
// Validate that non-negative codes don't have negative amounts
if (
!negativeAllowedCodesSet.has(record.code as NegativeAllowedBalanceSheetCode) &&
record.amount < 0
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Code ${codeToName(record.code as AllowedBalanceSheetCode)} does not allow negative amounts`,
path: ['amount'],
});
}
});
function cumulativeCodeValidationFormula(
code: BalanceSheetSummaryCode,
sectionCodes: readonly AllowedBalanceSheetCode[],
subtractSectionCodes?: readonly AllowedBalanceSheetCode[],
) {
return (map: Map<AllowedBalanceSheetCode, number>) => {
const sectionAmount = sectionCodes
.filter(sectionCode => sectionCode !== code)
.map(sectionCode => map.get(sectionCode) || 0)
.reduce((a, b) => a + b, 0);
const subtractAmount = subtractSectionCodes
? subtractSectionCodes
.filter(sectionCode => sectionCode !== code)
.map(sectionCode => map.get(sectionCode) || 0)
.reduce((a, b) => a + b, 0)
: 0;
return sectionAmount - subtractAmount;
};
}
function commonCumulativeCodeValidation(
code: CommonBalanceSheetSummaryCode,
sectionCodes: readonly AllowedBalanceSheetCode[],
subtractSectionCodes?: readonly AllowedBalanceSheetCode[],
) {
return {
formula: cumulativeCodeValidationFormula(code, sectionCodes, subtractSectionCodes),
description: 'Sum of all subsections',
};
}
// Define the validation logic for each summary code
const summaryValidations: Record<
BalanceSheetSummaryCode,
{ formula: (map: Map<AllowedBalanceSheetCode, number>) => number; description: string }
> = {
7000: commonCumulativeCodeValidation(7000, [7100, 7200, 7300, 7400, 7600, 7700, 7800]),
7100: commonCumulativeCodeValidation(7100, SECTION_1_27),
7200: commonCumulativeCodeValidation(7200, SECTION_1_28),
7300: {
formula: cumulativeCodeValidationFormula(7300, [7310, 7320, 7330, 7350, 7360, 7390], [7380]),
description: '7310+7320+7330+7350+7360-7380+7390',
},
7400: commonCumulativeCodeValidation(7400, SECTION_1_30),
7600: commonCumulativeCodeValidation(7600, SECTION_1_21),
7700: commonCumulativeCodeValidation(7700, SECTION_1_32),
7800: commonCumulativeCodeValidation(7800, SECTION_1_33),
8000: {
formula: cumulativeCodeValidationFormula(
8000,
[8010, 8020, 8025, 8030, 8040, 8050, 8060, 8080, 8090, 8095, 8100, 8105, 8170],
[8110, 8120, 8130, 8140, 8150, 8160, 8180, 8190],
),
description:
'8010+8020+8025+8030+8040+8050+8060+8080+8090+8095+8100+8105-8110-8120-8130-8140-8150-8160+8170-8180-8190',
},
8200: commonCumulativeCodeValidation(8200, SECTION_1_37),
8300: commonCumulativeCodeValidation(8300, SECTION_1_38),
8400: commonCumulativeCodeValidation(8400, SECTION_1_39),
8500: commonCumulativeCodeValidation(8500, SECTION_1_40),
8600: commonCumulativeCodeValidation(8600, SECTION_1_41),
8700: commonCumulativeCodeValidation(8700, SECTION_1_36),
8888: {
formula: cumulativeCodeValidationFormula(
8888,
[7000, 8000, 8700, 8200, 8300, 8400, 8500, 8600],
),
description: '7000+8000+8700+8200+8300+8400+8500+8600',
},
9000: {
formula: cumulativeCodeValidationFormula(9000, [9100, 9200, 9400, 9500]),
description: '9100+9200+9400+9500',
},
9100: commonCumulativeCodeValidation(9100, SECTION_1_43),
9200: commonCumulativeCodeValidation(9200, SECTION_1_44),
9400: commonCumulativeCodeValidation(9400, SECTION_1_45),
9500: commonCumulativeCodeValidation(9500, SECTION_1_46),
9600: commonCumulativeCodeValidation(9600, SECTION_1_48),
9700: {
formula: cumulativeCodeValidationFormula(9700, [9710, 9790], [9720]),
description: '9710-9720+9790',
},
9800: commonCumulativeCodeValidation(9800, SECTION_1_50),
9900: commonCumulativeCodeValidation(9900, SECTION_1_51),
9999: {
formula: cumulativeCodeValidationFormula(9999, [9000, 9600, 9700, 9800, 9900]),
description: '9000+9600+9700+9800+9900',
},
};
// Zod schema for the array of balance sheet records
export const balanceSheetArraySchema = z
.array(balanceSheetRecordSchema)
.max(150)
.superRefine((records, ctx) => {
// Check for unique code values & create a map for faster lookups
const codeToAmountMap = new Map<AllowedBalanceSheetCode, number>();
records.map((record, i) => {
const code = record.code as AllowedBalanceSheetCode;
if (codeToAmountMap.has(code)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate code: ${codeToName(code)}`,
path: [i, 'code'],
});
} else {
codeToAmountMap.set(code, record.amount);
}
});
// Validate each summary field with its formula
for (const [codeStr, validation] of Object.entries(summaryValidations)) {
const code = parseInt(codeStr) as AllowedBalanceSheetCode;
if (codeToAmountMap.has(code)) {
const actualAmount = codeToAmountMap.get(code);
const expectedAmount = validation.formula(codeToAmountMap);
if (actualAmount !== expectedAmount) {
const index = records.findIndex(r => r.code === code);
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Value for code ${codeToName(code)} should equal the sum: ${validation.description}`,
path: [index, 'amount'],
});
}
}
}
// Check total assets equals total liabilities + equity
const totalAssets = codeToAmountMap.get(8888);
const totalLiabilitiesEquity = codeToAmountMap.get(9999);
if (totalAssets !== totalLiabilitiesEquity) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Total assets (8888) must equal total liabilities + equity (9999). Got: ${totalAssets} vs ${totalLiabilitiesEquity}`,
});
}
// Check if all required summary codes are present
const requiredSummaryCodes = [7000, 8888, 9000, 9999];
for (const code of requiredSummaryCodes) {
if (!codeToAmountMap.has(code as AllowedBalanceSheetCode)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Missing required summary code: ${code}`,
});
}
}
});
// Type for the balance sheet records
export type BalanceSheetRecord = z.infer<typeof balanceSheetRecordSchema>;