UNPKG

@accounter/shaam-uniform-format-generator

Version:

Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).

264 lines (263 loc) • 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.B110Schema = void 0; exports.encodeB110 = encodeB110; exports.parseB110 = parseB110; const zod_1 = require("zod"); const index_js_1 = require("../../format/index.js"); const index_js_2 = require("../../types/index.js"); const encoder_js_1 = require("../format/encoder.js"); const monetary_js_1 = require("../format/monetary.js"); /** * B110 Record Schema - Account record * Fields 1400-1424 based on SHAAM 1.31 specification table */ exports.B110Schema = zod_1.z.object({ // Field 1400: Record code (4) - Required - Alphanumeric code: zod_1.z.literal('B110').describe('Record type code - always "B110"'), // Field 1401: Record number in file (9) - Required - Numeric recordNumber: zod_1.z .number() .int() .min(1) .max(999_999_999) .describe('Sequential record number in file'), // Field 1402: Tax ID (9) - Required - Numeric vatId: zod_1.z.string().min(1).max(9).regex(/^\d+$/).describe('Tax identification number'), // Field 1403: Account key (15) - Required - Alphanumeric accountKey: zod_1.z.string().min(1).max(15).describe('Account key - must be unique'), // Field 1404: Account name (50) - Required - Alphanumeric accountName: zod_1.z.string().max(50).optional().describe('Account name'), // Field 1405: Trial balance code (15) - Required - Alphanumeric trialBalanceCode: zod_1.z.string().min(1).max(15).describe('Trial balance code'), // Field 1406: Trial balance code description (30) - Optional - Alphanumeric trialBalanceCodeDescription: zod_1.z .string() .max(30) .optional() .describe('Trial balance code description'), // Field 1407: Customer/Supplier address - street (50) - Optional - Alphanumeric customerSupplierAddressStreet: zod_1.z .string() .max(50) .optional() .describe('Customer/Supplier address - street (only for customer/supplier accounts)'), // Field 1408: Customer/Supplier address - house number (10) - Optional - Alphanumeric customerSupplierAddressHouseNumber: zod_1.z .string() .max(10) .optional() .describe('Customer/Supplier address - house number (only for customer/supplier accounts)'), // Field 1409: Customer/Supplier address - city (30) - Optional - Alphanumeric customerSupplierAddressCity: zod_1.z .string() .max(30) .optional() .describe('Customer/Supplier address - city (only for customer/supplier accounts)'), // Field 1410: Customer/Supplier address - ZIP (8) - Optional - Alphanumeric customerSupplierAddressZip: zod_1.z .string() .max(8) .optional() .describe('Customer/Supplier address - ZIP (only for customer/supplier accounts)'), // Field 1411: Customer/Supplier address - country (30) - Optional - Alphanumeric customerSupplierAddressCountry: zod_1.z .string() .max(30) .optional() .describe('Customer/Supplier address - country (only for customer/supplier accounts)'), // Field 1412: Country code (2) - Optional - Alphanumeric countryCode: index_js_2.CountryCodeEnum.optional().describe('Country code (only for customer/supplier accounts)'), // Field 1413: Parent account key (15) - Optional - Alphanumeric parentAccountKey: zod_1.z.string().max(15).optional().describe('Parent account key'), // Field 1414: Account opening balance (15) - Optional - Numeric - Format: X9(12)V99 accountOpeningBalance: zod_1.z .number() .describe('Account opening balance (positive = debit, negative = credit)'), // Field 1415: Total debits (15) - Optional - Numeric - Format: X9(12)V99 totalDebits: zod_1.z.number().optional().describe('Total debits (excludes opening balance)'), // Field 1416: Total credits (15) - Optional - Numeric - Format: X9(12)V99 totalCredits: zod_1.z.number().optional().describe('Total credits (excludes opening balance)'), // Field 1417: Accounting classification code (4) - Conditional - Numeric accountingClassificationCode: zod_1.z .number() .int() .min(0) .max(9999) .optional() .describe('Accounting classification code (mandatory if 6111 report required)'), // Field 1418: Reserved field (0) - Deprecated - Alphanumeric - Skipped as it has 0 length // Field 1419: Supplier/Customer tax ID (9) - Conditional - Numeric supplierCustomerTaxId: zod_1.z .string() .max(9) .regex(/^$|^\d+$/) .optional() .describe('Supplier/Customer tax ID (required if double-entry bookkeeping, code 2 in field 1013)'), // Field 1420: Reserved field (0) - Deprecated - Alphanumeric - Skipped as it has 0 length // Field 1421: Branch ID (7) - Conditional - Alphanumeric branchId: zod_1.z .string() .max(7) .optional() .describe('Branch ID (required if field 1034 = 1, see Note 3)'), // Field 1422: Opening balance in foreign currency (15) - Optional - Numeric - Format: X9(12)V99 openingBalanceForeignCurrency: zod_1.z .number() .optional() .describe('Opening balance in foreign currency'), // Field 1423: Foreign currency code (3) - Optional - Alphanumeric foreignCurrencyCode: index_js_2.CurrencyCodeEnum.optional().describe('Foreign currency code (refers to field 1422)'), // Field 1424: Reserved field (16) - Optional - Alphanumeric reserved: zod_1.z.string().max(16).optional().describe('Reserved field for future use'), }); /** * Encodes a B110 record to fixed-width string format * Total line width: 376 characters + CRLF */ function encodeB110(input) { const fields = [ (0, encoder_js_1.formatField)(input.code, 4, 'left'), // Field 1400: Record code (4) (0, encoder_js_1.formatField)(input.recordNumber.toString().padStart(9, '0'), 9, 'left'), // Field 1401: Record number (9) (0, encoder_js_1.formatNumericField)(input.vatId, 9), // Field 1402: VAT ID (9) (0, encoder_js_1.formatField)(input.accountKey, 15, 'left'), // Field 1403: Account key (15) (0, encoder_js_1.formatField)(input.accountName ?? '', 50, 'left'), // Field 1404: Account name (50) (0, encoder_js_1.formatField)(input.trialBalanceCode, 15, 'left'), // Field 1405: Trial balance code (15) (0, encoder_js_1.formatField)(input.trialBalanceCodeDescription ?? '', 30, 'left'), // Field 1406: Trial balance code description (30) (0, encoder_js_1.formatField)(input.customerSupplierAddressStreet ?? '', 50, 'left'), // Field 1407: Customer/Supplier address - street (50) (0, encoder_js_1.formatField)(input.customerSupplierAddressHouseNumber ?? '', 10, 'left'), // Field 1408: Customer/Supplier address - house number (10) (0, encoder_js_1.formatField)(input.customerSupplierAddressCity ?? '', 30, 'left'), // Field 1409: Customer/Supplier address - city (30) (0, encoder_js_1.formatField)(input.customerSupplierAddressZip ?? '', 8, 'left'), // Field 1410: Customer/Supplier address - ZIP (8) (0, encoder_js_1.formatField)(input.customerSupplierAddressCountry ?? '', 30, 'left'), // Field 1411: Customer/Supplier address - country (30) (0, encoder_js_1.formatField)(input.countryCode ?? '', 2, 'left'), // Field 1412: Country code (2) (0, encoder_js_1.formatField)(input.parentAccountKey ?? '', 15, 'left'), // Field 1413: Parent account key (15) (0, monetary_js_1.formatOptionalMonetaryAmount)(input.accountOpeningBalance) || (0, monetary_js_1.formatMonetaryAmount)(0), // Field 1414: Account opening balance (15) (0, monetary_js_1.formatOptionalMonetaryAmount)(input.totalDebits) || (0, monetary_js_1.formatMonetaryAmount)(0), // Field 1415: Total debits (15) (0, monetary_js_1.formatOptionalMonetaryAmount)(input.totalCredits) || (0, monetary_js_1.formatMonetaryAmount)(0), // Field 1416: Total credits (15) (0, encoder_js_1.formatField)(input.accountingClassificationCode?.toString().padStart(4, '0') ?? '0000', 4, 'left'), // Field 1417: Accounting classification code (4) // Field 1418: Reserved field (0) - skipped as it has 0 length // Use original value if available and looks like raw field data, otherwise format input.supplierCustomerTaxId ? input.supplierCustomerTaxId.length === 9 ? input.supplierCustomerTaxId // Preserve original raw field data if exactly 9 chars : (0, encoder_js_1.formatField)(input.supplierCustomerTaxId, 9, 'left') // Right-pad with spaces for processed values : (0, encoder_js_1.formatField)('', 9, 'left'), // Field 1419: Supplier/Customer tax ID (9) - use spaces if empty // Field 1420: Reserved field (0) - skipped as it has 0 length (0, encoder_js_1.formatField)(input.branchId ?? '0', 7, 'left'), // Field 1421: Branch ID (7) (0, monetary_js_1.formatOptionalMonetaryAmount)(input.openingBalanceForeignCurrency) || (0, monetary_js_1.formatMonetaryAmount)(0), // Field 1422: Opening balance in foreign currency (15) (0, encoder_js_1.formatField)(input.foreignCurrencyCode ?? '', 3, 'left'), // Field 1423: Foreign currency code (3) (0, encoder_js_1.formatField)(input.reserved ?? '', 16, 'left'), // Field 1424: Reserved field (16) ]; return fields.join('') + index_js_1.CRLF; } /** * Parses a fixed-width B110 record line back to object * Expected line length: 376 characters (excluding CRLF) */ function parseB110(line) { // Remove CRLF if present const cleanLine = line.replace(/\r?\n$/, ''); if (cleanLine.length !== 376) { throw new Error(`Invalid B110 record length: expected 376 characters, got ${cleanLine.length}`); } // Extract fields at their fixed positions let pos = 0; const code = cleanLine.slice(pos, pos + 4).trim(); pos += 4; const recordNumber = parseInt(cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || '0', 10); pos += 9; const vatId = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || '0'; pos += 9; const accountKey = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const accountName = cleanLine.slice(pos, pos + 50).trim(); pos += 50; const trialBalanceCode = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const trialBalanceCodeDescription = cleanLine.slice(pos, pos + 30).trim(); pos += 30; const customerSupplierAddressStreet = cleanLine.slice(pos, pos + 50).trim(); pos += 50; const customerSupplierAddressHouseNumber = cleanLine.slice(pos, pos + 10).trim(); pos += 10; const customerSupplierAddressCity = cleanLine.slice(pos, pos + 30).trim(); pos += 30; const customerSupplierAddressZip = cleanLine.slice(pos, pos + 8).trim(); pos += 8; const customerSupplierAddressCountry = cleanLine.slice(pos, pos + 30).trim(); pos += 30; const countryCodeRaw = cleanLine.slice(pos, pos + 2).trim(); const countryCode = (countryCodeRaw || undefined); pos += 2; const parentAccountKey = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const accountOpeningBalance = cleanLine.slice(pos, pos + 15); pos += 15; const totalDebits = cleanLine.slice(pos, pos + 15); pos += 15; const totalCredits = cleanLine.slice(pos, pos + 15); pos += 15; const accountingClassificationCodeStr = cleanLine .slice(pos, pos + 4) .trim() .replace(/^0+/, '') || ''; const accountingClassificationCode = accountingClassificationCodeStr === '' ? undefined : parseInt(accountingClassificationCodeStr, 10); pos += 4; // Field 1418: Reserved field (0) - skipped as it has 0 length const supplierCustomerTaxId = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || ''; pos += 9; // Field 1420: Reserved field (0) - skipped as it has 0 length const branchId = cleanLine.slice(pos, pos + 7).trim(); pos += 7; const openingBalanceForeignCurrency = cleanLine.slice(pos, pos + 15); pos += 15; const foreignCurrencyCodeRaw = cleanLine.slice(pos, pos + 3).trim(); const foreignCurrencyCode = (foreignCurrencyCodeRaw || undefined); pos += 3; const reserved = cleanLine.slice(pos, pos + 16).trim(); pos += 16; // Validate the code field if (code !== 'B110') { throw new Error(`Invalid B110 record code: expected "B110", got "${code}"`); } const parsed = { code, recordNumber, vatId, accountKey, accountName, trialBalanceCode, trialBalanceCodeDescription, customerSupplierAddressStreet, customerSupplierAddressHouseNumber, customerSupplierAddressCity, customerSupplierAddressZip, customerSupplierAddressCountry, countryCode, parentAccountKey, accountOpeningBalance: (0, monetary_js_1.parseMonetaryAmount)(accountOpeningBalance), totalDebits: totalDebits.trim() ? (0, monetary_js_1.parseMonetaryAmount)(totalDebits) : undefined, totalCredits: totalCredits.trim() ? (0, monetary_js_1.parseMonetaryAmount)(totalCredits) : undefined, accountingClassificationCode, supplierCustomerTaxId, branchId, openingBalanceForeignCurrency: openingBalanceForeignCurrency.trim() ? (0, monetary_js_1.parseMonetaryAmount)(openingBalanceForeignCurrency) : undefined, foreignCurrencyCode, reserved, }; // Validate against schema return exports.B110Schema.parse(parsed); }