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