@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,556 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createMockTransaction, createMockDocument } from './test-helpers.js';
3
+
4
+ // Mock the module imports to avoid dependency issues
5
+ vi.mock('graphql-modules', () => ({
6
+ Injectable: () => (target: any) => target,
7
+ Inject: () => (target: any, propertyKey: string | symbol, parameterIndex: number) => {},
8
+ Injector: class {},
9
+ Scope: { Operation: 'Operation' },
10
+ CONTEXT: Symbol('CONTEXT'),
11
+ }));
12
+
13
+ vi.mock('@modules/charges/providers/charges.provider.js', () => ({
14
+ ChargesProvider: class {},
15
+ }));
16
+
17
+ vi.mock('@modules/documents/providers/documents.provider.js', () => ({
18
+ DocumentsProvider: class {},
19
+ }));
20
+
21
+ vi.mock('@modules/transactions/providers/transactions.provider.js', () => ({
22
+ TransactionsProvider: class {},
23
+ }));
24
+
25
+ vi.mock('@shared/helpers', () => ({
26
+ dateToTimelessDateString: (date: Date) => date.toISOString().split('T')[0],
27
+ }));
28
+
29
+ // Import after mocking
30
+ const { ChargesMatcherProvider } = await import('../providers/charges-matcher.provider.js');
31
+
32
+ type Injector = {
33
+ get: (token: any) => any;
34
+ };
35
+
36
+ // Test constants
37
+ const ADMIN_BUSINESS_ID = 'user-123';
38
+
39
+ // Mock charge object
40
+ function createCharge(id: string, ownerId: string) {
41
+ return {
42
+ id,
43
+ owner_id: ownerId,
44
+ transactions_min_event_date: new Date('2024-01-15'),
45
+ transactions_max_event_date: new Date('2024-01-15'),
46
+ };
47
+ }
48
+
49
+ describe('ChargesMatcherProvider - Integration Tests', () => {
50
+ describe('findMatchesForCharge', () => {
51
+ it('should find matches for transaction charge with mock data', async () => {
52
+ // Setup mock data
53
+ const sourceChargeId = 'tx-charge-1';
54
+ const sourceTransactions = [
55
+ createMockTransaction({
56
+ charge_id: sourceChargeId,
57
+ amount: "100",
58
+ currency: 'USD',
59
+ event_date: new Date('2024-01-15'),
60
+ }),
61
+ ];
62
+
63
+ const candidateCharge1Id = 'doc-charge-1';
64
+ const candidateDocuments1 = [
65
+ createMockDocument({
66
+ charge_id: candidateCharge1Id,
67
+ total_amount: 100,
68
+ currency_code: 'USD',
69
+ date: new Date('2024-01-15'),
70
+ }),
71
+ ];
72
+
73
+ const candidateCharge2Id = 'doc-charge-2';
74
+ const candidateDocuments2 = [
75
+ createMockDocument({
76
+ charge_id: candidateCharge2Id,
77
+ total_amount: 110,
78
+ currency_code: 'USD',
79
+ date: new Date('2024-01-16'),
80
+ }),
81
+ ];
82
+
83
+ // Mock providers
84
+ const mockChargesProvider = {
85
+ getChargeByIdLoader: {
86
+ load: vi.fn((id: string) => {
87
+ if (id === sourceChargeId) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
88
+ if (id === candidateCharge1Id) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
89
+ if (id === candidateCharge2Id) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
90
+ return Promise.resolve(new Error('Charge not found'));
91
+ }),
92
+ },
93
+ getChargesByFilters: vi.fn(() =>
94
+ Promise.resolve([
95
+ createCharge(candidateCharge1Id, ADMIN_BUSINESS_ID),
96
+ createCharge(candidateCharge2Id, ADMIN_BUSINESS_ID),
97
+ ]),
98
+ ),
99
+ };
100
+
101
+ const mockTransactionsProvider = {
102
+ transactionsByChargeIDLoader: {
103
+ load: vi.fn((id: string) => {
104
+ if (id === sourceChargeId) return Promise.resolve(sourceTransactions);
105
+ return Promise.resolve([]);
106
+ }),
107
+ },
108
+ };
109
+
110
+ const mockDocumentsProvider = {
111
+ getDocumentsByChargeIdLoader: {
112
+ load: vi.fn((id: string) => {
113
+ if (id === sourceChargeId) return Promise.resolve([]);
114
+ if (id === candidateCharge1Id) return Promise.resolve(candidateDocuments1);
115
+ if (id === candidateCharge2Id) return Promise.resolve(candidateDocuments2);
116
+ return Promise.resolve([]);
117
+ }),
118
+ },
119
+ };
120
+
121
+ const mockInjector = {
122
+ get: vi.fn((token: any) => {
123
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
124
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
125
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
126
+ return null;
127
+ }),
128
+ } as unknown as Injector;
129
+
130
+ // Execute
131
+ const provider = new ChargesMatcherProvider();
132
+ const result = await provider.findMatchesForCharge(sourceChargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any);
133
+
134
+ // Verify
135
+ expect(result.matches).toHaveLength(2);
136
+ expect(result.matches[0].chargeId).toBe(candidateCharge1Id); // Perfect match
137
+ expect(result.matches[0].confidenceScore).toBeGreaterThan(0.95);
138
+ expect(result.matches[1].chargeId).toBe(candidateCharge2Id); // Partial match
139
+ expect(result.matches[1].confidenceScore).toBeLessThan(0.95);
140
+ });
141
+
142
+ it('should find matches for document charge with mock data', async () => {
143
+ // Setup mock data
144
+ const sourceChargeId = 'doc-charge-1';
145
+ const sourceDocuments = [
146
+ createMockDocument({
147
+ charge_id: sourceChargeId,
148
+ total_amount: 200,
149
+ currency_code: 'USD',
150
+ date: new Date('2024-02-15'),
151
+ }),
152
+ ];
153
+
154
+ const candidateCharge1Id = 'tx-charge-1';
155
+ const candidateTransactions1 = [
156
+ createMockTransaction({
157
+ charge_id: candidateCharge1Id,
158
+ amount: "200",
159
+ currency: 'USD',
160
+ event_date: new Date('2024-02-15'),
161
+ }),
162
+ ];
163
+
164
+ const candidateCharge2Id = 'tx-charge-2';
165
+ const candidateTransactions2 = [
166
+ createMockTransaction({
167
+ charge_id: candidateCharge2Id,
168
+ amount: "195",
169
+ currency: 'USD',
170
+ event_date: new Date('2024-02-16'),
171
+ }),
172
+ ];
173
+
174
+ // Mock providers
175
+ const mockChargesProvider = {
176
+ getChargeByIdLoader: {
177
+ load: vi.fn((id: string) => {
178
+ if (id === sourceChargeId) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
179
+ return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
180
+ }),
181
+ },
182
+ getChargesByFilters: vi.fn(() =>
183
+ Promise.resolve([
184
+ createCharge(candidateCharge1Id, ADMIN_BUSINESS_ID),
185
+ createCharge(candidateCharge2Id, ADMIN_BUSINESS_ID),
186
+ ]),
187
+ ),
188
+ };
189
+
190
+ const mockTransactionsProvider = {
191
+ transactionsByChargeIDLoader: {
192
+ load: vi.fn((id: string) => {
193
+ if (id === candidateCharge1Id) return Promise.resolve(candidateTransactions1);
194
+ if (id === candidateCharge2Id) return Promise.resolve(candidateTransactions2);
195
+ return Promise.resolve([]);
196
+ }),
197
+ },
198
+ };
199
+
200
+ const mockDocumentsProvider = {
201
+ getDocumentsByChargeIdLoader: {
202
+ load: vi.fn((id: string) => {
203
+ if (id === sourceChargeId) return Promise.resolve(sourceDocuments);
204
+ return Promise.resolve([]);
205
+ }),
206
+ },
207
+ };
208
+
209
+ const mockInjector = {
210
+ get: vi.fn((token: any) => {
211
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
212
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
213
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
214
+ return null;
215
+ }),
216
+ } as unknown as Injector;
217
+
218
+ // Execute
219
+ const provider = new ChargesMatcherProvider();
220
+ const result = await provider.findMatchesForCharge(sourceChargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any);
221
+
222
+ // Verify
223
+ expect(result.matches).toHaveLength(2);
224
+ expect(result.matches[0].chargeId).toBe(candidateCharge1Id);
225
+ expect(result.matches[0].confidenceScore).toBeGreaterThan(0.95);
226
+ });
227
+
228
+ it('should throw error if charge not found', async () => {
229
+ const mockChargesProvider = {
230
+ getChargeByIdLoader: {
231
+ load: vi.fn(() => Promise.resolve(new Error('Not found'))),
232
+ },
233
+ };
234
+
235
+ const mockInjector = {
236
+ get: vi.fn((token: any) => {
237
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
238
+ return null;
239
+ }),
240
+ } as unknown as Injector;
241
+
242
+ const provider = new ChargesMatcherProvider();
243
+
244
+ await expect(
245
+ provider.findMatchesForCharge('non-existent', { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any),
246
+ ).rejects.toThrow(/Source charge not found/);
247
+ });
248
+
249
+ it('should throw error if charge is already matched', async () => {
250
+ const chargeId = 'matched-charge';
251
+
252
+ const mockChargesProvider = {
253
+ getChargeByIdLoader: {
254
+ load: vi.fn(() => Promise.resolve(createCharge(chargeId, ADMIN_BUSINESS_ID))),
255
+ },
256
+ };
257
+
258
+ const mockTransactionsProvider = {
259
+ transactionsByChargeIDLoader: {
260
+ load: vi.fn(() => Promise.resolve([createMockTransaction()])),
261
+ },
262
+ };
263
+
264
+ const mockDocumentsProvider = {
265
+ getDocumentsByChargeIdLoader: {
266
+ load: vi.fn(() => Promise.resolve([createMockDocument()])),
267
+ },
268
+ };
269
+
270
+ const mockInjector = {
271
+ get: vi.fn((token: any) => {
272
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
273
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
274
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
275
+ return null;
276
+ }),
277
+ } as unknown as Injector;
278
+
279
+ const provider = new ChargesMatcherProvider();
280
+
281
+ await expect(provider.findMatchesForCharge(chargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any)).rejects.toThrow(
282
+ /already matched/,
283
+ );
284
+ });
285
+
286
+ it('should throw error if charge has no transactions or documents', async () => {
287
+ const chargeId = 'empty-charge';
288
+
289
+ const mockChargesProvider = {
290
+ getChargeByIdLoader: {
291
+ load: vi.fn(() => Promise.resolve(createCharge(chargeId, ADMIN_BUSINESS_ID))),
292
+ },
293
+ };
294
+
295
+ const mockTransactionsProvider = {
296
+ transactionsByChargeIDLoader: {
297
+ load: vi.fn(() => Promise.resolve([])),
298
+ },
299
+ };
300
+
301
+ const mockDocumentsProvider = {
302
+ getDocumentsByChargeIdLoader: {
303
+ load: vi.fn(() => Promise.resolve([])),
304
+ },
305
+ };
306
+
307
+ const mockInjector = {
308
+ get: vi.fn((token: any) => {
309
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
310
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
311
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
312
+ return null;
313
+ }),
314
+ } as unknown as Injector;
315
+
316
+ const provider = new ChargesMatcherProvider();
317
+
318
+ await expect(provider.findMatchesForCharge(chargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any)).rejects.toThrow(
319
+ /no transactions or documents/,
320
+ );
321
+ });
322
+
323
+ it('should return empty matches if no candidates found', async () => {
324
+ const sourceChargeId = 'tx-charge-only';
325
+
326
+ const mockChargesProvider = {
327
+ getChargeByIdLoader: {
328
+ load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
329
+ },
330
+ getChargesByFilters: vi.fn(() => Promise.resolve([])),
331
+ };
332
+
333
+ const mockTransactionsProvider = {
334
+ transactionsByChargeIDLoader: {
335
+ load: vi.fn(() => Promise.resolve([createMockTransaction()])),
336
+ },
337
+ };
338
+
339
+ const mockDocumentsProvider = {
340
+ getDocumentsByChargeIdLoader: {
341
+ load: vi.fn(() => Promise.resolve([])),
342
+ },
343
+ };
344
+
345
+ const mockInjector = {
346
+ get: vi.fn((token: any) => {
347
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
348
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
349
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
350
+ return null;
351
+ }),
352
+ } as unknown as Injector;
353
+
354
+ const provider = new ChargesMatcherProvider();
355
+ const result = await provider.findMatchesForCharge(sourceChargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any);
356
+
357
+ expect(result.matches).toEqual([]);
358
+ });
359
+
360
+ it('should filter out matched candidates (having both transactions and documents)', async () => {
361
+ const sourceChargeId = 'tx-charge-1';
362
+ const matchedCandidateId = 'matched-charge';
363
+ const validCandidateId = 'doc-charge-1';
364
+
365
+ const mockChargesProvider = {
366
+ getChargeByIdLoader: {
367
+ load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
368
+ },
369
+ getChargesByFilters: vi.fn(() =>
370
+ Promise.resolve([
371
+ createCharge(matchedCandidateId, ADMIN_BUSINESS_ID),
372
+ createCharge(validCandidateId, ADMIN_BUSINESS_ID),
373
+ ]),
374
+ ),
375
+ };
376
+
377
+ const mockTransactionsProvider = {
378
+ transactionsByChargeIDLoader: {
379
+ load: vi.fn((id: string) => {
380
+ if (id === sourceChargeId) return Promise.resolve([createMockTransaction()]);
381
+ if (id === matchedCandidateId) return Promise.resolve([createMockTransaction()]); // Has transactions
382
+ return Promise.resolve([]);
383
+ }),
384
+ },
385
+ };
386
+
387
+ const mockDocumentsProvider = {
388
+ getDocumentsByChargeIdLoader: {
389
+ load: vi.fn((id: string) => {
390
+ if (id === matchedCandidateId) return Promise.resolve([createMockDocument()]); // Also has documents - MATCHED
391
+ if (id === validCandidateId) return Promise.resolve([createMockDocument()]); // Only documents - VALID
392
+ return Promise.resolve([]);
393
+ }),
394
+ },
395
+ };
396
+
397
+ const mockInjector = {
398
+ get: vi.fn((token: any) => {
399
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
400
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
401
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
402
+ return null;
403
+ }),
404
+ } as unknown as Injector;
405
+
406
+ const provider = new ChargesMatcherProvider();
407
+ const result = await provider.findMatchesForCharge(sourceChargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any);
408
+
409
+ // Should only include validCandidateId, not matchedCandidateId
410
+ expect(result.matches).toHaveLength(1);
411
+ expect(result.matches[0].chargeId).toBe(validCandidateId);
412
+ });
413
+
414
+ it('should handle date window filtering correctly', async () => {
415
+ const sourceChargeId = 'tx-charge-1';
416
+ const sourceDate = new Date('2024-06-15');
417
+
418
+ const withinWindowId = 'doc-charge-within';
419
+ const outsideWindowId = 'doc-charge-outside';
420
+
421
+ const mockChargesProvider = {
422
+ getChargeByIdLoader: {
423
+ load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
424
+ },
425
+ getChargesByFilters: vi.fn(params => {
426
+ // Verify date window parameters
427
+ expect(params.fromAnyDate).toBeDefined();
428
+ expect(params.toAnyDate).toBeDefined();
429
+ return Promise.resolve([
430
+ createCharge(withinWindowId, ADMIN_BUSINESS_ID),
431
+ createCharge(outsideWindowId, ADMIN_BUSINESS_ID),
432
+ ]);
433
+ }),
434
+ };
435
+
436
+ const mockTransactionsProvider = {
437
+ transactionsByChargeIDLoader: {
438
+ load: vi.fn((id: string) => {
439
+ if (id === sourceChargeId) {
440
+ return Promise.resolve([createMockTransaction({ event_date: sourceDate })]);
441
+ }
442
+ return Promise.resolve([]);
443
+ }),
444
+ },
445
+ };
446
+
447
+ const mockDocumentsProvider = {
448
+ getDocumentsByChargeIdLoader: {
449
+ load: vi.fn((id: string) => {
450
+ if (id === sourceChargeId) return Promise.resolve([]);
451
+ if (id === withinWindowId) {
452
+ // Within 12-month window
453
+ return Promise.resolve([
454
+ createMockDocument({ date: new Date('2024-06-20') }),
455
+ ]);
456
+ }
457
+ if (id === outsideWindowId) {
458
+ // Outside 12-month window
459
+ return Promise.resolve([
460
+ createMockDocument({ date: new Date('2025-07-15') }),
461
+ ]);
462
+ }
463
+ return Promise.resolve([]);
464
+ }),
465
+ },
466
+ };
467
+
468
+ const mockInjector = {
469
+ get: vi.fn((token: any) => {
470
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
471
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
472
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
473
+ return null;
474
+ }),
475
+ } as unknown as Injector;
476
+
477
+ const provider = new ChargesMatcherProvider();
478
+ const result = await provider.findMatchesForCharge(sourceChargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any);
479
+
480
+ // Should only include candidate within window
481
+ expect(result.matches).toHaveLength(1);
482
+ expect(result.matches[0].chargeId).toBe(withinWindowId);
483
+ });
484
+
485
+ it('should throw error if user ID not in context', async () => {
486
+ const mockInjector = {
487
+ get: vi.fn(() => null), // No user
488
+ } as unknown as Injector;
489
+
490
+ const provider = new ChargesMatcherProvider();
491
+
492
+ await expect(provider.findMatchesForCharge('any-id', { adminContext: { defaultAdminBusinessId: null }, injector: mockInjector } as any)).rejects.toThrow(
493
+ /Admin business not found in context/,
494
+ );
495
+ });
496
+
497
+ it('should return top 5 matches when more candidates exist', async () => {
498
+ const sourceChargeId = 'tx-charge-1';
499
+
500
+ // Create 7 candidate charges
501
+ const candidateIds = Array.from({ length: 7 }, (_, i) => `doc-charge-${i + 1}`);
502
+
503
+ const mockChargesProvider = {
504
+ getChargeByIdLoader: {
505
+ load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
506
+ },
507
+ getChargesByFilters: vi.fn(() =>
508
+ Promise.resolve(candidateIds.map(id => createCharge(id, ADMIN_BUSINESS_ID))),
509
+ ),
510
+ };
511
+
512
+ const mockTransactionsProvider = {
513
+ transactionsByChargeIDLoader: {
514
+ load: vi.fn((id: string) => {
515
+ if (id === sourceChargeId) {
516
+ return Promise.resolve([createMockTransaction({ amount: "100" })]);
517
+ }
518
+ return Promise.resolve([]);
519
+ }),
520
+ },
521
+ };
522
+
523
+ const mockDocumentsProvider = {
524
+ getDocumentsByChargeIdLoader: {
525
+ load: vi.fn((id: string) => {
526
+ if (id === sourceChargeId) return Promise.resolve([]);
527
+ // Create documents with varying amounts for different confidence scores
528
+ const index = parseInt(id.split('-')[2]);
529
+ return Promise.resolve([createMockDocument({ total_amount: 100 + index })]);
530
+ }),
531
+ },
532
+ };
533
+
534
+ const mockInjector = {
535
+ get: vi.fn((token: any) => {
536
+ if (token.name === 'ChargesProvider') return mockChargesProvider;
537
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
538
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
539
+ return null;
540
+ }),
541
+ } as unknown as Injector;
542
+
543
+ const provider = new ChargesMatcherProvider();
544
+ const result = await provider.findMatchesForCharge(sourceChargeId, { adminContext: { defaultAdminBusinessId: ADMIN_BUSINESS_ID }, injector: mockInjector } as any);
545
+
546
+ // Should return maximum of 5 matches
547
+ expect(result.matches).toHaveLength(5);
548
+ // Verify sorted by confidence (descending)
549
+ for (let i = 0; i < result.matches.length - 1; i++) {
550
+ expect(result.matches[i].confidenceScore).toBeGreaterThanOrEqual(
551
+ result.matches[i + 1].confidenceScore,
552
+ );
553
+ }
554
+ });
555
+ });
556
+ });