@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,608 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Document, DocumentCharge, Transaction, TransactionCharge } from '../types.js';
3
+ import {
4
+ findMatches,
5
+ } from '../providers/single-match.provider.js';
6
+ import { createMockTransaction, createMockDocument } from './test-helpers.js';
7
+
8
+ // Test user and business IDs
9
+ const USER_ID = 'user-123';
10
+ const BUSINESS_A = 'business-a';
11
+ const BUSINESS_B = 'business-b';
12
+
13
+ // Helper to create transaction charge
14
+ function createTxCharge(
15
+ chargeId: string,
16
+ transactions: Transaction[],
17
+ ): TransactionCharge {
18
+ return {
19
+ chargeId,
20
+ transactions,
21
+ };
22
+ }
23
+
24
+ // Helper to create document charge
25
+ function createDocCharge(
26
+ chargeId: string,
27
+ documents: Document[],
28
+ ): DocumentCharge {
29
+ return {
30
+ chargeId,
31
+ documents,
32
+ };
33
+ }
34
+
35
+ describe('Single-Match Provider', () => {
36
+ describe('findMatches', () => {
37
+ describe('Valid Transaction Charge → Finds Document Matches', () => {
38
+ it('should find matching document charges for transaction charge', () => {
39
+ const sourceCharge = createTxCharge('tx-charge-1', [
40
+ createMockTransaction({
41
+ amount: "100",
42
+ currency: 'USD',
43
+ event_date: new Date('2024-01-15'),
44
+ }),
45
+ ]);
46
+
47
+ const candidateCharges = [
48
+ createDocCharge('doc-charge-1', [
49
+ createMockDocument({
50
+ total_amount: 100,
51
+ currency_code: 'USD',
52
+ date: new Date('2024-01-15'),
53
+ }),
54
+ ]),
55
+ createDocCharge('doc-charge-2', [
56
+ createMockDocument({
57
+ total_amount: 110,
58
+ currency_code: 'USD',
59
+ date: new Date('2024-01-16'),
60
+ }),
61
+ ]),
62
+ ];
63
+
64
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
65
+
66
+ expect(results).toHaveLength(2);
67
+ expect(results[0].chargeId).toBe('doc-charge-1'); // Perfect match
68
+ expect(results[0].confidenceScore).toBeGreaterThan(0.95);
69
+ expect(results[1].chargeId).toBe('doc-charge-2'); // Partial match
70
+ expect(results[1].confidenceScore).toBeLessThan(0.95);
71
+ });
72
+
73
+ it('should exclude transaction candidates when source is transaction', () => {
74
+ const sourceCharge = createTxCharge('tx-charge-1', [createMockTransaction()]);
75
+
76
+ const candidateCharges = [
77
+ createTxCharge('tx-charge-2', [createMockTransaction()]), // Should be excluded
78
+ createDocCharge('doc-charge-1', [createMockDocument()]), // Should be included
79
+ ];
80
+
81
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
82
+
83
+ expect(results).toHaveLength(1);
84
+ expect(results[0].chargeId).toBe('doc-charge-1');
85
+ });
86
+ });
87
+
88
+ describe('Valid Document Charge → Finds Transaction Matches', () => {
89
+ it('should find matching transaction charges for document charge', () => {
90
+ const sourceCharge = createDocCharge('doc-charge-1', [
91
+ createMockDocument({
92
+ total_amount: 100,
93
+ currency_code: 'USD',
94
+ date: new Date('2024-01-15'),
95
+ }),
96
+ ]);
97
+
98
+ const candidateCharges = [
99
+ createTxCharge('tx-charge-1', [
100
+ createMockTransaction({
101
+ amount: "100",
102
+ currency: 'USD',
103
+ event_date: new Date('2024-01-15'),
104
+ }),
105
+ ]),
106
+ createTxCharge('tx-charge-2', [
107
+ createMockTransaction({
108
+ amount: "90",
109
+ currency: 'USD',
110
+ event_date: new Date('2024-01-16'),
111
+ }),
112
+ ]),
113
+ ];
114
+
115
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
116
+
117
+ expect(results).toHaveLength(2);
118
+ expect(results[0].chargeId).toBe('tx-charge-1'); // Perfect match
119
+ expect(results[1].chargeId).toBe('tx-charge-2'); // Partial match
120
+ });
121
+
122
+ it('should exclude document candidates when source is document', () => {
123
+ const sourceCharge = createDocCharge('doc-charge-1', [createMockDocument()]);
124
+
125
+ const candidateCharges = [
126
+ createDocCharge('doc-charge-2', [createMockDocument()]), // Should be excluded
127
+ createTxCharge('tx-charge-1', [createMockTransaction()]), // Should be included
128
+ ];
129
+
130
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
131
+
132
+ expect(results).toHaveLength(1);
133
+ expect(results[0].chargeId).toBe('tx-charge-1');
134
+ });
135
+ });
136
+
137
+ describe('Matched Charge Input → Throws Error', () => {
138
+ it('should throw error if source has both transactions and documents', () => {
139
+ const matchedCharge = {
140
+ chargeId: 'matched-charge',
141
+ transactions: [createMockTransaction()],
142
+ documents: [createMockDocument()],
143
+ } as any;
144
+
145
+ expect(() => findMatches(matchedCharge, [], USER_ID)).toThrow(
146
+ /already matched.*both transactions and documents/,
147
+ );
148
+ });
149
+
150
+ it('should throw error if source has neither transactions nor documents', () => {
151
+ const emptyCharge = {
152
+ chargeId: 'empty-charge',
153
+ transactions: [],
154
+ documents: [],
155
+ } as any;
156
+
157
+ expect(() => findMatches(emptyCharge, [], USER_ID)).toThrow(
158
+ /no transactions or documents/,
159
+ );
160
+ });
161
+ });
162
+
163
+ describe('Multiple Currencies in Source → Error Propagates', () => {
164
+ it('should throw error for source with mixed currencies', () => {
165
+ const mixedCurrencyCharge = createTxCharge('tx-charge-1', [
166
+ createMockTransaction({ currency: 'USD' }),
167
+ createMockTransaction({ currency: 'EUR' }),
168
+ ]);
169
+
170
+ expect(() => findMatches(mixedCurrencyCharge, [], USER_ID)).toThrow(
171
+ /failed validation.*multiple currencies/,
172
+ );
173
+ });
174
+
175
+ it('should throw error for source with multiple business IDs', () => {
176
+ const mixedBusinessCharge = createTxCharge('tx-charge-1', [
177
+ createMockTransaction({ business_id: BUSINESS_A }),
178
+ createMockTransaction({ business_id: BUSINESS_B }),
179
+ ]);
180
+
181
+ expect(() => findMatches(mixedBusinessCharge, [], USER_ID)).toThrow(
182
+ /failed validation.*multiple business/,
183
+ );
184
+ });
185
+
186
+ it('should throw error for source document with invalid business extraction', () => {
187
+ const invalidDocCharge = createDocCharge('doc-charge-1', [
188
+ createMockDocument({
189
+ creditor_id: 'other-user',
190
+ debtor_id: 'another-user',
191
+ }),
192
+ ]);
193
+
194
+ expect(() => findMatches(invalidDocCharge, [], USER_ID)).toThrow(/failed validation/);
195
+ });
196
+ });
197
+
198
+ describe('No Candidates Found → Empty Array', () => {
199
+ it('should return empty array when no candidates provided', () => {
200
+ const sourceCharge = createTxCharge('tx-charge-1', [createMockTransaction()]);
201
+
202
+ const results = findMatches(sourceCharge, [], USER_ID);
203
+
204
+ expect(results).toEqual([]);
205
+ });
206
+
207
+ it('should return empty array when all candidates are same type', () => {
208
+ const sourceCharge = createTxCharge('tx-charge-1', [createMockTransaction()]);
209
+
210
+ const candidateCharges = [
211
+ createTxCharge('tx-charge-2', [createMockTransaction()]),
212
+ createTxCharge('tx-charge-3', [createMockTransaction()]),
213
+ ];
214
+
215
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
216
+
217
+ expect(results).toEqual([]);
218
+ });
219
+
220
+ it('should return empty array when all candidates outside date window', () => {
221
+ const sourceCharge = createTxCharge('tx-charge-1', [
222
+ createMockTransaction({ event_date: new Date('2024-01-15') }),
223
+ ]);
224
+
225
+ const candidateCharges = [
226
+ createDocCharge('doc-charge-1', [
227
+ createMockDocument({ date: new Date('2023-01-01') }), // 1 year before
228
+ ]),
229
+ createDocCharge('doc-charge-2', [
230
+ createMockDocument({ date: new Date('2025-02-01') }), // 1 year after
231
+ ]),
232
+ ];
233
+
234
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
235
+
236
+ expect(results).toEqual([]);
237
+ });
238
+
239
+ it('should return empty array when all candidates fail scoring', () => {
240
+ const sourceCharge = createTxCharge('tx-charge-1', [
241
+ createMockTransaction({ currency: 'USD' }),
242
+ ]);
243
+
244
+ // Candidates with mixed currencies will fail scoring
245
+ const candidateCharges = [
246
+ createDocCharge('doc-charge-1', [
247
+ createMockDocument({ currency_code: 'USD' }),
248
+ createMockDocument({ currency_code: 'EUR' }),
249
+ ]),
250
+ ];
251
+
252
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
253
+
254
+ expect(results).toEqual([]);
255
+ });
256
+ });
257
+
258
+ describe('Candidate Count Handling', () => {
259
+ it('should return all candidates when fewer than 5', () => {
260
+ const sourceCharge = createTxCharge('tx-charge-1', [createMockTransaction()]);
261
+
262
+ const candidateCharges = [
263
+ createDocCharge('doc-charge-1', [createMockDocument()]),
264
+ createDocCharge('doc-charge-2', [createMockDocument()]),
265
+ createDocCharge('doc-charge-3', [createMockDocument()]),
266
+ ];
267
+
268
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
269
+
270
+ expect(results).toHaveLength(3);
271
+ });
272
+
273
+ it('should return top 5 when more than 5 candidates', () => {
274
+ const sourceCharge = createTxCharge('tx-charge-1', [
275
+ createMockTransaction({ amount: "100" }),
276
+ ]);
277
+
278
+ const candidateCharges = [
279
+ createDocCharge('doc-charge-1', [createMockDocument({ total_amount: 100 })]), // Perfect
280
+ createDocCharge('doc-charge-2', [createMockDocument({ total_amount: 101 })]),
281
+ createDocCharge('doc-charge-3', [createMockDocument({ total_amount: 102 })]),
282
+ createDocCharge('doc-charge-4', [createMockDocument({ total_amount: 103 })]),
283
+ createDocCharge('doc-charge-5', [createMockDocument({ total_amount: 104 })]),
284
+ createDocCharge('doc-charge-6', [createMockDocument({ total_amount: 105 })]),
285
+ createDocCharge('doc-charge-7', [createMockDocument({ total_amount: 106 })]),
286
+ ];
287
+
288
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
289
+
290
+ expect(results).toHaveLength(5);
291
+ expect(results[0].chargeId).toBe('doc-charge-1'); // Best match
292
+ });
293
+
294
+ it('should respect custom maxMatches option', () => {
295
+ const sourceCharge = createTxCharge('tx-charge-1', [createMockTransaction()]);
296
+
297
+ const candidateCharges = [
298
+ createDocCharge('doc-charge-1', [createMockDocument()]),
299
+ createDocCharge('doc-charge-2', [createMockDocument()]),
300
+ createDocCharge('doc-charge-3', [createMockDocument()]),
301
+ createDocCharge('doc-charge-4', [createMockDocument()]),
302
+ createDocCharge('doc-charge-5', [createMockDocument()]),
303
+ ];
304
+
305
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID, { maxMatches: 3 });
306
+
307
+ expect(results).toHaveLength(3);
308
+ });
309
+ });
310
+
311
+ describe('Tie-Breaking by Date Proximity', () => {
312
+ it('should use date proximity as tie-breaker when confidence scores equal', () => {
313
+ const sourceCharge = createTxCharge('tx-charge-1', [
314
+ createMockTransaction({
315
+ amount: "100",
316
+ event_date: new Date('2024-01-15'),
317
+ }),
318
+ ]);
319
+
320
+ // All have same amounts (same confidence), different dates
321
+ const candidateCharges = [
322
+ createDocCharge('doc-charge-far', [
323
+ createMockDocument({
324
+ total_amount: 100,
325
+ date: new Date('2024-01-05'), // 10 days away
326
+ }),
327
+ ]),
328
+ createDocCharge('doc-charge-close', [
329
+ createMockDocument({
330
+ total_amount: 100,
331
+ date: new Date('2024-01-14'), // 1 day away
332
+ }),
333
+ ]),
334
+ createDocCharge('doc-charge-medium', [
335
+ createMockDocument({
336
+ total_amount: 100,
337
+ date: new Date('2024-01-10'), // 5 days away
338
+ }),
339
+ ]),
340
+ ];
341
+
342
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
343
+
344
+ expect(results[0].chargeId).toBe('doc-charge-close'); // Closest date wins
345
+ expect(results[1].chargeId).toBe('doc-charge-medium');
346
+ expect(results[2].chargeId).toBe('doc-charge-far');
347
+ });
348
+
349
+ it('should include dateProximity in results', () => {
350
+ const sourceCharge = createTxCharge('tx-charge-1', [
351
+ createMockTransaction({ event_date: new Date('2024-01-15') }),
352
+ ]);
353
+
354
+ const candidateCharges = [
355
+ createDocCharge('doc-charge-1', [
356
+ createMockDocument({ date: new Date('2024-01-14') }), // 1 day
357
+ ]),
358
+ ];
359
+
360
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
361
+
362
+ expect(results[0].dateProximity).toBe(1);
363
+ });
364
+ });
365
+
366
+ describe('Date Window Filtering', () => {
367
+ it('should filter candidates outside 12-month window by default', () => {
368
+ const sourceCharge = createTxCharge('tx-charge-1', [
369
+ createMockTransaction({ event_date: new Date('2024-01-15') }),
370
+ ]);
371
+
372
+ const candidateCharges = [
373
+ createDocCharge('doc-inside', [
374
+ createMockDocument({ date: new Date('2024-06-15') }), // 5 months
375
+ ]),
376
+ createDocCharge('doc-outside', [
377
+ createMockDocument({ date: new Date('2025-02-15') }), // 13 months
378
+ ]),
379
+ ];
380
+
381
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
382
+
383
+ expect(results).toHaveLength(1);
384
+ expect(results[0].chargeId).toBe('doc-inside');
385
+ });
386
+
387
+ it('should respect custom dateWindowMonths option', () => {
388
+ const sourceCharge = createTxCharge('tx-charge-1', [
389
+ createMockTransaction({ event_date: new Date('2024-01-15') }),
390
+ ]);
391
+
392
+ const candidateCharges = [
393
+ createDocCharge('doc-inside', [
394
+ createMockDocument({ date: new Date('2024-03-15') }), // 2 months
395
+ ]),
396
+ createDocCharge('doc-outside', [
397
+ createMockDocument({ date: new Date('2024-05-15') }), // 4 months
398
+ ]),
399
+ ];
400
+
401
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID, {
402
+ dateWindowMonths: 3,
403
+ });
404
+
405
+ expect(results).toHaveLength(1);
406
+ expect(results[0].chargeId).toBe('doc-inside');
407
+ });
408
+
409
+ it('should include candidates on exact window boundary', () => {
410
+ const sourceCharge = createTxCharge('tx-charge-1', [
411
+ createMockTransaction({ event_date: new Date('2024-01-15') }),
412
+ ]);
413
+
414
+ const candidateCharges = [
415
+ createDocCharge('doc-boundary', [
416
+ createMockDocument({ date: new Date('2025-01-15') }), // Exactly 12 months
417
+ ]),
418
+ ];
419
+
420
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
421
+
422
+ expect(results).toHaveLength(1);
423
+ });
424
+ });
425
+
426
+ describe('Fee Transactions Excluded', () => {
427
+ it('should handle source with fee transactions via aggregator', () => {
428
+ const sourceCharge = createTxCharge('tx-charge-1', [
429
+ createMockTransaction({ amount: "100", is_fee: false }),
430
+ createMockTransaction({ amount: "5", is_fee: true }), // Should be excluded by aggregator
431
+ ]);
432
+
433
+ const candidateCharges = [
434
+ createDocCharge('doc-charge-1', [createMockDocument({ total_amount: 100 })]),
435
+ ];
436
+
437
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
438
+
439
+ expect(results).toHaveLength(1);
440
+ expect(results[0].confidenceScore).toBeGreaterThan(0.95); // Matches 100, not 105
441
+ });
442
+
443
+ it('should throw error if all source transactions are fees', () => {
444
+ const allFeesCharge = createTxCharge('tx-charge-1', [
445
+ createMockTransaction({ is_fee: true }),
446
+ createMockTransaction({ is_fee: true }),
447
+ ]);
448
+
449
+ expect(() => findMatches(allFeesCharge, [], USER_ID)).toThrow(
450
+ /all transactions are marked as fees/,
451
+ );
452
+ });
453
+ });
454
+
455
+ describe('Same ChargeId → Throws Error', () => {
456
+ it('should throw error when candidate has same chargeId as source', () => {
457
+ const sourceCharge = createTxCharge('same-charge-id', [createMockTransaction()]);
458
+
459
+ const candidateCharges = [
460
+ createDocCharge('same-charge-id', [createMockDocument()]), // Same ID!
461
+ ];
462
+
463
+ expect(() => findMatches(sourceCharge, candidateCharges, USER_ID)).toThrow(
464
+ /same ID as source charge/,
465
+ );
466
+ });
467
+ });
468
+
469
+ describe('Various Confidence Levels', () => {
470
+ it('should rank matches by confidence level correctly', () => {
471
+ const sourceCharge = createTxCharge('tx-charge-1', [
472
+ createMockTransaction({
473
+ amount: "100",
474
+ currency: 'USD',
475
+ event_date: new Date('2024-01-15'),
476
+ business_id: BUSINESS_A,
477
+ }),
478
+ ]);
479
+
480
+ const candidateCharges = [
481
+ // Perfect match - all fields align
482
+ createDocCharge('perfect', [
483
+ createMockDocument({
484
+ total_amount: 100,
485
+ currency_code: 'USD',
486
+ date: new Date('2024-01-15'),
487
+ creditor_id: BUSINESS_A,
488
+ debtor_id: USER_ID,
489
+ }),
490
+ ]),
491
+ // Good match - date difference
492
+ createDocCharge('good', [
493
+ createMockDocument({
494
+ total_amount: 100,
495
+ currency_code: 'USD',
496
+ date: new Date('2024-01-20'), // 5 days off
497
+ creditor_id: BUSINESS_A,
498
+ debtor_id: USER_ID,
499
+ }),
500
+ ]),
501
+ // Medium match - slight amount difference
502
+ createDocCharge('medium', [
503
+ createMockDocument({
504
+ total_amount: 100.5,
505
+ currency_code: 'USD',
506
+ date: new Date('2024-01-15'),
507
+ creditor_id: BUSINESS_A,
508
+ debtor_id: USER_ID,
509
+ }),
510
+ ]),
511
+ // Poor match - business mismatch
512
+ createDocCharge('poor', [
513
+ createMockDocument({
514
+ total_amount: 100,
515
+ currency_code: 'USD',
516
+ date: new Date('2024-01-15'),
517
+ creditor_id: BUSINESS_B, // Different business
518
+ debtor_id: USER_ID,
519
+ }),
520
+ ]),
521
+ ];
522
+
523
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
524
+
525
+ expect(results).toHaveLength(4);
526
+ expect(results[0].chargeId).toBe('perfect');
527
+ expect(results[0].confidenceScore).toBeGreaterThan(0.95);
528
+ expect(results[1].chargeId).toBe('good');
529
+ expect(results[2].chargeId).toBe('medium');
530
+ expect(results[3].chargeId).toBe('poor');
531
+ });
532
+
533
+ it('should include component scores in results', () => {
534
+ const sourceCharge = createTxCharge('tx-charge-1', [createMockTransaction()]);
535
+
536
+ const candidateCharges = [
537
+ createDocCharge('doc-charge-1', [createMockDocument()]),
538
+ ];
539
+
540
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
541
+
542
+ expect(results[0].components).toBeDefined();
543
+ expect(results[0].components.amount).toBeGreaterThanOrEqual(0);
544
+ expect(results[0].components.currency).toBeGreaterThanOrEqual(0);
545
+ expect(results[0].components.business).toBeGreaterThanOrEqual(0);
546
+ expect(results[0].components.date).toBeGreaterThanOrEqual(0);
547
+ });
548
+ });
549
+
550
+ describe('Edge Cases', () => {
551
+ it('should handle multiple transactions in source charge', () => {
552
+ const sourceCharge = createTxCharge('tx-charge-1', [
553
+ createMockTransaction({ amount: "50" }),
554
+ createMockTransaction({ amount: "50" }),
555
+ ]);
556
+
557
+ const candidateCharges = [
558
+ createDocCharge('doc-charge-1', [createMockDocument({ total_amount: 100 })]),
559
+ ];
560
+
561
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
562
+
563
+ expect(results).toHaveLength(1);
564
+ expect(results[0].confidenceScore).toBeGreaterThan(0.95); // 50+50 = 100
565
+ });
566
+
567
+ it('should handle multiple documents in candidate charge', () => {
568
+ const sourceCharge = createTxCharge('tx-charge-1', [
569
+ createMockTransaction({ amount: "100" }),
570
+ ]);
571
+
572
+ const candidateCharges = [
573
+ createDocCharge('doc-charge-1', [
574
+ createMockDocument({ total_amount: 60 }),
575
+ createMockDocument({ total_amount: 40 }),
576
+ ]),
577
+ ];
578
+
579
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
580
+
581
+ expect(results).toHaveLength(1);
582
+ expect(results[0].confidenceScore).toBeGreaterThan(0.95); // 60+40 = 100
583
+ });
584
+
585
+ it('should handle negative amounts (credit transactions)', () => {
586
+ const sourceCharge = createTxCharge('tx-charge-1', [
587
+ createMockTransaction({ amount: "-100" }),
588
+ ]);
589
+
590
+ const candidateCharges = [
591
+ createDocCharge('doc-charge-1', [
592
+ createMockDocument({
593
+ total_amount: 100,
594
+ type: 'CREDIT_INVOICE',
595
+ creditor_id: USER_ID, // User is creditor for credit invoice
596
+ debtor_id: BUSINESS_A,
597
+ }),
598
+ ]),
599
+ ];
600
+
601
+ const results = findMatches(sourceCharge, candidateCharges, USER_ID);
602
+
603
+ expect(results).toHaveLength(1);
604
+ expect(results[0].confidenceScore).toBeGreaterThan(0.9);
605
+ });
606
+ });
607
+ });
608
+ });