@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,714 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { createMockTransaction, createMockDocument } from './test-helpers.js';
3
+
4
+ // Mock the module imports
5
+ vi.mock('graphql-modules', () => ({
6
+ Injectable: () => (target: any) => target,
7
+ Injector: class {},
8
+ Scope: { Operation: 'Operation' },
9
+ }));
10
+
11
+ vi.mock('@modules/charges/providers/charges.provider.js', () => ({
12
+ ChargesProvider: class {},
13
+ }));
14
+
15
+ vi.mock('@modules/documents/providers/documents.provider.js', () => ({
16
+ DocumentsProvider: class {},
17
+ }));
18
+
19
+ vi.mock('@modules/transactions/providers/transactions.provider.js', () => ({
20
+ TransactionsProvider: class {},
21
+ }));
22
+
23
+ vi.mock('@modules/charges/helpers/merge-charges.hepler.js', () => ({
24
+ mergeChargesExecutor: vi.fn(),
25
+ }));
26
+
27
+ vi.mock('@shared/helpers', () => ({
28
+ dateToTimelessDateString: (date: Date) => date.toISOString().split('T')[0],
29
+ }));
30
+
31
+ // Import after mocking
32
+ const { ChargesMatcherProvider } = await import('../providers/charges-matcher.provider.js');
33
+ const { mergeChargesExecutor } = await import(
34
+ '@modules/charges/helpers/merge-charges.hepler.js'
35
+ );
36
+
37
+ type Injector = {
38
+ get: (token: any) => any;
39
+ };
40
+
41
+ // Test constants
42
+ const ADMIN_BUSINESS_ID = 'user-123';
43
+ const BUSINESS_A = 'business-a';
44
+
45
+ // Helper to create charge
46
+ function createCharge(id: string, ownerId: string) {
47
+ return {
48
+ id,
49
+ owner_id: ownerId,
50
+ };
51
+ }
52
+
53
+ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ });
57
+
58
+ describe('autoMatchCharges', () => {
59
+ it('should return 0 matches when database is empty', async () => {
60
+ const mockChargesProvider = {
61
+ getChargesByFilters: vi.fn(() => Promise.resolve([])),
62
+ };
63
+
64
+ const mockTransactionsProvider = {
65
+ transactionsByChargeIDLoader: {
66
+ load: vi.fn(() => Promise.resolve([])),
67
+ },
68
+ };
69
+
70
+ const mockDocumentsProvider = {
71
+ getDocumentsByChargeIdLoader: {
72
+ load: vi.fn(() => Promise.resolve([])),
73
+ },
74
+ };
75
+
76
+ const mockInjector = {
77
+ get: vi.fn((token: any) => {
78
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
79
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
80
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
81
+ return null;
82
+ }),
83
+ } as unknown as Injector;
84
+
85
+ const provider = new ChargesMatcherProvider();
86
+ const result = await provider.autoMatchCharges({
87
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
88
+ injector: mockInjector,
89
+ } as any);
90
+
91
+ expect(result.totalMatches).toBe(0);
92
+ expect(result.mergedCharges).toEqual([]);
93
+ expect(result.skippedCharges).toEqual([]);
94
+ expect(result.errors).toEqual([]);
95
+ });
96
+
97
+ it('should return 0 matches when all charges are already matched', async () => {
98
+ const matchedChargeId = 'matched-charge-1';
99
+
100
+ const mockChargesProvider = {
101
+ getChargesByFilters: vi.fn(() =>
102
+ Promise.resolve([createCharge(matchedChargeId, ADMIN_BUSINESS_ID)]),
103
+ ),
104
+ };
105
+
106
+ const mockTransactionsProvider = {
107
+ transactionsByChargeIDLoader: {
108
+ load: vi.fn(() => Promise.resolve([createMockTransaction()])),
109
+ },
110
+ };
111
+
112
+ const mockDocumentsProvider = {
113
+ getDocumentsByChargeIdLoader: {
114
+ load: vi.fn(() => Promise.resolve([createMockDocument()])),
115
+ },
116
+ };
117
+
118
+ const mockInjector = {
119
+ get: vi.fn((token: any) => {
120
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
121
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
122
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
123
+ return null;
124
+ }),
125
+ } as unknown as Injector;
126
+
127
+ const provider = new ChargesMatcherProvider();
128
+ const result = await provider.autoMatchCharges({
129
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
130
+ injector: mockInjector,
131
+ } as any);
132
+
133
+ expect(result.totalMatches).toBe(0);
134
+ expect(result.mergedCharges).toEqual([]);
135
+ expect(result.skippedCharges).toEqual([]);
136
+ });
137
+
138
+ it('should execute merge for single unmatched charge with good match', async () => {
139
+ const txChargeId = 'tx-charge-1';
140
+ const docChargeId = 'doc-charge-1';
141
+
142
+ const mockChargesProvider = {
143
+ getChargesByFilters: vi.fn(() =>
144
+ Promise.resolve([
145
+ createCharge(txChargeId, ADMIN_BUSINESS_ID),
146
+ createCharge(docChargeId, ADMIN_BUSINESS_ID),
147
+ ]),
148
+ ),
149
+ };
150
+
151
+ const mockTransactionsProvider = {
152
+ transactionsByChargeIDLoader: {
153
+ load: vi.fn((id: string) => {
154
+ if (id === txChargeId) {
155
+ return Promise.resolve([
156
+ createMockTransaction({
157
+ charge_id: txChargeId,
158
+ amount: "100",
159
+ currency: 'USD',
160
+ event_date: new Date('2024-01-15'),
161
+ }),
162
+ ]);
163
+ }
164
+ return Promise.resolve([]);
165
+ }),
166
+ },
167
+ };
168
+
169
+ const mockDocumentsProvider = {
170
+ getDocumentsByChargeIdLoader: {
171
+ load: vi.fn((id: string) => {
172
+ if (id === docChargeId) {
173
+ return Promise.resolve([
174
+ createMockDocument({
175
+ charge_id: docChargeId,
176
+ total_amount: 100,
177
+ currency_code: 'USD',
178
+ date: new Date('2024-01-15'),
179
+ }),
180
+ ]);
181
+ }
182
+ return Promise.resolve([]);
183
+ }),
184
+ },
185
+ };
186
+
187
+ const mockInjector = {
188
+ get: vi.fn((token: any) => {
189
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
190
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
191
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
192
+ return null;
193
+ }),
194
+ } as unknown as Injector;
195
+
196
+ const provider = new ChargesMatcherProvider();
197
+ const result = await provider.autoMatchCharges({
198
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
199
+ injector: mockInjector,
200
+ } as any);
201
+
202
+ expect(result.totalMatches).toBe(1);
203
+ expect(result.mergedCharges).toHaveLength(1);
204
+ expect(result.mergedCharges[0].confidenceScore).toBeGreaterThanOrEqual(0.95);
205
+ expect(result.skippedCharges).toEqual([]);
206
+ expect(result.errors).toEqual([]);
207
+
208
+ // Verify merge was called
209
+ expect(mergeChargesExecutor).toHaveBeenCalledTimes(1);
210
+ });
211
+
212
+ it('should skip charges with ambiguous matches', async () => {
213
+ const txChargeId = 'tx-charge-1';
214
+ const docCharge1Id = 'doc-charge-1';
215
+ const docCharge2Id = 'doc-charge-2';
216
+
217
+ const mockChargesProvider = {
218
+ getChargesByFilters: vi.fn(() =>
219
+ Promise.resolve([
220
+ createCharge(txChargeId, ADMIN_BUSINESS_ID),
221
+ createCharge(docCharge1Id, ADMIN_BUSINESS_ID),
222
+ createCharge(docCharge2Id, ADMIN_BUSINESS_ID),
223
+ ]),
224
+ ),
225
+ };
226
+
227
+ const mockTransactionsProvider = {
228
+ transactionsByChargeIDLoader: {
229
+ load: vi.fn((id: string) => {
230
+ if (id === txChargeId) {
231
+ return Promise.resolve([
232
+ createMockTransaction({
233
+ charge_id: txChargeId,
234
+ amount: "100",
235
+ currency: 'USD',
236
+ event_date: new Date('2024-01-15'),
237
+ }),
238
+ ]);
239
+ }
240
+ return Promise.resolve([]);
241
+ }),
242
+ },
243
+ };
244
+
245
+ const mockDocumentsProvider = {
246
+ getDocumentsByChargeIdLoader: {
247
+ load: vi.fn((id: string) => {
248
+ if (id === docCharge1Id) {
249
+ return Promise.resolve([
250
+ createMockDocument({
251
+ charge_id: docCharge1Id,
252
+ total_amount: 100,
253
+ currency_code: 'USD',
254
+ date: new Date('2024-01-15'),
255
+ }),
256
+ ]);
257
+ }
258
+ if (id === docCharge2Id) {
259
+ return Promise.resolve([
260
+ createMockDocument({
261
+ charge_id: docCharge2Id,
262
+ total_amount: 100,
263
+ currency_code: 'USD',
264
+ date: new Date('2024-01-15'),
265
+ }),
266
+ ]);
267
+ }
268
+ return Promise.resolve([]);
269
+ }),
270
+ },
271
+ };
272
+
273
+ const mockInjector = {
274
+ get: vi.fn((token: any) => {
275
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
276
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
277
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
278
+ return null;
279
+ }),
280
+ } as unknown as Injector;
281
+
282
+ const provider = new ChargesMatcherProvider();
283
+ const result = await provider.autoMatchCharges( {
284
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
285
+ injector: mockInjector,
286
+ } as any);
287
+
288
+ // The transaction charge should find 2 identical doc matches and skip (ambiguous)
289
+ // But the doc charges will also match each other with high confidence
290
+ // Since processing is sequential, one doc will match the other doc
291
+ // So we expect: 1 match (doc1-doc2), and tx skipped due to ambiguity
292
+ expect(result.totalMatches).toBe(1);
293
+ expect(result.skippedCharges).toHaveLength(1);
294
+ expect(result.skippedCharges[0]).toBe(txChargeId);
295
+ expect(mergeChargesExecutor).toHaveBeenCalledTimes(1);
296
+ });
297
+
298
+ it('should process multiple unmatched charges correctly', async () => {
299
+ const tx1Id = 'tx-charge-1';
300
+ const doc1Id = 'doc-charge-1';
301
+ const tx2Id = 'tx-charge-2';
302
+ const doc2Id = 'doc-charge-2';
303
+
304
+ const mockChargesProvider = {
305
+ getChargesByFilters: vi.fn(() =>
306
+ Promise.resolve([
307
+ createCharge(tx1Id, ADMIN_BUSINESS_ID),
308
+ createCharge(doc1Id, ADMIN_BUSINESS_ID),
309
+ createCharge(tx2Id, ADMIN_BUSINESS_ID),
310
+ createCharge(doc2Id, ADMIN_BUSINESS_ID),
311
+ ]),
312
+ ),
313
+ };
314
+
315
+ const mockTransactionsProvider = {
316
+ transactionsByChargeIDLoader: {
317
+ load: vi.fn((id: string) => {
318
+ if (id === tx1Id) {
319
+ return Promise.resolve([
320
+ createMockTransaction({ charge_id: tx1Id, amount: "100", currency: 'USD' }),
321
+ ]);
322
+ }
323
+ if (id === tx2Id) {
324
+ return Promise.resolve([
325
+ createMockTransaction({ charge_id: tx2Id, amount: "200", currency: 'EUR' }),
326
+ ]);
327
+ }
328
+ return Promise.resolve([]);
329
+ }),
330
+ },
331
+ };
332
+
333
+ const mockDocumentsProvider = {
334
+ getDocumentsByChargeIdLoader: {
335
+ load: vi.fn((id: string) => {
336
+ if (id === doc1Id) {
337
+ return Promise.resolve([
338
+ createMockDocument({ charge_id: doc1Id, total_amount: 100, currency_code: 'USD' }),
339
+ ]);
340
+ }
341
+ if (id === doc2Id) {
342
+ return Promise.resolve([
343
+ createMockDocument({ charge_id: doc2Id, total_amount: 200, currency_code: 'EUR' }),
344
+ ]);
345
+ }
346
+ return Promise.resolve([]);
347
+ }),
348
+ },
349
+ };
350
+
351
+ const mockInjector = {
352
+ get: vi.fn((token: any) => {
353
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
354
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
355
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
356
+ return null;
357
+ }),
358
+ } as unknown as Injector;
359
+
360
+ const provider = new ChargesMatcherProvider();
361
+ const result = await provider.autoMatchCharges({
362
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
363
+ injector: mockInjector,
364
+ } as any);
365
+
366
+ expect(result.totalMatches).toBe(2);
367
+ expect(result.mergedCharges).toHaveLength(2);
368
+ expect(mergeChargesExecutor).toHaveBeenCalledTimes(2);
369
+ });
370
+
371
+ it('should capture errors during merge but continue processing', async () => {
372
+ const tx1Id = 'tx-charge-1';
373
+ const doc1Id = 'doc-charge-1';
374
+ const tx2Id = 'tx-charge-2'; // This will have no match
375
+
376
+ const mockChargesProvider = {
377
+ getChargesByFilters: vi.fn(() =>
378
+ Promise.resolve([
379
+ createCharge(tx1Id, ADMIN_BUSINESS_ID),
380
+ createCharge(doc1Id, ADMIN_BUSINESS_ID),
381
+ createCharge(tx2Id, ADMIN_BUSINESS_ID),
382
+ ]),
383
+ ),
384
+ };
385
+
386
+ const mockTransactionsProvider = {
387
+ transactionsByChargeIDLoader: {
388
+ load: vi.fn((id: string) => {
389
+ if (id === tx1Id) {
390
+ return Promise.resolve([
391
+ createMockTransaction({
392
+ charge_id: id,
393
+ amount: "100",
394
+ currency: 'USD',
395
+ event_date: new Date('2024-01-15'),
396
+ }),
397
+ ]);
398
+ }
399
+ if (id === tx2Id) {
400
+ return Promise.resolve([
401
+ createMockTransaction({
402
+ charge_id: id,
403
+ amount: "999",
404
+ currency: 'GBP',
405
+ event_date: new Date('2024-03-15'),
406
+ }),
407
+ ]);
408
+ }
409
+ return Promise.resolve([]);
410
+ }),
411
+ },
412
+ };
413
+
414
+ const mockDocumentsProvider = {
415
+ getDocumentsByChargeIdLoader: {
416
+ load: vi.fn((id: string) => {
417
+ if (id === doc1Id) {
418
+ return Promise.resolve([
419
+ createMockDocument({
420
+ charge_id: id,
421
+ total_amount: 100,
422
+ currency_code: 'USD',
423
+ date: new Date('2024-01-15'),
424
+ }),
425
+ ]);
426
+ }
427
+ return Promise.resolve([]);
428
+ }),
429
+ },
430
+ };
431
+
432
+ // Mock merge to fail on first call, succeed on second
433
+ (mergeChargesExecutor as any).mockImplementationOnce(() => {
434
+ throw new Error('Merge failed for test');
435
+ });
436
+ (mergeChargesExecutor as any).mockImplementationOnce(() => Promise.resolve());
437
+
438
+ const mockInjector = {
439
+ get: vi.fn((token: any) => {
440
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
441
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
442
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
443
+ return null;
444
+ }),
445
+ } as unknown as Injector;
446
+
447
+ const provider = new ChargesMatcherProvider();
448
+ const result = await provider.autoMatchCharges( {
449
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
450
+ injector: mockInjector,
451
+ } as any);
452
+
453
+ // tx1 finds doc1, merge fails → error captured, charges not marked as merged
454
+ // doc1 finds tx1, merge succeeds → totalMatches++, charges marked as merged
455
+ // tx2 has no match → silent
456
+ expect(result.totalMatches).toBe(1);
457
+ expect(result.errors).toHaveLength(1);
458
+ expect(result.errors[0]).toContain('Failed to merge');
459
+ expect(mergeChargesExecutor).toHaveBeenCalledTimes(2);
460
+ });
461
+
462
+ it('should verify merge direction keeps transaction charge', async () => {
463
+ const txChargeId = 'tx-charge-1';
464
+ const docChargeId = 'doc-charge-1';
465
+
466
+ const mockChargesProvider = {
467
+ getChargesByFilters: vi.fn(() =>
468
+ Promise.resolve([
469
+ createCharge(txChargeId, ADMIN_BUSINESS_ID),
470
+ createCharge(docChargeId, ADMIN_BUSINESS_ID),
471
+ ]),
472
+ ),
473
+ };
474
+
475
+ const mockTransactionsProvider = {
476
+ transactionsByChargeIDLoader: {
477
+ load: vi.fn((id: string) => {
478
+ if (id === txChargeId) {
479
+ return Promise.resolve([createMockTransaction({ charge_id: txChargeId })]);
480
+ }
481
+ return Promise.resolve([]);
482
+ }),
483
+ },
484
+ };
485
+
486
+ const mockDocumentsProvider = {
487
+ getDocumentsByChargeIdLoader: {
488
+ load: vi.fn((id: string) => {
489
+ if (id === docChargeId) {
490
+ return Promise.resolve([createMockDocument({ charge_id: docChargeId })]);
491
+ }
492
+ return Promise.resolve([]);
493
+ }),
494
+ },
495
+ };
496
+
497
+ const mockInjector = {
498
+ get: vi.fn((token: any) => {
499
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
500
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
501
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
502
+ return null;
503
+ }),
504
+ } as unknown as Injector;
505
+
506
+ const provider = new ChargesMatcherProvider();
507
+ await provider.autoMatchCharges({
508
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
509
+ injector: mockInjector,
510
+ } as any);
511
+
512
+ // Verify merge was called with doc charge being merged into tx charge
513
+ expect(mergeChargesExecutor).toHaveBeenCalledWith([docChargeId], txChargeId, mockInjector);
514
+ });
515
+
516
+ it('should exclude merged charges from further processing in same run', async () => {
517
+ const tx1Id = 'tx-charge-1';
518
+ const doc1Id = 'doc-charge-1';
519
+ const tx2Id = 'tx-charge-2';
520
+
521
+ const mockChargesProvider = {
522
+ getChargesByFilters: vi.fn(() =>
523
+ Promise.resolve([
524
+ createCharge(tx1Id, ADMIN_BUSINESS_ID),
525
+ createCharge(doc1Id, ADMIN_BUSINESS_ID),
526
+ createCharge(tx2Id, ADMIN_BUSINESS_ID),
527
+ ]),
528
+ ),
529
+ };
530
+
531
+ const mockTransactionsProvider = {
532
+ transactionsByChargeIDLoader: {
533
+ load: vi.fn((id: string) => {
534
+ if (id === tx1Id || id === tx2Id) {
535
+ return Promise.resolve([createMockTransaction({ charge_id: id, amount: "100" })]);
536
+ }
537
+ return Promise.resolve([]);
538
+ }),
539
+ },
540
+ };
541
+
542
+ const mockDocumentsProvider = {
543
+ getDocumentsByChargeIdLoader: {
544
+ load: vi.fn((id: string) => {
545
+ if (id === doc1Id) {
546
+ return Promise.resolve([createMockDocument({ charge_id: doc1Id, total_amount: 100 })]);
547
+ }
548
+ return Promise.resolve([]);
549
+ }),
550
+ },
551
+ };
552
+
553
+ const mockInjector = {
554
+ get: vi.fn((token: any) => {
555
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
556
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
557
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
558
+ return null;
559
+ }),
560
+ } as unknown as Injector;
561
+
562
+ const provider = new ChargesMatcherProvider();
563
+ const result = await provider.autoMatchCharges({
564
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
565
+ injector: mockInjector,
566
+ } as any);
567
+
568
+ // Should only merge tx1 with doc1, tx2 should have no match
569
+ expect(result.totalMatches).toBe(1);
570
+ expect(mergeChargesExecutor).toHaveBeenCalledTimes(1);
571
+ });
572
+
573
+ it('should handle mixed scenarios: some match, some skip, some no-match', async () => {
574
+ const perfectMatchTx = 'tx-perfect';
575
+ const perfectMatchDoc = 'doc-perfect';
576
+ const ambiguousTx = 'tx-ambiguous';
577
+ const ambiguousDoc1 = 'doc-ambiguous-1';
578
+ const ambiguousDoc2 = 'doc-ambiguous-2';
579
+ const noMatchTx = 'tx-no-match';
580
+
581
+ const mockChargesProvider = {
582
+ getChargesByFilters: vi.fn(() =>
583
+ Promise.resolve([
584
+ createCharge(perfectMatchTx, ADMIN_BUSINESS_ID),
585
+ createCharge(perfectMatchDoc, ADMIN_BUSINESS_ID),
586
+ createCharge(ambiguousTx, ADMIN_BUSINESS_ID),
587
+ createCharge(ambiguousDoc1, ADMIN_BUSINESS_ID),
588
+ createCharge(ambiguousDoc2, ADMIN_BUSINESS_ID),
589
+ createCharge(noMatchTx, ADMIN_BUSINESS_ID),
590
+ ]),
591
+ ),
592
+ };
593
+
594
+ const mockTransactionsProvider = {
595
+ transactionsByChargeIDLoader: {
596
+ load: vi.fn((id: string) => {
597
+ if (id === perfectMatchTx) {
598
+ return Promise.resolve([
599
+ createMockTransaction({
600
+ charge_id: id,
601
+ amount: "100",
602
+ currency: 'USD',
603
+ event_date: new Date('2024-01-15'),
604
+ }),
605
+ ]);
606
+ }
607
+ if (id === ambiguousTx) {
608
+ return Promise.resolve([
609
+ createMockTransaction({
610
+ charge_id: id,
611
+ amount: "200",
612
+ currency: 'EUR',
613
+ event_date: new Date('2024-02-15'),
614
+ }),
615
+ ]);
616
+ }
617
+ if (id === noMatchTx) {
618
+ return Promise.resolve([
619
+ createMockTransaction({
620
+ charge_id: id,
621
+ amount: "500",
622
+ currency: 'GBP',
623
+ event_date: new Date('2024-03-15'),
624
+ }),
625
+ ]);
626
+ }
627
+ return Promise.resolve([]);
628
+ }),
629
+ },
630
+ };
631
+
632
+ const mockDocumentsProvider = {
633
+ getDocumentsByChargeIdLoader: {
634
+ load: vi.fn((id: string) => {
635
+ if (id === perfectMatchDoc) {
636
+ return Promise.resolve([
637
+ createMockDocument({
638
+ charge_id: id,
639
+ total_amount: 100,
640
+ currency_code: 'USD',
641
+ date: new Date('2024-01-15'),
642
+ }),
643
+ ]);
644
+ }
645
+ if (id === ambiguousDoc1) {
646
+ return Promise.resolve([
647
+ createMockDocument({
648
+ charge_id: id,
649
+ total_amount: 200,
650
+ currency_code: 'EUR',
651
+ date: new Date('2024-02-15'),
652
+ vat_amount: 10, // Different VAT
653
+ }),
654
+ ]);
655
+ }
656
+ if (id === ambiguousDoc2) {
657
+ return Promise.resolve([
658
+ createMockDocument({
659
+ charge_id: id,
660
+ total_amount: 200,
661
+ currency_code: 'EUR',
662
+ date: new Date('2024-02-15'),
663
+ vat_amount: 20, // Different VAT - but this won't affect matching score
664
+ }),
665
+ ]);
666
+ }
667
+ return Promise.resolve([]);
668
+ }),
669
+ },
670
+ };
671
+
672
+ const mockInjector = {
673
+ get: vi.fn((token: any) => {
674
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
675
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
676
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
677
+ return null;
678
+ }),
679
+ } as unknown as Injector;
680
+
681
+ (mergeChargesExecutor as any).mockImplementation(() => Promise.resolve());
682
+
683
+ const provider = new ChargesMatcherProvider();
684
+ const result = await provider.autoMatchCharges({
685
+ adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID },
686
+ injector: mockInjector,
687
+ } as any);
688
+
689
+ // Expected results:
690
+ // - perfectMatchTx finds perfectMatchDoc → merge (1)
691
+ // - perfectMatchDoc → already merged, skip processing
692
+ // - ambiguousTx finds 2 docs (ambiguousDoc1, ambiguousDoc2) → skipped due to ambiguity
693
+ // - ambiguousDoc1 finds ambiguousDoc2 (tx was skipped, not merged) → merge (2)
694
+ // - ambiguousDoc2 → already merged, skip processing
695
+ // - noMatchTx → no match, silent
696
+ // Total: 2 successful matches, 1 skipped charge (tx-ambiguous)
697
+ expect(result.totalMatches).toBe(2);
698
+ expect(result.mergedCharges).toHaveLength(2);
699
+ expect(result.skippedCharges).toHaveLength(1);
700
+ expect(result.skippedCharges[0]).toBe(ambiguousTx);
701
+ expect(mergeChargesExecutor).toHaveBeenCalledTimes(2);
702
+ });
703
+
704
+ it('should throw error if admin business ID not found in context', async () => {
705
+ const provider = new ChargesMatcherProvider();
706
+
707
+ await expect(
708
+ provider.autoMatchCharges({
709
+ adminContext: { defaultAdminBusinessId: null },
710
+ } as any),
711
+ ).rejects.toThrow(/Admin business not found/);
712
+ });
713
+ });
714
+ });