@accounter/server 0.0.8-alpha-20251102200443-d7162b8ce1dfc629b8b454df17dcec9ed005a052 → 0.0.8-alpha-20251102213150-c9d936f545d5351df0dc5326c2623266f1ad1f46
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 +47 -7
- package/dist/green-invoice-graphql/src/mesh-artifacts/index.d.ts +1 -1
- package/dist/server/src/__generated__/types.d.ts +77 -0
- package/dist/server/src/__generated__/types.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__generated__/types.d.ts +68 -0
- package/dist/server/src/modules/charges-matcher/__generated__/types.js +7 -0
- package/dist/server/src/modules/charges-matcher/__generated__/types.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js +218 -0
- package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +645 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +530 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js +143 -0
- package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js +186 -0
- package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +301 -0
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js +127 -0
- package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +246 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js +475 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +287 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js +151 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +550 -0
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js +410 -0
- package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +504 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +483 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.d.ts +46 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js +143 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js +137 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js +415 -0
- package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js +70 -0
- package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js +19 -0
- package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.d.ts +24 -0
- package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js +45 -0
- package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.d.ts +33 -0
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +65 -0
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js +18 -0
- package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +35 -0
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +49 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +58 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.d.ts +13 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js +37 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.d.ts +42 -0
- package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js +77 -0
- package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/index.d.ts +3 -0
- package/dist/server/src/modules/charges-matcher/index.js +15 -0
- package/dist/server/src/modules/charges-matcher/index.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +48 -0
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +133 -0
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.d.ts +38 -0
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +248 -0
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +61 -0
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +153 -0
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +25 -0
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +114 -0
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +39 -0
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +189 -0
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +54 -0
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js +93 -0
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js +22 -0
- package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js +24 -0
- package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/resolvers/index.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/resolvers/index.js +11 -0
- package/dist/server/src/modules/charges-matcher/resolvers/index.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js +47 -0
- package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/types.d.ts +179 -0
- package/dist/server/src/modules/charges-matcher/types.js +14 -0
- package/dist/server/src/modules/charges-matcher/types.js.map +1 -0
- package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js +2 -2
- package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js.map +1 -1
- package/dist/server/src/modules-app.js +2 -0
- package/dist/server/src/modules-app.js.map +1 -1
- package/dist/server/src/shared/types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/__generated__/types.ts +87 -0
- package/src/modules/charges-matcher/README.md +279 -0
- package/src/modules/charges-matcher/__generated__/types.ts +71 -0
- package/src/modules/charges-matcher/__tests__/amount-confidence.test.ts +260 -0
- package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +714 -0
- package/src/modules/charges-matcher/__tests__/auto-match.test.ts +621 -0
- package/src/modules/charges-matcher/__tests__/business-confidence.test.ts +177 -0
- package/src/modules/charges-matcher/__tests__/candidate-filter.test.ts +238 -0
- package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +374 -0
- package/src/modules/charges-matcher/__tests__/currency-confidence.test.ts +164 -0
- package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +291 -0
- package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +614 -0
- package/src/modules/charges-matcher/__tests__/document-amount.test.ts +352 -0
- package/src/modules/charges-matcher/__tests__/document-business.test.ts +192 -0
- package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +659 -0
- package/src/modules/charges-matcher/__tests__/overall-confidence.test.ts +502 -0
- package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +556 -0
- package/src/modules/charges-matcher/__tests__/single-match.test.ts +608 -0
- package/src/modules/charges-matcher/__tests__/test-helpers.ts +174 -0
- package/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts +177 -0
- package/src/modules/charges-matcher/__tests__/transaction-aggregator.test.ts +547 -0
- package/src/modules/charges-matcher/documentation/README.md +331 -0
- package/src/modules/charges-matcher/documentation/SPEC.md +1503 -0
- package/src/modules/charges-matcher/documentation/TODO.md +799 -0
- package/src/modules/charges-matcher/helpers/amount-confidence.helper.ts +88 -0
- package/src/modules/charges-matcher/helpers/business-confidence.helper.ts +23 -0
- package/src/modules/charges-matcher/helpers/candidate-filter.helper.ts +56 -0
- package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +100 -0
- package/src/modules/charges-matcher/helpers/currency-confidence.helper.ts +22 -0
- package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +41 -0
- package/src/modules/charges-matcher/helpers/document-amount.helper.ts +77 -0
- package/src/modules/charges-matcher/helpers/document-business.helper.ts +54 -0
- package/src/modules/charges-matcher/helpers/overall-confidence.helper.ts +90 -0
- package/src/modules/charges-matcher/index.ts +17 -0
- package/src/modules/charges-matcher/providers/auto-match.provider.ts +176 -0
- package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +322 -0
- package/src/modules/charges-matcher/providers/document-aggregator.ts +211 -0
- package/src/modules/charges-matcher/providers/match-scorer.provider.ts +154 -0
- package/src/modules/charges-matcher/providers/single-match.provider.ts +252 -0
- package/src/modules/charges-matcher/providers/transaction-aggregator.ts +131 -0
- package/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.ts +23 -0
- package/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.ts +25 -0
- package/src/modules/charges-matcher/resolvers/index.ts +12 -0
- package/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.ts +47 -0
- package/src/modules/charges-matcher/types.ts +200 -0
- package/src/modules/documents/resolvers/document-suggestions.resolver.ts +2 -2
- package/src/modules-app.ts +2 -0
- package/src/shared/types/index.ts +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Match Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the core auto-match logic for processing all unmatched charges
|
|
5
|
+
* and automatically merging charges with high-confidence matches (≥0.95).
|
|
6
|
+
*/
|
|
7
|
+
import { findMatches } from './single-match.provider.js';
|
|
8
|
+
/**
|
|
9
|
+
* Process a single unmatched charge and find best match
|
|
10
|
+
*
|
|
11
|
+
* This function uses findMatches() from single-match provider with NO date window
|
|
12
|
+
* restriction, then filters for high-confidence matches (≥0.95 threshold).
|
|
13
|
+
*
|
|
14
|
+
* @param sourceCharge - Unmatched charge to process (must have only transactions OR only documents)
|
|
15
|
+
* @param allCandidates - All candidate charges to search (complementary type to source)
|
|
16
|
+
* @param userId - Current user ID for business extraction
|
|
17
|
+
* @returns Processing result with match status
|
|
18
|
+
*
|
|
19
|
+
* @throws Error if sourceCharge is already matched (has both transactions and documents)
|
|
20
|
+
* @throws Error if sourceCharge has no transactions or documents
|
|
21
|
+
* @throws Error if any validation fails (propagated from findMatches)
|
|
22
|
+
*/
|
|
23
|
+
export function processChargeForAutoMatch(sourceCharge, allCandidates, userId) {
|
|
24
|
+
const AUTO_MATCH_THRESHOLD = 0.95;
|
|
25
|
+
// Prepare source charge for findMatches
|
|
26
|
+
const hasTransactions = sourceCharge.transactions && sourceCharge.transactions.length > 0;
|
|
27
|
+
const hasDocuments = sourceCharge.documents && sourceCharge.documents.length > 0;
|
|
28
|
+
let sourceForMatching;
|
|
29
|
+
let candidatesForMatching;
|
|
30
|
+
if (hasTransactions && !hasDocuments) {
|
|
31
|
+
// Transaction charge - convert to TransactionCharge format
|
|
32
|
+
sourceForMatching = {
|
|
33
|
+
chargeId: sourceCharge.chargeId,
|
|
34
|
+
transactions: sourceCharge.transactions,
|
|
35
|
+
};
|
|
36
|
+
// Filter candidates to only document charges
|
|
37
|
+
candidatesForMatching = allCandidates
|
|
38
|
+
.filter(c => c.documents && c.documents.length > 0 && (!c.transactions || c.transactions.length === 0))
|
|
39
|
+
.map(c => ({
|
|
40
|
+
chargeId: c.chargeId,
|
|
41
|
+
documents: c.documents,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
else if (hasDocuments && !hasTransactions) {
|
|
45
|
+
// Document charge - convert to DocumentCharge format
|
|
46
|
+
sourceForMatching = {
|
|
47
|
+
chargeId: sourceCharge.chargeId,
|
|
48
|
+
documents: sourceCharge.documents,
|
|
49
|
+
};
|
|
50
|
+
// Filter candidates to only transaction charges
|
|
51
|
+
candidatesForMatching = allCandidates
|
|
52
|
+
.filter(c => c.transactions && c.transactions.length > 0 && (!c.documents || c.documents.length === 0))
|
|
53
|
+
.map(c => ({
|
|
54
|
+
chargeId: c.chargeId,
|
|
55
|
+
transactions: c.transactions,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Invalid charge state
|
|
60
|
+
if (hasTransactions && hasDocuments) {
|
|
61
|
+
throw new Error(`Charge ${sourceCharge.chargeId} is already matched (has both transactions and documents)`);
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Charge ${sourceCharge.chargeId} has no transactions or documents`);
|
|
64
|
+
}
|
|
65
|
+
// Find all matches with no date window restriction
|
|
66
|
+
const allMatches = findMatches(sourceForMatching, candidatesForMatching, userId, {
|
|
67
|
+
dateWindowMonths: undefined, // No date restriction for auto-match
|
|
68
|
+
maxMatches: undefined, // Get all matches, we'll filter by threshold
|
|
69
|
+
});
|
|
70
|
+
// Filter for high-confidence matches (≥0.95)
|
|
71
|
+
const highConfidenceMatches = allMatches.filter(match => match.confidenceScore >= AUTO_MATCH_THRESHOLD);
|
|
72
|
+
// Determine result based on number of high-confidence matches
|
|
73
|
+
if (highConfidenceMatches.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
match: null,
|
|
76
|
+
status: 'no-match',
|
|
77
|
+
reason: allMatches.length > 0
|
|
78
|
+
? `Best match has confidence ${allMatches[0].confidenceScore.toFixed(2)}, below threshold ${AUTO_MATCH_THRESHOLD}`
|
|
79
|
+
: 'No candidates found',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (highConfidenceMatches.length === 1) {
|
|
83
|
+
return {
|
|
84
|
+
match: highConfidenceMatches[0],
|
|
85
|
+
status: 'matched',
|
|
86
|
+
reason: `Single high-confidence match found (${highConfidenceMatches[0].confidenceScore.toFixed(2)})`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Multiple high-confidence matches - ambiguous
|
|
90
|
+
return {
|
|
91
|
+
match: null,
|
|
92
|
+
status: 'skipped',
|
|
93
|
+
reason: `${highConfidenceMatches.length} high-confidence matches found (ambiguous)`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Determine merge direction for two charges
|
|
98
|
+
*
|
|
99
|
+
* The merge direction follows these rules:
|
|
100
|
+
* 1. If one charge is matched (has both transactions and documents), keep the matched one
|
|
101
|
+
* 2. If both are unmatched, keep the one with transactions (transaction charge is the "anchor")
|
|
102
|
+
* 3. If neither has transactions, keep the first one (arbitrary but consistent)
|
|
103
|
+
*
|
|
104
|
+
* @param charge1 - First charge
|
|
105
|
+
* @param charge2 - Second charge
|
|
106
|
+
* @returns [source, target] tuple where source will be merged INTO target (source is deleted)
|
|
107
|
+
*/
|
|
108
|
+
export function determineMergeDirection(charge1, charge2) {
|
|
109
|
+
const charge1HasTransactions = charge1.transactions && charge1.transactions.length > 0;
|
|
110
|
+
const charge1HasDocuments = charge1.documents && charge1.documents.length > 0;
|
|
111
|
+
const charge2HasTransactions = charge2.transactions && charge2.transactions.length > 0;
|
|
112
|
+
const charge2HasDocuments = charge2.documents && charge2.documents.length > 0;
|
|
113
|
+
const charge1IsMatched = charge1HasTransactions && charge1HasDocuments;
|
|
114
|
+
const charge2IsMatched = charge2HasTransactions && charge2HasDocuments;
|
|
115
|
+
// Rule 1: If one is matched, keep the matched one
|
|
116
|
+
if (charge1IsMatched && !charge2IsMatched) {
|
|
117
|
+
return [charge2, charge1]; // Merge charge2 INTO charge1 (keep charge1)
|
|
118
|
+
}
|
|
119
|
+
if (charge2IsMatched && !charge1IsMatched) {
|
|
120
|
+
return [charge1, charge2]; // Merge charge1 INTO charge2 (keep charge2)
|
|
121
|
+
}
|
|
122
|
+
// Rule 2: Both unmatched - keep the one with transactions
|
|
123
|
+
if (charge1HasTransactions && !charge2HasTransactions) {
|
|
124
|
+
return [charge2, charge1]; // Merge charge2 INTO charge1 (keep transaction charge)
|
|
125
|
+
}
|
|
126
|
+
if (charge2HasTransactions && !charge1HasTransactions) {
|
|
127
|
+
return [charge1, charge2]; // Merge charge1 INTO charge2 (keep transaction charge)
|
|
128
|
+
}
|
|
129
|
+
// Rule 3: Neither has transactions (both are document-only) or both have transactions
|
|
130
|
+
// Keep charge1 (arbitrary but consistent)
|
|
131
|
+
return [charge2, charge1]; // Merge charge2 INTO charge1 (keep charge1)
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=auto-match.provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-match.provider.js","sourceRoot":"","sources":["../../../../../../src/modules/charges-matcher/providers/auto-match.provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,WAAW,EAAoB,MAAM,4BAA4B,CAAC;AAc3E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,yBAAyB,CACvC,YAA4B,EAC5B,aAA+B,EAC/B,MAAc;IAEd,MAAM,oBAAoB,GAAG,IAAI,CAAC;IAElC,wCAAwC;IACxC,MAAM,eAAe,GAAG,YAAY,CAAC,YAAY,IAAI,YAAY,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1F,MAAM,YAAY,GAAG,YAAY,CAAC,SAAS,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAEjF,IAAI,iBAAqD,CAAC;IAC1D,IAAI,qBAA6D,CAAC;IAElE,IAAI,eAAe,IAAI,CAAC,YAAY,EAAE,CAAC;QACrC,2DAA2D;QAC3D,iBAAiB,GAAG;YAClB,QAAQ,EAAE,YAAY,CAAC,QAAQ;YAC/B,YAAY,EAAE,YAAY,CAAC,YAAY;SACxC,CAAC;QACF,6CAA6C;QAC7C,qBAAqB,GAAG,aAAa;aAClC,MAAM,CACL,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAC5F;aACA,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACT,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,SAAS,EAAE,CAAC,CAAC,SAAS;SACvB,CAAC,CAAC,CAAC;IACR,CAAC;SAAM,IAAI,YAAY,IAAI,CAAC,eAAe,EAAE,CAAC;QAC5C,qDAAqD;QACrD,iBAAiB,GAAG;YAClB,QAAQ,EAAE,YAAY,CAAC,QAAQ;YAC/B,SAAS,EAAE,YAAY,CAAC,SAAS;SAClC,CAAC;QACF,gDAAgD;QAChD,qBAAqB,GAAG,aAAa;aAClC,MAAM,CACL,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAC5F;aACA,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACT,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,YAAY,EAAE,CAAC,CAAC,YAAY;SAC7B,CAAC,CAAC,CAAC;IACR,CAAC;SAAM,CAAC;QACN,uBAAuB;QACvB,IAAI,eAAe,IAAI,YAAY,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,UAAU,YAAY,CAAC,QAAQ,2DAA2D,CAC3F,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,UAAU,YAAY,CAAC,QAAQ,mCAAmC,CAAC,CAAC;IACtF,CAAC;IAED,mDAAmD;IACnD,MAAM,UAAU,GAAG,WAAW,CAAC,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,EAAE;QAC/E,gBAAgB,EAAE,SAAS,EAAE,qCAAqC;QAClE,UAAU,EAAE,SAAS,EAAE,6CAA6C;KACrE,CAAC,CAAC;IAEH,6CAA6C;IAC7C,MAAM,qBAAqB,GAAG,UAAU,CAAC,MAAM,CAC7C,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,eAAe,IAAI,oBAAoB,CACvD,CAAC;IAEF,8DAA8D;IAC9D,IAAI,qBAAqB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,OAAO;YACL,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,UAAU;YAClB,MAAM,EACJ,UAAU,CAAC,MAAM,GAAG,CAAC;gBACnB,CAAC,CAAC,6BAA6B,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,oBAAoB,EAAE;gBAClH,CAAC,CAAC,qBAAqB;SAC5B,CAAC;IACJ,CAAC;IAED,IAAI,qBAAqB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,OAAO;YACL,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC;YAC/B,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,uCAAuC,qBAAqB,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;SACtG,CAAC;IACJ,CAAC;IAED,+CAA+C;IAC/C,OAAO;QACL,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,GAAG,qBAAqB,CAAC,MAAM,4CAA4C;KACpF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAuB,EACvB,OAAuB;IAEvB,MAAM,sBAAsB,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IACvF,MAAM,mBAAmB,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9E,MAAM,sBAAsB,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IACvF,MAAM,mBAAmB,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAE9E,MAAM,gBAAgB,GAAG,sBAAsB,IAAI,mBAAmB,CAAC;IACvE,MAAM,gBAAgB,GAAG,sBAAsB,IAAI,mBAAmB,CAAC;IAEvE,kDAAkD;IAClD,IAAI,gBAAgB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1C,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,4CAA4C;IACzE,CAAC;IACD,IAAI,gBAAgB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1C,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,4CAA4C;IACzE,CAAC;IAED,0DAA0D;IAC1D,IAAI,sBAAsB,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACtD,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,uDAAuD;IACpF,CAAC;IACD,IAAI,sBAAsB,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACtD,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,uDAAuD;IACpF,CAAC;IAED,sFAAsF;IACtF,0CAA0C;IAC1C,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,4CAA4C;AACzE,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charges Matcher Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides database-integrated charge matching functionality using the Injector pattern.
|
|
5
|
+
* Integrates with existing modules: charges, transactions, and documents.
|
|
6
|
+
*/
|
|
7
|
+
import { type AutoMatchChargesResult, type ChargeMatchesResult } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Charges Matcher Provider
|
|
10
|
+
*
|
|
11
|
+
* Provides high-level charge matching operations with database integration.
|
|
12
|
+
* Uses the Injector pattern to access existing providers from other modules.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ChargesMatcherProvider {
|
|
15
|
+
/**
|
|
16
|
+
* Find potential matches for an unmatched charge
|
|
17
|
+
*
|
|
18
|
+
* @param chargeId - ID of the unmatched charge to find matches for
|
|
19
|
+
* @param injector - GraphQL modules injector for provider access
|
|
20
|
+
* @returns Top 5 matches ordered by confidence score
|
|
21
|
+
* @throws Error if charge not found
|
|
22
|
+
* @throws Error if charge is already matched
|
|
23
|
+
* @throws Error if charge data is invalid
|
|
24
|
+
*/
|
|
25
|
+
findMatchesForCharge(chargeId: string, context: GraphQLModules.AppContext): Promise<ChargeMatchesResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Auto-match all unmatched charges
|
|
28
|
+
*
|
|
29
|
+
* Automatically merges charges that have a single high-confidence match (≥0.95).
|
|
30
|
+
* Skips charges with multiple high-confidence matches (ambiguous).
|
|
31
|
+
* Processes all unmatched charges and returns a summary of actions taken.
|
|
32
|
+
*
|
|
33
|
+
* @param injector - GraphQL modules injector for provider access
|
|
34
|
+
* @param context - GraphQL context with user information
|
|
35
|
+
* @returns Summary of matches made, skipped charges, and errors
|
|
36
|
+
*/
|
|
37
|
+
autoMatchCharges(context: GraphQLModules.AppContext): Promise<AutoMatchChargesResult>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charges Matcher Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides database-integrated charge matching functionality using the Injector pattern.
|
|
5
|
+
* Integrates with existing modules: charges, transactions, and documents.
|
|
6
|
+
*/
|
|
7
|
+
import { __decorate } from "tslib";
|
|
8
|
+
import { Injectable, Scope } from 'graphql-modules';
|
|
9
|
+
import { mergeChargesExecutor } from '../../charges/helpers/merge-charges.hepler.js';
|
|
10
|
+
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
|
|
11
|
+
import { DocumentsProvider } from '../../documents/providers/documents.provider.js';
|
|
12
|
+
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
|
|
13
|
+
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
|
|
14
|
+
import { validateChargeIsUnmatched } from '../helpers/charge-validator.helper.js';
|
|
15
|
+
import { ChargeType, } from '../types.js';
|
|
16
|
+
import { determineMergeDirection, processChargeForAutoMatch } from './auto-match.provider.js';
|
|
17
|
+
import { aggregateDocuments } from './document-aggregator.js';
|
|
18
|
+
import { findMatches } from './single-match.provider.js';
|
|
19
|
+
import { aggregateTransactions } from './transaction-aggregator.js';
|
|
20
|
+
/**
|
|
21
|
+
* Charges Matcher Provider
|
|
22
|
+
*
|
|
23
|
+
* Provides high-level charge matching operations with database integration.
|
|
24
|
+
* Uses the Injector pattern to access existing providers from other modules.
|
|
25
|
+
*/
|
|
26
|
+
let ChargesMatcherProvider = class ChargesMatcherProvider {
|
|
27
|
+
/**
|
|
28
|
+
* Find potential matches for an unmatched charge
|
|
29
|
+
*
|
|
30
|
+
* @param chargeId - ID of the unmatched charge to find matches for
|
|
31
|
+
* @param injector - GraphQL modules injector for provider access
|
|
32
|
+
* @returns Top 5 matches ordered by confidence score
|
|
33
|
+
* @throws Error if charge not found
|
|
34
|
+
* @throws Error if charge is already matched
|
|
35
|
+
* @throws Error if charge data is invalid
|
|
36
|
+
*/
|
|
37
|
+
async findMatchesForCharge(chargeId, context) {
|
|
38
|
+
const { adminContext: { defaultAdminBusinessId: adminBusinessId }, injector, } = context;
|
|
39
|
+
// Get current user ID from context
|
|
40
|
+
if (!adminBusinessId) {
|
|
41
|
+
throw new Error('Admin business not found in context');
|
|
42
|
+
}
|
|
43
|
+
// Get providers from injector
|
|
44
|
+
const chargesProvider = injector.get(ChargesProvider);
|
|
45
|
+
const transactionsProvider = injector.get(TransactionsProvider);
|
|
46
|
+
const documentsProvider = injector.get(DocumentsProvider);
|
|
47
|
+
// Step 1: Load source charge data
|
|
48
|
+
const sourceCharge = await chargesProvider.getChargeByIdLoader.load(chargeId);
|
|
49
|
+
if (sourceCharge instanceof Error) {
|
|
50
|
+
throw new Error(`Source charge not found: ${chargeId}`);
|
|
51
|
+
}
|
|
52
|
+
// Step 2: Load transactions and documents for source charge
|
|
53
|
+
const sourceTransactions = (await transactionsProvider.transactionsByChargeIDLoader.load(chargeId));
|
|
54
|
+
const sourceDocuments = (await documentsProvider.getDocumentsByChargeIdLoader.load(chargeId));
|
|
55
|
+
// Step 3: Validate source charge is unmatched
|
|
56
|
+
const sourceChargeWithData = {
|
|
57
|
+
...sourceCharge,
|
|
58
|
+
transactions: sourceTransactions,
|
|
59
|
+
documents: sourceDocuments,
|
|
60
|
+
};
|
|
61
|
+
validateChargeIsUnmatched(sourceChargeWithData);
|
|
62
|
+
// Step 4: Determine reference date and date window from source charge
|
|
63
|
+
let referenceDate;
|
|
64
|
+
const hasTransactions = sourceTransactions && sourceTransactions.length > 0;
|
|
65
|
+
if (hasTransactions) {
|
|
66
|
+
// Use earliest transaction event_date
|
|
67
|
+
const aggregated = aggregateTransactions(sourceTransactions);
|
|
68
|
+
referenceDate = aggregated.date;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Use latest document date
|
|
72
|
+
const aggregated = aggregateDocuments(sourceDocuments, adminBusinessId);
|
|
73
|
+
referenceDate = aggregated.date;
|
|
74
|
+
}
|
|
75
|
+
// Step 5: Load candidate charges from database
|
|
76
|
+
// Use 12-month window centered on reference date
|
|
77
|
+
const windowStart = new Date(referenceDate);
|
|
78
|
+
windowStart.setMonth(windowStart.getMonth() - 12);
|
|
79
|
+
const windowEnd = new Date(referenceDate);
|
|
80
|
+
windowEnd.setMonth(windowEnd.getMonth() + 12);
|
|
81
|
+
const candidateCharges = await chargesProvider.getChargesByFilters({
|
|
82
|
+
ownerIds: [adminBusinessId],
|
|
83
|
+
fromAnyDate: dateToTimelessDateString(windowStart),
|
|
84
|
+
toAnyDate: dateToTimelessDateString(windowEnd),
|
|
85
|
+
});
|
|
86
|
+
// Step 6: Load transactions and documents for all candidate charges
|
|
87
|
+
const candidateChargesWithData = [];
|
|
88
|
+
for (const candidate of candidateCharges) {
|
|
89
|
+
// Skip the source charge itself
|
|
90
|
+
if (candidate.id === chargeId) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const candidateTransactions = (await transactionsProvider.transactionsByChargeIDLoader.load(candidate.id));
|
|
94
|
+
const candidateDocuments = (await documentsProvider.getDocumentsByChargeIdLoader.load(candidate.id));
|
|
95
|
+
const hasTxs = candidateTransactions && candidateTransactions.length > 0;
|
|
96
|
+
const hasDocs = candidateDocuments && candidateDocuments.length > 0;
|
|
97
|
+
// Only include unmatched charges (not both types)
|
|
98
|
+
if (hasTxs && !hasDocs) {
|
|
99
|
+
candidateChargesWithData.push({
|
|
100
|
+
chargeId: candidate.id,
|
|
101
|
+
transactions: candidateTransactions,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else if (hasDocs && !hasTxs) {
|
|
105
|
+
candidateChargesWithData.push({
|
|
106
|
+
chargeId: candidate.id,
|
|
107
|
+
documents: candidateDocuments,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Skip matched charges (have both) and empty charges (have neither)
|
|
111
|
+
}
|
|
112
|
+
// Step 7: Build source charge object for findMatches
|
|
113
|
+
let sourceChargeData;
|
|
114
|
+
if (hasTransactions) {
|
|
115
|
+
sourceChargeData = {
|
|
116
|
+
chargeId,
|
|
117
|
+
transactions: sourceTransactions,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
sourceChargeData = {
|
|
122
|
+
chargeId,
|
|
123
|
+
documents: sourceDocuments,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Step 8: Call core findMatches function
|
|
127
|
+
const matches = findMatches(sourceChargeData, candidateChargesWithData, adminBusinessId, {
|
|
128
|
+
maxMatches: 5,
|
|
129
|
+
dateWindowMonths: 12,
|
|
130
|
+
});
|
|
131
|
+
// Step 9: Format and return result
|
|
132
|
+
return {
|
|
133
|
+
matches: matches.map(match => ({
|
|
134
|
+
chargeId: match.chargeId,
|
|
135
|
+
confidenceScore: match.confidenceScore,
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Auto-match all unmatched charges
|
|
141
|
+
*
|
|
142
|
+
* Automatically merges charges that have a single high-confidence match (≥0.95).
|
|
143
|
+
* Skips charges with multiple high-confidence matches (ambiguous).
|
|
144
|
+
* Processes all unmatched charges and returns a summary of actions taken.
|
|
145
|
+
*
|
|
146
|
+
* @param injector - GraphQL modules injector for provider access
|
|
147
|
+
* @param context - GraphQL context with user information
|
|
148
|
+
* @returns Summary of matches made, skipped charges, and errors
|
|
149
|
+
*/
|
|
150
|
+
async autoMatchCharges(context) {
|
|
151
|
+
const { adminContext: { defaultAdminBusinessId: adminBusinessId }, injector, } = context;
|
|
152
|
+
// Get current user ID from context
|
|
153
|
+
if (!adminBusinessId) {
|
|
154
|
+
throw new Error('Admin business not found in context');
|
|
155
|
+
}
|
|
156
|
+
// Get providers from injector
|
|
157
|
+
const chargesProvider = injector.get(ChargesProvider);
|
|
158
|
+
const transactionsProvider = injector.get(TransactionsProvider);
|
|
159
|
+
const documentsProvider = injector.get(DocumentsProvider);
|
|
160
|
+
// Step 1: Load all charges for this user
|
|
161
|
+
const allCharges = await chargesProvider.getChargesByFilters({
|
|
162
|
+
ownerIds: [adminBusinessId],
|
|
163
|
+
});
|
|
164
|
+
// Step 2: Load transactions and documents for all charges
|
|
165
|
+
const chargesWithData = [];
|
|
166
|
+
const mergedChargeIds = new Set(); // Track merged charges to exclude from processing
|
|
167
|
+
for (const charge of allCharges) {
|
|
168
|
+
const transactions = (await transactionsProvider.transactionsByChargeIDLoader.load(charge.id));
|
|
169
|
+
const documents = (await documentsProvider.getDocumentsByChargeIdLoader.load(charge.id));
|
|
170
|
+
chargesWithData.push({
|
|
171
|
+
chargeId: charge.id,
|
|
172
|
+
ownerId: charge.owner_id ?? adminBusinessId,
|
|
173
|
+
type: ChargeType.TRANSACTION_ONLY, // Will be determined by processChargeForAutoMatch
|
|
174
|
+
transactions: transactions || [],
|
|
175
|
+
documents: documents || [],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Step 3: Filter to get only unmatched charges
|
|
179
|
+
const unmatchedCharges = chargesWithData.filter(charge => {
|
|
180
|
+
const hasTx = charge.transactions && charge.transactions.length > 0;
|
|
181
|
+
const hasDocs = charge.documents && charge.documents.length > 0;
|
|
182
|
+
return (hasTx && !hasDocs) || (!hasTx && hasDocs);
|
|
183
|
+
});
|
|
184
|
+
// Step 4: Process each unmatched charge
|
|
185
|
+
const result = {
|
|
186
|
+
totalMatches: 0,
|
|
187
|
+
mergedCharges: [],
|
|
188
|
+
skippedCharges: [],
|
|
189
|
+
errors: [],
|
|
190
|
+
};
|
|
191
|
+
for (const sourceCharge of unmatchedCharges) {
|
|
192
|
+
// Skip if this charge was already merged in this run
|
|
193
|
+
if (mergedChargeIds.has(sourceCharge.chargeId)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
// Get candidates (exclude already merged charges)
|
|
198
|
+
const candidates = chargesWithData.filter(c => c.chargeId !== sourceCharge.chargeId && !mergedChargeIds.has(c.chargeId));
|
|
199
|
+
// Process this charge for auto-match
|
|
200
|
+
const processResult = processChargeForAutoMatch(sourceCharge, candidates, adminBusinessId);
|
|
201
|
+
if (processResult.status === 'matched' && processResult.match) {
|
|
202
|
+
// Found a single high-confidence match - execute merge
|
|
203
|
+
const matchedChargeId = processResult.match.chargeId;
|
|
204
|
+
const matchedCharge = chargesWithData.find(c => c.chargeId === matchedChargeId);
|
|
205
|
+
if (!matchedCharge) {
|
|
206
|
+
result.errors.push(`Matched charge ${matchedChargeId} not found in charge pool for ${sourceCharge.chargeId}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Determine merge direction
|
|
210
|
+
const [sourceToMerge, targetToKeep] = determineMergeDirection(sourceCharge, matchedCharge);
|
|
211
|
+
try {
|
|
212
|
+
// Execute merge via existing merge functionality
|
|
213
|
+
await mergeChargesExecutor([sourceToMerge.chargeId], targetToKeep.chargeId, injector);
|
|
214
|
+
// Track successful merge
|
|
215
|
+
result.totalMatches++;
|
|
216
|
+
result.mergedCharges.push({
|
|
217
|
+
chargeId: sourceToMerge.chargeId,
|
|
218
|
+
confidenceScore: processResult.match.confidenceScore,
|
|
219
|
+
});
|
|
220
|
+
// Mark both charges as processed (merged away charge and kept charge)
|
|
221
|
+
mergedChargeIds.add(sourceToMerge.chargeId);
|
|
222
|
+
mergedChargeIds.add(targetToKeep.chargeId); // Don't process the kept charge again
|
|
223
|
+
}
|
|
224
|
+
catch (mergeError) {
|
|
225
|
+
result.errors.push(`Failed to merge ${sourceToMerge.chargeId} into ${targetToKeep.chargeId}: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (processResult.status === 'skipped') {
|
|
229
|
+
// Multiple high-confidence matches - ambiguous
|
|
230
|
+
result.skippedCharges.push(sourceCharge.chargeId);
|
|
231
|
+
}
|
|
232
|
+
// status === 'no-match': do nothing, silently skip
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
// Capture error but continue processing other charges
|
|
236
|
+
result.errors.push(`Error processing charge ${sourceCharge.chargeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
ChargesMatcherProvider = __decorate([
|
|
243
|
+
Injectable({
|
|
244
|
+
scope: Scope.Operation,
|
|
245
|
+
})
|
|
246
|
+
], ChargesMatcherProvider);
|
|
247
|
+
export { ChargesMatcherProvider };
|
|
248
|
+
//# sourceMappingURL=charges-matcher.provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"charges-matcher.provider.js","sourceRoot":"","sources":["../../../../../../src/modules/charges-matcher/providers/charges-matcher.provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Aggregator
|
|
3
|
+
*
|
|
4
|
+
* Aggregates multiple documents from a single charge into a unified representation
|
|
5
|
+
* for matching purposes. Handles type priority filtering, business extraction,
|
|
6
|
+
* amount normalization, currency validation, date selection, and description concatenation.
|
|
7
|
+
*/
|
|
8
|
+
import { currency } from '../../documents/types.js';
|
|
9
|
+
import { type DocumentType } from '../helpers/document-amount.helper.js';
|
|
10
|
+
import { AggregatedDocument } from '../types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Minimal document interface for aggregation
|
|
13
|
+
* Based on database schema from accounter_schema.documents
|
|
14
|
+
* Note: Documents use charge_id for the FK
|
|
15
|
+
*/
|
|
16
|
+
export interface Document {
|
|
17
|
+
id: string;
|
|
18
|
+
charge_id: string | null;
|
|
19
|
+
creditor_id: string | null;
|
|
20
|
+
debtor_id: string | null;
|
|
21
|
+
currency_code: currency | null;
|
|
22
|
+
date: Date | null;
|
|
23
|
+
total_amount: number | null;
|
|
24
|
+
type: DocumentType;
|
|
25
|
+
serial_number: string | null;
|
|
26
|
+
image_url: string | null;
|
|
27
|
+
file_url: string | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Aggregate multiple documents into a single representation
|
|
31
|
+
*
|
|
32
|
+
* Per specification (section 4.2):
|
|
33
|
+
* 1. Filter by type priority: if both invoices AND receipts exist, use only invoices
|
|
34
|
+
* 2. Extract business ID from each document (creditor_id/debtor_id)
|
|
35
|
+
* 3. Normalize each amount based on business role and document type
|
|
36
|
+
* 4. Validate non-empty array
|
|
37
|
+
* 5. Check for mixed currencies → throw error
|
|
38
|
+
* 6. Check for multiple non-null business IDs → throw error
|
|
39
|
+
* 7. Sum all normalized amounts
|
|
40
|
+
* 8. Select latest date
|
|
41
|
+
* 9. Concatenate serial_number or file names with line breaks
|
|
42
|
+
* 10. Determine document type for result (use first after filtering)
|
|
43
|
+
*
|
|
44
|
+
* @param documents - Array of documents from a charge (use charge_id field for FK)
|
|
45
|
+
* @param adminBusinessId - Current admin business UUID for business extraction
|
|
46
|
+
* @returns Aggregated document data
|
|
47
|
+
*
|
|
48
|
+
* @throws {Error} If documents array is empty
|
|
49
|
+
* @throws {Error} If multiple different currencies exist
|
|
50
|
+
* @throws {Error} If multiple different non-null business IDs exist
|
|
51
|
+
* @throws {Error} If business extraction fails (propagates from extractDocumentBusiness)
|
|
52
|
+
* @throws {Error} If no valid date found in any document
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const aggregated = aggregateDocuments([
|
|
56
|
+
* { total_amount: 100, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: 'INVOICE', ... },
|
|
57
|
+
* { total_amount: 50, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: 'INVOICE', ... }
|
|
58
|
+
* ], 'u1');
|
|
59
|
+
* // Returns aggregated with normalized amounts summed
|
|
60
|
+
*/
|
|
61
|
+
export declare function aggregateDocuments(documents: Document[], adminBusinessId: string): Omit<AggregatedDocument, 'businessIsCreditor'>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Aggregator
|
|
3
|
+
*
|
|
4
|
+
* Aggregates multiple documents from a single charge into a unified representation
|
|
5
|
+
* for matching purposes. Handles type priority filtering, business extraction,
|
|
6
|
+
* amount normalization, currency validation, date selection, and description concatenation.
|
|
7
|
+
*/
|
|
8
|
+
import { normalizeDocumentAmount } from '../helpers/document-amount.helper.js';
|
|
9
|
+
import { extractDocumentBusiness } from '../helpers/document-business.helper.js';
|
|
10
|
+
/**
|
|
11
|
+
* Accounting document types (used for type priority filtering)
|
|
12
|
+
*/
|
|
13
|
+
const INVOICE_TYPES = ['INVOICE', 'CREDIT_INVOICE'];
|
|
14
|
+
const RECEIPT_TYPES = ['RECEIPT', 'INVOICE_RECEIPT'];
|
|
15
|
+
/**
|
|
16
|
+
* Check if document type is an invoice or credit invoice
|
|
17
|
+
*/
|
|
18
|
+
function isInvoiceType(type) {
|
|
19
|
+
return INVOICE_TYPES.includes(type);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if document type is a receipt or invoice-receipt
|
|
23
|
+
*/
|
|
24
|
+
function isReceiptType(type) {
|
|
25
|
+
return RECEIPT_TYPES.includes(type);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Aggregate multiple documents into a single representation
|
|
29
|
+
*
|
|
30
|
+
* Per specification (section 4.2):
|
|
31
|
+
* 1. Filter by type priority: if both invoices AND receipts exist, use only invoices
|
|
32
|
+
* 2. Extract business ID from each document (creditor_id/debtor_id)
|
|
33
|
+
* 3. Normalize each amount based on business role and document type
|
|
34
|
+
* 4. Validate non-empty array
|
|
35
|
+
* 5. Check for mixed currencies → throw error
|
|
36
|
+
* 6. Check for multiple non-null business IDs → throw error
|
|
37
|
+
* 7. Sum all normalized amounts
|
|
38
|
+
* 8. Select latest date
|
|
39
|
+
* 9. Concatenate serial_number or file names with line breaks
|
|
40
|
+
* 10. Determine document type for result (use first after filtering)
|
|
41
|
+
*
|
|
42
|
+
* @param documents - Array of documents from a charge (use charge_id field for FK)
|
|
43
|
+
* @param adminBusinessId - Current admin business UUID for business extraction
|
|
44
|
+
* @returns Aggregated document data
|
|
45
|
+
*
|
|
46
|
+
* @throws {Error} If documents array is empty
|
|
47
|
+
* @throws {Error} If multiple different currencies exist
|
|
48
|
+
* @throws {Error} If multiple different non-null business IDs exist
|
|
49
|
+
* @throws {Error} If business extraction fails (propagates from extractDocumentBusiness)
|
|
50
|
+
* @throws {Error} If no valid date found in any document
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const aggregated = aggregateDocuments([
|
|
54
|
+
* { total_amount: 100, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: 'INVOICE', ... },
|
|
55
|
+
* { total_amount: 50, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: 'INVOICE', ... }
|
|
56
|
+
* ], 'u1');
|
|
57
|
+
* // Returns aggregated with normalized amounts summed
|
|
58
|
+
*/
|
|
59
|
+
export function aggregateDocuments(documents, adminBusinessId) {
|
|
60
|
+
// Validate non-empty input
|
|
61
|
+
if (!documents || documents.length === 0) {
|
|
62
|
+
throw new Error('Cannot aggregate documents: array is empty');
|
|
63
|
+
}
|
|
64
|
+
// Apply type priority filtering
|
|
65
|
+
const hasInvoices = documents.some(d => isInvoiceType(d.type));
|
|
66
|
+
const hasReceipts = documents.some(d => isReceiptType(d.type));
|
|
67
|
+
let filteredDocuments = documents;
|
|
68
|
+
if (hasInvoices && hasReceipts) {
|
|
69
|
+
// If both invoices and receipts exist, use only invoices
|
|
70
|
+
filteredDocuments = documents.filter(d => isInvoiceType(d.type));
|
|
71
|
+
}
|
|
72
|
+
// Validate we still have documents after filtering
|
|
73
|
+
if (filteredDocuments.length === 0) {
|
|
74
|
+
throw new Error('Cannot aggregate documents: no valid documents after type priority filtering');
|
|
75
|
+
}
|
|
76
|
+
// Extract business info and normalize amounts for each document
|
|
77
|
+
const processedDocuments = filteredDocuments.map(doc => {
|
|
78
|
+
const businessInfo = extractDocumentBusiness(doc.creditor_id, doc.debtor_id, adminBusinessId);
|
|
79
|
+
const normalizedAmount = normalizeDocumentAmount(doc.total_amount ?? 0, businessInfo.isBusinessCreditor, doc.type);
|
|
80
|
+
return {
|
|
81
|
+
document: doc,
|
|
82
|
+
businessInfo,
|
|
83
|
+
normalizedAmount,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
// Validate single currency
|
|
87
|
+
const currencies = new Set(processedDocuments
|
|
88
|
+
.map(p => p.document.currency_code)
|
|
89
|
+
.filter((code) => code !== null && code !== undefined));
|
|
90
|
+
if (currencies.size === 0) {
|
|
91
|
+
throw new Error('Cannot aggregate documents: all documents have null currency_code');
|
|
92
|
+
}
|
|
93
|
+
if (currencies.size > 1) {
|
|
94
|
+
throw new Error(`Cannot aggregate documents: multiple currencies found (${Array.from(currencies).join(', ')})`);
|
|
95
|
+
}
|
|
96
|
+
// Validate single non-null business ID
|
|
97
|
+
const businessIds = processedDocuments
|
|
98
|
+
.map(p => p.businessInfo.businessId)
|
|
99
|
+
.filter((id) => id !== null && id !== undefined);
|
|
100
|
+
const uniqueBusinessIds = new Set(businessIds);
|
|
101
|
+
if (uniqueBusinessIds.size > 1) {
|
|
102
|
+
throw new Error(`Cannot aggregate documents: multiple business IDs found (${Array.from(uniqueBusinessIds).join(', ')})`);
|
|
103
|
+
}
|
|
104
|
+
// Sum normalized amounts
|
|
105
|
+
const totalAmount = processedDocuments.reduce((sum, p) => sum + p.normalizedAmount, 0);
|
|
106
|
+
// Get common currency (safe since we validated single currency)
|
|
107
|
+
const currency = Array.from(currencies)[0];
|
|
108
|
+
// Get business ID (single non-null or null if all null)
|
|
109
|
+
const businessId = uniqueBusinessIds.size === 1 ? Array.from(uniqueBusinessIds)[0] : null;
|
|
110
|
+
// Get latest date
|
|
111
|
+
const dates = filteredDocuments
|
|
112
|
+
.map(d => d.date)
|
|
113
|
+
.filter((date) => date !== null && date !== undefined);
|
|
114
|
+
if (dates.length === 0) {
|
|
115
|
+
throw new Error('Cannot aggregate documents: all documents have null date');
|
|
116
|
+
}
|
|
117
|
+
const latestDate = dates.reduce((latest, d) => {
|
|
118
|
+
return d > latest ? d : latest;
|
|
119
|
+
}, dates[0]);
|
|
120
|
+
// Concatenate descriptions (serial numbers, file names, or IDs)
|
|
121
|
+
const descriptions = filteredDocuments
|
|
122
|
+
.map(d => {
|
|
123
|
+
// Prefer serial_number, fallback to file_url or image_url name, or document ID
|
|
124
|
+
if (d.serial_number && d.serial_number.trim() !== '') {
|
|
125
|
+
return d.serial_number.trim();
|
|
126
|
+
}
|
|
127
|
+
if (d.file_url) {
|
|
128
|
+
// Extract filename from URL
|
|
129
|
+
const fileName = d.file_url.split('/').pop() || d.file_url;
|
|
130
|
+
return fileName;
|
|
131
|
+
}
|
|
132
|
+
if (d.image_url) {
|
|
133
|
+
// Extract filename from URL
|
|
134
|
+
const fileName = d.image_url.split('/').pop() || d.image_url;
|
|
135
|
+
return fileName;
|
|
136
|
+
}
|
|
137
|
+
// Fallback to document ID
|
|
138
|
+
return `Doc-${d.id.substring(0, 8)}`;
|
|
139
|
+
})
|
|
140
|
+
.filter(desc => desc !== null && desc !== undefined && desc.trim() !== '');
|
|
141
|
+
const description = descriptions.length > 0 ? descriptions.join('\n') : '';
|
|
142
|
+
// Determine document type (use first document after filtering)
|
|
143
|
+
const type = filteredDocuments[0].type;
|
|
144
|
+
return {
|
|
145
|
+
amount: totalAmount,
|
|
146
|
+
currency,
|
|
147
|
+
businessId,
|
|
148
|
+
date: latestDate,
|
|
149
|
+
type,
|
|
150
|
+
description,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=document-aggregator.js.map
|