@accounter/server
Version:
Accounter GraphQL server
140 lines • 6.25 kB
JavaScript
import { z } from 'zod';
import { ALLOWED_PROFIT_LOSS_CODES, NEGATIVE_ALLOWED_PROFIT_LOSS_CODES, PROFIT_LOSS_CODE_NAMES, SECTION_1_1, SECTION_1_4, SECTION_1_9, SECTION_1_10, SECTION_1_11, SECTION_1_12, SECTION_1_14, SECTION_1_15, SECTION_1_16, } from '../types/index.js';
/**
* Schema for validating profit and loss records in SHAAM6111 reports
* Based on the Israeli Tax Authority specification
*/
// Create a set for faster lookup of allowed codes
const allowedCodesSet = new Set(ALLOWED_PROFIT_LOSS_CODES);
const negativeAllowedCodesSet = new Set(NEGATIVE_ALLOWED_PROFIT_LOSS_CODES);
function codeToName(code) {
return `${PROFIT_LOSS_CODE_NAMES[code] ?? 'Unknown code'} (${code})`;
}
// Schema for a single profit and loss record
const profitLossRecordSchema = z
.object({
// Code field - must be one of the allowed codes
code: z
.number()
.int()
.refine(code => allowedCodesSet.has(code), {
message: 'Invalid profit and loss code',
}),
// Amount field - must be a valid integer
amount: z.number().int(),
})
.superRefine((record, ctx) => {
// Validate that non-negative codes don't have negative amounts
if (!negativeAllowedCodesSet.has(record.code) && record.amount < 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Code ${codeToName(record.code)} does not allow negative amounts`,
path: ['amount'],
});
}
});
function cumulativeCodeValidationFormula(code, sectionCodes, subtractSectionCodes) {
return (map) => {
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, sectionCodes, subtractSectionCodes) {
return {
formula: cumulativeCodeValidationFormula(code, sectionCodes, subtractSectionCodes),
description: 'Sum of all subsections',
};
}
// Define arrays for validation formulas
const INCOME_COST_CODES = [
1306, 1308, 1307, 1310, 1320, 1330, 1340, 1350, 1360, 1371, 1372, 1390, 1400,
];
// Define the validation logic for each summary code
const summaryValidations = {
1000: commonCumulativeCodeValidation(1000, SECTION_1_1),
1300: {
formula: cumulativeCodeValidationFormula(1300, INCOME_COST_CODES, [1450]),
description: `${INCOME_COST_CODES.join('+')}-1450`,
},
2000: {
formula: cumulativeCodeValidationFormula(2000, [
2005, 2006, 2011, 2012, 2015, 2020, 2025, 2030, 2035, 2040, 2045, 2050, 2060, 2066, 2067,
2070, 2075, 2080, 2085, 2090,
], [2068, 2095]),
description: '2005+2006+2011+2012+2015+2020+2025+2030+2035+2040+2045+2050+2060+2066+2067-2068+2070+2075+2080+2085+2090-2095',
},
2500: commonCumulativeCodeValidation(2500, SECTION_1_4),
3000: {
formula: cumulativeCodeValidationFormula(3000, [
3011, 3013, 3012, 3015, 3020, 3025, 3030, 3040, 3045, 3050, 3060, 3066, 3067, 3070, 3075,
3080, 3085, 3090, 3100, 3120, 3190,
], [3068]),
description: '3011+3013+3012+3015+3020+3025+3030+3040+3045+3050+3060+3066+3067-3068+3070+3075+3080+3085+3090+3100+3120+3190',
},
3500: {
formula: cumulativeCodeValidationFormula(3500, [
3511, 3513, 3512, 3515, 3520, 3530, 3535, 3540, 3545, 3550, 3560, 3566, 3567, 3570, 3575,
3580, 3590, 3595, 3600, 3610, 3620, 3625, 3631, 3632, 3640, 3650, 3660, 3665, 3680, 3685,
3690,
], [3568]),
description: '3511+3513+3512+3515+3520+3530+3535+3540+3545+3550+3560+3566+3567-3568+3570+3575+3580+3590+3595+3600+3610+3620+3625+3631+3632+3640+3650+3660+3665+3680+3685+3690',
},
5000: commonCumulativeCodeValidation(5000, SECTION_1_9),
5100: commonCumulativeCodeValidation(5100, SECTION_1_10),
5200: commonCumulativeCodeValidation(5200, SECTION_1_11),
5300: commonCumulativeCodeValidation(5300, SECTION_1_12),
5600: commonCumulativeCodeValidation(5600, SECTION_1_14),
5700: commonCumulativeCodeValidation(5700, SECTION_1_15),
5800: commonCumulativeCodeValidation(5800, SECTION_1_16),
6666: {
formula: cumulativeCodeValidationFormula(6666, [1000, 5000, 5100, 5200], [1300, 2000, 2500, 3000, 3500, 5300]),
description: '1000-1300-2000-2500-3000-3500+5000+5100+5200-5300',
},
};
// Schema for an array of profit and loss records
export const profitLossArraySchema = z
.array(profitLossRecordSchema)
.max(150)
.superRefine((records, ctx) => {
// Check for unique code values & create a map for faster lookups
const codeToAmountMap = new Map();
records.map((record, i) => {
const code = record.code;
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);
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'],
});
}
}
}
});
//# sourceMappingURL=profit-and-loss.js.map