@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
@@ -152,8 +152,7 @@ A charge is considered unmatched if it has:
152
152
  - ≥1 transactions AND 0 accounting documents, OR
153
153
  - 0 transactions AND ≥1 accounting documents
154
154
 
155
- **Note**: PROFORMA, OTHER, and UNPROCESSED document types don't count toward matched/unmatched
156
- status.
155
+ **Note**: OTHER, and UNPROCESSED document types don't count toward matched/unmatched status.
157
156
 
158
157
  ### Matched Charge
159
158
 
@@ -163,7 +162,7 @@ A charge is considered matched if it has:
163
162
 
164
163
  ### Accounting Documents
165
164
 
166
- Documents with types: INVOICE, CREDIT_INVOICE, RECEIPT, INVOICE_RECEIPT
165
+ Documents with types: INVOICE, CREDIT_INVOICE, RECEIPT, INVOICE_RECEIPT, PROFORMA
167
166
 
168
167
  ### Confidence Score
169
168
 
@@ -175,6 +174,18 @@ confidence = (amount × 0.4) + (currency × 0.2) + (business × 0.3) + (date ×
175
174
 
176
175
  Where each component score is between 0.0 and 1.0.
177
176
 
177
+ **Date Component Enhancement (v3.0 - Gentle Scoring):**
178
+
179
+ The date confidence uses "gentle scoring" for eligible client invoices:
180
+
181
+ - **Gentle Eligible:** Client same-business matches with OPEN INVOICE/PROFORMA documents dated on or
182
+ before the transaction date (within 365 days) receive near-maximum confidence (~1.00) with a
183
+ microscopic preference for earlier invoices
184
+ - **Standard:** All other scenarios use linear degradation from 1.0 (same day) to 0.0 (30+ days)
185
+ - **Tie-Breaker:** When scores are equal under gentle mode, earlier invoices are preferred
186
+
187
+ This ensures recurring client payments match to the earliest eligible open invoice.
188
+
178
189
  ### Auto-Match Threshold
179
190
 
180
191
  Charges are automatically matched only when:
@@ -20,6 +20,10 @@ vi.mock('../../transactions/providers/transactions.provider.js', () => ({
20
20
  TransactionsProvider: class {},
21
21
  }));
22
22
 
23
+ vi.mock('../../financial-entities/providers/clients.provider.js', () => ({
24
+ ClientsProvider: class {},
25
+ }));
26
+
23
27
  vi.mock('../../charges/helpers/merge-charges.helper.js', () => ({
24
28
  mergeChargesExecutor: vi.fn(),
25
29
  }));
@@ -28,6 +32,43 @@ vi.mock('../../../shared/helpers/index.js', () => ({
28
32
  dateToTimelessDateString: (date: Date) => date.toISOString().split('T')[0],
29
33
  }));
30
34
 
35
+ const getMockInjector = (mockChargesProvider?: {
36
+ getChargesByFilters: (filters: any) => Promise<any[]>;
37
+ },
38
+ mockTransactionsProvider?: {
39
+ transactionsByChargeIDLoader: { load: (id: string) => Promise<any[]>;
40
+ }},
41
+ mockDocumentsProvider?: {
42
+ getDocumentsByChargeIdLoader: { load: (id: string) => Promise<any[]>; };
43
+ }
44
+ ) => ({
45
+ get: vi.fn((token: {name: string}) => {
46
+ if (token.name === 'ChargesProvider') return mockChargesProvider ?? {
47
+ getChargesByFilters: vi.fn(() => Promise.resolve([])),
48
+ };
49
+ if (token.name === 'TransactionsProvider') return mockTransactionsProvider ?? {
50
+ transactionsByChargeIDLoader: {
51
+ load: vi.fn(() => Promise.resolve([])),
52
+ },
53
+ };
54
+ if (token.name === 'DocumentsProvider') return mockDocumentsProvider ?? {
55
+ getDocumentsByChargeIdLoader: {
56
+ load: vi.fn(() => Promise.resolve([])),
57
+ },
58
+ };
59
+ if (token.name === 'ClientsProvider')
60
+ return {
61
+ getClientByIdLoader: {
62
+ load: (businessId: string) => {
63
+ const isRegisteredClient = businessId.startsWith('client-');
64
+ return Promise.resolve(isRegisteredClient ? { id: businessId } : null);
65
+ },
66
+ },
67
+ };
68
+ return null;
69
+ }),
70
+ }) as Injector;
71
+
31
72
  // Import after mocking
32
73
  const { ChargesMatcherProvider } = await import('../providers/charges-matcher.provider.js');
33
74
  const { mergeChargesExecutor } = await import(
@@ -56,30 +97,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
56
97
 
57
98
  describe('autoMatchCharges', () => {
58
99
  it('should return 0 matches when database is empty', async () => {
59
- const mockChargesProvider = {
60
- getChargesByFilters: vi.fn(() => Promise.resolve([])),
61
- };
62
-
63
- const mockTransactionsProvider = {
64
- transactionsByChargeIDLoader: {
65
- load: vi.fn(() => Promise.resolve([])),
66
- },
67
- };
68
-
69
- const mockDocumentsProvider = {
70
- getDocumentsByChargeIdLoader: {
71
- load: vi.fn(() => Promise.resolve([])),
72
- },
73
- };
74
-
75
- const mockInjector = {
76
- get: vi.fn((token: any) => {
77
- if (token.name === 'ChargesProvider') return mockChargesProvider;
78
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
79
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
80
- return null;
81
- }),
82
- } as unknown as Injector;
100
+ const mockInjector = getMockInjector()
83
101
 
84
102
  const provider = new ChargesMatcherProvider();
85
103
  const result = await provider.autoMatchCharges({
@@ -102,26 +120,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
102
120
  ),
103
121
  };
104
122
 
105
- const mockTransactionsProvider = {
106
- transactionsByChargeIDLoader: {
107
- load: vi.fn(() => Promise.resolve([createMockTransaction()])),
108
- },
109
- };
110
-
111
- const mockDocumentsProvider = {
112
- getDocumentsByChargeIdLoader: {
113
- load: vi.fn(() => Promise.resolve([createMockDocument()])),
114
- },
115
- };
116
-
117
- const mockInjector = {
118
- get: vi.fn((token: any) => {
119
- if (token.name === 'ChargesProvider') return mockChargesProvider;
120
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
121
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
122
- return null;
123
- }),
124
- } as unknown as Injector;
123
+ const mockInjector = getMockInjector(mockChargesProvider)
125
124
 
126
125
  const provider = new ChargesMatcherProvider();
127
126
  const result = await provider.autoMatchCharges({
@@ -183,14 +182,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
183
182
  },
184
183
  };
185
184
 
186
- const mockInjector = {
187
- get: vi.fn((token: any) => {
188
- if (token.name === 'ChargesProvider') return mockChargesProvider;
189
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
190
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
191
- return null;
192
- }),
193
- } as unknown as Injector;
185
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
194
186
 
195
187
  const provider = new ChargesMatcherProvider();
196
188
  const result = await provider.autoMatchCharges({
@@ -269,14 +261,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
269
261
  },
270
262
  };
271
263
 
272
- const mockInjector = {
273
- get: vi.fn((token: any) => {
274
- if (token.name === 'ChargesProvider') return mockChargesProvider;
275
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
276
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
277
- return null;
278
- }),
279
- } as unknown as Injector;
264
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
280
265
 
281
266
  const provider = new ChargesMatcherProvider();
282
267
  const result = await provider.autoMatchCharges( {
@@ -347,14 +332,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
347
332
  },
348
333
  };
349
334
 
350
- const mockInjector = {
351
- get: vi.fn((token: any) => {
352
- if (token.name === 'ChargesProvider') return mockChargesProvider;
353
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
354
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
355
- return null;
356
- }),
357
- } as unknown as Injector;
335
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
358
336
 
359
337
  const provider = new ChargesMatcherProvider();
360
338
  const result = await provider.autoMatchCharges({
@@ -434,14 +412,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
434
412
  });
435
413
  (mergeChargesExecutor as any).mockImplementationOnce(() => Promise.resolve());
436
414
 
437
- const mockInjector = {
438
- get: vi.fn((token: any) => {
439
- if (token.name === 'ChargesProvider') return mockChargesProvider;
440
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
441
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
442
- return null;
443
- }),
444
- } as unknown as Injector;
415
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
445
416
 
446
417
  const provider = new ChargesMatcherProvider();
447
418
  const result = await provider.autoMatchCharges( {
@@ -493,14 +464,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
493
464
  },
494
465
  };
495
466
 
496
- const mockInjector = {
497
- get: vi.fn((token: any) => {
498
- if (token.name === 'ChargesProvider') return mockChargesProvider;
499
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
500
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
501
- return null;
502
- }),
503
- } as unknown as Injector;
467
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
504
468
 
505
469
  const provider = new ChargesMatcherProvider();
506
470
  await provider.autoMatchCharges({
@@ -549,14 +513,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
549
513
  },
550
514
  };
551
515
 
552
- const mockInjector = {
553
- get: vi.fn((token: any) => {
554
- if (token.name === 'ChargesProvider') return mockChargesProvider;
555
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
556
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
557
- return null;
558
- }),
559
- } as unknown as Injector;
516
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
560
517
 
561
518
  const provider = new ChargesMatcherProvider();
562
519
  const result = await provider.autoMatchCharges({
@@ -638,6 +595,8 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
638
595
  total_amount: 100,
639
596
  currency_code: 'USD',
640
597
  date: new Date('2024-01-15'),
598
+ debtor_id: ADMIN_BUSINESS_ID,
599
+ creditor_id: 'business-a',
641
600
  }),
642
601
  ]);
643
602
  }
@@ -668,14 +627,7 @@ describe('ChargesMatcherProvider - Auto-Match Integration', () => {
668
627
  },
669
628
  };
670
629
 
671
- const mockInjector = {
672
- get: vi.fn((token: any) => {
673
- if (token.name === 'ChargesProvider') return mockChargesProvider;
674
- if (token.name === 'TransactionsProvider') return mockTransactionsProvider;
675
- if (token.name === 'DocumentsProvider') return mockDocumentsProvider;
676
- return null;
677
- }),
678
- } as unknown as Injector;
630
+ const mockInjector = getMockInjector(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
679
631
 
680
632
  (mergeChargesExecutor as any).mockImplementation(() => Promise.resolve());
681
633
 
@@ -1,4 +1,5 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { Injector } from 'graphql-modules';
2
3
  import type { ChargeWithData } from '../types.js';
3
4
  import {
4
5
  determineMergeDirection,
@@ -6,10 +7,31 @@ import {
6
7
  } from '../providers/auto-match.provider.js';
7
8
  import { createMockTransaction, createMockDocument } from './test-helpers.js';
8
9
 
10
+ // Mock DI system and ClientsProvider
11
+ vi.mock('../../financial-entities/providers/clients.provider.js', () => ({
12
+ ClientsProvider: class {},
13
+ }));
14
+
9
15
  // Test constants
10
16
  const USER_ID = 'user-123';
11
17
  const BUSINESS_A = 'business-a';
12
18
 
19
+ // Create a mock injector for testing
20
+ const createMockInjector = () => ({
21
+ get: vi.fn((token: {name: string}) => {
22
+ if (token.name === 'ClientsProvider')
23
+ return {
24
+ getClientByIdLoader: {
25
+ load: (businessId: string) => {
26
+ const isRegisteredClient = businessId.startsWith('client-');
27
+ return Promise.resolve(isRegisteredClient ? { id: businessId } : null);
28
+ },
29
+ },
30
+ };
31
+ return null;
32
+ }),
33
+ }) as Injector;
34
+
13
35
  // Helper to create a charge with data
14
36
  function createCharge(overrides: Partial<ChargeWithData> = {}): ChargeWithData {
15
37
  return {
@@ -24,7 +46,7 @@ function createCharge(overrides: Partial<ChargeWithData> = {}): ChargeWithData {
24
46
 
25
47
  describe('processChargeForAutoMatch', () => {
26
48
  describe('Single high-confidence match', () => {
27
- it('should return matched status when exactly one match >= 0.95', () => {
49
+ it('should return matched status when exactly one match >= 0.95', async () => {
28
50
  const sourceCharge = createCharge({
29
51
  chargeId: 'tx-charge-1',
30
52
  transactions: [
@@ -54,7 +76,7 @@ describe('processChargeForAutoMatch', () => {
54
76
  ],
55
77
  });
56
78
 
57
- const result = processChargeForAutoMatch(sourceCharge, [perfectMatch], USER_ID);
79
+ const result = await processChargeForAutoMatch(sourceCharge, [perfectMatch], USER_ID, createMockInjector());
58
80
 
59
81
  expect(result.status).toBe('matched');
60
82
  expect(result.match).not.toBeNull();
@@ -62,7 +84,7 @@ describe('processChargeForAutoMatch', () => {
62
84
  expect(result.match?.confidenceScore).toBeGreaterThanOrEqual(0.95);
63
85
  });
64
86
 
65
- it('should return matched status for score exactly at 0.95 threshold', () => {
87
+ it('should return matched status for score exactly at 0.95 threshold', async () => {
66
88
  const sourceCharge = createCharge({
67
89
  chargeId: 'tx-charge-1',
68
90
  transactions: [
@@ -91,14 +113,14 @@ describe('processChargeForAutoMatch', () => {
91
113
  ],
92
114
  });
93
115
 
94
- const result = processChargeForAutoMatch(sourceCharge, [nearThresholdMatch], USER_ID);
116
+ const result = await processChargeForAutoMatch(sourceCharge, [nearThresholdMatch], USER_ID, createMockInjector());
95
117
 
96
118
  expect(result.status).toBe('matched');
97
119
  expect(result.match).not.toBeNull();
98
120
  expect(result.match?.confidenceScore).toBeGreaterThanOrEqual(0.95);
99
121
  });
100
122
 
101
- it('should work with document source charge', () => {
123
+ it('should work with document source charge', async () => {
102
124
  const sourceCharge = createCharge({
103
125
  chargeId: 'doc-charge-1',
104
126
  transactions: [],
@@ -125,7 +147,7 @@ describe('processChargeForAutoMatch', () => {
125
147
  documents: [],
126
148
  });
127
149
 
128
- const result = processChargeForAutoMatch(sourceCharge, [perfectMatch], USER_ID);
150
+ const result = await processChargeForAutoMatch(sourceCharge, [perfectMatch], USER_ID, createMockInjector());
129
151
 
130
152
  expect(result.status).toBe('matched');
131
153
  expect(result.match).not.toBeNull();
@@ -134,7 +156,7 @@ describe('processChargeForAutoMatch', () => {
134
156
  });
135
157
 
136
158
  describe('Multiple high-confidence matches', () => {
137
- it('should return skipped status when multiple matches >= 0.95', () => {
159
+ it('should return skipped status when multiple matches >= 0.95', async () => {
138
160
  const sourceCharge = createCharge({
139
161
  chargeId: 'tx-charge-1',
140
162
  transactions: [
@@ -174,14 +196,14 @@ describe('processChargeForAutoMatch', () => {
174
196
  ],
175
197
  });
176
198
 
177
- const result = processChargeForAutoMatch(sourceCharge, [match1, match2], USER_ID);
199
+ const result = await processChargeForAutoMatch(sourceCharge, [match1, match2], USER_ID, createMockInjector());
178
200
 
179
201
  expect(result.status).toBe('skipped');
180
202
  expect(result.match).toBeNull();
181
203
  expect(result.reason).toContain('ambiguous');
182
204
  });
183
205
 
184
- it('should skip even with many high-confidence matches', () => {
206
+ it('should skip even with many high-confidence matches', async () => {
185
207
  const sourceCharge = createCharge({
186
208
  chargeId: 'tx-charge-1',
187
209
  transactions: [
@@ -201,7 +223,7 @@ describe('processChargeForAutoMatch', () => {
201
223
  }),
202
224
  );
203
225
 
204
- const result = processChargeForAutoMatch(sourceCharge, candidates, USER_ID);
226
+ const result = await processChargeForAutoMatch(sourceCharge, candidates, USER_ID, createMockInjector());
205
227
 
206
228
  expect(result.status).toBe('skipped');
207
229
  expect(result.match).toBeNull();
@@ -209,7 +231,7 @@ describe('processChargeForAutoMatch', () => {
209
231
  });
210
232
 
211
233
  describe('No high-confidence matches', () => {
212
- it('should return no-match when best match is below 0.95 threshold', () => {
234
+ it('should return no-match when best match is below 0.95 threshold', async () => {
213
235
  const sourceCharge = createCharge({
214
236
  chargeId: 'tx-charge-1',
215
237
  transactions: [
@@ -237,28 +259,28 @@ describe('processChargeForAutoMatch', () => {
237
259
  ],
238
260
  });
239
261
 
240
- const result = processChargeForAutoMatch(sourceCharge, [poorMatch], USER_ID);
262
+ const result = await processChargeForAutoMatch(sourceCharge, [poorMatch], USER_ID, createMockInjector());
241
263
 
242
264
  expect(result.status).toBe('no-match');
243
265
  expect(result.match).toBeNull();
244
266
  expect(result.reason).toContain('below threshold');
245
267
  });
246
268
 
247
- it('should return no-match when no candidates exist', () => {
269
+ it('should return no-match when no candidates exist', async () => {
248
270
  const sourceCharge = createCharge({
249
271
  chargeId: 'tx-charge-1',
250
272
  transactions: [createMockTransaction()],
251
273
  documents: [],
252
274
  });
253
275
 
254
- const result = processChargeForAutoMatch(sourceCharge, [], USER_ID);
276
+ const result = await processChargeForAutoMatch(sourceCharge, [], USER_ID, createMockInjector());
255
277
 
256
278
  expect(result.status).toBe('no-match');
257
279
  expect(result.match).toBeNull();
258
280
  expect(result.reason).toContain('No candidates found');
259
281
  });
260
282
 
261
- it('should handle match just below threshold (0.949)', () => {
283
+ it('should handle match just below threshold (0.949)', async () => {
262
284
  const sourceCharge = createCharge({
263
285
  chargeId: 'tx-charge-1',
264
286
  transactions: [
@@ -286,7 +308,7 @@ describe('processChargeForAutoMatch', () => {
286
308
  ],
287
309
  });
288
310
 
289
- const result = processChargeForAutoMatch(sourceCharge, [nearMissMatch], USER_ID);
311
+ const result = await processChargeForAutoMatch(sourceCharge, [nearMissMatch], USER_ID, createMockInjector());
290
312
 
291
313
  // Should be no-match since it's below threshold
292
314
  expect(result.status).toBe('no-match');
@@ -295,31 +317,31 @@ describe('processChargeForAutoMatch', () => {
295
317
  });
296
318
 
297
319
  describe('Edge cases and validation', () => {
298
- it('should throw error if source charge is already matched', () => {
320
+ it('should throw error if source charge is already matched', async () => {
299
321
  const matchedCharge = createCharge({
300
322
  chargeId: 'matched-charge',
301
323
  transactions: [createMockTransaction()],
302
324
  documents: [createMockDocument()],
303
325
  });
304
326
 
305
- expect(() => {
306
- processChargeForAutoMatch(matchedCharge, [], USER_ID);
307
- }).toThrow(/already matched/);
327
+ await expect(processChargeForAutoMatch(matchedCharge, [], USER_ID, createMockInjector())).rejects.toThrow(
328
+ /already matched/,
329
+ );
308
330
  });
309
331
 
310
- it('should throw error if source charge has no transactions or documents', () => {
332
+ it('should throw error if source charge has no transactions or documents', async () => {
311
333
  const emptyCharge = createCharge({
312
334
  chargeId: 'empty-charge',
313
335
  transactions: [],
314
336
  documents: [],
315
337
  });
316
338
 
317
- expect(() => {
318
- processChargeForAutoMatch(emptyCharge, [], USER_ID);
319
- }).toThrow(/no transactions or documents/);
339
+ await expect(processChargeForAutoMatch(emptyCharge, [], USER_ID, createMockInjector())).rejects.toThrow(
340
+ /no transactions or documents/,
341
+ );
320
342
  });
321
343
 
322
- it('should filter candidates to complementary type only', () => {
344
+ it('should filter candidates to complementary type only', async () => {
323
345
  const sourceCharge = createCharge({
324
346
  chargeId: 'tx-charge-1',
325
347
  transactions: [createMockTransaction({ amount: "100" })],
@@ -345,14 +367,14 @@ describe('processChargeForAutoMatch', () => {
345
367
  }),
346
368
  ];
347
369
 
348
- const result = processChargeForAutoMatch(sourceCharge, candidates, USER_ID);
370
+ const result = await processChargeForAutoMatch(sourceCharge, candidates, USER_ID, createMockInjector());
349
371
 
350
372
  // Should only consider document charges (doc-charge-1 and doc-charge-2)
351
373
  // Both are perfect matches, so should be skipped as ambiguous
352
374
  expect(result.status).toBe('skipped');
353
375
  });
354
376
 
355
- it('should handle various confidence levels correctly', () => {
377
+ it('should handle various confidence levels correctly', async () => {
356
378
  const sourceCharge = createCharge({
357
379
  chargeId: 'tx-charge-1',
358
380
  transactions: [
@@ -404,7 +426,7 @@ describe('processChargeForAutoMatch', () => {
404
426
  }),
405
427
  ];
406
428
 
407
- const result = processChargeForAutoMatch(sourceCharge, candidates, USER_ID);
429
+ const result = await processChargeForAutoMatch(sourceCharge, candidates, USER_ID, createMockInjector());
408
430
 
409
431
  // Only one match >= 0.95 (the perfect one)
410
432
  expect(result.status).toBe('matched');
@@ -167,17 +167,6 @@ describe('Charge Validator', () => {
167
167
  expect(isChargeMatched(charge)).toBe(false);
168
168
  });
169
169
 
170
- it('should return false for charge with transactions and PROFORMA (non-accounting doc)', () => {
171
- const charge = {
172
- id: 'charge-1',
173
- owner_id: 'user-1',
174
- transactions: [createMockTransaction({ charge_id: 'charge-1' })],
175
- documents: [createMockDocument({ charge_id: 'charge-1', type: 'PROFORMA' })],
176
- };
177
-
178
- expect(isChargeMatched(charge)).toBe(false);
179
- });
180
-
181
170
  it('should return false for charge with transactions and OTHER (non-accounting doc)', () => {
182
171
  const charge = {
183
172
  id: 'charge-1',
@@ -218,7 +207,7 @@ describe('Charge Validator', () => {
218
207
  id: 'charge-1',
219
208
  owner_id: 'user-1',
220
209
  transactions: [createMockTransaction({ charge_id: 'charge-1' })],
221
- documents: [createMockDocument({ charge_id: 'charge-1', type: 'PROFORMA' })],
210
+ documents: [createMockDocument({ charge_id: 'charge-1', type: 'OTHER' })],
222
211
  };
223
212
 
224
213
  expect(hasOnlyTransactions(charge)).toBe(true);
@@ -286,7 +275,7 @@ describe('Charge Validator', () => {
286
275
  id: 'charge-1',
287
276
  owner_id: 'user-1',
288
277
  transactions: [],
289
- documents: [createMockDocument({ charge_id: 'charge-1', type: 'PROFORMA' })],
278
+ documents: [createMockDocument({ charge_id: 'charge-1', type: 'OTHER' })],
290
279
  };
291
280
 
292
281
  expect(hasOnlyDocuments(charge)).toBe(false);
@@ -2,6 +2,35 @@ import { describe, expect, it } from 'vitest';
2
2
  import { calculateDateConfidence } from '../helpers/date-confidence.helper.js';
3
3
 
4
4
  describe('calculateDateConfidence', () => {
5
+ describe('client-aware flag', () => {
6
+ it('returns 1.0 for client same-business on same day', () => {
7
+ const date = new Date('2024-01-15');
8
+ const result = calculateDateConfidence(date, date, true);
9
+ expect(result).toBe(1.0);
10
+ });
11
+
12
+ it('returns 1.0 for client same-business 30 days apart', () => {
13
+ const date1 = new Date('2024-01-01');
14
+ const date2 = new Date('2024-01-31');
15
+ const result = calculateDateConfidence(date1, date2, true);
16
+ expect(result).toBe(1.0);
17
+ });
18
+
19
+ it('returns 1.0 for client same-business 365 days apart', () => {
20
+ const date1 = new Date('2023-01-01');
21
+ const date2 = new Date('2024-01-01');
22
+ const result = calculateDateConfidence(date1, date2, true);
23
+ expect(result).toBe(1.0);
24
+ });
25
+
26
+ it('uses standard degradation when not a client match', () => {
27
+ const date1 = new Date('2024-01-01');
28
+ const date2 = new Date('2024-01-31');
29
+ const result = calculateDateConfidence(date1, date2, false);
30
+ expect(result).toBe(0.0);
31
+ });
32
+ });
33
+
5
34
  describe('same day', () => {
6
35
  it('should return 1.0 for identical dates', () => {
7
36
  const date = new Date('2024-01-15T10:30:00Z');
@@ -3,7 +3,6 @@ import {
3
3
  aggregateDocuments,
4
4
  type Document,
5
5
  } from '../providers/document-aggregator.js';
6
- import type { DocumentType } from '../helpers/document-amount.helper.js';
7
6
 
8
7
  describe('Document Aggregator', () => {
9
8
  const USER_ID = 'user-123';