@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
|
@@ -1,15 +1,71 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { AggregatedTransaction, DocumentCharge, TransactionCharge } from '../types.js';
|
|
3
3
|
import {
|
|
4
4
|
scoreMatch,
|
|
5
5
|
selectTransactionDate,
|
|
6
6
|
} from '../providers/match-scorer.provider.js';
|
|
7
7
|
import { createMockTransaction, createMockDocument } from './test-helpers.js';
|
|
8
|
+
import { Injector } from 'graphql-modules';
|
|
9
|
+
import { DocumentType } from '../../../shared/enums.js';
|
|
10
|
+
|
|
11
|
+
// Mock DI system and ClientsProvider / IssuedDocumentsProvider
|
|
12
|
+
vi.mock('../../financial-entities/providers/clients.provider.js', () => ({
|
|
13
|
+
ClientsProvider: class {},
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('../../documents/providers/issued-documents.provider.js', () => ({
|
|
16
|
+
IssuedDocumentsProvider: class {},
|
|
17
|
+
}));
|
|
8
18
|
|
|
9
19
|
// Test user ID
|
|
10
20
|
const USER_ID = 'user-123';
|
|
11
21
|
const BUSINESS_ID = 'business-abc';
|
|
12
22
|
|
|
23
|
+
// Create a mock injector for testing client matching
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// Create a mock injector for testing
|
|
27
|
+
const createMockInjector = () => ({
|
|
28
|
+
get: vi.fn((token: {name: string}) => {
|
|
29
|
+
if (token.name === 'ClientsProvider')
|
|
30
|
+
return {
|
|
31
|
+
getClientByIdLoader: {
|
|
32
|
+
load: (businessId: string) => {
|
|
33
|
+
const isRegisteredClient = businessId.startsWith('client-');
|
|
34
|
+
return Promise.resolve(isRegisteredClient ? { id: businessId } : null);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
if (token.name === 'IssuedDocumentsProvider')
|
|
39
|
+
return {
|
|
40
|
+
getIssuedDocumentsStatusByChargeIdLoader: {
|
|
41
|
+
load: async (_chargeId: string) => ({ charge_id: _chargeId, open_docs_flag: true }),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
return null;
|
|
45
|
+
}),
|
|
46
|
+
}) as Injector;
|
|
47
|
+
|
|
48
|
+
// Spy-able injector to assert DataLoader usage
|
|
49
|
+
const createSpyInjector = (loaderImpl?: (businessId: string) => Promise<any>) => {
|
|
50
|
+
const load = vi.fn(loaderImpl ?? (() => Promise.resolve(null)));
|
|
51
|
+
const get = vi.fn((token: {name: string}) => {
|
|
52
|
+
if (token.name === 'ClientsProvider') {
|
|
53
|
+
return {
|
|
54
|
+
getClientByIdLoader: { load },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (token.name === 'IssuedDocumentsProvider') {
|
|
58
|
+
return {
|
|
59
|
+
getIssuedDocumentsStatusByChargeIdLoader: {
|
|
60
|
+
load: vi.fn(async (chargeId: string) => ({ charge_id: chargeId, open_docs_flag: true })),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
});
|
|
66
|
+
return { get, load } as unknown as Injector & { load: typeof load };
|
|
67
|
+
};
|
|
68
|
+
|
|
13
69
|
describe('Match Scorer', () => {
|
|
14
70
|
describe('selectTransactionDate', () => {
|
|
15
71
|
const transaction: AggregatedTransaction = {
|
|
@@ -22,50 +78,50 @@ describe('Match Scorer', () => {
|
|
|
22
78
|
};
|
|
23
79
|
|
|
24
80
|
it('should use event_date for INVOICE', () => {
|
|
25
|
-
const result = selectTransactionDate(transaction,
|
|
81
|
+
const result = selectTransactionDate(transaction, DocumentType.Invoice);
|
|
26
82
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
27
83
|
});
|
|
28
84
|
|
|
29
85
|
it('should use event_date for CREDIT_INVOICE', () => {
|
|
30
|
-
const result = selectTransactionDate(transaction,
|
|
86
|
+
const result = selectTransactionDate(transaction, DocumentType.CreditInvoice);
|
|
31
87
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
32
88
|
});
|
|
33
89
|
|
|
34
90
|
it('should use event_date for RECEIPT', () => {
|
|
35
|
-
const result = selectTransactionDate(transaction,
|
|
91
|
+
const result = selectTransactionDate(transaction, DocumentType.Receipt);
|
|
36
92
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
37
93
|
});
|
|
38
94
|
|
|
39
95
|
it('should use event_date for INVOICE_RECEIPT', () => {
|
|
40
|
-
const result = selectTransactionDate(transaction,
|
|
96
|
+
const result = selectTransactionDate(transaction, DocumentType.InvoiceReceipt);
|
|
41
97
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
42
98
|
});
|
|
43
99
|
|
|
44
100
|
it('should use event_date for RECEIPT when debitDate is null', () => {
|
|
45
101
|
const txWithoutDebitDate = { ...transaction, debitDate: null };
|
|
46
|
-
const result = selectTransactionDate(txWithoutDebitDate,
|
|
102
|
+
const result = selectTransactionDate(txWithoutDebitDate, DocumentType.Receipt);
|
|
47
103
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
48
104
|
});
|
|
49
105
|
|
|
50
106
|
it('should use event_date for OTHER', () => {
|
|
51
|
-
const result = selectTransactionDate(transaction,
|
|
107
|
+
const result = selectTransactionDate(transaction, DocumentType.Other);
|
|
52
108
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
53
109
|
});
|
|
54
110
|
|
|
55
111
|
it('should use event_date for PROFORMA', () => {
|
|
56
|
-
const result = selectTransactionDate(transaction,
|
|
112
|
+
const result = selectTransactionDate(transaction, DocumentType.Proforma);
|
|
57
113
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
58
114
|
});
|
|
59
115
|
|
|
60
116
|
it('should use event_date for UNPROCESSED', () => {
|
|
61
|
-
const result = selectTransactionDate(transaction,
|
|
117
|
+
const result = selectTransactionDate(transaction, DocumentType.Unprocessed);
|
|
62
118
|
expect(result).toEqual(new Date('2024-01-15'));
|
|
63
119
|
});
|
|
64
120
|
});
|
|
65
121
|
|
|
66
122
|
describe('scoreMatch', () => {
|
|
67
123
|
describe('Perfect Matches', () => {
|
|
68
|
-
it('should score perfect match close to 1.0', () => {
|
|
124
|
+
it('should score perfect match close to 1.0', async () => {
|
|
69
125
|
const txCharge: TransactionCharge = {
|
|
70
126
|
chargeId: 'charge-tx-1',
|
|
71
127
|
transactions: [
|
|
@@ -92,17 +148,18 @@ describe('Match Scorer', () => {
|
|
|
92
148
|
],
|
|
93
149
|
};
|
|
94
150
|
|
|
95
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
151
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
96
152
|
|
|
97
153
|
expect(result.chargeId).toBe('charge-doc-1');
|
|
98
154
|
expect(result.confidenceScore).toBeGreaterThan(0.95);
|
|
99
155
|
expect(result.components.amount).toBe(1.0);
|
|
100
156
|
expect(result.components.currency).toBe(1.0);
|
|
101
157
|
expect(result.components.business).toBe(1.0);
|
|
158
|
+
// Non-client same-business: uses standard date calculation, not flat 1.0
|
|
102
159
|
expect(result.components.date).toBe(1.0);
|
|
103
160
|
});
|
|
104
161
|
|
|
105
|
-
it('should handle receipt matching with event_date', () => {
|
|
162
|
+
it('should handle receipt matching with event_date', async () => {
|
|
106
163
|
const txCharge: TransactionCharge = {
|
|
107
164
|
chargeId: 'charge-tx-2',
|
|
108
165
|
transactions: [
|
|
@@ -111,6 +168,7 @@ describe('Match Scorer', () => {
|
|
|
111
168
|
event_date: new Date('2024-01-10'),
|
|
112
169
|
debit_date: new Date('2024-01-15'),
|
|
113
170
|
debit_timestamp: null,
|
|
171
|
+
business_id: 'customer-receipt', // Different business for cross-business test
|
|
114
172
|
}),
|
|
115
173
|
],
|
|
116
174
|
};
|
|
@@ -122,19 +180,20 @@ describe('Match Scorer', () => {
|
|
|
122
180
|
total_amount: 200,
|
|
123
181
|
date: new Date('2024-01-15'),
|
|
124
182
|
type: 'RECEIPT',
|
|
183
|
+
creditor_id: 'vendor-receipt', // Different business
|
|
125
184
|
}),
|
|
126
185
|
],
|
|
127
186
|
};
|
|
128
187
|
|
|
129
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
188
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
130
189
|
|
|
131
|
-
expect(result.confidenceScore).toBeGreaterThan(0.
|
|
190
|
+
expect(result.confidenceScore).toBeGreaterThan(0.70); // Amount + currency + date good, business mismatch
|
|
132
191
|
expect(result.components.date).toBeCloseTo(0.83, 1);
|
|
133
192
|
});
|
|
134
193
|
});
|
|
135
194
|
|
|
136
195
|
describe('Partial Matches', () => {
|
|
137
|
-
it('should handle amount mismatch', () => {
|
|
196
|
+
it('should handle amount mismatch', async () => {
|
|
138
197
|
const txCharge: TransactionCharge = {
|
|
139
198
|
chargeId: 'charge-tx-3',
|
|
140
199
|
transactions: [createMockTransaction({ amount: "100.00" })],
|
|
@@ -145,14 +204,14 @@ describe('Match Scorer', () => {
|
|
|
145
204
|
documents: [createMockDocument({ total_amount: 110 })], // 10% difference
|
|
146
205
|
};
|
|
147
206
|
|
|
148
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
207
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
149
208
|
|
|
150
209
|
expect(result.components.amount).toBeLessThan(1.0);
|
|
151
210
|
expect(result.components.amount).toBeGreaterThan(0.0);
|
|
152
211
|
expect(result.confidenceScore).toBeLessThan(0.95);
|
|
153
212
|
});
|
|
154
213
|
|
|
155
|
-
it('should handle currency mismatch', () => {
|
|
214
|
+
it('should handle currency mismatch', async () => {
|
|
156
215
|
const txCharge: TransactionCharge = {
|
|
157
216
|
chargeId: 'charge-tx-4',
|
|
158
217
|
transactions: [createMockTransaction({ currency: 'USD' })],
|
|
@@ -163,14 +222,14 @@ describe('Match Scorer', () => {
|
|
|
163
222
|
documents: [createMockDocument({ currency_code: 'EUR' })],
|
|
164
223
|
};
|
|
165
224
|
|
|
166
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
225
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
167
226
|
|
|
168
227
|
expect(result.components.currency).toBe(0.0); // Different currencies = 0.0
|
|
169
228
|
// Overall confidence can still be decent if other factors match
|
|
170
229
|
expect(result.confidenceScore).toBeLessThan(1.0);
|
|
171
230
|
});
|
|
172
231
|
|
|
173
|
-
it('should handle business mismatch', () => {
|
|
232
|
+
it('should handle business mismatch', async () => {
|
|
174
233
|
const txCharge: TransactionCharge = {
|
|
175
234
|
chargeId: 'charge-tx-5',
|
|
176
235
|
transactions: [createMockTransaction({ business_id: 'business-1' })],
|
|
@@ -186,17 +245,18 @@ describe('Match Scorer', () => {
|
|
|
186
245
|
],
|
|
187
246
|
};
|
|
188
247
|
|
|
189
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
248
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
190
249
|
|
|
191
250
|
expect(result.components.business).toBe(0.2);
|
|
192
251
|
});
|
|
193
252
|
|
|
194
|
-
it('should handle date difference', () => {
|
|
253
|
+
it('should handle date difference', async () => {
|
|
195
254
|
const txCharge: TransactionCharge = {
|
|
196
255
|
chargeId: 'charge-tx-6',
|
|
197
256
|
transactions: [
|
|
198
257
|
createMockTransaction({
|
|
199
258
|
event_date: new Date('2024-01-01'),
|
|
259
|
+
business_id: 'customer-1', // Different business for cross-business test
|
|
200
260
|
}),
|
|
201
261
|
],
|
|
202
262
|
};
|
|
@@ -206,18 +266,374 @@ describe('Match Scorer', () => {
|
|
|
206
266
|
documents: [
|
|
207
267
|
createMockDocument({
|
|
208
268
|
date: new Date('2024-01-16'), // 15 days difference
|
|
269
|
+
creditor_id: 'vendor-1', // Different business
|
|
209
270
|
}),
|
|
210
271
|
],
|
|
211
272
|
};
|
|
212
273
|
|
|
213
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
274
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
214
275
|
|
|
215
276
|
expect(result.components.date).toBe(0.5); // 15/30 = 0.5
|
|
216
277
|
});
|
|
217
278
|
});
|
|
218
279
|
|
|
280
|
+
describe('Client-aware date confidence', () => {
|
|
281
|
+
it('returns flat 1.0 for client same-business on same day', async () => {
|
|
282
|
+
const txCharge: TransactionCharge = {
|
|
283
|
+
chargeId: 'charge-tx-client-same-day',
|
|
284
|
+
transactions: [
|
|
285
|
+
createMockTransaction({
|
|
286
|
+
amount: "100.00",
|
|
287
|
+
currency: 'USD',
|
|
288
|
+
event_date: new Date('2024-01-15'),
|
|
289
|
+
business_id: 'client-abc',
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const docCharge: DocumentCharge = {
|
|
295
|
+
chargeId: 'charge-doc-client-same-day',
|
|
296
|
+
documents: [
|
|
297
|
+
createMockDocument({
|
|
298
|
+
total_amount: 100,
|
|
299
|
+
currency_code: 'USD',
|
|
300
|
+
date: new Date('2024-01-15'),
|
|
301
|
+
creditor_id: 'client-abc',
|
|
302
|
+
debtor_id: USER_ID,
|
|
303
|
+
type: 'INVOICE',
|
|
304
|
+
}),
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const spyInjector = createSpyInjector(async () => ({ id: 'client-abc' }));
|
|
309
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector as any);
|
|
310
|
+
|
|
311
|
+
expect(result.components.date).toBe(1.0);
|
|
312
|
+
expect(spyInjector.load).toHaveBeenCalledTimes(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('returns flat 1.0 for client same-business 30 days apart', async () => {
|
|
316
|
+
const txCharge: TransactionCharge = {
|
|
317
|
+
chargeId: 'charge-tx-client-30',
|
|
318
|
+
transactions: [
|
|
319
|
+
createMockTransaction({
|
|
320
|
+
event_date: new Date('2024-01-31'),
|
|
321
|
+
business_id: 'client-xyz',
|
|
322
|
+
}),
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const docCharge: DocumentCharge = {
|
|
327
|
+
chargeId: 'charge-doc-client-30',
|
|
328
|
+
documents: [
|
|
329
|
+
createMockDocument({
|
|
330
|
+
date: new Date('2024-01-01'),
|
|
331
|
+
creditor_id: 'client-xyz',
|
|
332
|
+
debtor_id: USER_ID,
|
|
333
|
+
type: 'INVOICE',
|
|
334
|
+
}),
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const spyInjector = createSpyInjector(async () => ({ id: 'client-xyz' }));
|
|
339
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector as any);
|
|
340
|
+
|
|
341
|
+
expect(result.components.date).toBe(1.0);
|
|
342
|
+
expect(spyInjector.load).toHaveBeenCalledTimes(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('returns flat 1.0 for client same-business 365 days apart', async () => {
|
|
346
|
+
const txCharge: TransactionCharge = {
|
|
347
|
+
chargeId: 'charge-tx-client-365',
|
|
348
|
+
transactions: [
|
|
349
|
+
createMockTransaction({
|
|
350
|
+
event_date: new Date('2024-01-01'),
|
|
351
|
+
business_id: 'client-long',
|
|
352
|
+
}),
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const docCharge: DocumentCharge = {
|
|
357
|
+
chargeId: 'charge-doc-client-365',
|
|
358
|
+
documents: [
|
|
359
|
+
createMockDocument({
|
|
360
|
+
date: new Date('2023-01-01'),
|
|
361
|
+
creditor_id: 'client-long',
|
|
362
|
+
debtor_id: USER_ID,
|
|
363
|
+
type: 'INVOICE',
|
|
364
|
+
}),
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const spyInjector = createSpyInjector(async () => ({ id: 'client-long' }));
|
|
369
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector as any);
|
|
370
|
+
|
|
371
|
+
expect(result.components.date).toBe(1.0);
|
|
372
|
+
expect(spyInjector.load).toHaveBeenCalledTimes(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('uses standard degradation for non-client same-business (15 days)', async () => {
|
|
376
|
+
const txCharge: TransactionCharge = {
|
|
377
|
+
chargeId: 'charge-tx-nonclient-15',
|
|
378
|
+
transactions: [
|
|
379
|
+
createMockTransaction({
|
|
380
|
+
event_date: new Date('2024-01-01'),
|
|
381
|
+
business_id: 'vendor-plain',
|
|
382
|
+
}),
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const docCharge: DocumentCharge = {
|
|
387
|
+
chargeId: 'charge-doc-nonclient-15',
|
|
388
|
+
documents: [
|
|
389
|
+
createMockDocument({
|
|
390
|
+
date: new Date('2024-01-16'),
|
|
391
|
+
creditor_id: 'vendor-plain',
|
|
392
|
+
debtor_id: USER_ID,
|
|
393
|
+
type: 'INVOICE',
|
|
394
|
+
}),
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
399
|
+
|
|
400
|
+
expect(result.components.date).toBe(0.5);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('uses standard degradation for non-client same-business (45 days)', async () => {
|
|
404
|
+
const txCharge: TransactionCharge = {
|
|
405
|
+
chargeId: 'charge-tx-nonclient-45',
|
|
406
|
+
transactions: [
|
|
407
|
+
createMockTransaction({
|
|
408
|
+
event_date: new Date('2024-01-01'),
|
|
409
|
+
business_id: 'vendor-far',
|
|
410
|
+
}),
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const docCharge: DocumentCharge = {
|
|
415
|
+
chargeId: 'charge-doc-nonclient-45',
|
|
416
|
+
documents: [
|
|
417
|
+
createMockDocument({
|
|
418
|
+
date: new Date('2024-02-15'),
|
|
419
|
+
creditor_id: 'vendor-far',
|
|
420
|
+
debtor_id: USER_ID,
|
|
421
|
+
type: 'INVOICE',
|
|
422
|
+
}),
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
427
|
+
|
|
428
|
+
expect(result.components.date).toBe(0.0);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('uses standard degradation for cross-business matches', async () => {
|
|
432
|
+
const txCharge: TransactionCharge = {
|
|
433
|
+
chargeId: 'charge-tx-cross',
|
|
434
|
+
transactions: [
|
|
435
|
+
createMockTransaction({
|
|
436
|
+
event_date: new Date('2024-01-01'),
|
|
437
|
+
business_id: 'customer-xyz',
|
|
438
|
+
}),
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const docCharge: DocumentCharge = {
|
|
443
|
+
chargeId: 'charge-doc-cross',
|
|
444
|
+
documents: [
|
|
445
|
+
createMockDocument({
|
|
446
|
+
date: new Date('2024-01-06'),
|
|
447
|
+
creditor_id: 'vendor-xyz',
|
|
448
|
+
debtor_id: USER_ID,
|
|
449
|
+
type: 'INVOICE',
|
|
450
|
+
}),
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
455
|
+
|
|
456
|
+
expect(result.components.date).toBeCloseTo(0.83, 1);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('skips client lookup when businessId is null', async () => {
|
|
460
|
+
const txCharge: TransactionCharge = {
|
|
461
|
+
chargeId: 'charge-tx-null-business',
|
|
462
|
+
transactions: [
|
|
463
|
+
createMockTransaction({
|
|
464
|
+
event_date: new Date('2024-01-01'),
|
|
465
|
+
business_id: null,
|
|
466
|
+
}),
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const docCharge: DocumentCharge = {
|
|
471
|
+
chargeId: 'charge-doc-null-business',
|
|
472
|
+
documents: [
|
|
473
|
+
createMockDocument({
|
|
474
|
+
date: new Date('2024-01-16'),
|
|
475
|
+
creditor_id: USER_ID,
|
|
476
|
+
debtor_id: null,
|
|
477
|
+
type: 'INVOICE',
|
|
478
|
+
}),
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const spyInjector = createSpyInjector();
|
|
483
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector as any);
|
|
484
|
+
|
|
485
|
+
expect(result.components.date).toBe(0.5);
|
|
486
|
+
expect(spyInjector.load).not.toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('uses ClientsProvider result when lookup succeeds', async () => {
|
|
490
|
+
const txCharge: TransactionCharge = {
|
|
491
|
+
chargeId: 'charge-tx-client-lookup',
|
|
492
|
+
transactions: [
|
|
493
|
+
createMockTransaction({
|
|
494
|
+
event_date: new Date('2024-01-01'),
|
|
495
|
+
business_id: 'client-lookup',
|
|
496
|
+
}),
|
|
497
|
+
],
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const docCharge: DocumentCharge = {
|
|
501
|
+
chargeId: 'charge-doc-client-lookup',
|
|
502
|
+
documents: [
|
|
503
|
+
createMockDocument({
|
|
504
|
+
date: new Date('2023-12-01'),
|
|
505
|
+
creditor_id: 'client-lookup',
|
|
506
|
+
debtor_id: USER_ID,
|
|
507
|
+
type: 'INVOICE',
|
|
508
|
+
}),
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const spyInjector = createSpyInjector(async () => ({ id: 'client-lookup' }));
|
|
513
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector as any);
|
|
514
|
+
|
|
515
|
+
expect(result.components.date).toBe(1.0);
|
|
516
|
+
expect(spyInjector.load).toHaveBeenCalledTimes(1);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('uses standard degradation when ClientsProvider returns null', async () => {
|
|
520
|
+
const txCharge: TransactionCharge = {
|
|
521
|
+
chargeId: 'charge-tx-client-null',
|
|
522
|
+
transactions: [
|
|
523
|
+
createMockTransaction({
|
|
524
|
+
event_date: new Date('2024-01-01'),
|
|
525
|
+
business_id: 'client-missing',
|
|
526
|
+
}),
|
|
527
|
+
],
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const docCharge: DocumentCharge = {
|
|
531
|
+
chargeId: 'charge-doc-client-null',
|
|
532
|
+
documents: [
|
|
533
|
+
createMockDocument({
|
|
534
|
+
date: new Date('2024-01-11'),
|
|
535
|
+
creditor_id: 'client-missing',
|
|
536
|
+
debtor_id: USER_ID,
|
|
537
|
+
type: 'INVOICE',
|
|
538
|
+
}),
|
|
539
|
+
],
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const spyInjector = createSpyInjector(async () => null);
|
|
543
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, spyInjector as any);
|
|
544
|
+
|
|
545
|
+
expect(result.components.date).toBeCloseTo(0.67, 1);
|
|
546
|
+
expect(spyInjector.load).toHaveBeenCalledTimes(1);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('gives equal date scores for multiple client invoices regardless of recency', async () => {
|
|
550
|
+
const txCharge: TransactionCharge = {
|
|
551
|
+
chargeId: 'charge-tx-client-multi',
|
|
552
|
+
transactions: [
|
|
553
|
+
createMockTransaction({
|
|
554
|
+
event_date: new Date('2024-01-15'),
|
|
555
|
+
business_id: 'client-multi',
|
|
556
|
+
}),
|
|
557
|
+
],
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const janDoc: DocumentCharge = {
|
|
561
|
+
chargeId: 'charge-doc-client-jan',
|
|
562
|
+
documents: [
|
|
563
|
+
createMockDocument({
|
|
564
|
+
date: new Date('2023-12-30'),
|
|
565
|
+
creditor_id: 'client-multi',
|
|
566
|
+
debtor_id: USER_ID,
|
|
567
|
+
type: 'INVOICE',
|
|
568
|
+
}),
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const febDoc: DocumentCharge = {
|
|
573
|
+
chargeId: 'charge-doc-client-feb',
|
|
574
|
+
documents: [
|
|
575
|
+
createMockDocument({
|
|
576
|
+
date: new Date('2023-11-30'),
|
|
577
|
+
creditor_id: 'client-multi',
|
|
578
|
+
debtor_id: USER_ID,
|
|
579
|
+
type: 'INVOICE',
|
|
580
|
+
}),
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const spyInjector = createSpyInjector(async () => ({ id: 'client-multi' }));
|
|
585
|
+
const janScore = await scoreMatch(txCharge, janDoc, USER_ID, spyInjector as any);
|
|
586
|
+
const febScore = await scoreMatch(txCharge, febDoc, USER_ID, spyInjector as any);
|
|
587
|
+
|
|
588
|
+
expect(janScore.components.date).toBe(1.0);
|
|
589
|
+
expect(febScore.components.date).toBe(1.0);
|
|
590
|
+
expect(spyInjector.load).toHaveBeenCalledTimes(2);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('prefers closer date for non-client when amounts tie', async () => {
|
|
594
|
+
const txCharge: TransactionCharge = {
|
|
595
|
+
chargeId: 'charge-tx-nonclient-multi',
|
|
596
|
+
transactions: [
|
|
597
|
+
createMockTransaction({
|
|
598
|
+
event_date: new Date('2024-01-15'),
|
|
599
|
+
business_id: 'vendor-multi',
|
|
600
|
+
}),
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const nearDoc: DocumentCharge = {
|
|
605
|
+
chargeId: 'charge-doc-nonclient-near',
|
|
606
|
+
documents: [
|
|
607
|
+
createMockDocument({
|
|
608
|
+
date: new Date('2024-01-17'),
|
|
609
|
+
creditor_id: 'vendor-multi',
|
|
610
|
+
debtor_id: USER_ID,
|
|
611
|
+
type: 'INVOICE',
|
|
612
|
+
}),
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const farDoc: DocumentCharge = {
|
|
617
|
+
chargeId: 'charge-doc-nonclient-far',
|
|
618
|
+
documents: [
|
|
619
|
+
createMockDocument({
|
|
620
|
+
date: new Date('2024-02-20'),
|
|
621
|
+
creditor_id: 'vendor-multi',
|
|
622
|
+
debtor_id: USER_ID,
|
|
623
|
+
type: 'INVOICE',
|
|
624
|
+
}),
|
|
625
|
+
],
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const nearScore = await scoreMatch(txCharge, nearDoc, USER_ID, createMockInjector());
|
|
629
|
+
const farScore = await scoreMatch(txCharge, farDoc, USER_ID, createMockInjector());
|
|
630
|
+
|
|
631
|
+
expect(nearScore.components.date).toBeGreaterThan(farScore.components.date);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
219
635
|
describe('Date Type Selection', () => {
|
|
220
|
-
it('should use event_date for INVOICE matching', () => {
|
|
636
|
+
it('should use event_date for INVOICE matching', async () => {
|
|
221
637
|
const txCharge: TransactionCharge = {
|
|
222
638
|
chargeId: 'charge-tx-7',
|
|
223
639
|
transactions: [
|
|
@@ -238,12 +654,12 @@ describe('Match Scorer', () => {
|
|
|
238
654
|
],
|
|
239
655
|
};
|
|
240
656
|
|
|
241
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
657
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
242
658
|
|
|
243
659
|
expect(result.components.date).toBe(1.0); // Should match event_date
|
|
244
660
|
});
|
|
245
661
|
|
|
246
|
-
it('should use event_date for RECEIPT matching', () => {
|
|
662
|
+
it('should use event_date for RECEIPT matching', async () => {
|
|
247
663
|
const txCharge: TransactionCharge = {
|
|
248
664
|
chargeId: 'charge-tx-8',
|
|
249
665
|
transactions: [
|
|
@@ -251,6 +667,7 @@ describe('Match Scorer', () => {
|
|
|
251
667
|
event_date: new Date('2024-01-10'),
|
|
252
668
|
debit_date: new Date('2024-01-15'),
|
|
253
669
|
debit_timestamp: null,
|
|
670
|
+
business_id: 'customer-2', // Different business for cross-business test
|
|
254
671
|
}),
|
|
255
672
|
],
|
|
256
673
|
};
|
|
@@ -261,16 +678,17 @@ describe('Match Scorer', () => {
|
|
|
261
678
|
createMockDocument({
|
|
262
679
|
date: new Date('2024-01-15'), // Matches debit_date
|
|
263
680
|
type: 'RECEIPT',
|
|
681
|
+
creditor_id: 'vendor-2', // Different business
|
|
264
682
|
}),
|
|
265
683
|
],
|
|
266
684
|
};
|
|
267
685
|
|
|
268
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
686
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
269
687
|
|
|
270
688
|
expect(result.components.date).toBeCloseTo(0.83, 1);
|
|
271
689
|
});
|
|
272
690
|
|
|
273
|
-
it('should fall back to event_date for RECEIPT when debit_date is null', () => {
|
|
691
|
+
it('should fall back to event_date for RECEIPT when debit_date is null', async () => {
|
|
274
692
|
const txCharge: TransactionCharge = {
|
|
275
693
|
chargeId: 'charge-tx-9',
|
|
276
694
|
transactions: [
|
|
@@ -292,14 +710,14 @@ describe('Match Scorer', () => {
|
|
|
292
710
|
],
|
|
293
711
|
};
|
|
294
712
|
|
|
295
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
713
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
296
714
|
|
|
297
715
|
expect(result.components.date).toBe(1.0);
|
|
298
716
|
});
|
|
299
717
|
});
|
|
300
718
|
|
|
301
719
|
describe('Flexible Document Types (PROFORMA/OTHER/UNPROCESSED)', () => {
|
|
302
|
-
it('should use event_date for PROFORMA and use better score', () => {
|
|
720
|
+
it('should use event_date for PROFORMA and use better score', async () => {
|
|
303
721
|
const txCharge: TransactionCharge = {
|
|
304
722
|
chargeId: 'charge-tx-10',
|
|
305
723
|
transactions: [
|
|
@@ -307,6 +725,7 @@ describe('Match Scorer', () => {
|
|
|
307
725
|
event_date: new Date('2024-01-01'), // Far from document date
|
|
308
726
|
debit_date: new Date('2024-01-15'), // Matches document date
|
|
309
727
|
debit_timestamp: null,
|
|
728
|
+
business_id: 'customer-3', // Different business for cross-business test
|
|
310
729
|
}),
|
|
311
730
|
],
|
|
312
731
|
};
|
|
@@ -317,18 +736,19 @@ describe('Match Scorer', () => {
|
|
|
317
736
|
createMockDocument({
|
|
318
737
|
date: new Date('2024-01-15'),
|
|
319
738
|
type: 'PROFORMA',
|
|
739
|
+
creditor_id: 'vendor-3', // Different business
|
|
320
740
|
}),
|
|
321
741
|
],
|
|
322
742
|
};
|
|
323
743
|
|
|
324
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
744
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
325
745
|
|
|
326
746
|
// Should use event_date (2024-01-01) vs latest doc date (2024-01-15) = 14 days
|
|
327
747
|
// Date confidence: 1.0 - 14/30 = 0.53
|
|
328
748
|
expect(result.components.date).toBeCloseTo(0.53, 1);
|
|
329
749
|
});
|
|
330
750
|
|
|
331
|
-
it('should handle OTHER type with both dates', () => {
|
|
751
|
+
it('should handle OTHER type with both dates', async () => {
|
|
332
752
|
const txCharge: TransactionCharge = {
|
|
333
753
|
chargeId: 'charge-tx-11',
|
|
334
754
|
transactions: [
|
|
@@ -349,13 +769,13 @@ describe('Match Scorer', () => {
|
|
|
349
769
|
],
|
|
350
770
|
};
|
|
351
771
|
|
|
352
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
772
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
353
773
|
|
|
354
774
|
// Should use event_date since it matches better
|
|
355
775
|
expect(result.components.date).toBe(1.0);
|
|
356
776
|
});
|
|
357
777
|
|
|
358
|
-
it('should handle UNPROCESSED type', () => {
|
|
778
|
+
it('should handle UNPROCESSED type', async () => {
|
|
359
779
|
const txCharge: TransactionCharge = {
|
|
360
780
|
chargeId: 'charge-tx-12',
|
|
361
781
|
transactions: [
|
|
@@ -377,12 +797,12 @@ describe('Match Scorer', () => {
|
|
|
377
797
|
],
|
|
378
798
|
};
|
|
379
799
|
|
|
380
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
800
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
381
801
|
|
|
382
802
|
expect(result.components.date).toBe(1.0);
|
|
383
803
|
});
|
|
384
804
|
|
|
385
|
-
it('should handle flexible type without debit_date', () => {
|
|
805
|
+
it('should handle flexible type without debit_date', async () => {
|
|
386
806
|
const txCharge: TransactionCharge = {
|
|
387
807
|
chargeId: 'charge-tx-13',
|
|
388
808
|
transactions: [
|
|
@@ -404,7 +824,7 @@ describe('Match Scorer', () => {
|
|
|
404
824
|
],
|
|
405
825
|
};
|
|
406
826
|
|
|
407
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
827
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
408
828
|
|
|
409
829
|
// Should only use event_date
|
|
410
830
|
expect(result.components.date).toBe(1.0);
|
|
@@ -412,7 +832,7 @@ describe('Match Scorer', () => {
|
|
|
412
832
|
});
|
|
413
833
|
|
|
414
834
|
describe('Amount Variations', () => {
|
|
415
|
-
it('should handle small amount differences', () => {
|
|
835
|
+
it('should handle small amount differences', async () => {
|
|
416
836
|
const txCharge: TransactionCharge = {
|
|
417
837
|
chargeId: 'charge-tx-14',
|
|
418
838
|
transactions: [createMockTransaction({ amount: "100.00" })],
|
|
@@ -423,12 +843,12 @@ describe('Match Scorer', () => {
|
|
|
423
843
|
documents: [createMockDocument({ total_amount: 100.5 })], // 0.5 difference
|
|
424
844
|
};
|
|
425
845
|
|
|
426
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
846
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
427
847
|
|
|
428
848
|
expect(result.components.amount).toBeGreaterThanOrEqual(0.9); // Within 1 unit = exactly 0.9
|
|
429
849
|
});
|
|
430
850
|
|
|
431
|
-
it('should handle large amount differences', () => {
|
|
851
|
+
it('should handle large amount differences', async () => {
|
|
432
852
|
const txCharge: TransactionCharge = {
|
|
433
853
|
chargeId: 'charge-tx-15',
|
|
434
854
|
transactions: [createMockTransaction({ amount: "100.00" })],
|
|
@@ -439,14 +859,14 @@ describe('Match Scorer', () => {
|
|
|
439
859
|
documents: [createMockDocument({ total_amount: 150 })], // 50% difference
|
|
440
860
|
};
|
|
441
861
|
|
|
442
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
862
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
443
863
|
|
|
444
864
|
expect(result.components.amount).toBe(0.0);
|
|
445
865
|
});
|
|
446
866
|
});
|
|
447
867
|
|
|
448
868
|
describe('Integration Tests', () => {
|
|
449
|
-
it('should handle multiple transactions and documents', () => {
|
|
869
|
+
it('should handle multiple transactions and documents', async () => {
|
|
450
870
|
const txCharge: TransactionCharge = {
|
|
451
871
|
chargeId: 'charge-tx-16',
|
|
452
872
|
transactions: [
|
|
@@ -462,13 +882,13 @@ describe('Match Scorer', () => {
|
|
|
462
882
|
],
|
|
463
883
|
};
|
|
464
884
|
|
|
465
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
885
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
466
886
|
|
|
467
887
|
expect(result.components.amount).toBe(1.0); // 50 + 50 = 100
|
|
468
888
|
expect(result.confidenceScore).toBeGreaterThan(0.95);
|
|
469
889
|
});
|
|
470
890
|
|
|
471
|
-
it('should handle credit invoice (negative amounts)', () => {
|
|
891
|
+
it('should handle credit invoice (negative amounts)', async () => {
|
|
472
892
|
const txCharge: TransactionCharge = {
|
|
473
893
|
chargeId: 'charge-tx-17',
|
|
474
894
|
transactions: [createMockTransaction({ amount: "-100.00" })],
|
|
@@ -486,12 +906,12 @@ describe('Match Scorer', () => {
|
|
|
486
906
|
],
|
|
487
907
|
};
|
|
488
908
|
|
|
489
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
909
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
490
910
|
|
|
491
911
|
expect(result.components.amount).toBe(1.0);
|
|
492
912
|
});
|
|
493
913
|
|
|
494
|
-
it('should handle real-world scenario with slight variations', () => {
|
|
914
|
+
it('should handle real-world scenario with slight variations', async () => {
|
|
495
915
|
const txCharge: TransactionCharge = {
|
|
496
916
|
chargeId: 'charge-tx-18',
|
|
497
917
|
transactions: [
|
|
@@ -519,7 +939,7 @@ describe('Match Scorer', () => {
|
|
|
519
939
|
],
|
|
520
940
|
};
|
|
521
941
|
|
|
522
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
942
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
523
943
|
|
|
524
944
|
expect(result.components.amount).toBeGreaterThanOrEqual(0.9); // Within 1 unit = 0.9
|
|
525
945
|
expect(result.components.currency).toBe(1.0);
|
|
@@ -527,10 +947,82 @@ describe('Match Scorer', () => {
|
|
|
527
947
|
expect(result.components.date).toBeGreaterThan(0.95); // 1 day = 0.97
|
|
528
948
|
expect(result.confidenceScore).toBeGreaterThanOrEqual(0.9);
|
|
529
949
|
});
|
|
950
|
+
|
|
951
|
+
it('should apply flat date confidence for CLIENT same-business matches', async () => {
|
|
952
|
+
const txCharge: TransactionCharge = {
|
|
953
|
+
chargeId: 'charge-tx-client-1',
|
|
954
|
+
transactions: [
|
|
955
|
+
createMockTransaction({
|
|
956
|
+
amount: "500.00",
|
|
957
|
+
currency: 'USD',
|
|
958
|
+
event_date: new Date('2024-02-01'),
|
|
959
|
+
business_id: 'client-acme', // Registered client
|
|
960
|
+
}),
|
|
961
|
+
],
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const docCharge: DocumentCharge = {
|
|
965
|
+
chargeId: 'charge-doc-client-1',
|
|
966
|
+
documents: [
|
|
967
|
+
createMockDocument({
|
|
968
|
+
total_amount: 500,
|
|
969
|
+
currency_code: 'USD',
|
|
970
|
+
date: new Date('2024-01-04'), // 28 days earlier - gentle eligible
|
|
971
|
+
creditor_id: 'client-acme', // Same business, registered client
|
|
972
|
+
debtor_id: USER_ID,
|
|
973
|
+
type: 'INVOICE',
|
|
974
|
+
}),
|
|
975
|
+
],
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector() as any);
|
|
979
|
+
|
|
980
|
+
// For client same-business: date confidence should be 1.0 (flat)
|
|
981
|
+
expect(result.components.date).toBe(1.0);
|
|
982
|
+
// Overall confidence should be very high
|
|
983
|
+
expect(result.confidenceScore).toBeGreaterThan(0.95);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it('should use standard date degradation for NON-CLIENT same-business matches', async () => {
|
|
987
|
+
const txCharge: TransactionCharge = {
|
|
988
|
+
chargeId: 'charge-tx-nonc-1',
|
|
989
|
+
transactions: [
|
|
990
|
+
createMockTransaction({
|
|
991
|
+
amount: "500.00",
|
|
992
|
+
currency: 'USD',
|
|
993
|
+
event_date: new Date('2024-02-01'),
|
|
994
|
+
business_id: 'vendor-bob', // NOT a client (no 'client-' prefix)
|
|
995
|
+
}),
|
|
996
|
+
],
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const docCharge: DocumentCharge = {
|
|
1000
|
+
chargeId: 'charge-doc-nonc-1',
|
|
1001
|
+
documents: [
|
|
1002
|
+
createMockDocument({
|
|
1003
|
+
total_amount: 500,
|
|
1004
|
+
currency_code: 'USD',
|
|
1005
|
+
date: new Date('2024-03-01'), // 28 days later = 1.0 - 28/30 = 0.07
|
|
1006
|
+
creditor_id: 'vendor-bob', // Same business, but NOT a client
|
|
1007
|
+
debtor_id: USER_ID,
|
|
1008
|
+
type: 'INVOICE',
|
|
1009
|
+
}),
|
|
1010
|
+
],
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
1014
|
+
|
|
1015
|
+
// For non-client same-business: date confidence should use standard degradation
|
|
1016
|
+
expect(result.components.date).toBeCloseTo(0.07, 1);
|
|
1017
|
+
// Overall confidence lower due to date mismatch
|
|
1018
|
+
// Calculation: (amount: 1.0 * 0.4) + (currency: 1.0 * 0.2) + (business: 1.0 * 0.3) + (date: 0.07 * 0.1)
|
|
1019
|
+
// = 0.4 + 0.2 + 0.3 + 0.007 = 0.907 ≈ 0.91
|
|
1020
|
+
expect(result.confidenceScore).toBeLessThanOrEqual(0.91);
|
|
1021
|
+
});
|
|
530
1022
|
});
|
|
531
1023
|
|
|
532
1024
|
describe('Error Propagation', () => {
|
|
533
|
-
it('should throw error for mixed currencies in transactions', () => {
|
|
1025
|
+
it('should throw error for mixed currencies in transactions', async () => {
|
|
534
1026
|
const txCharge: TransactionCharge = {
|
|
535
1027
|
chargeId: 'charge-tx-19',
|
|
536
1028
|
transactions: [
|
|
@@ -544,10 +1036,10 @@ describe('Match Scorer', () => {
|
|
|
544
1036
|
documents: [createMockDocument()],
|
|
545
1037
|
};
|
|
546
1038
|
|
|
547
|
-
expect(
|
|
1039
|
+
await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow(/multiple currencies/);
|
|
548
1040
|
});
|
|
549
1041
|
|
|
550
|
-
it('should throw error for mixed currencies in documents', () => {
|
|
1042
|
+
it('should throw error for mixed currencies in documents', async () => {
|
|
551
1043
|
const txCharge: TransactionCharge = {
|
|
552
1044
|
chargeId: 'charge-tx-20',
|
|
553
1045
|
transactions: [createMockTransaction()],
|
|
@@ -561,10 +1053,10 @@ describe('Match Scorer', () => {
|
|
|
561
1053
|
],
|
|
562
1054
|
};
|
|
563
1055
|
|
|
564
|
-
expect(
|
|
1056
|
+
await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow(/multiple currencies/);
|
|
565
1057
|
});
|
|
566
1058
|
|
|
567
|
-
it('should throw error for multiple business IDs in transactions', () => {
|
|
1059
|
+
it('should throw error for multiple business IDs in transactions', async () => {
|
|
568
1060
|
const txCharge: TransactionCharge = {
|
|
569
1061
|
chargeId: 'charge-tx-21',
|
|
570
1062
|
transactions: [
|
|
@@ -578,10 +1070,10 @@ describe('Match Scorer', () => {
|
|
|
578
1070
|
documents: [createMockDocument()],
|
|
579
1071
|
};
|
|
580
1072
|
|
|
581
|
-
expect(
|
|
1073
|
+
await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow(/multiple business/);
|
|
582
1074
|
});
|
|
583
1075
|
|
|
584
|
-
it('should throw error for invalid document business extraction', () => {
|
|
1076
|
+
it('should throw error for invalid document business extraction', async () => {
|
|
585
1077
|
const txCharge: TransactionCharge = {
|
|
586
1078
|
chargeId: 'charge-tx-22',
|
|
587
1079
|
transactions: [createMockTransaction()],
|
|
@@ -597,12 +1089,12 @@ describe('Match Scorer', () => {
|
|
|
597
1089
|
],
|
|
598
1090
|
};
|
|
599
1091
|
|
|
600
|
-
expect(
|
|
1092
|
+
await expect(scoreMatch(txCharge, docCharge, USER_ID, createMockInjector())).rejects.toThrow();
|
|
601
1093
|
});
|
|
602
1094
|
});
|
|
603
1095
|
|
|
604
1096
|
describe('Edge Cases', () => {
|
|
605
|
-
it('should handle null business IDs', () => {
|
|
1097
|
+
it('should handle null business IDs', async () => {
|
|
606
1098
|
const txCharge: TransactionCharge = {
|
|
607
1099
|
chargeId: 'charge-tx-23',
|
|
608
1100
|
transactions: [createMockTransaction({ business_id: null })],
|
|
@@ -618,12 +1110,12 @@ describe('Match Scorer', () => {
|
|
|
618
1110
|
],
|
|
619
1111
|
};
|
|
620
1112
|
|
|
621
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
1113
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
622
1114
|
|
|
623
1115
|
expect(result.components.business).toBe(0.5);
|
|
624
1116
|
});
|
|
625
1117
|
|
|
626
|
-
it('should handle zero amounts', () => {
|
|
1118
|
+
it('should handle zero amounts', async () => {
|
|
627
1119
|
const txCharge: TransactionCharge = {
|
|
628
1120
|
chargeId: 'charge-tx-24',
|
|
629
1121
|
transactions: [createMockTransaction({ amount: '0.00' })],
|
|
@@ -634,12 +1126,12 @@ describe('Match Scorer', () => {
|
|
|
634
1126
|
documents: [createMockDocument({ total_amount: 0 })],
|
|
635
1127
|
};
|
|
636
1128
|
|
|
637
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
1129
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
638
1130
|
|
|
639
1131
|
expect(result.components.amount).toBe(1.0);
|
|
640
1132
|
});
|
|
641
1133
|
|
|
642
|
-
it('should handle very large amounts', () => {
|
|
1134
|
+
it('should handle very large amounts', async () => {
|
|
643
1135
|
const txCharge: TransactionCharge = {
|
|
644
1136
|
chargeId: 'charge-tx-25',
|
|
645
1137
|
transactions: [createMockTransaction({ amount: '1000000.00' })],
|
|
@@ -650,7 +1142,7 @@ describe('Match Scorer', () => {
|
|
|
650
1142
|
documents: [createMockDocument({ total_amount: 1000000 })],
|
|
651
1143
|
};
|
|
652
1144
|
|
|
653
|
-
const result = scoreMatch(txCharge, docCharge, USER_ID);
|
|
1145
|
+
const result = await scoreMatch(txCharge, docCharge, USER_ID, createMockInjector());
|
|
654
1146
|
|
|
655
1147
|
expect(result.components.amount).toBe(1.0);
|
|
656
1148
|
});
|