@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,621 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ChargeWithData } from '../types.js';
3
+ import {
4
+ determineMergeDirection,
5
+ processChargeForAutoMatch,
6
+ } from '../providers/auto-match.provider.js';
7
+ import { createMockTransaction, createMockDocument } from './test-helpers.js';
8
+
9
+ // Test constants
10
+ const USER_ID = 'user-123';
11
+ const BUSINESS_A = 'business-a';
12
+
13
+ // Helper to create a charge with data
14
+ function createCharge(overrides: Partial<ChargeWithData> = {}): ChargeWithData {
15
+ return {
16
+ chargeId: `charge-${Math.random()}`,
17
+ ownerId: USER_ID,
18
+ type: 'TRANSACTION_ONLY' as any,
19
+ transactions: [],
20
+ documents: [],
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ describe('processChargeForAutoMatch', () => {
26
+ describe('Single high-confidence match', () => {
27
+ it('should return matched status when exactly one match >= 0.95', () => {
28
+ const sourceCharge = createCharge({
29
+ chargeId: 'tx-charge-1',
30
+ transactions: [
31
+ createMockTransaction({
32
+ charge_id: 'tx-charge-1',
33
+ amount: '100',
34
+ currency: 'USD',
35
+ event_date: new Date('2024-01-15'),
36
+ business_id: BUSINESS_A,
37
+ }),
38
+ ],
39
+ documents: [],
40
+ });
41
+
42
+ const perfectMatch = createCharge({
43
+ chargeId: 'doc-charge-1',
44
+ transactions: [],
45
+ documents: [
46
+ createMockDocument({
47
+ charge_id: 'doc-charge-1',
48
+ total_amount: 100,
49
+ currency_code: 'USD',
50
+ date: new Date('2024-01-15'),
51
+ creditor_id: BUSINESS_A,
52
+ debtor_id: USER_ID,
53
+ }),
54
+ ],
55
+ });
56
+
57
+ const result = processChargeForAutoMatch(sourceCharge, [perfectMatch], USER_ID);
58
+
59
+ expect(result.status).toBe('matched');
60
+ expect(result.match).not.toBeNull();
61
+ expect(result.match?.chargeId).toBe('doc-charge-1');
62
+ expect(result.match?.confidenceScore).toBeGreaterThanOrEqual(0.95);
63
+ });
64
+
65
+ it('should return matched status for score exactly at 0.95 threshold', () => {
66
+ const sourceCharge = createCharge({
67
+ chargeId: 'tx-charge-1',
68
+ transactions: [
69
+ createMockTransaction({
70
+ charge_id: 'tx-charge-1',
71
+ amount: '100',
72
+ currency: 'USD',
73
+ event_date: new Date('2024-01-15'),
74
+ }),
75
+ ],
76
+ documents: [],
77
+ });
78
+
79
+ // Create a match that will score close to 0.95
80
+ // Same currency (1.0), same business (1.0), same date (1.0), slightly different amount
81
+ const nearThresholdMatch = createCharge({
82
+ chargeId: 'doc-charge-1',
83
+ transactions: [],
84
+ documents: [
85
+ createMockDocument({
86
+ charge_id: 'doc-charge-1',
87
+ total_amount: 100,
88
+ currency_code: 'USD',
89
+ date: new Date('2024-01-15'),
90
+ }),
91
+ ],
92
+ });
93
+
94
+ const result = processChargeForAutoMatch(sourceCharge, [nearThresholdMatch], USER_ID);
95
+
96
+ expect(result.status).toBe('matched');
97
+ expect(result.match).not.toBeNull();
98
+ expect(result.match?.confidenceScore).toBeGreaterThanOrEqual(0.95);
99
+ });
100
+
101
+ it('should work with document source charge', () => {
102
+ const sourceCharge = createCharge({
103
+ chargeId: 'doc-charge-1',
104
+ transactions: [],
105
+ documents: [
106
+ createMockDocument({
107
+ charge_id: 'doc-charge-1',
108
+ total_amount: 200,
109
+ currency_code: 'EUR',
110
+ date: new Date('2024-02-10'),
111
+ }),
112
+ ],
113
+ });
114
+
115
+ const perfectMatch = createCharge({
116
+ chargeId: 'tx-charge-1',
117
+ transactions: [
118
+ createMockTransaction({
119
+ charge_id: 'tx-charge-1',
120
+ amount: '200',
121
+ currency: 'EUR',
122
+ event_date: new Date('2024-02-10'),
123
+ }),
124
+ ],
125
+ documents: [],
126
+ });
127
+
128
+ const result = processChargeForAutoMatch(sourceCharge, [perfectMatch], USER_ID);
129
+
130
+ expect(result.status).toBe('matched');
131
+ expect(result.match).not.toBeNull();
132
+ expect(result.match?.chargeId).toBe('tx-charge-1');
133
+ });
134
+ });
135
+
136
+ describe('Multiple high-confidence matches', () => {
137
+ it('should return skipped status when multiple matches >= 0.95', () => {
138
+ const sourceCharge = createCharge({
139
+ chargeId: 'tx-charge-1',
140
+ transactions: [
141
+ createMockTransaction({
142
+ charge_id: 'tx-charge-1',
143
+ amount: "100",
144
+ currency: 'USD',
145
+ event_date: new Date('2024-01-15'),
146
+ }),
147
+ ],
148
+ documents: [],
149
+ });
150
+
151
+ const match1 = createCharge({
152
+ chargeId: 'doc-charge-1',
153
+ transactions: [],
154
+ documents: [
155
+ createMockDocument({
156
+ charge_id: 'doc-charge-1',
157
+ total_amount: 100,
158
+ currency_code: 'USD',
159
+ date: new Date('2024-01-15'),
160
+ }),
161
+ ],
162
+ });
163
+
164
+ const match2 = createCharge({
165
+ chargeId: 'doc-charge-2',
166
+ transactions: [],
167
+ documents: [
168
+ createMockDocument({
169
+ charge_id: 'doc-charge-2',
170
+ total_amount: 100,
171
+ currency_code: 'USD',
172
+ date: new Date('2024-01-15'),
173
+ }),
174
+ ],
175
+ });
176
+
177
+ const result = processChargeForAutoMatch(sourceCharge, [match1, match2], USER_ID);
178
+
179
+ expect(result.status).toBe('skipped');
180
+ expect(result.match).toBeNull();
181
+ expect(result.reason).toContain('ambiguous');
182
+ });
183
+
184
+ it('should skip even with many high-confidence matches', () => {
185
+ const sourceCharge = createCharge({
186
+ chargeId: 'tx-charge-1',
187
+ transactions: [
188
+ createMockTransaction({
189
+ charge_id: 'tx-charge-1',
190
+ amount: "100",
191
+ }),
192
+ ],
193
+ documents: [],
194
+ });
195
+
196
+ const candidates = Array.from({ length: 5 }, (_, i) =>
197
+ createCharge({
198
+ chargeId: `doc-charge-${i}`,
199
+ transactions: [],
200
+ documents: [createMockDocument({ charge_id: `doc-charge-${i}`, total_amount: 100 })],
201
+ }),
202
+ );
203
+
204
+ const result = processChargeForAutoMatch(sourceCharge, candidates, USER_ID);
205
+
206
+ expect(result.status).toBe('skipped');
207
+ expect(result.match).toBeNull();
208
+ });
209
+ });
210
+
211
+ describe('No high-confidence matches', () => {
212
+ it('should return no-match when best match is below 0.95 threshold', () => {
213
+ const sourceCharge = createCharge({
214
+ chargeId: 'tx-charge-1',
215
+ transactions: [
216
+ createMockTransaction({
217
+ charge_id: 'tx-charge-1',
218
+ amount: "100",
219
+ currency: 'USD',
220
+ event_date: new Date('2024-01-15'),
221
+ }),
222
+ ],
223
+ documents: [],
224
+ });
225
+
226
+ // Create a poor match - different amount and currency
227
+ const poorMatch = createCharge({
228
+ chargeId: 'doc-charge-1',
229
+ transactions: [],
230
+ documents: [
231
+ createMockDocument({
232
+ charge_id: 'doc-charge-1',
233
+ total_amount: 500, // Very different amount
234
+ currency_code: 'EUR', // Different currency
235
+ date: new Date('2024-01-15'),
236
+ }),
237
+ ],
238
+ });
239
+
240
+ const result = processChargeForAutoMatch(sourceCharge, [poorMatch], USER_ID);
241
+
242
+ expect(result.status).toBe('no-match');
243
+ expect(result.match).toBeNull();
244
+ expect(result.reason).toContain('below threshold');
245
+ });
246
+
247
+ it('should return no-match when no candidates exist', () => {
248
+ const sourceCharge = createCharge({
249
+ chargeId: 'tx-charge-1',
250
+ transactions: [createMockTransaction()],
251
+ documents: [],
252
+ });
253
+
254
+ const result = processChargeForAutoMatch(sourceCharge, [], USER_ID);
255
+
256
+ expect(result.status).toBe('no-match');
257
+ expect(result.match).toBeNull();
258
+ expect(result.reason).toContain('No candidates found');
259
+ });
260
+
261
+ it('should handle match just below threshold (0.949)', () => {
262
+ const sourceCharge = createCharge({
263
+ chargeId: 'tx-charge-1',
264
+ transactions: [
265
+ createMockTransaction({
266
+ charge_id: 'tx-charge-1',
267
+ amount: "100",
268
+ currency: 'USD',
269
+ event_date: new Date('2024-01-15'),
270
+ }),
271
+ ],
272
+ documents: [],
273
+ });
274
+
275
+ // Create a match with small amount difference to get score just below 0.95
276
+ const nearMissMatch = createCharge({
277
+ chargeId: 'doc-charge-1',
278
+ transactions: [],
279
+ documents: [
280
+ createMockDocument({
281
+ charge_id: 'doc-charge-1',
282
+ total_amount: 105, // Small difference
283
+ currency_code: 'USD',
284
+ date: new Date('2024-01-16'), // One day off
285
+ }),
286
+ ],
287
+ });
288
+
289
+ const result = processChargeForAutoMatch(sourceCharge, [nearMissMatch], USER_ID);
290
+
291
+ // Should be no-match since it's below threshold
292
+ expect(result.status).toBe('no-match');
293
+ expect(result.match).toBeNull();
294
+ });
295
+ });
296
+
297
+ describe('Edge cases and validation', () => {
298
+ it('should throw error if source charge is already matched', () => {
299
+ const matchedCharge = createCharge({
300
+ chargeId: 'matched-charge',
301
+ transactions: [createMockTransaction()],
302
+ documents: [createMockDocument()],
303
+ });
304
+
305
+ expect(() => {
306
+ processChargeForAutoMatch(matchedCharge, [], USER_ID);
307
+ }).toThrow(/already matched/);
308
+ });
309
+
310
+ it('should throw error if source charge has no transactions or documents', () => {
311
+ const emptyCharge = createCharge({
312
+ chargeId: 'empty-charge',
313
+ transactions: [],
314
+ documents: [],
315
+ });
316
+
317
+ expect(() => {
318
+ processChargeForAutoMatch(emptyCharge, [], USER_ID);
319
+ }).toThrow(/no transactions or documents/);
320
+ });
321
+
322
+ it('should filter candidates to complementary type only', () => {
323
+ const sourceCharge = createCharge({
324
+ chargeId: 'tx-charge-1',
325
+ transactions: [createMockTransaction({ amount: "100" })],
326
+ documents: [],
327
+ });
328
+
329
+ // Mix of document charges and transaction charges
330
+ const candidates = [
331
+ createCharge({
332
+ chargeId: 'doc-charge-1',
333
+ transactions: [],
334
+ documents: [createMockDocument({ total_amount: 100 })],
335
+ }),
336
+ createCharge({
337
+ chargeId: 'tx-charge-2',
338
+ transactions: [createMockTransaction({ amount: "100" })], // Should be filtered out
339
+ documents: [],
340
+ }),
341
+ createCharge({
342
+ chargeId: 'doc-charge-2',
343
+ transactions: [],
344
+ documents: [createMockDocument({ total_amount: 100 })],
345
+ }),
346
+ ];
347
+
348
+ const result = processChargeForAutoMatch(sourceCharge, candidates, USER_ID);
349
+
350
+ // Should only consider document charges (doc-charge-1 and doc-charge-2)
351
+ // Both are perfect matches, so should be skipped as ambiguous
352
+ expect(result.status).toBe('skipped');
353
+ });
354
+
355
+ it('should handle various confidence levels correctly', () => {
356
+ const sourceCharge = createCharge({
357
+ chargeId: 'tx-charge-1',
358
+ transactions: [
359
+ createMockTransaction({
360
+ amount: "100",
361
+ currency: 'USD',
362
+ event_date: new Date('2024-01-15'),
363
+ }),
364
+ ],
365
+ documents: [],
366
+ });
367
+
368
+ const candidates = [
369
+ // Perfect match - confidence ~0.99+
370
+ createCharge({
371
+ chargeId: 'doc-charge-perfect',
372
+ transactions: [],
373
+ documents: [
374
+ createMockDocument({
375
+ total_amount: 100,
376
+ currency_code: 'USD',
377
+ date: new Date('2024-01-15'),
378
+ }),
379
+ ],
380
+ }),
381
+ // Good match but below threshold - confidence ~0.85
382
+ createCharge({
383
+ chargeId: 'doc-charge-good',
384
+ transactions: [],
385
+ documents: [
386
+ createMockDocument({
387
+ total_amount: 110,
388
+ currency_code: 'USD',
389
+ date: new Date('2024-01-20'),
390
+ }),
391
+ ],
392
+ }),
393
+ // Poor match - confidence ~0.40
394
+ createCharge({
395
+ chargeId: 'doc-charge-poor',
396
+ transactions: [],
397
+ documents: [
398
+ createMockDocument({
399
+ total_amount: 200,
400
+ currency_code: 'EUR',
401
+ date: new Date('2024-02-15'),
402
+ }),
403
+ ],
404
+ }),
405
+ ];
406
+
407
+ const result = processChargeForAutoMatch(sourceCharge, candidates, USER_ID);
408
+
409
+ // Only one match >= 0.95 (the perfect one)
410
+ expect(result.status).toBe('matched');
411
+ expect(result.match?.chargeId).toBe('doc-charge-perfect');
412
+ });
413
+ });
414
+ });
415
+
416
+ describe('determineMergeDirection', () => {
417
+ describe('Matched charge priority', () => {
418
+ it('should keep matched charge when one is matched and one is unmatched', () => {
419
+ const matchedCharge = createCharge({
420
+ chargeId: 'matched-1',
421
+ transactions: [createMockTransaction()],
422
+ documents: [createMockDocument()],
423
+ });
424
+
425
+ const unmatchedCharge = createCharge({
426
+ chargeId: 'unmatched-1',
427
+ transactions: [createMockTransaction()],
428
+ documents: [],
429
+ });
430
+
431
+ const [source, target] = determineMergeDirection(matchedCharge, unmatchedCharge);
432
+
433
+ expect(target.chargeId).toBe('matched-1');
434
+ expect(source.chargeId).toBe('unmatched-1');
435
+ });
436
+
437
+ it('should keep matched charge regardless of order', () => {
438
+ const matchedCharge = createCharge({
439
+ chargeId: 'matched-1',
440
+ transactions: [createMockTransaction()],
441
+ documents: [createMockDocument()],
442
+ });
443
+
444
+ const unmatchedCharge = createCharge({
445
+ chargeId: 'unmatched-1',
446
+ transactions: [],
447
+ documents: [createMockDocument()],
448
+ });
449
+
450
+ const [source, target] = determineMergeDirection(unmatchedCharge, matchedCharge);
451
+
452
+ expect(target.chargeId).toBe('matched-1');
453
+ expect(source.chargeId).toBe('unmatched-1');
454
+ });
455
+ });
456
+
457
+ describe('Transaction charge priority', () => {
458
+ it('should keep transaction charge when both are unmatched', () => {
459
+ const transactionCharge = createCharge({
460
+ chargeId: 'tx-charge-1',
461
+ transactions: [createMockTransaction()],
462
+ documents: [],
463
+ });
464
+
465
+ const documentCharge = createCharge({
466
+ chargeId: 'doc-charge-1',
467
+ transactions: [],
468
+ documents: [createMockDocument()],
469
+ });
470
+
471
+ const [source, target] = determineMergeDirection(transactionCharge, documentCharge);
472
+
473
+ expect(target.chargeId).toBe('tx-charge-1');
474
+ expect(source.chargeId).toBe('doc-charge-1');
475
+ });
476
+
477
+ it('should keep transaction charge regardless of order', () => {
478
+ const transactionCharge = createCharge({
479
+ chargeId: 'tx-charge-1',
480
+ transactions: [createMockTransaction()],
481
+ documents: [],
482
+ });
483
+
484
+ const documentCharge = createCharge({
485
+ chargeId: 'doc-charge-1',
486
+ transactions: [],
487
+ documents: [createMockDocument()],
488
+ });
489
+
490
+ const [source, target] = determineMergeDirection(documentCharge, transactionCharge);
491
+
492
+ expect(target.chargeId).toBe('tx-charge-1');
493
+ expect(source.chargeId).toBe('doc-charge-1');
494
+ });
495
+ });
496
+
497
+ describe('Default behavior', () => {
498
+ it('should keep first charge when both are document-only', () => {
499
+ const docCharge1 = createCharge({
500
+ chargeId: 'doc-charge-1',
501
+ transactions: [],
502
+ documents: [createMockDocument()],
503
+ });
504
+
505
+ const docCharge2 = createCharge({
506
+ chargeId: 'doc-charge-2',
507
+ transactions: [],
508
+ documents: [createMockDocument()],
509
+ });
510
+
511
+ const [source, target] = determineMergeDirection(docCharge1, docCharge2);
512
+
513
+ expect(target.chargeId).toBe('doc-charge-1');
514
+ expect(source.chargeId).toBe('doc-charge-2');
515
+ });
516
+
517
+ it('should keep first charge when both are transaction-only', () => {
518
+ const txCharge1 = createCharge({
519
+ chargeId: 'tx-charge-1',
520
+ transactions: [createMockTransaction()],
521
+ documents: [],
522
+ });
523
+
524
+ const txCharge2 = createCharge({
525
+ chargeId: 'tx-charge-2',
526
+ transactions: [createMockTransaction()],
527
+ documents: [],
528
+ });
529
+
530
+ const [source, target] = determineMergeDirection(txCharge1, txCharge2);
531
+
532
+ expect(target.chargeId).toBe('tx-charge-1');
533
+ expect(source.chargeId).toBe('tx-charge-2');
534
+ });
535
+
536
+ it('should keep first charge when both are matched', () => {
537
+ const matched1 = createCharge({
538
+ chargeId: 'matched-1',
539
+ transactions: [createMockTransaction()],
540
+ documents: [createMockDocument()],
541
+ });
542
+
543
+ const matched2 = createCharge({
544
+ chargeId: 'matched-2',
545
+ transactions: [createMockTransaction()],
546
+ documents: [createMockDocument()],
547
+ });
548
+
549
+ const [source, target] = determineMergeDirection(matched1, matched2);
550
+
551
+ expect(target.chargeId).toBe('matched-1');
552
+ expect(source.chargeId).toBe('matched-2');
553
+ });
554
+ });
555
+
556
+ describe('Complex scenarios', () => {
557
+ it('should handle charge with multiple transactions', () => {
558
+ const multiTxCharge = createCharge({
559
+ chargeId: 'multi-tx',
560
+ transactions: [
561
+ createMockTransaction({ amount: "50" }),
562
+ createMockTransaction({ amount: "50" }),
563
+ ],
564
+ documents: [],
565
+ });
566
+
567
+ const singleDocCharge = createCharge({
568
+ chargeId: 'single-doc',
569
+ transactions: [],
570
+ documents: [createMockDocument()],
571
+ });
572
+
573
+ const [source, target] = determineMergeDirection(multiTxCharge, singleDocCharge);
574
+
575
+ expect(target.chargeId).toBe('multi-tx');
576
+ expect(source.chargeId).toBe('single-doc');
577
+ });
578
+
579
+ it('should handle charge with multiple documents', () => {
580
+ const txCharge = createCharge({
581
+ chargeId: 'tx-charge',
582
+ transactions: [createMockTransaction()],
583
+ documents: [],
584
+ });
585
+
586
+ const multiDocCharge = createCharge({
587
+ chargeId: 'multi-doc',
588
+ transactions: [],
589
+ documents: [
590
+ createMockDocument({ serial_number: 'INV-001' }),
591
+ createMockDocument({ serial_number: 'INV-002' }),
592
+ ],
593
+ });
594
+
595
+ const [source, target] = determineMergeDirection(txCharge, multiDocCharge);
596
+
597
+ expect(target.chargeId).toBe('tx-charge');
598
+ expect(source.chargeId).toBe('multi-doc');
599
+ });
600
+
601
+ it('should prioritize matched over transaction when choosing', () => {
602
+ const matched = createCharge({
603
+ chargeId: 'matched',
604
+ transactions: [createMockTransaction()],
605
+ documents: [createMockDocument()],
606
+ });
607
+
608
+ const txOnly = createCharge({
609
+ chargeId: 'tx-only',
610
+ transactions: [createMockTransaction()],
611
+ documents: [],
612
+ });
613
+
614
+ const [source, target] = determineMergeDirection(txOnly, matched);
615
+
616
+ // Matched should be kept even though txOnly has transactions
617
+ expect(target.chargeId).toBe('matched');
618
+ expect(source.chargeId).toBe('tx-only');
619
+ });
620
+ });
621
+ });