@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,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Injector } from 'graphql-modules';
|
|
2
|
+
import { DocumentType } from '../../../shared/enums.js';
|
|
3
|
+
import type { IGetIssuedDocumentsStatusByChargeIdsResult } from '../../documents/__generated__/issued-documents.types.js';
|
|
4
|
+
import { IssuedDocumentsProvider } from '../../documents/providers/issued-documents.provider.js';
|
|
5
|
+
import { ClientsProvider } from '../../financial-entities/providers/clients.provider.js';
|
|
2
6
|
import { calculateAmountConfidence } from '../helpers/amount-confidence.helper.js';
|
|
3
7
|
import { calculateBusinessConfidence } from '../helpers/business-confidence.helper.js';
|
|
4
8
|
import { calculateCurrencyConfidence } from '../helpers/currency-confidence.helper.js';
|
|
@@ -28,19 +32,19 @@ import { aggregateTransactions } from './transaction-aggregator.js';
|
|
|
28
32
|
*/
|
|
29
33
|
export function selectTransactionDate(
|
|
30
34
|
transaction: AggregatedTransaction,
|
|
31
|
-
documentType:
|
|
35
|
+
documentType: DocumentType,
|
|
32
36
|
): Date {
|
|
33
37
|
switch (documentType) {
|
|
34
|
-
case
|
|
35
|
-
case
|
|
36
|
-
case
|
|
37
|
-
case
|
|
38
|
+
case DocumentType.Invoice:
|
|
39
|
+
case DocumentType.CreditInvoice:
|
|
40
|
+
case DocumentType.Receipt:
|
|
41
|
+
case DocumentType.InvoiceReceipt:
|
|
38
42
|
// For invoices, use event_date
|
|
39
43
|
return transaction.date;
|
|
40
44
|
|
|
41
|
-
case
|
|
42
|
-
case
|
|
43
|
-
case
|
|
45
|
+
case DocumentType.Other:
|
|
46
|
+
case DocumentType.Proforma:
|
|
47
|
+
case DocumentType.Unprocessed:
|
|
44
48
|
// For flexible types, use event_date as default
|
|
45
49
|
// (caller should calculate both and use better score)
|
|
46
50
|
return transaction.date;
|
|
@@ -58,14 +62,16 @@ export function selectTransactionDate(
|
|
|
58
62
|
* @param txCharge - Transaction charge to match
|
|
59
63
|
* @param docCharge - Document charge candidate
|
|
60
64
|
* @param userId - Current user UUID for business extraction
|
|
65
|
+
* @param injector - Optional GraphQL modules injector for provider access (for client matching)
|
|
61
66
|
* @returns Match score with confidence and component breakdown
|
|
62
67
|
* @throws Error if aggregation fails (mixed currencies, multiple businesses, etc.)
|
|
63
68
|
*/
|
|
64
|
-
export function scoreMatch(
|
|
69
|
+
export async function scoreMatch(
|
|
65
70
|
txCharge: TransactionCharge,
|
|
66
71
|
docCharge: DocumentCharge,
|
|
67
72
|
userId: string,
|
|
68
|
-
|
|
73
|
+
injector: Injector,
|
|
74
|
+
): Promise<MatchScore> {
|
|
69
75
|
// Aggregate transaction data
|
|
70
76
|
const aggregatedTransaction = aggregateTransactions(txCharge.transactions);
|
|
71
77
|
|
|
@@ -79,20 +85,22 @@ export function scoreMatch(
|
|
|
79
85
|
// aggregatedDocument.type === 'UNPROCESSED'
|
|
80
86
|
// ) {
|
|
81
87
|
// // Calculate score with event_date
|
|
82
|
-
// const scoreWithEventDate = calculateScoreWithDate(
|
|
88
|
+
// const scoreWithEventDate = await calculateScoreWithDate(
|
|
83
89
|
// aggregatedTransaction,
|
|
84
90
|
// aggregatedDocument,
|
|
85
91
|
// aggregatedTransaction.date,
|
|
86
92
|
// docCharge.chargeId,
|
|
93
|
+
// injector,
|
|
87
94
|
// );
|
|
88
95
|
|
|
89
96
|
// // Calculate score with debit_date (if available)
|
|
90
97
|
// if (aggregatedTransaction.debitDate) {
|
|
91
|
-
// const scoreWithDebitDate = calculateScoreWithDate(
|
|
98
|
+
// const scoreWithDebitDate = await calculateScoreWithDate(
|
|
92
99
|
// aggregatedTransaction,
|
|
93
100
|
// aggregatedDocument,
|
|
94
101
|
// aggregatedTransaction.debitDate,
|
|
95
102
|
// docCharge.chargeId,
|
|
103
|
+
// injector,
|
|
96
104
|
// );
|
|
97
105
|
|
|
98
106
|
// // Return the better score
|
|
@@ -105,13 +113,17 @@ export function scoreMatch(
|
|
|
105
113
|
// }
|
|
106
114
|
|
|
107
115
|
// For specific document types, use the appropriate date
|
|
108
|
-
const transactionDate = selectTransactionDate(
|
|
116
|
+
const transactionDate = selectTransactionDate(
|
|
117
|
+
aggregatedTransaction,
|
|
118
|
+
aggregatedDocument.type as DocumentType,
|
|
119
|
+
);
|
|
109
120
|
|
|
110
121
|
return calculateScoreWithDate(
|
|
111
122
|
aggregatedTransaction,
|
|
112
123
|
aggregatedDocument,
|
|
113
124
|
transactionDate,
|
|
114
125
|
docCharge.chargeId,
|
|
126
|
+
injector,
|
|
115
127
|
);
|
|
116
128
|
}
|
|
117
129
|
|
|
@@ -121,19 +133,84 @@ export function scoreMatch(
|
|
|
121
133
|
* @param document - Aggregated document
|
|
122
134
|
* @param transactionDate - Date to use from transaction
|
|
123
135
|
* @param chargeId - Document charge ID
|
|
136
|
+
* @param injector - Optional GraphQL modules injector for provider access
|
|
124
137
|
* @returns Match score
|
|
125
138
|
*/
|
|
126
|
-
function calculateScoreWithDate(
|
|
139
|
+
async function calculateScoreWithDate(
|
|
127
140
|
transaction: Omit<AggregatedTransaction, 'debitDate'>,
|
|
128
141
|
document: Omit<AggregatedDocument, 'businessIsCreditor'>,
|
|
129
142
|
transactionDate: Date,
|
|
130
143
|
chargeId: string,
|
|
131
|
-
|
|
144
|
+
injector: Injector,
|
|
145
|
+
): Promise<MatchScore> {
|
|
146
|
+
// Check if transaction and document share the same business entity
|
|
147
|
+
const businessesMatch =
|
|
148
|
+
transaction.businessId != null && transaction.businessId === document.businessId;
|
|
149
|
+
|
|
150
|
+
// If businesses match, verify if it's a registered CLIENT
|
|
151
|
+
let isClientMatch = false;
|
|
152
|
+
if (businessesMatch && transaction.businessId) {
|
|
153
|
+
try {
|
|
154
|
+
const client = await injector
|
|
155
|
+
.get(ClientsProvider)
|
|
156
|
+
.getClientByIdLoader.load(transaction.businessId);
|
|
157
|
+
// isClientMatch is true only if business is a registered client
|
|
158
|
+
isClientMatch = client != null;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`Error looking up client for business ID ${transaction.businessId}:`, error);
|
|
161
|
+
isClientMatch = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Determine gentle eligibility for client matches
|
|
166
|
+
// Conditions:
|
|
167
|
+
// - Same business and registered client
|
|
168
|
+
// - Document type is INVOICE or PROFORMA
|
|
169
|
+
// - Document status is OPEN (via issued documents status by charge id)
|
|
170
|
+
// - Document date <= transaction date (date-only compare)
|
|
171
|
+
let isGentleEligible = false;
|
|
172
|
+
if (isClientMatch) {
|
|
173
|
+
// Type gating
|
|
174
|
+
const typeIsEligible =
|
|
175
|
+
document.type === DocumentType.Invoice || document.type === DocumentType.Proforma;
|
|
176
|
+
|
|
177
|
+
// Date-only comparison: doc.date <= transactionDate
|
|
178
|
+
const docDate = new Date(
|
|
179
|
+
document.date.getFullYear(),
|
|
180
|
+
document.date.getMonth(),
|
|
181
|
+
document.date.getDate(),
|
|
182
|
+
);
|
|
183
|
+
const txDate = new Date(
|
|
184
|
+
transactionDate.getFullYear(),
|
|
185
|
+
transactionDate.getMonth(),
|
|
186
|
+
transactionDate.getDate(),
|
|
187
|
+
);
|
|
188
|
+
const dateIsEligible = docDate.getTime() <= txDate.getTime();
|
|
189
|
+
|
|
190
|
+
// Status gating via DataLoader
|
|
191
|
+
let statusIsEligible = false;
|
|
192
|
+
try {
|
|
193
|
+
const status = (await injector
|
|
194
|
+
.get(IssuedDocumentsProvider)
|
|
195
|
+
.getIssuedDocumentsStatusByChargeIdLoader.load(
|
|
196
|
+
chargeId,
|
|
197
|
+
)) as IGetIssuedDocumentsStatusByChargeIdsResult | null;
|
|
198
|
+
// Expect shape with open_docs_flag boolean
|
|
199
|
+
statusIsEligible = !!(status && status.open_docs_flag === true);
|
|
200
|
+
} catch {
|
|
201
|
+
// If status lookup fails, treat as ineligible (do not throw)
|
|
202
|
+
statusIsEligible = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
isGentleEligible = typeIsEligible && dateIsEligible && statusIsEligible;
|
|
206
|
+
}
|
|
207
|
+
|
|
132
208
|
// Calculate individual confidence scores
|
|
133
209
|
const amountScore = calculateAmountConfidence(transaction.amount, document.amount);
|
|
134
210
|
const currencyScore = calculateCurrencyConfidence(transaction.currency, document.currency);
|
|
135
211
|
const businessScore = calculateBusinessConfidence(transaction.businessId, document.businessId);
|
|
136
|
-
|
|
212
|
+
// Use gentle eligibility to compute date confidence
|
|
213
|
+
const dateScore = calculateDateConfidence(transactionDate, document.date, isGentleEligible);
|
|
137
214
|
|
|
138
215
|
// Create components object
|
|
139
216
|
const components: ConfidenceScores = {
|
|
@@ -150,5 +227,8 @@ function calculateScoreWithDate(
|
|
|
150
227
|
chargeId,
|
|
151
228
|
confidenceScore,
|
|
152
229
|
components,
|
|
230
|
+
// Expose gentle flag for tie-breaker handling upstream
|
|
231
|
+
// (optional property tolerated by consumer types)
|
|
232
|
+
gentleMode: isGentleEligible,
|
|
153
233
|
};
|
|
154
234
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This is a pure function implementation without database dependencies.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Injector } from 'graphql-modules';
|
|
8
9
|
import { isWithinDateWindow } from '../helpers/candidate-filter.helper.js';
|
|
9
10
|
import type { DocumentCharge, MatchScore, TransactionCharge } from '../types.js';
|
|
10
11
|
import { aggregateDocuments } from './document-aggregator.js';
|
|
@@ -24,6 +25,7 @@ export interface MatchResult {
|
|
|
24
25
|
date: number;
|
|
25
26
|
};
|
|
26
27
|
dateProximity?: number; // Days between earliest tx date and latest doc date (for tie-breaking)
|
|
28
|
+
gentleMode?: boolean; // Whether gentle client scoring applied
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -119,16 +121,17 @@ function calculateDateProximity(txCharge: TransactionCharge, docCharge: Document
|
|
|
119
121
|
* @param sourceCharge - The unmatched charge (transactions OR documents)
|
|
120
122
|
* @param candidateCharges - All potential match candidates
|
|
121
123
|
* @param userId - Current user ID
|
|
122
|
-
* @param options - Optional configuration (maxMatches, dateWindowMonths)
|
|
124
|
+
* @param options - Optional configuration (maxMatches, dateWindowMonths, injector)
|
|
123
125
|
* @returns Top matches sorted by confidence
|
|
124
126
|
* @throws Error if source charge is matched or has validation issues
|
|
125
127
|
*/
|
|
126
|
-
export function findMatches(
|
|
128
|
+
export async function findMatches(
|
|
127
129
|
sourceCharge: TransactionCharge | DocumentCharge,
|
|
128
130
|
candidateCharges: Array<TransactionCharge | DocumentCharge>,
|
|
129
131
|
userId: string,
|
|
132
|
+
injector: Injector,
|
|
130
133
|
options?: FindMatchesOptions,
|
|
131
|
-
): MatchResult[] {
|
|
134
|
+
): Promise<MatchResult[]> {
|
|
132
135
|
const maxMatches = options?.maxMatches ?? 5;
|
|
133
136
|
const dateWindowMonths = options?.dateWindowMonths ?? 12;
|
|
134
137
|
|
|
@@ -208,11 +211,11 @@ export function findMatches(
|
|
|
208
211
|
if (isSourceTransaction) {
|
|
209
212
|
txCharge = sourceCharge;
|
|
210
213
|
docCharge = candidate as DocumentCharge;
|
|
211
|
-
matchScore = scoreMatch(txCharge, docCharge, userId);
|
|
214
|
+
matchScore = await scoreMatch(txCharge, docCharge, userId, injector);
|
|
212
215
|
} else {
|
|
213
216
|
txCharge = candidate as TransactionCharge;
|
|
214
217
|
docCharge = sourceCharge;
|
|
215
|
-
matchScore = scoreMatch(txCharge, docCharge, userId);
|
|
218
|
+
matchScore = await scoreMatch(txCharge, docCharge, userId, injector);
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
// Calculate date proximity for tie-breaking
|
|
@@ -223,6 +226,7 @@ export function findMatches(
|
|
|
223
226
|
confidenceScore: matchScore.confidenceScore,
|
|
224
227
|
components: matchScore.components,
|
|
225
228
|
dateProximity,
|
|
229
|
+
gentleMode: matchScore.gentleMode === true,
|
|
226
230
|
_txCharge: txCharge,
|
|
227
231
|
_docCharge: docCharge,
|
|
228
232
|
});
|
|
@@ -233,15 +237,24 @@ export function findMatches(
|
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
// Step 9: Sort by confidence descending, then by date proximity
|
|
240
|
+
// Step 9: Sort by confidence descending, then by date proximity tie-breaker
|
|
237
241
|
scoredCandidates.sort((a, b) => {
|
|
238
242
|
// Primary: confidence score (descending)
|
|
239
243
|
if (a.confidenceScore !== b.confidenceScore) {
|
|
240
244
|
return b.confidenceScore - a.confidenceScore;
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
// Tie-breaker:
|
|
244
|
-
|
|
247
|
+
// Tie-breaker:
|
|
248
|
+
// - If both in gentle mode: prefer earlier document (larger proximity)
|
|
249
|
+
// - Otherwise: prefer closer dates (smaller proximity)
|
|
250
|
+
const aProx = a.dateProximity ?? Infinity;
|
|
251
|
+
const bProx = b.dateProximity ?? Infinity;
|
|
252
|
+
|
|
253
|
+
if (a.gentleMode && b.gentleMode) {
|
|
254
|
+
return bProx - aProx;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return aProx - bProx;
|
|
245
258
|
});
|
|
246
259
|
|
|
247
260
|
// Step 10: Return top N matches
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* validation, amount summation, date selection, and description concatenation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { currency } from '../../
|
|
9
|
+
import type { currency } from '../../transactions/types.js';
|
|
10
10
|
import type { AggregatedTransaction } from '../types.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { currency, document_type, IGetAllDocumentsResult } from '../documents/types.js';
|
|
2
2
|
import type { IGetTransactionsByIdsResult } from '../transactions/types.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Re-export shared types from other modules
|
|
6
|
-
*/
|
|
7
|
-
export type { currency as Currency, document_type as DocumentType } from '../documents/types.js';
|
|
8
|
-
|
|
9
4
|
/**
|
|
10
5
|
* Transaction interface matching the database schema
|
|
11
6
|
* Uses the complete type from transactions module
|
|
@@ -174,6 +169,8 @@ export interface MatchScore {
|
|
|
174
169
|
confidenceScore: number;
|
|
175
170
|
/** Individual confidence component scores */
|
|
176
171
|
components: ConfidenceScores;
|
|
172
|
+
/** Whether client gentle scoring was applied (for tie-breakers) */
|
|
173
|
+
gentleMode?: boolean;
|
|
177
174
|
}
|
|
178
175
|
|
|
179
176
|
/**
|