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).

294 lines (283 loc) • 12.7 kB
import { z } from 'zod'; import { CRLF } from '../../format/index.js'; import { DocumentTypeEnum } from '../../types/index.js'; import { formatField, formatNumericField } from '../index.js'; /** * D120 - Payment/receipt record * Fields 1300-1324 based on SHAAM 1.31 specification */ export const D120Schema = z.object({ // Field 1300: Record code (4) - Required - Alphanumeric - Value: D120 code: z.literal('D120').describe('Record type code - always "D120"'), // Field 1301: Record number in file (9) - Required - Numeric recordNumber: z .string() .min(1) .max(9) .regex(/^\d+$/) .describe('Sequential record number in file'), // Field 1302: Tax ID (9) - Required - Numeric vatId: z.string().min(1).max(9).regex(/^\d+$/).describe('VAT identification number'), // Field 1303: Document type (3) - Required - Numeric documentType: DocumentTypeEnum.describe('Document type - see Appendix 1, Note 1'), // Field 1304: Document number (20) - Required - Alphanumeric documentNumber: z.string().min(1).max(20).describe('Document number'), // Field 1305: Line number in document (4) - Required - Numeric lineNumber: z.string().min(1).max(4).regex(/^\d+$/).describe('Line number in document'), // Field 1306: Payment method (1) - Required - Numeric paymentMethod: z .string() .min(1) .max(1) .regex(/^[1-9]$/) .describe( 'Payment method type: 1-Cash, 2-Check, 3-Credit card, 4-Bank transfer, 5-Voucher, 6-Exchange slip, 7-Promissory note, 8-Standing order, 9-Other', ), // Field 1307: Bank number (10) - Conditional - Numeric - Check only bankNumber: z.string().max(10).regex(/^\d*$/).default('').describe('Bank number - check only'), // Field 1308: Branch number (10) - Conditional - Numeric - Check only branchNumber: z .string() .max(10) .regex(/^\d*$/) .default('') .describe('Branch number - check only'), // Field 1309: Account number (15) - Conditional - Numeric - Check only accountNumber: z .string() .max(15) .regex(/^\d*$/) .default('') .describe('Account number - check only'), // Field 1310: Check number (10) - Conditional - Numeric - Check only checkNumber: z.string().max(10).regex(/^\d*$/).default('').describe('Check number - check only'), // Field 1311: Payment due date (8) - Optional - Numeric - Format YYYYMMDD paymentDueDate: z .string() .max(8) .regex(/^(\d{8}|)$/) .default('') .describe('Payment due date YYYYMMDD - check or credit card only'), // Field 1312: Line amount (15) - Required - Alphanumeric - Format X9(12)V99 lineAmount: z .string() .min(1) .max(15) .regex(/^(\d{1,13}(\.\d{2})?|)$/) .describe('Line amount'), // Field 1313: Acquirer code (1) - Optional - Numeric acquirerCode: z .string() .max(1) .regex(/^[1-6]?$/) .default('') .describe('Acquirer code: 1-Isracard, 2-Cal, 3-Diners, 4-Amex, 6-Leumi Card'), // Field 1314: Card brand (20) - Optional - Alphanumeric cardBrand: z.string().max(20).default('').describe('Card brand name'), // Field 1315: Credit transaction type (1) - Optional - Numeric creditTransactionType: z .string() .max(1) .regex(/^[1-5]?$/) .default('') .describe('Credit transaction type: 1-Regular, 2-Installments, 3-Credit, 4-Deferred, 5-Other'), // Field 1316: First payment amount (0) - Deprecated - Alphanumeric firstPaymentAmount: z.string().max(0).default('').describe('First payment amount (deprecated)'), // Field 1317: Installments count (0) - Deprecated - Alphanumeric installmentsCount: z.string().max(0).default('').describe('Installments count (deprecated)'), // Field 1318: Additional payment amount (0) - Deprecated - Alphanumeric additionalPaymentAmount: z .string() .max(0) .default('') .describe('Additional payment amount (deprecated)'), // Field 1319: Reserved field (0) - Deprecated - Alphanumeric reserved1: z.string().max(0).default('').describe('Reserved field 1 (deprecated)'), // Field 1320: Branch ID (7) - Required - Alphanumeric - Required if field 1034 = 1 branchId: z .string() .max(7) .default('') .describe('Branch/sector ID - required if field 1034 = 1, see Note 3'), // Field 1321: Reserved field (0) - Deprecated - Alphanumeric reserved2: z.string().max(0).default('').describe('Reserved field 2 (deprecated)'), // Field 1322: Document date (8) - Required - Numeric - Format YYYYMMDD documentDate: z .string() .min(1) .max(8) .regex(/^\d{8}$/) .describe('Document date YYYYMMDD - see Note 12'), // Field 1323: Header link field (7) - Optional - Numeric - For linking to C100 headerLinkField: z .string() .max(7) .regex(/^\d*$/) .default('') .describe('Header connecting field - for linking to C100, see Note 11'), // Field 1324: Reserved field (60) - Optional - Alphanumeric reserved: z.string().max(60).default('').describe('Reserved field for future use'), }); export type D120 = z.infer<typeof D120Schema>; /** * Encodes a D120 record to fixed-width string format * Total line width: 222 characters + CRLF (4+9+9+3+20+4+1+10+10+15+10+8+15+1+20+1+0+0+0+0+7+0+8+7+60) */ export function encodeD120(input: D120): string { const fields = [ formatField(input.code, 4, 'left'), // Field 1300: Record code (4) - Alphanumeric formatNumericField(input.recordNumber, 9), // Field 1301: Record number (9) - Numeric, zero-padded formatNumericField(input.vatId, 9), // Field 1302: VAT ID (9) - Numeric, zero-padded formatNumericField(input.documentType, 3), // Field 1303: Document type (3) - Numeric, zero-padded formatField(input.documentNumber, 20, 'left'), // Field 1304: Document number (20) - Alphanumeric formatNumericField(input.lineNumber, 4), // Field 1305: Line number (4) - Numeric, zero-padded formatNumericField(input.paymentMethod, 1), // Field 1306: Payment method (1) - Numeric, zero-padded formatField(input.bankNumber, 10, 'left'), // Field 1307: Bank number (10) - Conditional, space-padded when empty formatField(input.branchNumber, 10, 'left'), // Field 1308: Branch number (10) - Conditional, space-padded when empty formatField(input.accountNumber, 15, 'left'), // Field 1309: Account number (15) - Conditional, space-padded when empty formatField(input.checkNumber, 10, 'left'), // Field 1310: Check number (10) - Conditional, space-padded when empty formatField(input.paymentDueDate, 8, 'left'), // Field 1311: Payment due date (8) - Optional date field, space-padded when empty formatField(input.lineAmount, 15, 'left'), // Field 1312: Line amount (15) - Alphanumeric monetary format formatField(input.acquirerCode, 1, 'left'), // Field 1313: Acquirer code (1) - Optional, space-padded when empty formatField(input.cardBrand, 20, 'left'), // Field 1314: Card brand (20) - Alphanumeric formatField(input.creditTransactionType, 1, 'left'), // Field 1315: Credit transaction type (1) - Optional, space-padded when empty formatField(input.firstPaymentAmount, 0, 'left'), // Field 1316: First payment amount (0) - Deprecated formatField(input.installmentsCount, 0, 'left'), // Field 1317: Installments count (0) - Deprecated formatField(input.additionalPaymentAmount, 0, 'left'), // Field 1318: Additional payment amount (0) - Deprecated formatField(input.reserved1, 0, 'left'), // Field 1319: Reserved 1 (0) - Deprecated formatField(input.branchId, 7, 'left'), // Field 1320: Branch ID (7) - Alphanumeric formatField(input.reserved2, 0, 'left'), // Field 1321: Reserved 2 (0) - Deprecated formatNumericField(input.documentDate, 8), // Field 1322: Document date (8) - Numeric, zero-padded formatField(input.headerLinkField, 7, 'left'), // Field 1323: Header link field (7) - Optional, space-padded when empty formatField(input.reserved, 60, 'left'), // Field 1324: Reserved (60) - Alphanumeric ]; return fields.join('') + CRLF; } /** * Parses a fixed-width D120 record line back to object * Expected line length: 222 characters (excluding CRLF) */ export function parseD120(line: string): D120 { // Remove CRLF if present const cleanLine = line.replace(/\r?\n$/, ''); if (cleanLine.length !== 222) { throw new Error(`Invalid D120 record length: expected 222 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 = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || '0'; // Strip leading zeros pos += 9; const vatId = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || '0'; // Strip leading zeros pos += 9; const documentType = (cleanLine .slice(pos, pos + 3) .trim() .replace(/^0+/, '') || '0') as z.infer<typeof DocumentTypeEnum>; // Strip leading zeros pos += 3; const documentNumber = cleanLine.slice(pos, pos + 20).trim(); pos += 20; const lineNumber = cleanLine .slice(pos, pos + 4) .trim() .replace(/^0+/, '') || '0'; // Strip leading zeros pos += 4; const paymentMethod = cleanLine .slice(pos, pos + 1) .trim() .replace(/^0+/, '') || '0'; // Strip leading zeros pos += 1; const bankNumber = cleanLine.slice(pos, pos + 10).trim(); const bankNumberProcessed = bankNumber === '' ? '' : bankNumber.replace(/^0+/, '') || '0'; // Strip leading zeros only if not empty pos += 10; const branchNumber = cleanLine.slice(pos, pos + 10).trim(); const branchNumberProcessed = branchNumber === '' ? '' : branchNumber.replace(/^0+/, '') || '0'; // Strip leading zeros only if not empty pos += 10; const accountNumber = cleanLine.slice(pos, pos + 15).trim(); const accountNumberProcessed = accountNumber === '' ? '' : accountNumber.replace(/^0+/, '') || '0'; // Strip leading zeros only if not empty pos += 15; const checkNumber = cleanLine.slice(pos, pos + 10).trim(); const checkNumberProcessed = checkNumber === '' ? '' : checkNumber.replace(/^0+/, '') || '0'; // Strip leading zeros only if not empty pos += 10; const paymentDueDate = cleanLine.slice(pos, pos + 8).trim(); const paymentDueDateProcessed = paymentDueDate; // Keep as-is since it's a date field or empty pos += 8; const lineAmount = cleanLine.slice(pos, pos + 15).trim(); // Alphanumeric monetary format pos += 15; const acquirerCode = cleanLine.slice(pos, pos + 1).trim(); const acquirerCodeProcessed = acquirerCode; // Keep as-is since it's a single digit field or empty pos += 1; const cardBrand = cleanLine.slice(pos, pos + 20).trim(); pos += 20; const creditTransactionType = cleanLine.slice(pos, pos + 1).trim(); const creditTransactionTypeProcessed = creditTransactionType; // Keep as-is since it's a single digit field or empty pos += 1; const firstPaymentAmount = cleanLine.slice(pos, pos + 0).trim(); pos += 0; // 0 width const installmentsCount = cleanLine.slice(pos, pos + 0).trim(); pos += 0; // 0 width const additionalPaymentAmount = cleanLine.slice(pos, pos + 0).trim(); pos += 0; // 0 width const reserved1 = cleanLine.slice(pos, pos + 0).trim(); pos += 0; // 0 width const branchId = cleanLine.slice(pos, pos + 7).trim(); pos += 7; const reserved2 = cleanLine.slice(pos, pos + 0).trim(); pos += 0; // 0 width const documentDate = cleanLine .slice(pos, pos + 8) .trim() .replace(/^0+/, '') || '0'; // Strip leading zeros pos += 8; const headerLinkField = cleanLine.slice(pos, pos + 7).trim(); const headerLinkFieldProcessed = headerLinkField === '' ? '' : headerLinkField.replace(/^0+/, '') || '0'; // Strip leading zeros only if not empty pos += 7; const reserved = cleanLine.slice(pos, pos + 60).trim(); // Validate the code field if (code !== 'D120') { throw new Error(`Invalid D120 record code: expected "D120", got "${code}"`); } const parsed: D120 = { code, recordNumber, vatId, documentType, documentNumber, lineNumber, paymentMethod, bankNumber: bankNumberProcessed, branchNumber: branchNumberProcessed, accountNumber: accountNumberProcessed, checkNumber: checkNumberProcessed, paymentDueDate: paymentDueDateProcessed, lineAmount, acquirerCode: acquirerCodeProcessed, cardBrand, creditTransactionType: creditTransactionTypeProcessed, firstPaymentAmount, installmentsCount, additionalPaymentAmount, reserved1, branchId, reserved2, documentDate, headerLinkField: headerLinkFieldProcessed, reserved, }; // Validate against schema return D120Schema.parse(parsed); }