@accounter/server 0.0.8-alpha-20251102200443-d7162b8ce1dfc629b8b454df17dcec9ed005a052 → 0.0.8-alpha-20251102213150-c9d936f545d5351df0dc5326c2623266f1ad1f46

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 (178) hide show
  1. package/CHANGELOG.md +47 -7
  2. package/dist/green-invoice-graphql/src/mesh-artifacts/index.d.ts +1 -1
  3. package/dist/server/src/__generated__/types.d.ts +77 -0
  4. package/dist/server/src/__generated__/types.js.map +1 -1
  5. package/dist/server/src/modules/charges-matcher/__generated__/types.d.ts +68 -0
  6. package/dist/server/src/modules/charges-matcher/__generated__/types.js +7 -0
  7. package/dist/server/src/modules/charges-matcher/__generated__/types.js.map +1 -0
  8. package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.d.ts +1 -0
  9. package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js +218 -0
  10. package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js.map +1 -0
  11. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.d.ts +1 -0
  12. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +645 -0
  13. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -0
  14. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.d.ts +1 -0
  15. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +530 -0
  16. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -0
  17. package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.d.ts +1 -0
  18. package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js +143 -0
  19. package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js.map +1 -0
  20. package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.d.ts +1 -0
  21. package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js +186 -0
  22. package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js.map +1 -0
  23. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.d.ts +1 -0
  24. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +301 -0
  25. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -0
  26. package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.d.ts +1 -0
  27. package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js +127 -0
  28. package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js.map +1 -0
  29. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.d.ts +1 -0
  30. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +246 -0
  31. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -0
  32. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.d.ts +1 -0
  33. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js +475 -0
  34. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -0
  35. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.d.ts +1 -0
  36. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +287 -0
  37. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -0
  38. package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.d.ts +1 -0
  39. package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js +151 -0
  40. package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js.map +1 -0
  41. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.d.ts +1 -0
  42. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +550 -0
  43. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -0
  44. package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.d.ts +1 -0
  45. package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js +410 -0
  46. package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js.map +1 -0
  47. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.d.ts +1 -0
  48. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +504 -0
  49. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -0
  50. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.d.ts +1 -0
  51. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +483 -0
  52. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -0
  53. package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.d.ts +46 -0
  54. package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js +143 -0
  55. package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js.map +1 -0
  56. package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.d.ts +1 -0
  57. package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js +137 -0
  58. package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js.map +1 -0
  59. package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.d.ts +1 -0
  60. package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js +415 -0
  61. package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js.map +1 -0
  62. package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.d.ts +7 -0
  63. package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js +70 -0
  64. package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js.map +1 -0
  65. package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.d.ts +7 -0
  66. package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js +19 -0
  67. package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js.map +1 -0
  68. package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.d.ts +24 -0
  69. package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js +45 -0
  70. package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js.map +1 -0
  71. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.d.ts +33 -0
  72. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +65 -0
  73. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -0
  74. package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.d.ts +7 -0
  75. package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js +18 -0
  76. package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js.map +1 -0
  77. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +7 -0
  78. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +35 -0
  79. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -0
  80. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +49 -0
  81. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +58 -0
  82. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -0
  83. package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.d.ts +13 -0
  84. package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js +37 -0
  85. package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js.map +1 -0
  86. package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.d.ts +42 -0
  87. package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js +77 -0
  88. package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js.map +1 -0
  89. package/dist/server/src/modules/charges-matcher/index.d.ts +3 -0
  90. package/dist/server/src/modules/charges-matcher/index.js +15 -0
  91. package/dist/server/src/modules/charges-matcher/index.js.map +1 -0
  92. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +48 -0
  93. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +133 -0
  94. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -0
  95. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.d.ts +38 -0
  96. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +248 -0
  97. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -0
  98. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +61 -0
  99. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +153 -0
  100. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -0
  101. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +25 -0
  102. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +114 -0
  103. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -0
  104. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +39 -0
  105. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +189 -0
  106. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -0
  107. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +54 -0
  108. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js +93 -0
  109. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js.map +1 -0
  110. package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.d.ts +2 -0
  111. package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js +22 -0
  112. package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js.map +1 -0
  113. package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.d.ts +2 -0
  114. package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js +24 -0
  115. package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js.map +1 -0
  116. package/dist/server/src/modules/charges-matcher/resolvers/index.d.ts +2 -0
  117. package/dist/server/src/modules/charges-matcher/resolvers/index.js +11 -0
  118. package/dist/server/src/modules/charges-matcher/resolvers/index.js.map +1 -0
  119. package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.d.ts +2 -0
  120. package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js +47 -0
  121. package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js.map +1 -0
  122. package/dist/server/src/modules/charges-matcher/types.d.ts +179 -0
  123. package/dist/server/src/modules/charges-matcher/types.js +14 -0
  124. package/dist/server/src/modules/charges-matcher/types.js.map +1 -0
  125. package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js +2 -2
  126. package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js.map +1 -1
  127. package/dist/server/src/modules-app.js +2 -0
  128. package/dist/server/src/modules-app.js.map +1 -1
  129. package/dist/server/src/shared/types/index.d.ts +1 -1
  130. package/package.json +4 -4
  131. package/src/__generated__/types.ts +87 -0
  132. package/src/modules/charges-matcher/README.md +279 -0
  133. package/src/modules/charges-matcher/__generated__/types.ts +71 -0
  134. package/src/modules/charges-matcher/__tests__/amount-confidence.test.ts +260 -0
  135. package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +714 -0
  136. package/src/modules/charges-matcher/__tests__/auto-match.test.ts +621 -0
  137. package/src/modules/charges-matcher/__tests__/business-confidence.test.ts +177 -0
  138. package/src/modules/charges-matcher/__tests__/candidate-filter.test.ts +238 -0
  139. package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +374 -0
  140. package/src/modules/charges-matcher/__tests__/currency-confidence.test.ts +164 -0
  141. package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +291 -0
  142. package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +614 -0
  143. package/src/modules/charges-matcher/__tests__/document-amount.test.ts +352 -0
  144. package/src/modules/charges-matcher/__tests__/document-business.test.ts +192 -0
  145. package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +659 -0
  146. package/src/modules/charges-matcher/__tests__/overall-confidence.test.ts +502 -0
  147. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +556 -0
  148. package/src/modules/charges-matcher/__tests__/single-match.test.ts +608 -0
  149. package/src/modules/charges-matcher/__tests__/test-helpers.ts +174 -0
  150. package/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts +177 -0
  151. package/src/modules/charges-matcher/__tests__/transaction-aggregator.test.ts +547 -0
  152. package/src/modules/charges-matcher/documentation/README.md +331 -0
  153. package/src/modules/charges-matcher/documentation/SPEC.md +1503 -0
  154. package/src/modules/charges-matcher/documentation/TODO.md +799 -0
  155. package/src/modules/charges-matcher/helpers/amount-confidence.helper.ts +88 -0
  156. package/src/modules/charges-matcher/helpers/business-confidence.helper.ts +23 -0
  157. package/src/modules/charges-matcher/helpers/candidate-filter.helper.ts +56 -0
  158. package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +100 -0
  159. package/src/modules/charges-matcher/helpers/currency-confidence.helper.ts +22 -0
  160. package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +41 -0
  161. package/src/modules/charges-matcher/helpers/document-amount.helper.ts +77 -0
  162. package/src/modules/charges-matcher/helpers/document-business.helper.ts +54 -0
  163. package/src/modules/charges-matcher/helpers/overall-confidence.helper.ts +90 -0
  164. package/src/modules/charges-matcher/index.ts +17 -0
  165. package/src/modules/charges-matcher/providers/auto-match.provider.ts +176 -0
  166. package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +322 -0
  167. package/src/modules/charges-matcher/providers/document-aggregator.ts +211 -0
  168. package/src/modules/charges-matcher/providers/match-scorer.provider.ts +154 -0
  169. package/src/modules/charges-matcher/providers/single-match.provider.ts +252 -0
  170. package/src/modules/charges-matcher/providers/transaction-aggregator.ts +131 -0
  171. package/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.ts +23 -0
  172. package/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.ts +25 -0
  173. package/src/modules/charges-matcher/resolvers/index.ts +12 -0
  174. package/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.ts +47 -0
  175. package/src/modules/charges-matcher/types.ts +200 -0
  176. package/src/modules/documents/resolvers/document-suggestions.resolver.ts +2 -2
  177. package/src/modules-app.ts +2 -0
  178. package/src/shared/types/index.ts +1 -1
@@ -0,0 +1,614 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ aggregateDocuments,
4
+ type Document,
5
+ } from '../providers/document-aggregator.js';
6
+ import type { DocumentType } from '../helpers/document-amount.helper.js';
7
+
8
+ describe('Document Aggregator', () => {
9
+ const USER_ID = 'user-123';
10
+
11
+ // Helper to create test documents
12
+ const createDocument = (overrides: Partial<Document> = {}): Document => ({
13
+ id: 'doc-' + Math.random().toString(36).substr(2, 9),
14
+ charge_id: 'charge-123',
15
+ creditor_id: USER_ID, // Default: user is creditor
16
+ debtor_id: 'business-1', // Default: business is debtor
17
+ currency_code: 'USD',
18
+ date: new Date('2024-01-15'),
19
+ total_amount: 100,
20
+ type: 'INVOICE',
21
+ serial_number: null,
22
+ image_url: null,
23
+ file_url: null,
24
+ ...overrides,
25
+ });
26
+
27
+ describe('Single Document', () => {
28
+ it('should return single document with normalized amount', () => {
29
+ const document = createDocument({
30
+ total_amount: 150.5,
31
+ currency_code: 'USD',
32
+ creditor_id: USER_ID,
33
+ debtor_id: 'business-1',
34
+ type: 'INVOICE',
35
+ date: new Date('2024-01-15'),
36
+ serial_number: 'INV-001',
37
+ });
38
+
39
+ const result = aggregateDocuments([document], USER_ID);
40
+
41
+ expect(result.amount).toBe(150.5); // Positive: business is debtor
42
+ expect(result.currency).toBe('USD');
43
+ expect(result.businessId).toBe('business-1');
44
+ expect(result.date).toEqual(new Date('2024-01-15'));
45
+ expect(result.type).toBe('INVOICE');
46
+ expect(result.description).toBe('INV-001');
47
+ });
48
+
49
+ it('should normalize amount when business is creditor', () => {
50
+ const document = createDocument({
51
+ total_amount: 200,
52
+ creditor_id: 'business-1', // Business is creditor
53
+ debtor_id: USER_ID, // User is debtor
54
+ type: 'INVOICE',
55
+ });
56
+
57
+ const result = aggregateDocuments([document], USER_ID);
58
+
59
+ expect(result.amount).toBe(-200); // Negative: business is creditor
60
+ expect(result.businessId).toBe('business-1');
61
+ });
62
+
63
+ it('should handle credit invoice normalization', () => {
64
+ const document = createDocument({
65
+ total_amount: 100,
66
+ creditor_id: 'business-1', // Business is creditor
67
+ debtor_id: USER_ID, // User is debtor
68
+ type: 'CREDIT_INVOICE',
69
+ });
70
+
71
+ const result = aggregateDocuments([document], USER_ID);
72
+
73
+ expect(result.amount).toBe(100); // Positive: double negation (-1 * -1)
74
+ expect(result.type).toBe('CREDIT_INVOICE');
75
+ });
76
+
77
+ it('should use file_url as description when serial_number is null', () => {
78
+ const document = createDocument({
79
+ serial_number: null,
80
+ file_url: 'https://example.com/files/invoice_2024.pdf',
81
+ });
82
+
83
+ const result = aggregateDocuments([document], USER_ID);
84
+
85
+ expect(result.description).toBe('invoice_2024.pdf');
86
+ });
87
+
88
+ it('should use image_url as description when both serial_number and file_url are null', () => {
89
+ const document = createDocument({
90
+ serial_number: null,
91
+ file_url: null,
92
+ image_url: 'https://example.com/images/scan_001.jpg',
93
+ });
94
+
95
+ const result = aggregateDocuments([document], USER_ID);
96
+
97
+ expect(result.description).toBe('scan_001.jpg');
98
+ });
99
+
100
+ it('should use document ID as fallback description', () => {
101
+ const document = createDocument({
102
+ id: 'doc-abc123def456',
103
+ serial_number: null,
104
+ file_url: null,
105
+ image_url: null,
106
+ });
107
+
108
+ const result = aggregateDocuments([document], USER_ID);
109
+
110
+ expect(result.description).toBe('Doc-doc-abc1');
111
+ });
112
+ });
113
+
114
+ describe('Multiple Invoices', () => {
115
+ it('should sum normalized amounts correctly', () => {
116
+ const documents = [
117
+ createDocument({ total_amount: 100, type: 'INVOICE' }), // +100
118
+ createDocument({ total_amount: 200, type: 'INVOICE' }), // +200
119
+ createDocument({ total_amount: 50, type: 'INVOICE' }), // +50
120
+ ];
121
+
122
+ const result = aggregateDocuments(documents, USER_ID);
123
+
124
+ expect(result.amount).toBe(350);
125
+ expect(result.type).toBe('INVOICE');
126
+ });
127
+
128
+ it('should handle mixed regular and credit invoices', () => {
129
+ const documents = [
130
+ createDocument({
131
+ total_amount: 500,
132
+ type: 'INVOICE',
133
+ creditor_id: USER_ID,
134
+ debtor_id: 'business-1',
135
+ }), // +500
136
+ createDocument({
137
+ total_amount: 100,
138
+ type: 'CREDIT_INVOICE',
139
+ creditor_id: USER_ID,
140
+ debtor_id: 'business-1',
141
+ }), // -100
142
+ ];
143
+
144
+ const result = aggregateDocuments(documents, USER_ID);
145
+
146
+ expect(result.amount).toBe(400); // 500 - 100
147
+ });
148
+
149
+ it('should handle invoices where business is creditor', () => {
150
+ const documents = [
151
+ createDocument({
152
+ total_amount: 200,
153
+ creditor_id: 'business-1',
154
+ debtor_id: USER_ID,
155
+ }), // -200
156
+ createDocument({
157
+ total_amount: 100,
158
+ creditor_id: 'business-1',
159
+ debtor_id: USER_ID,
160
+ }), // -100
161
+ ];
162
+
163
+ const result = aggregateDocuments(documents, USER_ID);
164
+
165
+ expect(result.amount).toBe(-300);
166
+ });
167
+ });
168
+
169
+ describe('Multiple Receipts', () => {
170
+ it('should sum receipt amounts correctly', () => {
171
+ const documents = [
172
+ createDocument({ total_amount: 50, type: 'RECEIPT' }),
173
+ createDocument({ total_amount: 75, type: 'RECEIPT' }),
174
+ ];
175
+
176
+ const result = aggregateDocuments(documents, USER_ID);
177
+
178
+ expect(result.amount).toBe(125);
179
+ expect(result.type).toBe('RECEIPT');
180
+ });
181
+
182
+ it('should handle invoice-receipts', () => {
183
+ const documents = [
184
+ createDocument({ total_amount: 100, type: 'INVOICE_RECEIPT' }),
185
+ createDocument({ total_amount: 150, type: 'INVOICE_RECEIPT' }),
186
+ ];
187
+
188
+ const result = aggregateDocuments(documents, USER_ID);
189
+
190
+ expect(result.amount).toBe(250);
191
+ expect(result.type).toBe('INVOICE_RECEIPT');
192
+ });
193
+ });
194
+
195
+ describe('Type Priority Filtering', () => {
196
+ it('should use only invoices when both invoices and receipts exist', () => {
197
+ const documents = [
198
+ createDocument({ total_amount: 100, type: 'INVOICE', serial_number: 'INV-001' }),
199
+ createDocument({ total_amount: 200, type: 'RECEIPT', serial_number: 'REC-001' }),
200
+ createDocument({ total_amount: 50, type: 'INVOICE', serial_number: 'INV-002' }),
201
+ ];
202
+
203
+ const result = aggregateDocuments(documents, USER_ID);
204
+
205
+ expect(result.amount).toBe(150); // Only invoices: 100 + 50
206
+ expect(result.type).toBe('INVOICE');
207
+ expect(result.description).toContain('INV-001');
208
+ expect(result.description).toContain('INV-002');
209
+ expect(result.description).not.toContain('REC-001'); // Receipt excluded
210
+ });
211
+
212
+ it('should use only credit invoices when both credit invoices and receipts exist', () => {
213
+ const documents = [
214
+ createDocument({ total_amount: 100, type: 'CREDIT_INVOICE' }),
215
+ createDocument({ total_amount: 200, type: 'RECEIPT' }),
216
+ createDocument({ total_amount: 50, type: 'CREDIT_INVOICE' }),
217
+ ];
218
+
219
+ const result = aggregateDocuments(documents, USER_ID);
220
+
221
+ // Credit invoices with user as creditor (business debtor): negate
222
+ expect(result.amount).toBe(-150); // -(100 + 50)
223
+ expect(result.type).toBe('CREDIT_INVOICE');
224
+ });
225
+
226
+ it('should use only invoices/credit-invoices when mixed with invoice-receipts', () => {
227
+ const documents = [
228
+ createDocument({ total_amount: 100, type: 'INVOICE' }),
229
+ createDocument({ total_amount: 200, type: 'INVOICE_RECEIPT' }), // Should be excluded
230
+ createDocument({ total_amount: 50, type: 'CREDIT_INVOICE' }),
231
+ ];
232
+
233
+ const result = aggregateDocuments(documents, USER_ID);
234
+
235
+ expect(result.amount).toBe(50); // 100 - 50 (credit invoice negates)
236
+ });
237
+
238
+ it('should use receipts when no invoices exist', () => {
239
+ const documents = [
240
+ createDocument({ total_amount: 100, type: 'RECEIPT' }),
241
+ createDocument({ total_amount: 50, type: 'INVOICE_RECEIPT' }),
242
+ ];
243
+
244
+ const result = aggregateDocuments(documents, USER_ID);
245
+
246
+ expect(result.amount).toBe(150);
247
+ expect(result.type).toBe('RECEIPT');
248
+ });
249
+ });
250
+
251
+ describe('Currency Validation', () => {
252
+ it('should throw error with multiple currencies', () => {
253
+ const documents = [
254
+ createDocument({ currency_code: 'USD' }),
255
+ createDocument({ currency_code: 'EUR' }),
256
+ ];
257
+
258
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
259
+ 'Cannot aggregate documents: multiple currencies found (USD, EUR)',
260
+ );
261
+ });
262
+
263
+ it('should throw error with three different currencies', () => {
264
+ const documents = [
265
+ createDocument({ currency_code: 'USD' }),
266
+ createDocument({ currency_code: 'EUR' }),
267
+ createDocument({ currency_code: 'GBP' }),
268
+ ];
269
+
270
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(/multiple currencies found/);
271
+ });
272
+
273
+ it('should not throw when all have same currency', () => {
274
+ const documents = [
275
+ createDocument({ currency_code: 'ILS' }),
276
+ createDocument({ currency_code: 'ILS' }),
277
+ createDocument({ currency_code: 'ILS' }),
278
+ ];
279
+
280
+ expect(() => aggregateDocuments(documents, USER_ID)).not.toThrow();
281
+ });
282
+
283
+ it('should throw error when all currencies are null', () => {
284
+ const documents = [
285
+ createDocument({ currency_code: null }),
286
+ createDocument({ currency_code: null }),
287
+ ];
288
+
289
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
290
+ 'Cannot aggregate documents: all documents have null currency_code',
291
+ );
292
+ });
293
+
294
+ it('should ignore currency of filtered-out receipts', () => {
295
+ const documents = [
296
+ createDocument({ currency_code: 'USD', type: 'INVOICE' }),
297
+ createDocument({ currency_code: 'EUR', type: 'RECEIPT' }), // Different currency but will be filtered
298
+ createDocument({ currency_code: 'USD', type: 'INVOICE' }),
299
+ ];
300
+
301
+ const result = aggregateDocuments(documents, USER_ID);
302
+
303
+ expect(result.currency).toBe('USD');
304
+ });
305
+ });
306
+
307
+ describe('Business ID Validation', () => {
308
+ it('should throw error with multiple different business IDs', () => {
309
+ const documents = [
310
+ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }),
311
+ createDocument({ debtor_id: 'business-2', creditor_id: USER_ID }),
312
+ ];
313
+
314
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
315
+ 'Cannot aggregate documents: multiple business IDs found (business-1, business-2)',
316
+ );
317
+ });
318
+
319
+ it('should return null when all business IDs are null', () => {
320
+ const documents = [
321
+ createDocument({ debtor_id: null, creditor_id: USER_ID }),
322
+ createDocument({ debtor_id: null, creditor_id: USER_ID }),
323
+ ];
324
+
325
+ const result = aggregateDocuments(documents, USER_ID);
326
+
327
+ expect(result.businessId).toBeNull();
328
+ });
329
+
330
+ it('should return single non-null business ID', () => {
331
+ const documents = [
332
+ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }),
333
+ createDocument({ debtor_id: null, creditor_id: USER_ID }),
334
+ createDocument({ debtor_id: null, creditor_id: USER_ID }),
335
+ ];
336
+
337
+ const result = aggregateDocuments(documents, USER_ID);
338
+
339
+ expect(result.businessId).toBe('business-1');
340
+ });
341
+
342
+ it('should not throw when all have same business ID', () => {
343
+ const documents = [
344
+ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }),
345
+ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }),
346
+ ];
347
+
348
+ const result = aggregateDocuments(documents, USER_ID);
349
+
350
+ expect(result.businessId).toBe('business-1');
351
+ });
352
+ });
353
+
354
+ describe('Date Selection', () => {
355
+ it('should select latest date', () => {
356
+ const documents = [
357
+ createDocument({ date: new Date('2024-01-10') }),
358
+ createDocument({ date: new Date('2024-03-15') }), // Latest
359
+ createDocument({ date: new Date('2024-02-20') }),
360
+ ];
361
+
362
+ const result = aggregateDocuments(documents, USER_ID);
363
+
364
+ expect(result.date).toEqual(new Date('2024-03-15'));
365
+ });
366
+
367
+ it('should handle dates spanning years', () => {
368
+ const documents = [
369
+ createDocument({ date: new Date('2023-12-31') }),
370
+ createDocument({ date: new Date('2024-02-01') }), // Latest
371
+ createDocument({ date: new Date('2024-01-15') }),
372
+ ];
373
+
374
+ const result = aggregateDocuments(documents, USER_ID);
375
+
376
+ expect(result.date).toEqual(new Date('2024-02-01'));
377
+ });
378
+
379
+ it('should handle same date for all documents', () => {
380
+ const documents = [
381
+ createDocument({ date: new Date('2024-01-15') }),
382
+ createDocument({ date: new Date('2024-01-15') }),
383
+ ];
384
+
385
+ const result = aggregateDocuments(documents, USER_ID);
386
+
387
+ expect(result.date).toEqual(new Date('2024-01-15'));
388
+ });
389
+
390
+ it('should throw error when all dates are null', () => {
391
+ const documents = [
392
+ createDocument({ date: null }),
393
+ createDocument({ date: null }),
394
+ ];
395
+
396
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
397
+ 'Cannot aggregate documents: all documents have null date',
398
+ );
399
+ });
400
+
401
+ it('should handle mix of null and valid dates', () => {
402
+ const documents = [
403
+ createDocument({ date: null }),
404
+ createDocument({ date: new Date('2024-01-15') }),
405
+ createDocument({ date: new Date('2024-02-20') }), // Latest
406
+ ];
407
+
408
+ const result = aggregateDocuments(documents, USER_ID);
409
+
410
+ expect(result.date).toEqual(new Date('2024-02-20'));
411
+ });
412
+ });
413
+
414
+ describe('Description Concatenation', () => {
415
+ it('should concatenate serial numbers with line breaks', () => {
416
+ const documents = [
417
+ createDocument({ serial_number: 'INV-001' }),
418
+ createDocument({ serial_number: 'INV-002' }),
419
+ createDocument({ serial_number: 'INV-003' }),
420
+ ];
421
+
422
+ const result = aggregateDocuments(documents, USER_ID);
423
+
424
+ expect(result.description).toBe('INV-001\nINV-002\nINV-003');
425
+ });
426
+
427
+ it('should prefer serial_number over file URLs', () => {
428
+ const documents = [
429
+ createDocument({ serial_number: 'INV-001', file_url: 'file1.pdf' }),
430
+ createDocument({ serial_number: null, file_url: 'file2.pdf' }),
431
+ ];
432
+
433
+ const result = aggregateDocuments(documents, USER_ID);
434
+
435
+ expect(result.description).toBe('INV-001\nfile2.pdf');
436
+ });
437
+
438
+ it('should use image URLs when no serial or file URL', () => {
439
+ const documents = [
440
+ createDocument({ serial_number: null, file_url: null, image_url: 'img1.jpg' }),
441
+ createDocument({ serial_number: null, file_url: null, image_url: 'img2.jpg' }),
442
+ ];
443
+
444
+ const result = aggregateDocuments(documents, USER_ID);
445
+
446
+ expect(result.description).toBe('img1.jpg\nimg2.jpg');
447
+ });
448
+
449
+ it('should fallback to document ID when no other identifiers', () => {
450
+ const documents = [
451
+ createDocument({
452
+ id: 'doc-abc123',
453
+ serial_number: null,
454
+ file_url: null,
455
+ image_url: null,
456
+ }),
457
+ createDocument({ id: 'doc-def456', serial_number: 'INV-001' }),
458
+ ];
459
+
460
+ const result = aggregateDocuments(documents, USER_ID);
461
+
462
+ expect(result.description).toContain('Doc-doc-abc1');
463
+ expect(result.description).toContain('INV-001');
464
+ });
465
+
466
+ it('should handle empty descriptions gracefully', () => {
467
+ const documents = [
468
+ createDocument({ serial_number: ' ', file_url: null, image_url: null, id: 'doc-123' }),
469
+ ];
470
+
471
+ const result = aggregateDocuments(documents, USER_ID);
472
+
473
+ expect(result.description).toBe('Doc-doc-123');
474
+ });
475
+ });
476
+
477
+ describe('Business Extraction Error Propagation', () => {
478
+ it('should propagate error when both creditor and debtor are user', () => {
479
+ const documents = [
480
+ createDocument({
481
+ creditor_id: USER_ID,
482
+ debtor_id: USER_ID, // Invalid: both are user
483
+ }),
484
+ ];
485
+
486
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
487
+ /both creditor_id and debtor_id equal to user ID/,
488
+ );
489
+ });
490
+
491
+ it('should propagate error when neither creditor nor debtor is user', () => {
492
+ const documents = [
493
+ createDocument({
494
+ creditor_id: 'business-1',
495
+ debtor_id: 'business-2', // Invalid: neither is user
496
+ }),
497
+ ];
498
+
499
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
500
+ /neither creditor_id nor debtor_id equal to user ID/,
501
+ );
502
+ });
503
+
504
+ it('should propagate error when both IDs are null', () => {
505
+ const documents = [
506
+ createDocument({
507
+ creditor_id: null,
508
+ debtor_id: null, // Invalid: both null
509
+ }),
510
+ ];
511
+
512
+ expect(() => aggregateDocuments(documents, USER_ID)).toThrow(
513
+ /both creditor_id and debtor_id as null/,
514
+ );
515
+ });
516
+ });
517
+
518
+ describe('Input Validation', () => {
519
+ it('should throw error for empty array', () => {
520
+ expect(() => aggregateDocuments([], USER_ID)).toThrow(
521
+ 'Cannot aggregate documents: array is empty',
522
+ );
523
+ });
524
+
525
+ it('should throw error for null input', () => {
526
+ expect(() => aggregateDocuments(null as any, USER_ID)).toThrow(
527
+ 'Cannot aggregate documents: array is empty',
528
+ );
529
+ });
530
+
531
+ it('should throw error for undefined input', () => {
532
+ expect(() => aggregateDocuments(undefined as any, USER_ID)).toThrow(
533
+ 'Cannot aggregate documents: array is empty',
534
+ );
535
+ });
536
+ });
537
+
538
+ describe('Complex Scenarios', () => {
539
+ it('should handle real-world mixed document scenario', () => {
540
+ const documents = [
541
+ createDocument({
542
+ total_amount: 1000,
543
+ type: 'INVOICE',
544
+ creditor_id: USER_ID,
545
+ debtor_id: 'business-abc',
546
+ date: new Date('2024-01-15'),
547
+ serial_number: 'INV-2024-001',
548
+ currency_code: 'USD',
549
+ }),
550
+ createDocument({
551
+ total_amount: 500,
552
+ type: 'RECEIPT', // Will be filtered out
553
+ creditor_id: USER_ID,
554
+ debtor_id: 'business-abc',
555
+ date: new Date('2024-01-20'),
556
+ serial_number: 'REC-2024-001',
557
+ currency_code: 'EUR', // Different currency but will be filtered
558
+ }),
559
+ createDocument({
560
+ total_amount: 200,
561
+ type: 'CREDIT_INVOICE',
562
+ creditor_id: USER_ID,
563
+ debtor_id: 'business-abc',
564
+ date: new Date('2024-01-25'), // Latest
565
+ serial_number: 'CRD-2024-001',
566
+ currency_code: 'USD',
567
+ }),
568
+ ];
569
+
570
+ const result = aggregateDocuments(documents, USER_ID);
571
+
572
+ expect(result.amount).toBe(800); // 1000 - 200 (credit invoice)
573
+ expect(result.currency).toBe('USD');
574
+ expect(result.businessId).toBe('business-abc');
575
+ expect(result.date).toEqual(new Date('2024-01-25')); // Latest among invoices
576
+ expect(result.type).toBe('INVOICE'); // First invoice type
577
+ expect(result.description).toContain('INV-2024-001');
578
+ expect(result.description).toContain('CRD-2024-001');
579
+ expect(result.description).not.toContain('REC-2024-001'); // Receipt excluded
580
+ });
581
+
582
+ it('should handle documents with null business IDs', () => {
583
+ const documents = [
584
+ createDocument({
585
+ total_amount: 100,
586
+ creditor_id: USER_ID,
587
+ debtor_id: null, // No business
588
+ }),
589
+ createDocument({
590
+ total_amount: 200,
591
+ creditor_id: USER_ID,
592
+ debtor_id: null, // No business
593
+ }),
594
+ ];
595
+
596
+ const result = aggregateDocuments(documents, USER_ID);
597
+
598
+ expect(result.amount).toBe(300);
599
+ expect(result.businessId).toBeNull();
600
+ });
601
+
602
+ it('should handle cryptocurrency documents', () => {
603
+ const documents = [
604
+ createDocument({ total_amount: 0.5, currency_code: 'ETH' }),
605
+ createDocument({ total_amount: 0.3, currency_code: 'ETH' }),
606
+ ];
607
+
608
+ const result = aggregateDocuments(documents, USER_ID);
609
+
610
+ expect(result.amount).toBe(0.8);
611
+ expect(result.currency).toBe('ETH');
612
+ });
613
+ });
614
+ });