@accounter/server 0.0.9-alpha-20251217093036-7168648b507d62946aa287af4ea690b73b077b2d → 0.0.9-alpha-20251217131153-65f961a4072436d7f1042ea8ea4d96534cb3650e

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 (69) hide show
  1. package/CHANGELOG.md +16 -8
  2. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +45 -122
  3. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -1
  4. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +45 -29
  5. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -1
  6. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +2 -11
  7. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -1
  8. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +25 -0
  9. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -1
  10. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -1
  11. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +65 -64
  12. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -1
  13. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +494 -60
  14. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -1
  15. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +34 -98
  16. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
  17. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +79 -59
  18. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -1
  19. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +6 -4
  20. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -1
  21. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +9 -2
  22. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +24 -2
  23. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -1
  24. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +1 -4
  25. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +2 -1
  26. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -1
  27. package/dist/server/src/modules/charges-matcher/index.d.ts +0 -1
  28. package/dist/server/src/modules/charges-matcher/index.js +0 -1
  29. package/dist/server/src/modules/charges-matcher/index.js.map +1 -1
  30. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +2 -1
  31. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +2 -2
  32. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -1
  33. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +2 -2
  34. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -1
  35. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +4 -5
  36. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +5 -4
  37. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -1
  38. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +5 -3
  39. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +70 -13
  40. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -1
  41. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +4 -2
  42. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +15 -7
  43. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -1
  44. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +1 -1
  45. package/dist/server/src/modules/charges-matcher/types.d.ts +2 -4
  46. package/dist/server/src/modules/charges-matcher/types.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/modules/charges-matcher/README.md +14 -3
  49. package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +52 -100
  50. package/src/modules/charges-matcher/__tests__/auto-match.test.ts +51 -29
  51. package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +2 -13
  52. package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +29 -0
  53. package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +0 -1
  54. package/src/modules/charges-matcher/__tests__/document-amount.test.ts +66 -65
  55. package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +552 -60
  56. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +43 -73
  57. package/src/modules/charges-matcher/__tests__/single-match.test.ts +81 -59
  58. package/src/modules/charges-matcher/documentation/SPEC.md +276 -4
  59. package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +7 -5
  60. package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +32 -2
  61. package/src/modules/charges-matcher/helpers/document-amount.helper.ts +2 -12
  62. package/src/modules/charges-matcher/index.ts +0 -1
  63. package/src/modules/charges-matcher/providers/auto-match.provider.ts +5 -3
  64. package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +8 -2
  65. package/src/modules/charges-matcher/providers/document-aggregator.ts +12 -11
  66. package/src/modules/charges-matcher/providers/match-scorer.provider.ts +97 -17
  67. package/src/modules/charges-matcher/providers/single-match.provider.ts +21 -8
  68. package/src/modules/charges-matcher/providers/transaction-aggregator.ts +1 -1
  69. package/src/modules/charges-matcher/types.ts +2 -5
@@ -1,9 +1,61 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import { scoreMatch, selectTransactionDate, } from '../providers/match-scorer.provider.js';
3
3
  import { createMockTransaction, createMockDocument } from './test-helpers.js';
4
+ import { DocumentType } from '../../../shared/enums.js';
5
+ // Mock DI system and ClientsProvider / IssuedDocumentsProvider
6
+ vi.mock('../../financial-entities/providers/clients.provider.js', () => ({
7
+ ClientsProvider: class {
8
+ },
9
+ }));
10
+ vi.mock('../../documents/providers/issued-documents.provider.js', () => ({
11
+ IssuedDocumentsProvider: class {
12
+ },
13
+ }));
4
14
  // Test user ID
5
15
  const USER_ID = 'user-123';
6
16
  const BUSINESS_ID = 'business-abc';
17
+ // Create a mock injector for testing client matching
18
+ // Create a mock injector for testing
19
+ const createMockInjector = () => ({
20
+ get: vi.fn((token) => {
21
+ if (token.name === 'ClientsProvider')
22
+ return {
23
+ getClientByIdLoader: {
24
+ load: (businessId) => {
25
+ const isRegisteredClient = businessId.startsWith('client-');
26
+ return Promise.resolve(isRegisteredClient ? { id: businessId } : null);
27
+ },
28
+ },
29
+ };
30
+ if (token.name === 'IssuedDocumentsProvider')
31
+ return {
32
+ getIssuedDocumentsStatusByChargeIdLoader: {
33
+ load: async (_chargeId) => ({ charge_id: _chargeId, open_docs_flag: true }),
34
+ },
35
+ };
36
+ return null;
37
+ }),
38
+ });
39
+ // Spy-able injector to assert DataLoader usage
40
+ const createSpyInjector = (loaderImpl) => {
41
+ const load = vi.fn(loaderImpl ?? (() => Promise.resolve(null)));
42
+ const get = vi.fn((token) => {
43
+ if (token.name === 'ClientsProvider') {
44
+ return {
45
+ getClientByIdLoader: { load },
46
+ };
47
+ }
48
+ if (token.name === 'IssuedDocumentsProvider') {
49
+ return {
50
+ getIssuedDocumentsStatusByChargeIdLoader: {
51
+ load: vi.fn(async (chargeId) => ({ charge_id: chargeId, open_docs_flag: true })),
52
+ },
53
+ };
54
+ }
55
+ return null;
56
+ });
57
+ return { get, load };
58
+ };
7
59
  describe('Match Scorer', () => {
8
60
  describe('selectTransactionDate', () => {
9
61
  const transaction = {
@@ -15,42 +67,42 @@ describe('Match Scorer', () => {
15
67
  description: 'Test',
16
68
  };
17
69
  it('should use event_date for INVOICE', () => {
18
- const result = selectTransactionDate(transaction, 'INVOICE');
70
+ const result = selectTransactionDate(transaction, DocumentType.Invoice);
19
71
  expect(result).toEqual(new Date('2024-01-15'));
20
72
  });
21
73
  it('should use event_date for CREDIT_INVOICE', () => {
22
- const result = selectTransactionDate(transaction, 'CREDIT_INVOICE');
74
+ const result = selectTransactionDate(transaction, DocumentType.CreditInvoice);
23
75
  expect(result).toEqual(new Date('2024-01-15'));
24
76
  });
25
77
  it('should use event_date for RECEIPT', () => {
26
- const result = selectTransactionDate(transaction, 'RECEIPT');
78
+ const result = selectTransactionDate(transaction, DocumentType.Receipt);
27
79
  expect(result).toEqual(new Date('2024-01-15'));
28
80
  });
29
81
  it('should use event_date for INVOICE_RECEIPT', () => {
30
- const result = selectTransactionDate(transaction, 'INVOICE_RECEIPT');
82
+ const result = selectTransactionDate(transaction, DocumentType.InvoiceReceipt);
31
83
  expect(result).toEqual(new Date('2024-01-15'));
32
84
  });
33
85
  it('should use event_date for RECEIPT when debitDate is null', () => {
34
86
  const txWithoutDebitDate = { ...transaction, debitDate: null };
35
- const result = selectTransactionDate(txWithoutDebitDate, 'RECEIPT');
87
+ const result = selectTransactionDate(txWithoutDebitDate, DocumentType.Receipt);
36
88
  expect(result).toEqual(new Date('2024-01-15'));
37
89
  });
38
90
  it('should use event_date for OTHER', () => {
39
- const result = selectTransactionDate(transaction, 'OTHER');
91
+ const result = selectTransactionDate(transaction, DocumentType.Other);
40
92
  expect(result).toEqual(new Date('2024-01-15'));
41
93
  });
42
94
  it('should use event_date for PROFORMA', () => {
43
- const result = selectTransactionDate(transaction, 'PROFORMA');
95
+ const result = selectTransactionDate(transaction, DocumentType.Proforma);
44
96
  expect(result).toEqual(new Date('2024-01-15'));
45
97
  });
46
98
  it('should use event_date for UNPROCESSED', () => {
47
- const result = selectTransactionDate(transaction, 'UNPROCESSED');
99
+ const result = selectTransactionDate(transaction, DocumentType.Unprocessed);
48
100
  expect(result).toEqual(new Date('2024-01-15'));
49
101
  });
50
102
  });
51
103
  describe('scoreMatch', () => {
52
104
  describe('Perfect Matches', () => {
53
- it('should score perfect match close to 1.0', () => {
105
+ it('should score perfect match close to 1.0', async () => {
54
106
  const txCharge = {
55
107
  chargeId: 'charge-tx-1',
56
108
  transactions: [
@@ -75,15 +127,16 @@ describe('Match Scorer', () => {
75
127
  }),
76
128
  ],
77
129
  };
78
- const result = scoreMatch(txCharge, docCharge, USER_ID);
130
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
79
131
  expect(result.chargeId).toBe('charge-doc-1');
80
132
  expect(result.confidenceScore).toBeGreaterThan(0.95);
81
133
  expect(result.components.amount).toBe(1.0);
82
134
  expect(result.components.currency).toBe(1.0);
83
135
  expect(result.components.business).toBe(1.0);
136
+ // Non-client same-business: uses standard date calculation, not flat 1.0
84
137
  expect(result.components.date).toBe(1.0);
85
138
  });
86
- it('should handle receipt matching with event_date', () => {
139
+ it('should handle receipt matching with event_date', async () => {
87
140
  const txCharge = {
88
141
  chargeId: 'charge-tx-2',
89
142
  transactions: [
@@ -92,6 +145,7 @@ describe('Match Scorer', () => {
92
145
  event_date: new Date('2024-01-10'),
93
146
  debit_date: new Date('2024-01-15'),
94
147
  debit_timestamp: null,
148
+ business_id: 'customer-receipt', // Different business for cross-business test
95
149
  }),
96
150
  ],
97
151
  };
@@ -102,16 +156,17 @@ describe('Match Scorer', () => {
102
156
  total_amount: 200,
103
157
  date: new Date('2024-01-15'),
104
158
  type: 'RECEIPT',
159
+ creditor_id: 'vendor-receipt', // Different business
105
160
  }),
106
161
  ],
107
162
  };
108
- const result = scoreMatch(txCharge, docCharge, USER_ID);
109
- expect(result.confidenceScore).toBeGreaterThan(0.80);
163
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
164
+ expect(result.confidenceScore).toBeGreaterThan(0.70); // Amount + currency + date good, business mismatch
110
165
  expect(result.components.date).toBeCloseTo(0.83, 1);
111
166
  });
112
167
  });
113
168
  describe('Partial Matches', () => {
114
- it('should handle amount mismatch', () => {
169
+ it('should handle amount mismatch', async () => {
115
170
  const txCharge = {
116
171
  chargeId: 'charge-tx-3',
117
172
  transactions: [createMockTransaction({ amount: "100.00" })],
@@ -120,12 +175,12 @@ describe('Match Scorer', () => {
120
175
  chargeId: 'charge-doc-3',
121
176
  documents: [createMockDocument({ total_amount: 110 })], // 10% difference
122
177
  };
123
- const result = scoreMatch(txCharge, docCharge, USER_ID);
178
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
124
179
  expect(result.components.amount).toBeLessThan(1.0);
125
180
  expect(result.components.amount).toBeGreaterThan(0.0);
126
181
  expect(result.confidenceScore).toBeLessThan(0.95);
127
182
  });
128
- it('should handle currency mismatch', () => {
183
+ it('should handle currency mismatch', async () => {
129
184
  const txCharge = {
130
185
  chargeId: 'charge-tx-4',
131
186
  transactions: [createMockTransaction({ currency: 'USD' })],
@@ -134,12 +189,12 @@ describe('Match Scorer', () => {
134
189
  chargeId: 'charge-doc-4',
135
190
  documents: [createMockDocument({ currency_code: 'EUR' })],
136
191
  };
137
- const result = scoreMatch(txCharge, docCharge, USER_ID);
192
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
138
193
  expect(result.components.currency).toBe(0.0); // Different currencies = 0.0
139
194
  // Overall confidence can still be decent if other factors match
140
195
  expect(result.confidenceScore).toBeLessThan(1.0);
141
196
  });
142
- it('should handle business mismatch', () => {
197
+ it('should handle business mismatch', async () => {
143
198
  const txCharge = {
144
199
  chargeId: 'charge-tx-5',
145
200
  transactions: [createMockTransaction({ business_id: 'business-1' })],
@@ -153,15 +208,16 @@ describe('Match Scorer', () => {
153
208
  }),
154
209
  ],
155
210
  };
156
- const result = scoreMatch(txCharge, docCharge, USER_ID);
211
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
157
212
  expect(result.components.business).toBe(0.2);
158
213
  });
159
- it('should handle date difference', () => {
214
+ it('should handle date difference', async () => {
160
215
  const txCharge = {
161
216
  chargeId: 'charge-tx-6',
162
217
  transactions: [
163
218
  createMockTransaction({
164
219
  event_date: new Date('2024-01-01'),
220
+ business_id: 'customer-1', // Different business for cross-business test
165
221
  }),
166
222
  ],
167
223
  };
@@ -170,15 +226,325 @@ describe('Match Scorer', () => {
170
226
  documents: [
171
227
  createMockDocument({
172
228
  date: new Date('2024-01-16'), // 15 days difference
229
+ creditor_id: 'vendor-1', // Different business
173
230
  }),
174
231
  ],
175
232
  };
176
- const result = scoreMatch(txCharge, docCharge, USER_ID);
233
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
177
234
  expect(result.components.date).toBe(0.5); // 15/30 = 0.5
178
235
  });
179
236
  });
237
+ describe('Client-aware date confidence', () => {
238
+ it('returns flat 1.0 for client same-business on same day', async () => {
239
+ const txCharge = {
240
+ chargeId: 'charge-tx-client-same-day',
241
+ transactions: [
242
+ createMockTransaction({
243
+ amount: "100.00",
244
+ currency: 'USD',
245
+ event_date: new Date('2024-01-15'),
246
+ business_id: 'client-abc',
247
+ }),
248
+ ],
249
+ };
250
+ const docCharge = {
251
+ chargeId: 'charge-doc-client-same-day',
252
+ documents: [
253
+ createMockDocument({
254
+ total_amount: 100,
255
+ currency_code: 'USD',
256
+ date: new Date('2024-01-15'),
257
+ creditor_id: 'client-abc',
258
+ debtor_id: USER_ID,
259
+ type: 'INVOICE',
260
+ }),
261
+ ],
262
+ };
263
+ const spyInjector = createSpyInjector(async () => ({ id: 'client-abc' }));
264
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector);
265
+ expect(result.components.date).toBe(1.0);
266
+ expect(spyInjector.load).toHaveBeenCalledTimes(1);
267
+ });
268
+ it('returns flat 1.0 for client same-business 30 days apart', async () => {
269
+ const txCharge = {
270
+ chargeId: 'charge-tx-client-30',
271
+ transactions: [
272
+ createMockTransaction({
273
+ event_date: new Date('2024-01-31'),
274
+ business_id: 'client-xyz',
275
+ }),
276
+ ],
277
+ };
278
+ const docCharge = {
279
+ chargeId: 'charge-doc-client-30',
280
+ documents: [
281
+ createMockDocument({
282
+ date: new Date('2024-01-01'),
283
+ creditor_id: 'client-xyz',
284
+ debtor_id: USER_ID,
285
+ type: 'INVOICE',
286
+ }),
287
+ ],
288
+ };
289
+ const spyInjector = createSpyInjector(async () => ({ id: 'client-xyz' }));
290
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector);
291
+ expect(result.components.date).toBe(1.0);
292
+ expect(spyInjector.load).toHaveBeenCalledTimes(1);
293
+ });
294
+ it('returns flat 1.0 for client same-business 365 days apart', async () => {
295
+ const txCharge = {
296
+ chargeId: 'charge-tx-client-365',
297
+ transactions: [
298
+ createMockTransaction({
299
+ event_date: new Date('2024-01-01'),
300
+ business_id: 'client-long',
301
+ }),
302
+ ],
303
+ };
304
+ const docCharge = {
305
+ chargeId: 'charge-doc-client-365',
306
+ documents: [
307
+ createMockDocument({
308
+ date: new Date('2023-01-01'),
309
+ creditor_id: 'client-long',
310
+ debtor_id: USER_ID,
311
+ type: 'INVOICE',
312
+ }),
313
+ ],
314
+ };
315
+ const spyInjector = createSpyInjector(async () => ({ id: 'client-long' }));
316
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector);
317
+ expect(result.components.date).toBe(1.0);
318
+ expect(spyInjector.load).toHaveBeenCalledTimes(1);
319
+ });
320
+ it('uses standard degradation for non-client same-business (15 days)', async () => {
321
+ const txCharge = {
322
+ chargeId: 'charge-tx-nonclient-15',
323
+ transactions: [
324
+ createMockTransaction({
325
+ event_date: new Date('2024-01-01'),
326
+ business_id: 'vendor-plain',
327
+ }),
328
+ ],
329
+ };
330
+ const docCharge = {
331
+ chargeId: 'charge-doc-nonclient-15',
332
+ documents: [
333
+ createMockDocument({
334
+ date: new Date('2024-01-16'),
335
+ creditor_id: 'vendor-plain',
336
+ debtor_id: USER_ID,
337
+ type: 'INVOICE',
338
+ }),
339
+ ],
340
+ };
341
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
342
+ expect(result.components.date).toBe(0.5);
343
+ });
344
+ it('uses standard degradation for non-client same-business (45 days)', async () => {
345
+ const txCharge = {
346
+ chargeId: 'charge-tx-nonclient-45',
347
+ transactions: [
348
+ createMockTransaction({
349
+ event_date: new Date('2024-01-01'),
350
+ business_id: 'vendor-far',
351
+ }),
352
+ ],
353
+ };
354
+ const docCharge = {
355
+ chargeId: 'charge-doc-nonclient-45',
356
+ documents: [
357
+ createMockDocument({
358
+ date: new Date('2024-02-15'),
359
+ creditor_id: 'vendor-far',
360
+ debtor_id: USER_ID,
361
+ type: 'INVOICE',
362
+ }),
363
+ ],
364
+ };
365
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
366
+ expect(result.components.date).toBe(0.0);
367
+ });
368
+ it('uses standard degradation for cross-business matches', async () => {
369
+ const txCharge = {
370
+ chargeId: 'charge-tx-cross',
371
+ transactions: [
372
+ createMockTransaction({
373
+ event_date: new Date('2024-01-01'),
374
+ business_id: 'customer-xyz',
375
+ }),
376
+ ],
377
+ };
378
+ const docCharge = {
379
+ chargeId: 'charge-doc-cross',
380
+ documents: [
381
+ createMockDocument({
382
+ date: new Date('2024-01-06'),
383
+ creditor_id: 'vendor-xyz',
384
+ debtor_id: USER_ID,
385
+ type: 'INVOICE',
386
+ }),
387
+ ],
388
+ };
389
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
390
+ expect(result.components.date).toBeCloseTo(0.83, 1);
391
+ });
392
+ it('skips client lookup when businessId is null', async () => {
393
+ const txCharge = {
394
+ chargeId: 'charge-tx-null-business',
395
+ transactions: [
396
+ createMockTransaction({
397
+ event_date: new Date('2024-01-01'),
398
+ business_id: null,
399
+ }),
400
+ ],
401
+ };
402
+ const docCharge = {
403
+ chargeId: 'charge-doc-null-business',
404
+ documents: [
405
+ createMockDocument({
406
+ date: new Date('2024-01-16'),
407
+ creditor_id: USER_ID,
408
+ debtor_id: null,
409
+ type: 'INVOICE',
410
+ }),
411
+ ],
412
+ };
413
+ const spyInjector = createSpyInjector();
414
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector);
415
+ expect(result.components.date).toBe(0.5);
416
+ expect(spyInjector.load).not.toHaveBeenCalled();
417
+ });
418
+ it('uses ClientsProvider result when lookup succeeds', async () => {
419
+ const txCharge = {
420
+ chargeId: 'charge-tx-client-lookup',
421
+ transactions: [
422
+ createMockTransaction({
423
+ event_date: new Date('2024-01-01'),
424
+ business_id: 'client-lookup',
425
+ }),
426
+ ],
427
+ };
428
+ const docCharge = {
429
+ chargeId: 'charge-doc-client-lookup',
430
+ documents: [
431
+ createMockDocument({
432
+ date: new Date('2023-12-01'),
433
+ creditor_id: 'client-lookup',
434
+ debtor_id: USER_ID,
435
+ type: 'INVOICE',
436
+ }),
437
+ ],
438
+ };
439
+ const spyInjector = createSpyInjector(async () => ({ id: 'client-lookup' }));
440
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector);
441
+ expect(result.components.date).toBe(1.0);
442
+ expect(spyInjector.load).toHaveBeenCalledTimes(1);
443
+ });
444
+ it('uses standard degradation when ClientsProvider returns null', async () => {
445
+ const txCharge = {
446
+ chargeId: 'charge-tx-client-null',
447
+ transactions: [
448
+ createMockTransaction({
449
+ event_date: new Date('2024-01-01'),
450
+ business_id: 'client-missing',
451
+ }),
452
+ ],
453
+ };
454
+ const docCharge = {
455
+ chargeId: 'charge-doc-client-null',
456
+ documents: [
457
+ createMockDocument({
458
+ date: new Date('2024-01-11'),
459
+ creditor_id: 'client-missing',
460
+ debtor_id: USER_ID,
461
+ type: 'INVOICE',
462
+ }),
463
+ ],
464
+ };
465
+ const spyInjector = createSpyInjector(async () => null);
466
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector);
467
+ expect(result.components.date).toBeCloseTo(0.67, 1);
468
+ expect(spyInjector.load).toHaveBeenCalledTimes(1);
469
+ });
470
+ it('gives equal date scores for multiple client invoices regardless of recency', async () => {
471
+ const txCharge = {
472
+ chargeId: 'charge-tx-client-multi',
473
+ transactions: [
474
+ createMockTransaction({
475
+ event_date: new Date('2024-01-15'),
476
+ business_id: 'client-multi',
477
+ }),
478
+ ],
479
+ };
480
+ const janDoc = {
481
+ chargeId: 'charge-doc-client-jan',
482
+ documents: [
483
+ createMockDocument({
484
+ date: new Date('2023-12-30'),
485
+ creditor_id: 'client-multi',
486
+ debtor_id: USER_ID,
487
+ type: 'INVOICE',
488
+ }),
489
+ ],
490
+ };
491
+ const febDoc = {
492
+ chargeId: 'charge-doc-client-feb',
493
+ documents: [
494
+ createMockDocument({
495
+ date: new Date('2023-11-30'),
496
+ creditor_id: 'client-multi',
497
+ debtor_id: USER_ID,
498
+ type: 'INVOICE',
499
+ }),
500
+ ],
501
+ };
502
+ const spyInjector = createSpyInjector(async () => ({ id: 'client-multi' }));
503
+ const janScore = await scoreMatch(txCharge, janDoc, USER_ID, spyInjector);
504
+ const febScore = await scoreMatch(txCharge, febDoc, USER_ID, spyInjector);
505
+ expect(janScore.components.date).toBe(1.0);
506
+ expect(febScore.components.date).toBe(1.0);
507
+ expect(spyInjector.load).toHaveBeenCalledTimes(2);
508
+ });
509
+ it('prefers closer date for non-client when amounts tie', async () => {
510
+ const txCharge = {
511
+ chargeId: 'charge-tx-nonclient-multi',
512
+ transactions: [
513
+ createMockTransaction({
514
+ event_date: new Date('2024-01-15'),
515
+ business_id: 'vendor-multi',
516
+ }),
517
+ ],
518
+ };
519
+ const nearDoc = {
520
+ chargeId: 'charge-doc-nonclient-near',
521
+ documents: [
522
+ createMockDocument({
523
+ date: new Date('2024-01-17'),
524
+ creditor_id: 'vendor-multi',
525
+ debtor_id: USER_ID,
526
+ type: 'INVOICE',
527
+ }),
528
+ ],
529
+ };
530
+ const farDoc = {
531
+ chargeId: 'charge-doc-nonclient-far',
532
+ documents: [
533
+ createMockDocument({
534
+ date: new Date('2024-02-20'),
535
+ creditor_id: 'vendor-multi',
536
+ debtor_id: USER_ID,
537
+ type: 'INVOICE',
538
+ }),
539
+ ],
540
+ };
541
+ const nearScore = await scoreMatch(txCharge, nearDoc, USER_ID, createMockInjector());
542
+ const farScore = await scoreMatch(txCharge, farDoc, USER_ID, createMockInjector());
543
+ expect(nearScore.components.date).toBeGreaterThan(farScore.components.date);
544
+ });
545
+ });
180
546
  describe('Date Type Selection', () => {
181
- it('should use event_date for INVOICE matching', () => {
547
+ it('should use event_date for INVOICE matching', async () => {
182
548
  const txCharge = {
183
549
  chargeId: 'charge-tx-7',
184
550
  transactions: [
@@ -197,10 +563,10 @@ describe('Match Scorer', () => {
197
563
  }),
198
564
  ],
199
565
  };
200
- const result = scoreMatch(txCharge, docCharge, USER_ID);
566
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
201
567
  expect(result.components.date).toBe(1.0); // Should match event_date
202
568
  });
203
- it('should use event_date for RECEIPT matching', () => {
569
+ it('should use event_date for RECEIPT matching', async () => {
204
570
  const txCharge = {
205
571
  chargeId: 'charge-tx-8',
206
572
  transactions: [
@@ -208,6 +574,7 @@ describe('Match Scorer', () => {
208
574
  event_date: new Date('2024-01-10'),
209
575
  debit_date: new Date('2024-01-15'),
210
576
  debit_timestamp: null,
577
+ business_id: 'customer-2', // Different business for cross-business test
211
578
  }),
212
579
  ],
213
580
  };
@@ -217,13 +584,14 @@ describe('Match Scorer', () => {
217
584
  createMockDocument({
218
585
  date: new Date('2024-01-15'), // Matches debit_date
219
586
  type: 'RECEIPT',
587
+ creditor_id: 'vendor-2', // Different business
220
588
  }),
221
589
  ],
222
590
  };
223
- const result = scoreMatch(txCharge, docCharge, USER_ID);
591
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
224
592
  expect(result.components.date).toBeCloseTo(0.83, 1);
225
593
  });
226
- it('should fall back to event_date for RECEIPT when debit_date is null', () => {
594
+ it('should fall back to event_date for RECEIPT when debit_date is null', async () => {
227
595
  const txCharge = {
228
596
  chargeId: 'charge-tx-9',
229
597
  transactions: [
@@ -243,12 +611,12 @@ describe('Match Scorer', () => {
243
611
  }),
244
612
  ],
245
613
  };
246
- const result = scoreMatch(txCharge, docCharge, USER_ID);
614
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
247
615
  expect(result.components.date).toBe(1.0);
248
616
  });
249
617
  });
250
618
  describe('Flexible Document Types (PROFORMA/OTHER/UNPROCESSED)', () => {
251
- it('should use event_date for PROFORMA and use better score', () => {
619
+ it('should use event_date for PROFORMA and use better score', async () => {
252
620
  const txCharge = {
253
621
  chargeId: 'charge-tx-10',
254
622
  transactions: [
@@ -256,6 +624,7 @@ describe('Match Scorer', () => {
256
624
  event_date: new Date('2024-01-01'), // Far from document date
257
625
  debit_date: new Date('2024-01-15'), // Matches document date
258
626
  debit_timestamp: null,
627
+ business_id: 'customer-3', // Different business for cross-business test
259
628
  }),
260
629
  ],
261
630
  };
@@ -265,15 +634,16 @@ describe('Match Scorer', () => {
265
634
  createMockDocument({
266
635
  date: new Date('2024-01-15'),
267
636
  type: 'PROFORMA',
637
+ creditor_id: 'vendor-3', // Different business
268
638
  }),
269
639
  ],
270
640
  };
271
- const result = scoreMatch(txCharge, docCharge, USER_ID);
641
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
272
642
  // Should use event_date (2024-01-01) vs latest doc date (2024-01-15) = 14 days
273
643
  // Date confidence: 1.0 - 14/30 = 0.53
274
644
  expect(result.components.date).toBeCloseTo(0.53, 1);
275
645
  });
276
- it('should handle OTHER type with both dates', () => {
646
+ it('should handle OTHER type with both dates', async () => {
277
647
  const txCharge = {
278
648
  chargeId: 'charge-tx-11',
279
649
  transactions: [
@@ -292,11 +662,11 @@ describe('Match Scorer', () => {
292
662
  }),
293
663
  ],
294
664
  };
295
- const result = scoreMatch(txCharge, docCharge, USER_ID);
665
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
296
666
  // Should use event_date since it matches better
297
667
  expect(result.components.date).toBe(1.0);
298
668
  });
299
- it('should handle UNPROCESSED type', () => {
669
+ it('should handle UNPROCESSED type', async () => {
300
670
  const txCharge = {
301
671
  chargeId: 'charge-tx-12',
302
672
  transactions: [
@@ -316,10 +686,10 @@ describe('Match Scorer', () => {
316
686
  }),
317
687
  ],
318
688
  };
319
- const result = scoreMatch(txCharge, docCharge, USER_ID);
689
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
320
690
  expect(result.components.date).toBe(1.0);
321
691
  });
322
- it('should handle flexible type without debit_date', () => {
692
+ it('should handle flexible type without debit_date', async () => {
323
693
  const txCharge = {
324
694
  chargeId: 'charge-tx-13',
325
695
  transactions: [
@@ -339,13 +709,13 @@ describe('Match Scorer', () => {
339
709
  }),
340
710
  ],
341
711
  };
342
- const result = scoreMatch(txCharge, docCharge, USER_ID);
712
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
343
713
  // Should only use event_date
344
714
  expect(result.components.date).toBe(1.0);
345
715
  });
346
716
  });
347
717
  describe('Amount Variations', () => {
348
- it('should handle small amount differences', () => {
718
+ it('should handle small amount differences', async () => {
349
719
  const txCharge = {
350
720
  chargeId: 'charge-tx-14',
351
721
  transactions: [createMockTransaction({ amount: "100.00" })],
@@ -354,10 +724,10 @@ describe('Match Scorer', () => {
354
724
  chargeId: 'charge-doc-14',
355
725
  documents: [createMockDocument({ total_amount: 100.5 })], // 0.5 difference
356
726
  };
357
- const result = scoreMatch(txCharge, docCharge, USER_ID);
727
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
358
728
  expect(result.components.amount).toBeGreaterThanOrEqual(0.9); // Within 1 unit = exactly 0.9
359
729
  });
360
- it('should handle large amount differences', () => {
730
+ it('should handle large amount differences', async () => {
361
731
  const txCharge = {
362
732
  chargeId: 'charge-tx-15',
363
733
  transactions: [createMockTransaction({ amount: "100.00" })],
@@ -366,12 +736,12 @@ describe('Match Scorer', () => {
366
736
  chargeId: 'charge-doc-15',
367
737
  documents: [createMockDocument({ total_amount: 150 })], // 50% difference
368
738
  };
369
- const result = scoreMatch(txCharge, docCharge, USER_ID);
739
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
370
740
  expect(result.components.amount).toBe(0.0);
371
741
  });
372
742
  });
373
743
  describe('Integration Tests', () => {
374
- it('should handle multiple transactions and documents', () => {
744
+ it('should handle multiple transactions and documents', async () => {
375
745
  const txCharge = {
376
746
  chargeId: 'charge-tx-16',
377
747
  transactions: [
@@ -385,11 +755,11 @@ describe('Match Scorer', () => {
385
755
  createMockDocument({ total_amount: 100, serial_number: 'INV-001' }),
386
756
  ],
387
757
  };
388
- const result = scoreMatch(txCharge, docCharge, USER_ID);
758
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
389
759
  expect(result.components.amount).toBe(1.0); // 50 + 50 = 100
390
760
  expect(result.confidenceScore).toBeGreaterThan(0.95);
391
761
  });
392
- it('should handle credit invoice (negative amounts)', () => {
762
+ it('should handle credit invoice (negative amounts)', async () => {
393
763
  const txCharge = {
394
764
  chargeId: 'charge-tx-17',
395
765
  transactions: [createMockTransaction({ amount: "-100.00" })],
@@ -405,10 +775,10 @@ describe('Match Scorer', () => {
405
775
  }),
406
776
  ],
407
777
  };
408
- const result = scoreMatch(txCharge, docCharge, USER_ID);
778
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
409
779
  expect(result.components.amount).toBe(1.0);
410
780
  });
411
- it('should handle real-world scenario with slight variations', () => {
781
+ it('should handle real-world scenario with slight variations', async () => {
412
782
  const txCharge = {
413
783
  chargeId: 'charge-tx-18',
414
784
  transactions: [
@@ -434,16 +804,80 @@ describe('Match Scorer', () => {
434
804
  }),
435
805
  ],
436
806
  };
437
- const result = scoreMatch(txCharge, docCharge, USER_ID);
807
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
438
808
  expect(result.components.amount).toBeGreaterThanOrEqual(0.9); // Within 1 unit = 0.9
439
809
  expect(result.components.currency).toBe(1.0);
440
810
  expect(result.components.business).toBe(1.0);
441
811
  expect(result.components.date).toBeGreaterThan(0.95); // 1 day = 0.97
442
812
  expect(result.confidenceScore).toBeGreaterThanOrEqual(0.9);
443
813
  });
814
+ it('should apply flat date confidence for CLIENT same-business matches', async () => {
815
+ const txCharge = {
816
+ chargeId: 'charge-tx-client-1',
817
+ transactions: [
818
+ createMockTransaction({
819
+ amount: "500.00",
820
+ currency: 'USD',
821
+ event_date: new Date('2024-02-01'),
822
+ business_id: 'client-acme', // Registered client
823
+ }),
824
+ ],
825
+ };
826
+ const docCharge = {
827
+ chargeId: 'charge-doc-client-1',
828
+ documents: [
829
+ createMockDocument({
830
+ total_amount: 500,
831
+ currency_code: 'USD',
832
+ date: new Date('2024-01-04'), // 28 days earlier - gentle eligible
833
+ creditor_id: 'client-acme', // Same business, registered client
834
+ debtor_id: USER_ID,
835
+ type: 'INVOICE',
836
+ }),
837
+ ],
838
+ };
839
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
840
+ // For client same-business: date confidence should be 1.0 (flat)
841
+ expect(result.components.date).toBe(1.0);
842
+ // Overall confidence should be very high
843
+ expect(result.confidenceScore).toBeGreaterThan(0.95);
844
+ });
845
+ it('should use standard date degradation for NON-CLIENT same-business matches', async () => {
846
+ const txCharge = {
847
+ chargeId: 'charge-tx-nonc-1',
848
+ transactions: [
849
+ createMockTransaction({
850
+ amount: "500.00",
851
+ currency: 'USD',
852
+ event_date: new Date('2024-02-01'),
853
+ business_id: 'vendor-bob', // NOT a client (no 'client-' prefix)
854
+ }),
855
+ ],
856
+ };
857
+ const docCharge = {
858
+ chargeId: 'charge-doc-nonc-1',
859
+ documents: [
860
+ createMockDocument({
861
+ total_amount: 500,
862
+ currency_code: 'USD',
863
+ date: new Date('2024-03-01'), // 28 days later = 1.0 - 28/30 = 0.07
864
+ creditor_id: 'vendor-bob', // Same business, but NOT a client
865
+ debtor_id: USER_ID,
866
+ type: 'INVOICE',
867
+ }),
868
+ ],
869
+ };
870
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
871
+ // For non-client same-business: date confidence should use standard degradation
872
+ expect(result.components.date).toBeCloseTo(0.07, 1);
873
+ // Overall confidence lower due to date mismatch
874
+ // Calculation: (amount: 1.0 * 0.4) + (currency: 1.0 * 0.2) + (business: 1.0 * 0.3) + (date: 0.07 * 0.1)
875
+ // = 0.4 + 0.2 + 0.3 + 0.007 = 0.907 ≈ 0.91
876
+ expect(result.confidenceScore).toBeLessThanOrEqual(0.91);
877
+ });
444
878
  });
445
879
  describe('Error Propagation', () => {
446
- it('should throw error for mixed currencies in transactions', () => {
880
+ it('should throw error for mixed currencies in transactions', async () => {
447
881
  const txCharge = {
448
882
  chargeId: 'charge-tx-19',
449
883
  transactions: [
@@ -455,9 +889,9 @@ describe('Match Scorer', () => {
455
889
  chargeId: 'charge-doc-19',
456
890
  documents: [createMockDocument()],
457
891
  };
458
- expect(() => scoreMatch(txCharge, docCharge, USER_ID)).toThrow(/multiple currencies/);
892
+ await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow(/multiple currencies/);
459
893
  });
460
- it('should throw error for mixed currencies in documents', () => {
894
+ it('should throw error for mixed currencies in documents', async () => {
461
895
  const txCharge = {
462
896
  chargeId: 'charge-tx-20',
463
897
  transactions: [createMockTransaction()],
@@ -469,9 +903,9 @@ describe('Match Scorer', () => {
469
903
  createMockDocument({ currency_code: 'EUR' }),
470
904
  ],
471
905
  };
472
- expect(() => scoreMatch(txCharge, docCharge, USER_ID)).toThrow(/multiple currencies/);
906
+ await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow(/multiple currencies/);
473
907
  });
474
- it('should throw error for multiple business IDs in transactions', () => {
908
+ it('should throw error for multiple business IDs in transactions', async () => {
475
909
  const txCharge = {
476
910
  chargeId: 'charge-tx-21',
477
911
  transactions: [
@@ -483,9 +917,9 @@ describe('Match Scorer', () => {
483
917
  chargeId: 'charge-doc-21',
484
918
  documents: [createMockDocument()],
485
919
  };
486
- expect(() => scoreMatch(txCharge, docCharge, USER_ID)).toThrow(/multiple business/);
920
+ await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow(/multiple business/);
487
921
  });
488
- it('should throw error for invalid document business extraction', () => {
922
+ it('should throw error for invalid document business extraction', async () => {
489
923
  const txCharge = {
490
924
  chargeId: 'charge-tx-22',
491
925
  transactions: [createMockTransaction()],
@@ -499,11 +933,11 @@ describe('Match Scorer', () => {
499
933
  }),
500
934
  ],
501
935
  };
502
- expect(() => scoreMatch(txCharge, docCharge, USER_ID)).toThrow();
936
+ await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow();
503
937
  });
504
938
  });
505
939
  describe('Edge Cases', () => {
506
- it('should handle null business IDs', () => {
940
+ it('should handle null business IDs', async () => {
507
941
  const txCharge = {
508
942
  chargeId: 'charge-tx-23',
509
943
  transactions: [createMockTransaction({ business_id: null })],
@@ -517,10 +951,10 @@ describe('Match Scorer', () => {
517
951
  }),
518
952
  ],
519
953
  };
520
- const result = scoreMatch(txCharge, docCharge, USER_ID);
954
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
521
955
  expect(result.components.business).toBe(0.5);
522
956
  });
523
- it('should handle zero amounts', () => {
957
+ it('should handle zero amounts', async () => {
524
958
  const txCharge = {
525
959
  chargeId: 'charge-tx-24',
526
960
  transactions: [createMockTransaction({ amount: '0.00' })],
@@ -529,10 +963,10 @@ describe('Match Scorer', () => {
529
963
  chargeId: 'charge-doc-24',
530
964
  documents: [createMockDocument({ total_amount: 0 })],
531
965
  };
532
- const result = scoreMatch(txCharge, docCharge, USER_ID);
966
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
533
967
  expect(result.components.amount).toBe(1.0);
534
968
  });
535
- it('should handle very large amounts', () => {
969
+ it('should handle very large amounts', async () => {
536
970
  const txCharge = {
537
971
  chargeId: 'charge-tx-25',
538
972
  transactions: [createMockTransaction({ amount: '1000000.00' })],
@@ -541,7 +975,7 @@ describe('Match Scorer', () => {
541
975
  chargeId: 'charge-doc-25',
542
976
  documents: [createMockDocument({ total_amount: 1000000 })],
543
977
  };
544
- const result = scoreMatch(txCharge, docCharge, USER_ID);
978
+ const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
545
979
  expect(result.components.amount).toBe(1.0);
546
980
  });
547
981
  });