@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.
- package/CHANGELOG.md +16 -8
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +45 -122
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +45 -29
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +2 -11
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +25 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +65 -64
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +494 -60
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +34 -98
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +79 -59
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +6 -4
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +9 -2
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +24 -2
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +1 -4
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +2 -1
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/index.d.ts +0 -1
- package/dist/server/src/modules/charges-matcher/index.js +0 -1
- package/dist/server/src/modules/charges-matcher/index.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +2 -1
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +2 -2
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +2 -2
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +4 -5
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +5 -4
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +5 -3
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +70 -13
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +4 -2
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +15 -7
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +1 -1
- package/dist/server/src/modules/charges-matcher/types.d.ts +2 -4
- package/dist/server/src/modules/charges-matcher/types.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/charges-matcher/README.md +14 -3
- package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +52 -100
- package/src/modules/charges-matcher/__tests__/auto-match.test.ts +51 -29
- package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +2 -13
- package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +29 -0
- package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +0 -1
- package/src/modules/charges-matcher/__tests__/document-amount.test.ts +66 -65
- package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +552 -60
- package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +43 -73
- package/src/modules/charges-matcher/__tests__/single-match.test.ts +81 -59
- package/src/modules/charges-matcher/documentation/SPEC.md +276 -4
- package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +7 -5
- package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +32 -2
- package/src/modules/charges-matcher/helpers/document-amount.helper.ts +2 -12
- package/src/modules/charges-matcher/index.ts +0 -1
- package/src/modules/charges-matcher/providers/auto-match.provider.ts +5 -3
- package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +8 -2
- package/src/modules/charges-matcher/providers/document-aggregator.ts +12 -11
- package/src/modules/charges-matcher/providers/match-scorer.provider.ts +97 -17
- package/src/modules/charges-matcher/providers/single-match.provider.ts +21 -8
- package/src/modules/charges-matcher/providers/transaction-aggregator.ts +1 -1
- 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**:
|
|
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
|
|
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
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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: '
|
|
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: '
|
|
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');
|