@accounter/shaam-uniform-format-generator 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +366 -13
  2. package/dist/README.md +476 -0
  3. package/dist/cjs/api/generate-report-legacy.js +6 -0
  4. package/dist/cjs/api/generate-report.js +328 -0
  5. package/dist/cjs/constants.js +11 -0
  6. package/dist/cjs/generator/format/encoder.js +86 -0
  7. package/dist/cjs/generator/records/a000-sum.js +66 -0
  8. package/dist/cjs/generator/records/a000.js +349 -0
  9. package/dist/cjs/generator/records/a100.js +107 -0
  10. package/dist/cjs/generator/records/b100.js +305 -0
  11. package/dist/cjs/generator/records/b110.js +255 -0
  12. package/dist/cjs/generator/records/c100.js +338 -0
  13. package/dist/cjs/generator/records/d110.js +272 -0
  14. package/dist/cjs/generator/records/d120.js +278 -0
  15. package/{cjs → dist/cjs}/generator/records/index.js +1 -0
  16. package/dist/cjs/generator/records/m100.js +177 -0
  17. package/dist/cjs/generator/records/z900.js +93 -0
  18. package/{cjs → dist/cjs}/index.js +3 -0
  19. package/dist/cjs/records/a100.js +78 -0
  20. package/dist/cjs/records/index.js +20 -0
  21. package/dist/cjs/records/z900.js +82 -0
  22. package/dist/cjs/types/enums.js +457 -0
  23. package/{cjs → dist/cjs}/types/index.js +6 -1
  24. package/dist/cjs/utils/file-helpers.js +198 -0
  25. package/dist/cjs/utils/index.js +8 -0
  26. package/dist/cjs/utils/key-generator.js +71 -0
  27. package/dist/esm/api/generate-report-legacy.js +2 -0
  28. package/dist/esm/api/generate-report.js +325 -0
  29. package/dist/esm/constants.js +8 -0
  30. package/dist/esm/generator/format/encoder.js +77 -0
  31. package/dist/esm/generator/records/a000-sum.js +61 -0
  32. package/dist/esm/generator/records/a000.js +344 -0
  33. package/dist/esm/generator/records/a100.js +102 -0
  34. package/dist/esm/generator/records/b100.js +300 -0
  35. package/dist/esm/generator/records/b110.js +250 -0
  36. package/dist/esm/generator/records/c100.js +333 -0
  37. package/dist/esm/generator/records/d110.js +267 -0
  38. package/dist/esm/generator/records/d120.js +273 -0
  39. package/{esm → dist/esm}/generator/records/index.js +1 -0
  40. package/dist/esm/generator/records/m100.js +172 -0
  41. package/dist/esm/generator/records/z900.js +88 -0
  42. package/{esm → dist/esm}/index.js +3 -0
  43. package/dist/esm/records/a100.js +73 -0
  44. package/dist/esm/records/index.js +11 -0
  45. package/dist/esm/records/z900.js +77 -0
  46. package/dist/esm/types/enums.js +454 -0
  47. package/{esm → dist/esm}/types/index.js +5 -1
  48. package/dist/esm/utils/file-helpers.js +188 -0
  49. package/dist/esm/utils/index.js +5 -0
  50. package/dist/esm/utils/key-generator.js +65 -0
  51. package/dist/package.json +54 -0
  52. package/dist/typings/api/generate-report-legacy.d.cts +1 -0
  53. package/dist/typings/api/generate-report-legacy.d.ts +1 -0
  54. package/dist/typings/constants.d.cts +8 -0
  55. package/dist/typings/constants.d.ts +8 -0
  56. package/dist/typings/generator/format/encoder.d.cts +57 -0
  57. package/dist/typings/generator/format/encoder.d.ts +57 -0
  58. package/dist/typings/generator/records/a000-sum.d.cts +40 -0
  59. package/dist/typings/generator/records/a000-sum.d.ts +40 -0
  60. package/dist/typings/generator/records/a000.d.cts +238 -0
  61. package/dist/typings/generator/records/a000.d.ts +238 -0
  62. package/dist/typings/generator/records/a100.d.cts +59 -0
  63. package/dist/typings/generator/records/a100.d.ts +59 -0
  64. package/dist/typings/generator/records/b100.d.cts +101 -0
  65. package/dist/typings/generator/records/b100.d.ts +101 -0
  66. package/dist/typings/generator/records/b110.d.cts +89 -0
  67. package/dist/typings/generator/records/b110.d.ts +89 -0
  68. package/dist/typings/generator/records/c100.d.cts +133 -0
  69. package/dist/typings/generator/records/c100.d.ts +133 -0
  70. package/dist/typings/generator/records/d110.d.cts +98 -0
  71. package/dist/typings/generator/records/d110.d.ts +98 -0
  72. package/dist/typings/generator/records/d120.d.cts +95 -0
  73. package/dist/typings/generator/records/d120.d.ts +95 -0
  74. package/{typings → dist/typings}/generator/records/index.d.cts +1 -0
  75. package/{typings → dist/typings}/generator/records/index.d.ts +1 -0
  76. package/dist/typings/generator/records/m100.d.cts +69 -0
  77. package/dist/typings/generator/records/m100.d.ts +69 -0
  78. package/dist/typings/generator/records/z900.d.cts +61 -0
  79. package/dist/typings/generator/records/z900.d.ts +61 -0
  80. package/{typings → dist/typings}/index.d.cts +3 -0
  81. package/{typings → dist/typings}/index.d.ts +3 -0
  82. package/dist/typings/records/a100.d.cts +35 -0
  83. package/dist/typings/records/a100.d.ts +35 -0
  84. package/dist/typings/records/index.d.cts +2 -0
  85. package/dist/typings/records/index.d.ts +2 -0
  86. package/dist/typings/records/z900.d.cts +38 -0
  87. package/dist/typings/records/z900.d.ts +38 -0
  88. package/dist/typings/types/enums.d.cts +162 -0
  89. package/dist/typings/types/enums.d.ts +162 -0
  90. package/{typings → dist/typings}/types/index.d.cts +17 -14
  91. package/{typings → dist/typings}/types/index.d.ts +17 -14
  92. package/dist/typings/utils/file-helpers.d.cts +131 -0
  93. package/dist/typings/utils/file-helpers.d.ts +131 -0
  94. package/dist/typings/utils/index.d.cts +5 -0
  95. package/dist/typings/utils/index.d.ts +5 -0
  96. package/dist/typings/utils/key-generator.d.cts +41 -0
  97. package/dist/typings/utils/key-generator.d.ts +41 -0
  98. package/documentation/IncomeTax_IncomeTaxSoftwareHousesInfo_horaot1.31_2_05.pdf +0 -0
  99. package/documentation/_4D6963726F736F667420576F7264202D20F8E5E0E9ED20F8E7E5F720F8E5E0E9ED20F9F7E5F32E646F63_.pdf +0 -0
  100. package/documentation/a000-sum.csv +3 -0
  101. package/documentation/a000.csv +37 -0
  102. package/documentation/a100.csv +7 -0
  103. package/documentation/b100.csv +29 -0
  104. package/documentation/b110.csv +26 -0
  105. package/documentation/c100.csv +37 -0
  106. package/documentation/d110.csv +27 -0
  107. package/documentation/d120.csv +26 -0
  108. package/documentation/m100.csv +17 -0
  109. package/documentation/z900.csv +8 -0
  110. package/package.json +50 -29
  111. package/prompt_plan.md +259 -0
  112. package/spec.md +206 -0
  113. package/src/api/generate-report.ts +366 -0
  114. package/src/api/parse-files.ts +33 -0
  115. package/src/constants.ts +9 -0
  116. package/src/format/index.ts +6 -0
  117. package/src/format/newline.ts +8 -0
  118. package/src/format/padding.ts +39 -0
  119. package/src/generator/format/decoder.ts +15 -0
  120. package/src/generator/format/encoder.ts +95 -0
  121. package/src/generator/format/index.ts +6 -0
  122. package/src/generator/index.ts +6 -0
  123. package/src/generator/records/a000-sum.ts +78 -0
  124. package/src/generator/records/a000.ts +373 -0
  125. package/src/generator/records/a100.ts +118 -0
  126. package/src/generator/records/b100.ts +317 -0
  127. package/src/generator/records/b110.ts +267 -0
  128. package/src/generator/records/c100.ts +347 -0
  129. package/src/generator/records/d110.ts +286 -0
  130. package/src/generator/records/d120.ts +293 -0
  131. package/src/generator/records/index.ts +14 -0
  132. package/src/generator/records/m100.ts +185 -0
  133. package/src/generator/records/z900.ts +104 -0
  134. package/src/index.ts +18 -0
  135. package/src/parser/data-parser.ts +14 -0
  136. package/src/parser/index.ts +6 -0
  137. package/src/parser/ini-parser.ts +14 -0
  138. package/src/types/enums.ts +531 -0
  139. package/src/types/index.ts +110 -0
  140. package/src/utils/file-helpers.ts +221 -0
  141. package/src/utils/index.ts +6 -0
  142. package/src/utils/key-generator.ts +75 -0
  143. package/src/validation/errors.ts +35 -0
  144. package/src/validation/index.ts +6 -0
  145. package/src/validation/validate-input.ts +67 -0
  146. package/tests/debug-output.test.ts +81 -0
  147. package/tests/format/crlf-join.test.ts +124 -0
  148. package/tests/format/encoder.test.ts +80 -0
  149. package/tests/format/newline.test.ts +19 -0
  150. package/tests/format/padding.test.ts +74 -0
  151. package/tests/index.test.ts +29 -0
  152. package/tests/ini-text.test.ts +122 -0
  153. package/tests/integration/comprehensive.integration.test.ts +350 -0
  154. package/tests/integration/roundtrip.integration.test.ts +377 -0
  155. package/tests/records/a000-sum.test.ts +278 -0
  156. package/tests/records/a000.test.ts +318 -0
  157. package/tests/records/a100.test.ts +239 -0
  158. package/tests/records/b100.test.ts +419 -0
  159. package/tests/records/b110.test.ts +445 -0
  160. package/tests/records/c100.test.ts +333 -0
  161. package/tests/records/d110.test.ts +93 -0
  162. package/tests/records/d120.test.ts +275 -0
  163. package/tests/records/m100.test.ts +437 -0
  164. package/tests/records/z900.test.ts +254 -0
  165. package/tests/types/enums.test.ts +290 -0
  166. package/tests/utils/file-helpers.test.ts +276 -0
  167. package/tests/utils/key-generator.test.ts +121 -0
  168. package/tests/validation/document-type-validation.test.ts +521 -0
  169. package/tests/validation/validate-input.test.ts +219 -0
  170. package/todo.md +203 -0
  171. package/tsconfig.json +10 -0
  172. package/vitest.config.ts +11 -0
  173. package/cjs/api/generate-report.js +0 -53
  174. package/cjs/generator/format/encoder.js +0 -46
  175. package/cjs/generator/records/a000.js +0 -8
  176. package/cjs/generator/records/a100.js +0 -8
  177. package/cjs/generator/records/b100.js +0 -8
  178. package/cjs/generator/records/b110.js +0 -8
  179. package/cjs/generator/records/c100.js +0 -8
  180. package/cjs/generator/records/d110.js +0 -8
  181. package/cjs/generator/records/d120.js +0 -8
  182. package/cjs/generator/records/m100.js +0 -8
  183. package/cjs/generator/records/z900.js +0 -8
  184. package/esm/api/generate-report.js +0 -50
  185. package/esm/generator/format/encoder.js +0 -42
  186. package/esm/generator/records/a000.js +0 -5
  187. package/esm/generator/records/a100.js +0 -5
  188. package/esm/generator/records/b100.js +0 -5
  189. package/esm/generator/records/b110.js +0 -5
  190. package/esm/generator/records/c100.js +0 -5
  191. package/esm/generator/records/d110.js +0 -5
  192. package/esm/generator/records/d120.js +0 -5
  193. package/esm/generator/records/m100.js +0 -5
  194. package/esm/generator/records/z900.js +0 -5
  195. package/typings/generator/format/encoder.d.cts +0 -33
  196. package/typings/generator/format/encoder.d.ts +0 -33
  197. package/typings/generator/records/a000.d.cts +0 -4
  198. package/typings/generator/records/a000.d.ts +0 -4
  199. package/typings/generator/records/a100.d.cts +0 -4
  200. package/typings/generator/records/a100.d.ts +0 -4
  201. package/typings/generator/records/b100.d.cts +0 -4
  202. package/typings/generator/records/b100.d.ts +0 -4
  203. package/typings/generator/records/b110.d.cts +0 -4
  204. package/typings/generator/records/b110.d.ts +0 -4
  205. package/typings/generator/records/c100.d.cts +0 -4
  206. package/typings/generator/records/c100.d.ts +0 -4
  207. package/typings/generator/records/d110.d.cts +0 -4
  208. package/typings/generator/records/d110.d.ts +0 -4
  209. package/typings/generator/records/d120.d.cts +0 -4
  210. package/typings/generator/records/d120.d.ts +0 -4
  211. package/typings/generator/records/m100.d.cts +0 -4
  212. package/typings/generator/records/m100.d.ts +0 -4
  213. package/typings/generator/records/z900.d.cts +0 -4
  214. package/typings/generator/records/z900.d.ts +0 -4
  215. /package/{cjs → dist/cjs}/api/parse-files.js +0 -0
  216. /package/{cjs → dist/cjs}/format/index.js +0 -0
  217. /package/{cjs → dist/cjs}/format/newline.js +0 -0
  218. /package/{cjs → dist/cjs}/format/padding.js +0 -0
  219. /package/{cjs → dist/cjs}/generator/format/decoder.js +0 -0
  220. /package/{cjs → dist/cjs}/generator/format/index.js +0 -0
  221. /package/{cjs → dist/cjs}/generator/index.js +0 -0
  222. /package/{cjs → dist/cjs}/package.json +0 -0
  223. /package/{cjs → dist/cjs}/parser/data-parser.js +0 -0
  224. /package/{cjs → dist/cjs}/parser/index.js +0 -0
  225. /package/{cjs → dist/cjs}/parser/ini-parser.js +0 -0
  226. /package/{cjs → dist/cjs}/validation/errors.js +0 -0
  227. /package/{cjs → dist/cjs}/validation/index.js +0 -0
  228. /package/{cjs → dist/cjs}/validation/validate-input.js +0 -0
  229. /package/{esm → dist/esm}/api/parse-files.js +0 -0
  230. /package/{esm → dist/esm}/format/index.js +0 -0
  231. /package/{esm → dist/esm}/format/newline.js +0 -0
  232. /package/{esm → dist/esm}/format/padding.js +0 -0
  233. /package/{esm → dist/esm}/generator/format/decoder.js +0 -0
  234. /package/{esm → dist/esm}/generator/format/index.js +0 -0
  235. /package/{esm → dist/esm}/generator/index.js +0 -0
  236. /package/{esm → dist/esm}/parser/data-parser.js +0 -0
  237. /package/{esm → dist/esm}/parser/index.js +0 -0
  238. /package/{esm → dist/esm}/parser/ini-parser.js +0 -0
  239. /package/{esm → dist/esm}/validation/errors.js +0 -0
  240. /package/{esm → dist/esm}/validation/index.js +0 -0
  241. /package/{esm → dist/esm}/validation/validate-input.js +0 -0
  242. /package/{typings → dist/typings}/api/generate-report.d.cts +0 -0
  243. /package/{typings → dist/typings}/api/generate-report.d.ts +0 -0
  244. /package/{typings → dist/typings}/api/parse-files.d.cts +0 -0
  245. /package/{typings → dist/typings}/api/parse-files.d.ts +0 -0
  246. /package/{typings → dist/typings}/format/index.d.cts +0 -0
  247. /package/{typings → dist/typings}/format/index.d.ts +0 -0
  248. /package/{typings → dist/typings}/format/newline.d.cts +0 -0
  249. /package/{typings → dist/typings}/format/newline.d.ts +0 -0
  250. /package/{typings → dist/typings}/format/padding.d.cts +0 -0
  251. /package/{typings → dist/typings}/format/padding.d.ts +0 -0
  252. /package/{typings → dist/typings}/generator/format/decoder.d.cts +0 -0
  253. /package/{typings → dist/typings}/generator/format/decoder.d.ts +0 -0
  254. /package/{typings → dist/typings}/generator/format/index.d.cts +0 -0
  255. /package/{typings → dist/typings}/generator/format/index.d.ts +0 -0
  256. /package/{typings → dist/typings}/generator/index.d.cts +0 -0
  257. /package/{typings → dist/typings}/generator/index.d.ts +0 -0
  258. /package/{typings → dist/typings}/parser/data-parser.d.cts +0 -0
  259. /package/{typings → dist/typings}/parser/data-parser.d.ts +0 -0
  260. /package/{typings → dist/typings}/parser/index.d.cts +0 -0
  261. /package/{typings → dist/typings}/parser/index.d.ts +0 -0
  262. /package/{typings → dist/typings}/parser/ini-parser.d.cts +0 -0
  263. /package/{typings → dist/typings}/parser/ini-parser.d.ts +0 -0
  264. /package/{typings → dist/typings}/validation/errors.d.cts +0 -0
  265. /package/{typings → dist/typings}/validation/errors.d.ts +0 -0
  266. /package/{typings → dist/typings}/validation/index.d.cts +0 -0
  267. /package/{typings → dist/typings}/validation/index.d.ts +0 -0
  268. /package/{typings → dist/typings}/validation/validate-input.d.cts +0 -0
  269. /package/{typings → dist/typings}/validation/validate-input.d.ts +0 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Integration test for round-trip generation and parsing
3
+ * This test ensures that data can be generated to SHAAM format and parsed back correctly
4
+ */
5
+
6
+ import { describe, expect, it } from 'vitest';
7
+ import { generateUniformFormatReport } from '../../src/api/generate-report';
8
+ import {
9
+ parseA100,
10
+ parseB100,
11
+ parseB110,
12
+ parseC100,
13
+ parseD110,
14
+ parseD120,
15
+ parseM100,
16
+ parseZ900,
17
+ } from '../../src/generator/records/index';
18
+ import type { ReportInput } from '../../src/types/index';
19
+
20
+ describe('SHAAM Format Round-trip Integration Test', () => {
21
+ it('should generate and parse back a complete report', () => {
22
+ // Full ReportInput fixture
23
+ const input: ReportInput = {
24
+ business: {
25
+ businessId: '12345',
26
+ name: 'Test Company Ltd',
27
+ taxId: '123456789',
28
+ reportingPeriod: {
29
+ startDate: '2024-01-01',
30
+ endDate: '2024-12-31',
31
+ },
32
+ },
33
+ documents: [
34
+ {
35
+ id: 'DOC001',
36
+ type: '320', // Invoice
37
+ date: '2024-03-15',
38
+ amount: 1000.5,
39
+ description: 'Consulting services',
40
+ },
41
+ {
42
+ id: 'DOC002',
43
+ type: '330', // Credit memo
44
+ date: '2024-03-20',
45
+ amount: 250.75,
46
+ description: 'Product return',
47
+ },
48
+ ],
49
+ journalEntries: [
50
+ {
51
+ id: 'JE001',
52
+ date: '2024-03-15',
53
+ amount: 1000.5,
54
+ accountId: '1100',
55
+ description: 'Sales revenue',
56
+ },
57
+ {
58
+ id: 'JE002',
59
+ date: '2024-03-20',
60
+ amount: -250.75,
61
+ accountId: '1200',
62
+ description: 'Returns and allowances',
63
+ },
64
+ ],
65
+ accounts: [
66
+ {
67
+ id: '1100',
68
+ name: 'Cash',
69
+ type: 'Asset',
70
+ balance: 5000.0,
71
+ },
72
+ {
73
+ id: '1200',
74
+ name: 'Accounts Receivable',
75
+ type: 'Asset',
76
+ balance: 3000.0,
77
+ },
78
+ {
79
+ id: '4000',
80
+ name: 'Sales Revenue',
81
+ type: 'Revenue',
82
+ balance: 8000.0,
83
+ },
84
+ ],
85
+ inventory: [
86
+ {
87
+ id: 'ITEM001',
88
+ name: 'Product A',
89
+ quantity: 100,
90
+ unitPrice: 25.0,
91
+ },
92
+ {
93
+ id: 'ITEM002',
94
+ name: 'Product B',
95
+ quantity: 50,
96
+ unitPrice: 45.0,
97
+ },
98
+ ],
99
+ };
100
+
101
+ // Generate the report
102
+ const result = generateUniformFormatReport(input);
103
+
104
+ expect(result).toBeDefined();
105
+ expect(result.dataText).toBeDefined();
106
+ expect(result.iniText).toBeDefined();
107
+ expect(result.summary.totalRecords).toBeGreaterThan(0);
108
+
109
+ // Split dataText into lines and parse each record
110
+ const lines = result.dataText.split('\r\n').filter(line => line.trim().length > 0);
111
+
112
+ interface ParsedData {
113
+ businessMetadata: ReturnType<typeof parseA100> | null;
114
+ documents: ReturnType<typeof parseC100>[];
115
+ documentLines: ReturnType<typeof parseD110>[];
116
+ payments: ReturnType<typeof parseD120>[];
117
+ journalEntries: ReturnType<typeof parseB100>[];
118
+ accounts: ReturnType<typeof parseB110>[];
119
+ inventory: ReturnType<typeof parseM100>[];
120
+ closingRecord: ReturnType<typeof parseZ900> | null;
121
+ }
122
+
123
+ const parsedData: ParsedData = {
124
+ businessMetadata: null,
125
+ documents: [],
126
+ documentLines: [],
127
+ payments: [],
128
+ journalEntries: [],
129
+ accounts: [],
130
+ inventory: [],
131
+ closingRecord: null,
132
+ };
133
+
134
+ // Parse each line based on record type
135
+ for (const line of lines) {
136
+ const recordType = line.substring(0, 4);
137
+
138
+ switch (recordType) {
139
+ case 'A100':
140
+ parsedData.businessMetadata = parseA100(line);
141
+ break;
142
+ case 'C100':
143
+ parsedData.documents.push(parseC100(line));
144
+ break;
145
+ case 'D110':
146
+ parsedData.documentLines.push(parseD110(line));
147
+ break;
148
+ case 'D120':
149
+ parsedData.payments.push(parseD120(line));
150
+ break;
151
+ case 'B100':
152
+ parsedData.journalEntries.push(parseB100(line));
153
+ break;
154
+ case 'B110':
155
+ parsedData.accounts.push(parseB110(line));
156
+ break;
157
+ case 'M100':
158
+ parsedData.inventory.push(parseM100(line));
159
+ break;
160
+ case 'Z900':
161
+ parsedData.closingRecord = parseZ900(line);
162
+ break;
163
+ default:
164
+ // Unknown record type, skip silently for test purposes
165
+ break;
166
+ }
167
+ }
168
+
169
+ // Verify business metadata
170
+ expect(parsedData.businessMetadata).toBeDefined();
171
+ expect(parsedData.businessMetadata?.vatId).toBe(input.business.taxId);
172
+ // primaryIdentifier is now auto-generated, so just verify it's a valid 15-digit string
173
+ expect(parsedData.businessMetadata?.primaryIdentifier).toMatch(/^\d{15}$/);
174
+
175
+ // Verify documents
176
+ expect(parsedData.documents).toHaveLength(input.documents.length);
177
+ for (let i = 0; i < input.documents.length; i++) {
178
+ const original = input.documents[i];
179
+ const parsed = parsedData.documents[i];
180
+ expect(parsed.documentId).toBe(original.id);
181
+ expect(parsed.documentType).toBe(original.type);
182
+ expect(parsed.documentIssueDate).toBe(original.date.replace(/-/g, ''));
183
+ }
184
+
185
+ // Verify document lines
186
+ expect(parsedData.documentLines).toHaveLength(input.documents.length);
187
+ for (let i = 0; i < input.documents.length; i++) {
188
+ const original = input.documents[i];
189
+ const parsed = parsedData.documentLines[i];
190
+ expect(parsed.documentNumber).toBe(original.id);
191
+ expect(parsed.documentType).toBe(original.type);
192
+ expect(parsed.goodsServiceDescription).toBe(original.description || 'Item');
193
+ }
194
+
195
+ // Verify payments
196
+ expect(parsedData.payments).toHaveLength(input.documents.length);
197
+ for (let i = 0; i < input.documents.length; i++) {
198
+ const original = input.documents[i];
199
+ const parsed = parsedData.payments[i];
200
+ expect(parsed.documentNumber).toBe(original.id);
201
+ expect(parsed.documentType).toBe(original.type);
202
+ expect(parsed.lineAmount).toBe(original.amount.toFixed(2));
203
+ }
204
+
205
+ // Verify journal entries
206
+ expect(parsedData.journalEntries).toHaveLength(input.journalEntries.length);
207
+ for (let i = 0; i < input.journalEntries.length; i++) {
208
+ const original = input.journalEntries[i];
209
+ const parsed = parsedData.journalEntries[i];
210
+ const expectedTransactionNumber =
211
+ (original.id.replace(/\D/g, '') || '1').replace(/^0+/, '') || '0';
212
+ expect(parsed.transactionNumber).toBe(expectedTransactionNumber); // Numeric part with leading zeros stripped
213
+ expect(parsed.accountKey).toBe(original.accountId);
214
+ expect(parsed.transactionAmount).toBe(Math.abs(original.amount).toFixed(2));
215
+ expect(parsed.debitCreditIndicator).toBe(original.amount >= 0 ? '1' : '2');
216
+ }
217
+
218
+ // Verify accounts
219
+ expect(parsedData.accounts).toHaveLength(input.accounts.length);
220
+ for (let i = 0; i < input.accounts.length; i++) {
221
+ const original = input.accounts[i];
222
+ const parsed = parsedData.accounts[i];
223
+ expect(parsed.accountKey).toBe(original.id);
224
+ expect(parsed.accountName).toBe(original.name);
225
+ expect(parsed.trialBalanceCode).toBe(original.type);
226
+ }
227
+
228
+ // Verify inventory
229
+ expect(parsedData.inventory).toHaveLength(input.inventory.length);
230
+ for (let i = 0; i < input.inventory.length; i++) {
231
+ const original = input.inventory[i];
232
+ const parsed = parsedData.inventory[i];
233
+ expect(parsed.internalItemCode).toBe(original.id);
234
+ expect(parsed.itemName).toBe(original.name);
235
+ expect(parsed.totalStockOut).toBe(original.quantity.toString());
236
+ }
237
+
238
+ // Verify closing record
239
+ expect(parsedData.closingRecord).toBeDefined();
240
+ expect(parsedData.closingRecord?.vatId).toBe(input.business.taxId);
241
+ // uniqueId is now auto-generated and should match the primaryIdentifier
242
+ expect(parsedData.closingRecord?.uniqueId).toMatch(/^\d{15}$/);
243
+ expect(parsedData.closingRecord?.uniqueId).toBe(parsedData.businessMetadata?.primaryIdentifier);
244
+ expect(parseInt(parsedData.closingRecord?.totalRecords || '0')).toBeGreaterThan(0);
245
+
246
+ // Verify summary record counts match parsed data
247
+ const expectedTotalRecords =
248
+ 1 + // A000 (INI file)
249
+ 8 + // A000Sum records (one for each record type: A100, C100, D110, D120, B100, B110, M100, Z900)
250
+ 1 + // A100
251
+ input.documents.length + // C100 records
252
+ input.documents.length + // D110 records
253
+ input.documents.length + // D120 records
254
+ input.journalEntries.length + // B100 records
255
+ input.accounts.length + // B110 records
256
+ input.inventory.length + // M100 records
257
+ 1; // Z900
258
+
259
+ expect(result.summary.totalRecords).toBe(expectedTotalRecords);
260
+ expect(parseInt(parsedData.closingRecord?.totalRecords || '0')).toBe(expectedTotalRecords - 10); // Z900 counts data records only (excludes A000, A000Sum records, and Z900 itself)
261
+
262
+ // Verify record type counts in summary
263
+ expect(result.summary.perType.A000).toBe(1);
264
+ expect(result.summary.perType.A100).toBe(1);
265
+ expect(result.summary.perType.C100).toBe(input.documents.length);
266
+ expect(result.summary.perType.D110).toBe(input.documents.length);
267
+ expect(result.summary.perType.D120).toBe(input.documents.length);
268
+ expect(result.summary.perType.B100).toBe(input.journalEntries.length);
269
+ expect(result.summary.perType.B110).toBe(input.accounts.length);
270
+ expect(result.summary.perType.M100).toBe(input.inventory.length);
271
+ expect(result.summary.perType.Z900).toBe(1);
272
+ });
273
+
274
+ it('should handle empty data sections correctly', () => {
275
+ const minimalInput: ReportInput = {
276
+ business: {
277
+ businessId: '54321',
278
+ name: 'Minimal Company',
279
+ taxId: '987654321',
280
+ reportingPeriod: {
281
+ startDate: '2024-01-01',
282
+ endDate: '2024-12-31',
283
+ },
284
+ },
285
+ documents: [],
286
+ journalEntries: [],
287
+ accounts: [],
288
+ inventory: [],
289
+ };
290
+
291
+ const result = generateUniformFormatReport(minimalInput);
292
+
293
+ expect(result).toBeDefined();
294
+ expect(result.dataText).toBeDefined();
295
+ expect(result.summary.totalRecords).toBe(5); // A000 + 2 A000Sum (A100, Z900) + A100 + Z900
296
+
297
+ const lines = result.dataText.split('\r\n').filter(line => line.trim().length > 0);
298
+ expect(lines).toHaveLength(2);
299
+
300
+ // Should have A100 and Z900 records only
301
+ expect(lines[0].startsWith('A100')).toBe(true);
302
+ expect(lines[1].startsWith('Z900')).toBe(true);
303
+
304
+ // Parse and verify
305
+ const businessRecord = parseA100(lines[0]);
306
+ const closingRecord = parseZ900(lines[1]);
307
+
308
+ expect(businessRecord.vatId).toBe(minimalInput.business.taxId);
309
+ expect(closingRecord.vatId).toBe(minimalInput.business.taxId);
310
+ expect(closingRecord.totalRecords).toBe('1'); // Only A100 counted
311
+ });
312
+
313
+ it('should maintain data integrity for monetary values', () => {
314
+ const input: ReportInput = {
315
+ business: {
316
+ businessId: '99999',
317
+ name: 'Financial Test Co',
318
+ taxId: '111111111',
319
+ reportingPeriod: {
320
+ startDate: '2024-01-01',
321
+ endDate: '2024-12-31',
322
+ },
323
+ },
324
+ documents: [
325
+ {
326
+ id: 'HIGH-VAL',
327
+ type: '320',
328
+ date: '2024-06-15',
329
+ amount: 999_999.99,
330
+ description: 'High value transaction',
331
+ },
332
+ ],
333
+ journalEntries: [
334
+ {
335
+ id: 'PRECISION-TEST',
336
+ date: '2024-06-15',
337
+ amount: 123.45,
338
+ accountId: '2000',
339
+ description: 'Precision test',
340
+ },
341
+ ],
342
+ accounts: [
343
+ {
344
+ id: '2000',
345
+ name: 'Test Account',
346
+ type: 'Liability',
347
+ balance: 123_456.78,
348
+ },
349
+ ],
350
+ inventory: [
351
+ {
352
+ id: 'EXPENSIVE-ITEM',
353
+ name: 'Expensive Product',
354
+ quantity: 1,
355
+ unitPrice: 999.99,
356
+ },
357
+ ],
358
+ };
359
+
360
+ const result = generateUniformFormatReport(input);
361
+ const lines = result.dataText.split('\r\n').filter(line => line.trim().length > 0);
362
+
363
+ // Find and parse the payment record for the high-value document
364
+ const paymentLine = lines.find(line => line.startsWith('D120'));
365
+ expect(paymentLine).toBeDefined();
366
+
367
+ const paymentRecord = parseD120(paymentLine!);
368
+ expect(paymentRecord.lineAmount).toBe('999999.99');
369
+
370
+ // Find and parse the journal entry
371
+ const journalLine = lines.find(line => line.startsWith('B100'));
372
+ expect(journalLine).toBeDefined();
373
+
374
+ const journalRecord = parseB100(journalLine!);
375
+ expect(journalRecord.transactionAmount).toBe('123.45');
376
+ });
377
+ });
@@ -0,0 +1,278 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ A000SumSchema,
4
+ encodeA000Sum,
5
+ parseA000Sum,
6
+ type A000Sum,
7
+ type A000SumInput,
8
+ } from '../../src/generator/records/a000-sum';
9
+
10
+ describe('A000Sum Record', () => {
11
+ const validA000Sum: A000Sum = {
12
+ code: 'A100',
13
+ recordCount: '12345',
14
+ };
15
+
16
+ const validA000SumInput: A000SumInput = {
17
+ code: 'B100',
18
+ recordCount: '67890',
19
+ };
20
+
21
+ describe('Schema Validation', () => {
22
+ it('should validate a valid A000Sum record', () => {
23
+ expect(() => A000SumSchema.parse(validA000Sum)).not.toThrow();
24
+ });
25
+
26
+ it('should reject empty code', () => {
27
+ const invalidRecord = { ...validA000Sum, code: '' };
28
+ expect(() => A000SumSchema.parse(invalidRecord)).toThrow();
29
+ });
30
+
31
+ it('should reject code longer than 4 characters', () => {
32
+ const invalidRecord = { ...validA000Sum, code: 'TOOLONG' };
33
+ expect(() => A000SumSchema.parse(invalidRecord)).toThrow();
34
+ });
35
+
36
+ it('should reject empty record count', () => {
37
+ const invalidRecord = { ...validA000Sum, recordCount: '' };
38
+ expect(() => A000SumSchema.parse(invalidRecord)).toThrow();
39
+ });
40
+
41
+ it('should reject record count longer than 15 characters', () => {
42
+ const invalidRecord = { ...validA000Sum, recordCount: '1234567890123456' };
43
+ expect(() => A000SumSchema.parse(invalidRecord)).toThrow();
44
+ });
45
+
46
+ it('should reject non-numeric record count', () => {
47
+ const invalidRecord = { ...validA000Sum, recordCount: 'abc123' };
48
+ expect(() => A000SumSchema.parse(invalidRecord)).toThrow();
49
+ });
50
+
51
+ it('should accept valid record codes', () => {
52
+ const validCodes = ['A100', 'B100', 'C100', 'D110', 'M100', 'Z900'];
53
+ for (const code of validCodes) {
54
+ const record = { ...validA000Sum, code };
55
+ expect(() => A000SumSchema.parse(record)).not.toThrow();
56
+ }
57
+ });
58
+
59
+ it('should accept record count with leading zeros when parsed as string', () => {
60
+ const record = { ...validA000Sum, recordCount: '000123' };
61
+ expect(() => A000SumSchema.parse(record)).not.toThrow();
62
+ });
63
+ });
64
+
65
+ describe('encodeA000Sum', () => {
66
+ it('should encode a valid A000Sum record to fixed-width string', () => {
67
+ const encoded = encodeA000Sum(validA000SumInput);
68
+
69
+ // Should end with CRLF
70
+ expect(encoded).toMatch(/\r\n$/);
71
+
72
+ // Should have correct length (19 + 2 for CRLF)
73
+ expect(encoded.length).toBe(21);
74
+
75
+ // Should start with the record code
76
+ expect(encoded.slice(0, 4)).toBe('B100');
77
+ });
78
+
79
+ it('should pad numeric fields with leading zeros', () => {
80
+ const shortCountInput: A000SumInput = {
81
+ code: 'A100',
82
+ recordCount: '123',
83
+ };
84
+
85
+ const encoded = encodeA000Sum(shortCountInput);
86
+
87
+ // Record count should be zero-padded to 15 digits (position 4-18)
88
+ const recordCountField = encoded.slice(4, 19);
89
+ expect(recordCountField).toBe('000000000000123');
90
+ });
91
+
92
+ it('should handle maximum length fields', () => {
93
+ const maxLengthInput: A000SumInput = {
94
+ code: 'ABCD',
95
+ recordCount: '123456789012345',
96
+ };
97
+
98
+ const encoded = encodeA000Sum(maxLengthInput);
99
+ expect(encoded.length).toBe(21); // Should still have correct total length
100
+
101
+ // Check that fields are not truncated
102
+ expect(encoded.slice(0, 4)).toBe('ABCD');
103
+ expect(encoded.slice(4, 19)).toBe('123456789012345');
104
+ });
105
+
106
+ it('should handle single digit record count', () => {
107
+ const singleDigitInput: A000SumInput = {
108
+ code: 'Z900',
109
+ recordCount: '1',
110
+ };
111
+
112
+ const encoded = encodeA000Sum(singleDigitInput);
113
+ const recordCountField = encoded.slice(4, 19);
114
+ expect(recordCountField).toBe('000000000000001');
115
+ });
116
+ });
117
+
118
+ describe('parseA000Sum', () => {
119
+ it('should parse a valid fixed-width A000Sum record string', () => {
120
+ const encoded = encodeA000Sum(validA000SumInput);
121
+ const parsed = parseA000Sum(encoded);
122
+
123
+ expect(parsed.code).toBe('B100');
124
+ expect(parsed.recordCount).toBe('67890');
125
+ });
126
+
127
+ it('should handle lines with and without CRLF', () => {
128
+ const encoded = encodeA000Sum(validA000SumInput);
129
+ const withoutCRLF = encoded.replace(/\r\n$/, '');
130
+
131
+ expect(() => parseA000Sum(encoded)).not.toThrow();
132
+ expect(() => parseA000Sum(withoutCRLF)).not.toThrow();
133
+
134
+ const parsedWithCRLF = parseA000Sum(encoded);
135
+ const parsedWithoutCRLF = parseA000Sum(withoutCRLF);
136
+
137
+ expect(parsedWithCRLF).toEqual(parsedWithoutCRLF);
138
+ });
139
+
140
+ it('should throw error for invalid record length', () => {
141
+ const shortLine = 'A100' + ' '.repeat(10);
142
+ const longLine = 'A100' + ' '.repeat(50);
143
+
144
+ expect(() => parseA000Sum(shortLine)).toThrow(
145
+ 'Invalid A000Sum record length: expected 19 characters, got 14',
146
+ );
147
+ expect(() => parseA000Sum(longLine)).toThrow(
148
+ 'Invalid A000Sum record length: expected 19 characters, got 54',
149
+ );
150
+ });
151
+
152
+ it('should properly strip leading zeros from numeric fields', () => {
153
+ const paddedInput: A000SumInput = {
154
+ code: 'M100',
155
+ recordCount: '00000000000042',
156
+ };
157
+
158
+ const encoded = encodeA000Sum(paddedInput);
159
+ const parsed = parseA000Sum(encoded);
160
+
161
+ // Should strip leading zeros
162
+ expect(parsed.recordCount).toBe('42');
163
+ });
164
+
165
+ it('should handle zero record count correctly', () => {
166
+ const zeroInput: A000SumInput = {
167
+ code: 'C100',
168
+ recordCount: '0',
169
+ };
170
+
171
+ const encoded = encodeA000Sum(zeroInput);
172
+ const parsed = parseA000Sum(encoded);
173
+
174
+ expect(parsed.recordCount).toBe('0');
175
+ });
176
+
177
+ it('should preserve code field correctly', () => {
178
+ const testCodes = ['A100', 'B100', 'C100', 'D110', 'M100', 'Z900'];
179
+
180
+ for (const code of testCodes) {
181
+ const input: A000SumInput = { code, recordCount: '100' };
182
+ const encoded = encodeA000Sum(input);
183
+ const parsed = parseA000Sum(encoded);
184
+
185
+ expect(parsed.code).toBe(code);
186
+ }
187
+ });
188
+ });
189
+
190
+ describe('Round-trip Tests', () => {
191
+ it('should maintain data integrity through encode-parse round trip', () => {
192
+ const encoded = encodeA000Sum(validA000SumInput);
193
+ const parsed = parseA000Sum(encoded);
194
+ const reEncoded = encodeA000Sum(parsed);
195
+
196
+ expect(reEncoded).toBe(encoded);
197
+ });
198
+
199
+ it('should preserve all field values in round trip', () => {
200
+ const encoded = encodeA000Sum(validA000SumInput);
201
+ const parsed = parseA000Sum(encoded);
202
+
203
+ expect(parsed.code).toBe(validA000SumInput.code);
204
+ expect(parsed.recordCount).toBe(validA000SumInput.recordCount);
205
+ });
206
+
207
+ it('should handle edge cases in round trip', () => {
208
+ const edgeCases: A000SumInput[] = [
209
+ { code: 'A', recordCount: '1' }, // Minimum lengths
210
+ { code: 'ABCD', recordCount: '123456789012345' }, // Maximum lengths
211
+ { code: 'Z900', recordCount: '0' }, // Zero count
212
+ { code: 'TEST', recordCount: '999999999999999' }, // Large number
213
+ ];
214
+
215
+ for (const testCase of edgeCases) {
216
+ const encoded = encodeA000Sum(testCase);
217
+ const parsed = parseA000Sum(encoded);
218
+ const reEncoded = encodeA000Sum(parsed);
219
+
220
+ expect(reEncoded).toBe(encoded);
221
+ expect(parsed.code).toBe(testCase.code);
222
+ expect(parsed.recordCount).toBe(testCase.recordCount);
223
+ }
224
+ });
225
+
226
+ it('should handle record count with various leading zero patterns', () => {
227
+ const testCases = [
228
+ { input: '1', expected: '1' },
229
+ { input: '01', expected: '1' },
230
+ { input: '001', expected: '1' },
231
+ { input: '0001234', expected: '1234' },
232
+ { input: '000000000000001', expected: '1' },
233
+ ];
234
+
235
+ for (const { input, expected } of testCases) {
236
+ const testInput: A000SumInput = { code: 'TEST', recordCount: input };
237
+ const encoded = encodeA000Sum(testInput);
238
+ const parsed = parseA000Sum(encoded);
239
+
240
+ expect(parsed.recordCount).toBe(expected);
241
+ }
242
+ });
243
+ });
244
+
245
+ describe('Error Handling', () => {
246
+ it('should throw error when feeding invalid line to parseA000Sum', () => {
247
+ const invalidLines = [
248
+ '', // Empty line
249
+ 'A100', // Too short
250
+ 'A100' + ' '.repeat(100), // Too long
251
+ 'XY', // Way too short
252
+ ];
253
+
254
+ for (const line of invalidLines) {
255
+ expect(() => parseA000Sum(line)).toThrow();
256
+ }
257
+ });
258
+
259
+ it('should validate schema after parsing', () => {
260
+ // Create a line with valid length but invalid content that should fail schema validation
261
+ const validEncoded = encodeA000Sum(validA000SumInput);
262
+
263
+ // This should pass since the content is valid
264
+ expect(() => parseA000Sum(validEncoded)).not.toThrow();
265
+ });
266
+
267
+ it('should handle malformed input gracefully', () => {
268
+ // Test with various malformed inputs that should fail schema validation
269
+
270
+ // Test first input separately to debug - all spaces should fail because code becomes empty
271
+ expect(() => parseA000Sum(' '.repeat(19)), 'All spaces input should fail').toThrow();
272
+
273
+ // Test second input - this creates "A" + 18 spaces = "A " which is valid
274
+ // Let's use something that should actually fail
275
+ expect(() => parseA000Sum('ABCDE' + ' '.repeat(14)), 'Code too long should fail').toThrow();
276
+ });
277
+ });
278
+ });