@accounter/server 0.0.8-alpha-20251102200443-d7162b8ce1dfc629b8b454df17dcec9ed005a052 → 0.0.8-alpha-20251103003648-f6467c8cb9c739ec4439c260bccc7325f6a761ae
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 +7 -7
- 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/green-invoice/helpers/contract-to-draft.helper.js +1 -1
- package/dist/server/src/modules/green-invoice/helpers/contract-to-draft.helper.js.map +1 -1
- package/dist/server/src/modules/green-invoice/helpers/green-invoice.helper.d.ts +1 -1
- package/dist/server/src/modules/green-invoice/helpers/green-invoice.helper.js +1 -1
- package/dist/server/src/modules/green-invoice/helpers/green-invoice.helper.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/green-invoice/helpers/contract-to-draft.helper.ts +1 -1
- package/src/modules/green-invoice/helpers/green-invoice.helper.ts +1 -1
- package/src/modules-app.ts +2 -0
- package/src/shared/types/index.ts +1 -1
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
|
|
8
|
+
import type { ChargeWithData, DocumentCharge, TransactionCharge } from '../types.js';
|
|
9
|
+
import { findMatches, type MatchResult } from './single-match.provider.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Result of processing a single charge for auto-matching
|
|
13
|
+
*/
|
|
14
|
+
export interface ProcessChargeResult {
|
|
15
|
+
/** Match result if a valid match was found, null otherwise */
|
|
16
|
+
match: MatchResult | null;
|
|
17
|
+
/** Status of the processing */
|
|
18
|
+
status: 'matched' | 'skipped' | 'no-match';
|
|
19
|
+
/** Reason for the status (useful for debugging/logging) */
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Process a single unmatched charge and find best match
|
|
25
|
+
*
|
|
26
|
+
* This function uses findMatches() from single-match provider with NO date window
|
|
27
|
+
* restriction, then filters for high-confidence matches (≥0.95 threshold).
|
|
28
|
+
*
|
|
29
|
+
* @param sourceCharge - Unmatched charge to process (must have only transactions OR only documents)
|
|
30
|
+
* @param allCandidates - All candidate charges to search (complementary type to source)
|
|
31
|
+
* @param userId - Current user ID for business extraction
|
|
32
|
+
* @returns Processing result with match status
|
|
33
|
+
*
|
|
34
|
+
* @throws Error if sourceCharge is already matched (has both transactions and documents)
|
|
35
|
+
* @throws Error if sourceCharge has no transactions or documents
|
|
36
|
+
* @throws Error if any validation fails (propagated from findMatches)
|
|
37
|
+
*/
|
|
38
|
+
export function processChargeForAutoMatch(
|
|
39
|
+
sourceCharge: ChargeWithData,
|
|
40
|
+
allCandidates: ChargeWithData[],
|
|
41
|
+
userId: string,
|
|
42
|
+
): ProcessChargeResult {
|
|
43
|
+
const AUTO_MATCH_THRESHOLD = 0.95;
|
|
44
|
+
|
|
45
|
+
// Prepare source charge for findMatches
|
|
46
|
+
const hasTransactions = sourceCharge.transactions && sourceCharge.transactions.length > 0;
|
|
47
|
+
const hasDocuments = sourceCharge.documents && sourceCharge.documents.length > 0;
|
|
48
|
+
|
|
49
|
+
let sourceForMatching: TransactionCharge | DocumentCharge;
|
|
50
|
+
let candidatesForMatching: (TransactionCharge | DocumentCharge)[];
|
|
51
|
+
|
|
52
|
+
if (hasTransactions && !hasDocuments) {
|
|
53
|
+
// Transaction charge - convert to TransactionCharge format
|
|
54
|
+
sourceForMatching = {
|
|
55
|
+
chargeId: sourceCharge.chargeId,
|
|
56
|
+
transactions: sourceCharge.transactions,
|
|
57
|
+
};
|
|
58
|
+
// Filter candidates to only document charges
|
|
59
|
+
candidatesForMatching = allCandidates
|
|
60
|
+
.filter(
|
|
61
|
+
c =>
|
|
62
|
+
c.documents && c.documents.length > 0 && (!c.transactions || c.transactions.length === 0),
|
|
63
|
+
)
|
|
64
|
+
.map(c => ({
|
|
65
|
+
chargeId: c.chargeId,
|
|
66
|
+
documents: c.documents,
|
|
67
|
+
}));
|
|
68
|
+
} else if (hasDocuments && !hasTransactions) {
|
|
69
|
+
// Document charge - convert to DocumentCharge format
|
|
70
|
+
sourceForMatching = {
|
|
71
|
+
chargeId: sourceCharge.chargeId,
|
|
72
|
+
documents: sourceCharge.documents,
|
|
73
|
+
};
|
|
74
|
+
// Filter candidates to only transaction charges
|
|
75
|
+
candidatesForMatching = allCandidates
|
|
76
|
+
.filter(
|
|
77
|
+
c =>
|
|
78
|
+
c.transactions && c.transactions.length > 0 && (!c.documents || c.documents.length === 0),
|
|
79
|
+
)
|
|
80
|
+
.map(c => ({
|
|
81
|
+
chargeId: c.chargeId,
|
|
82
|
+
transactions: c.transactions,
|
|
83
|
+
}));
|
|
84
|
+
} else {
|
|
85
|
+
// Invalid charge state
|
|
86
|
+
if (hasTransactions && hasDocuments) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Charge ${sourceCharge.chargeId} is already matched (has both transactions and documents)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`Charge ${sourceCharge.chargeId} has no transactions or documents`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Find all matches with no date window restriction
|
|
95
|
+
const allMatches = findMatches(sourceForMatching, candidatesForMatching, userId, {
|
|
96
|
+
dateWindowMonths: undefined, // No date restriction for auto-match
|
|
97
|
+
maxMatches: undefined, // Get all matches, we'll filter by threshold
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Filter for high-confidence matches (≥0.95)
|
|
101
|
+
const highConfidenceMatches = allMatches.filter(
|
|
102
|
+
match => match.confidenceScore >= AUTO_MATCH_THRESHOLD,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Determine result based on number of high-confidence matches
|
|
106
|
+
if (highConfidenceMatches.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
match: null,
|
|
109
|
+
status: 'no-match',
|
|
110
|
+
reason:
|
|
111
|
+
allMatches.length > 0
|
|
112
|
+
? `Best match has confidence ${allMatches[0].confidenceScore.toFixed(2)}, below threshold ${AUTO_MATCH_THRESHOLD}`
|
|
113
|
+
: 'No candidates found',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (highConfidenceMatches.length === 1) {
|
|
118
|
+
return {
|
|
119
|
+
match: highConfidenceMatches[0],
|
|
120
|
+
status: 'matched',
|
|
121
|
+
reason: `Single high-confidence match found (${highConfidenceMatches[0].confidenceScore.toFixed(2)})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Multiple high-confidence matches - ambiguous
|
|
126
|
+
return {
|
|
127
|
+
match: null,
|
|
128
|
+
status: 'skipped',
|
|
129
|
+
reason: `${highConfidenceMatches.length} high-confidence matches found (ambiguous)`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Determine merge direction for two charges
|
|
135
|
+
*
|
|
136
|
+
* The merge direction follows these rules:
|
|
137
|
+
* 1. If one charge is matched (has both transactions and documents), keep the matched one
|
|
138
|
+
* 2. If both are unmatched, keep the one with transactions (transaction charge is the "anchor")
|
|
139
|
+
* 3. If neither has transactions, keep the first one (arbitrary but consistent)
|
|
140
|
+
*
|
|
141
|
+
* @param charge1 - First charge
|
|
142
|
+
* @param charge2 - Second charge
|
|
143
|
+
* @returns [source, target] tuple where source will be merged INTO target (source is deleted)
|
|
144
|
+
*/
|
|
145
|
+
export function determineMergeDirection(
|
|
146
|
+
charge1: ChargeWithData,
|
|
147
|
+
charge2: ChargeWithData,
|
|
148
|
+
): [ChargeWithData, ChargeWithData] {
|
|
149
|
+
const charge1HasTransactions = charge1.transactions && charge1.transactions.length > 0;
|
|
150
|
+
const charge1HasDocuments = charge1.documents && charge1.documents.length > 0;
|
|
151
|
+
const charge2HasTransactions = charge2.transactions && charge2.transactions.length > 0;
|
|
152
|
+
const charge2HasDocuments = charge2.documents && charge2.documents.length > 0;
|
|
153
|
+
|
|
154
|
+
const charge1IsMatched = charge1HasTransactions && charge1HasDocuments;
|
|
155
|
+
const charge2IsMatched = charge2HasTransactions && charge2HasDocuments;
|
|
156
|
+
|
|
157
|
+
// Rule 1: If one is matched, keep the matched one
|
|
158
|
+
if (charge1IsMatched && !charge2IsMatched) {
|
|
159
|
+
return [charge2, charge1]; // Merge charge2 INTO charge1 (keep charge1)
|
|
160
|
+
}
|
|
161
|
+
if (charge2IsMatched && !charge1IsMatched) {
|
|
162
|
+
return [charge1, charge2]; // Merge charge1 INTO charge2 (keep charge2)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Rule 2: Both unmatched - keep the one with transactions
|
|
166
|
+
if (charge1HasTransactions && !charge2HasTransactions) {
|
|
167
|
+
return [charge2, charge1]; // Merge charge2 INTO charge1 (keep transaction charge)
|
|
168
|
+
}
|
|
169
|
+
if (charge2HasTransactions && !charge1HasTransactions) {
|
|
170
|
+
return [charge1, charge2]; // Merge charge1 INTO charge2 (keep transaction charge)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Rule 3: Neither has transactions (both are document-only) or both have transactions
|
|
174
|
+
// Keep charge1 (arbitrary but consistent)
|
|
175
|
+
return [charge2, charge1]; // Merge charge2 INTO charge1 (keep charge1)
|
|
176
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
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
|
+
|
|
8
|
+
import { Injectable, Scope } from 'graphql-modules';
|
|
9
|
+
import { mergeChargesExecutor } from '@modules/charges/helpers/merge-charges.hepler.js';
|
|
10
|
+
import { ChargesProvider } from '@modules/charges/providers/charges.provider.js';
|
|
11
|
+
import { DocumentsProvider } from '@modules/documents/providers/documents.provider.js';
|
|
12
|
+
import { TransactionsProvider } from '@modules/transactions/providers/transactions.provider.js';
|
|
13
|
+
import { dateToTimelessDateString } from '@shared/helpers';
|
|
14
|
+
import { validateChargeIsUnmatched } from '../helpers/charge-validator.helper.js';
|
|
15
|
+
import {
|
|
16
|
+
ChargeType,
|
|
17
|
+
type AutoMatchChargesResult,
|
|
18
|
+
type ChargeMatchesResult,
|
|
19
|
+
type ChargeWithData,
|
|
20
|
+
type Document,
|
|
21
|
+
type DocumentCharge,
|
|
22
|
+
type Transaction,
|
|
23
|
+
type TransactionCharge,
|
|
24
|
+
} from '../types.js';
|
|
25
|
+
import { determineMergeDirection, processChargeForAutoMatch } from './auto-match.provider.js';
|
|
26
|
+
import { aggregateDocuments } from './document-aggregator.js';
|
|
27
|
+
import { findMatches, type MatchResult } from './single-match.provider.js';
|
|
28
|
+
import { aggregateTransactions } from './transaction-aggregator.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Charges Matcher Provider
|
|
32
|
+
*
|
|
33
|
+
* Provides high-level charge matching operations with database integration.
|
|
34
|
+
* Uses the Injector pattern to access existing providers from other modules.
|
|
35
|
+
*/
|
|
36
|
+
@Injectable({
|
|
37
|
+
scope: Scope.Operation,
|
|
38
|
+
})
|
|
39
|
+
export class ChargesMatcherProvider {
|
|
40
|
+
/**
|
|
41
|
+
* Find potential matches for an unmatched charge
|
|
42
|
+
*
|
|
43
|
+
* @param chargeId - ID of the unmatched charge to find matches for
|
|
44
|
+
* @param injector - GraphQL modules injector for provider access
|
|
45
|
+
* @returns Top 5 matches ordered by confidence score
|
|
46
|
+
* @throws Error if charge not found
|
|
47
|
+
* @throws Error if charge is already matched
|
|
48
|
+
* @throws Error if charge data is invalid
|
|
49
|
+
*/
|
|
50
|
+
async findMatchesForCharge(
|
|
51
|
+
chargeId: string,
|
|
52
|
+
context: GraphQLModules.AppContext,
|
|
53
|
+
): Promise<ChargeMatchesResult> {
|
|
54
|
+
const {
|
|
55
|
+
adminContext: { defaultAdminBusinessId: adminBusinessId },
|
|
56
|
+
injector,
|
|
57
|
+
} = context;
|
|
58
|
+
// Get current user ID from context
|
|
59
|
+
if (!adminBusinessId) {
|
|
60
|
+
throw new Error('Admin business not found in context');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get providers from injector
|
|
64
|
+
const chargesProvider = injector.get(ChargesProvider);
|
|
65
|
+
const transactionsProvider = injector.get(TransactionsProvider);
|
|
66
|
+
const documentsProvider = injector.get(DocumentsProvider);
|
|
67
|
+
|
|
68
|
+
// Step 1: Load source charge data
|
|
69
|
+
const sourceCharge = await chargesProvider.getChargeByIdLoader.load(chargeId);
|
|
70
|
+
if (sourceCharge instanceof Error) {
|
|
71
|
+
throw new Error(`Source charge not found: ${chargeId}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 2: Load transactions and documents for source charge
|
|
75
|
+
const sourceTransactions = (await transactionsProvider.transactionsByChargeIDLoader.load(
|
|
76
|
+
chargeId,
|
|
77
|
+
)) as Transaction[];
|
|
78
|
+
const sourceDocuments = (await documentsProvider.getDocumentsByChargeIdLoader.load(
|
|
79
|
+
chargeId,
|
|
80
|
+
)) as Document[];
|
|
81
|
+
|
|
82
|
+
// Step 3: Validate source charge is unmatched
|
|
83
|
+
const sourceChargeWithData = {
|
|
84
|
+
...sourceCharge,
|
|
85
|
+
transactions: sourceTransactions,
|
|
86
|
+
documents: sourceDocuments,
|
|
87
|
+
};
|
|
88
|
+
validateChargeIsUnmatched(sourceChargeWithData);
|
|
89
|
+
|
|
90
|
+
// Step 4: Determine reference date and date window from source charge
|
|
91
|
+
let referenceDate: Date;
|
|
92
|
+
const hasTransactions = sourceTransactions && sourceTransactions.length > 0;
|
|
93
|
+
if (hasTransactions) {
|
|
94
|
+
// Use earliest transaction event_date
|
|
95
|
+
const aggregated = aggregateTransactions(sourceTransactions);
|
|
96
|
+
referenceDate = aggregated.date;
|
|
97
|
+
} else {
|
|
98
|
+
// Use latest document date
|
|
99
|
+
const aggregated = aggregateDocuments(sourceDocuments, adminBusinessId);
|
|
100
|
+
referenceDate = aggregated.date;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Step 5: Load candidate charges from database
|
|
104
|
+
// Use 12-month window centered on reference date
|
|
105
|
+
const windowStart = new Date(referenceDate);
|
|
106
|
+
windowStart.setMonth(windowStart.getMonth() - 12);
|
|
107
|
+
const windowEnd = new Date(referenceDate);
|
|
108
|
+
windowEnd.setMonth(windowEnd.getMonth() + 12);
|
|
109
|
+
|
|
110
|
+
const candidateCharges = await chargesProvider.getChargesByFilters({
|
|
111
|
+
ownerIds: [adminBusinessId],
|
|
112
|
+
fromAnyDate: dateToTimelessDateString(windowStart),
|
|
113
|
+
toAnyDate: dateToTimelessDateString(windowEnd),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Step 6: Load transactions and documents for all candidate charges
|
|
117
|
+
const candidateChargesWithData: Array<TransactionCharge | DocumentCharge> = [];
|
|
118
|
+
|
|
119
|
+
for (const candidate of candidateCharges) {
|
|
120
|
+
// Skip the source charge itself
|
|
121
|
+
if (candidate.id === chargeId) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const candidateTransactions = (await transactionsProvider.transactionsByChargeIDLoader.load(
|
|
126
|
+
candidate.id,
|
|
127
|
+
)) as Transaction[];
|
|
128
|
+
const candidateDocuments = (await documentsProvider.getDocumentsByChargeIdLoader.load(
|
|
129
|
+
candidate.id,
|
|
130
|
+
)) as Document[];
|
|
131
|
+
|
|
132
|
+
const hasTxs = candidateTransactions && candidateTransactions.length > 0;
|
|
133
|
+
const hasDocs = candidateDocuments && candidateDocuments.length > 0;
|
|
134
|
+
|
|
135
|
+
// Only include unmatched charges (not both types)
|
|
136
|
+
if (hasTxs && !hasDocs) {
|
|
137
|
+
candidateChargesWithData.push({
|
|
138
|
+
chargeId: candidate.id,
|
|
139
|
+
transactions: candidateTransactions,
|
|
140
|
+
});
|
|
141
|
+
} else if (hasDocs && !hasTxs) {
|
|
142
|
+
candidateChargesWithData.push({
|
|
143
|
+
chargeId: candidate.id,
|
|
144
|
+
documents: candidateDocuments,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Skip matched charges (have both) and empty charges (have neither)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Step 7: Build source charge object for findMatches
|
|
151
|
+
let sourceChargeData: TransactionCharge | DocumentCharge;
|
|
152
|
+
if (hasTransactions) {
|
|
153
|
+
sourceChargeData = {
|
|
154
|
+
chargeId,
|
|
155
|
+
transactions: sourceTransactions,
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
sourceChargeData = {
|
|
159
|
+
chargeId,
|
|
160
|
+
documents: sourceDocuments,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 8: Call core findMatches function
|
|
165
|
+
const matches: MatchResult[] = findMatches(
|
|
166
|
+
sourceChargeData,
|
|
167
|
+
candidateChargesWithData,
|
|
168
|
+
adminBusinessId,
|
|
169
|
+
{
|
|
170
|
+
maxMatches: 5,
|
|
171
|
+
dateWindowMonths: 12,
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Step 9: Format and return result
|
|
176
|
+
return {
|
|
177
|
+
matches: matches.map(match => ({
|
|
178
|
+
chargeId: match.chargeId,
|
|
179
|
+
confidenceScore: match.confidenceScore,
|
|
180
|
+
})),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Auto-match all unmatched charges
|
|
186
|
+
*
|
|
187
|
+
* Automatically merges charges that have a single high-confidence match (≥0.95).
|
|
188
|
+
* Skips charges with multiple high-confidence matches (ambiguous).
|
|
189
|
+
* Processes all unmatched charges and returns a summary of actions taken.
|
|
190
|
+
*
|
|
191
|
+
* @param injector - GraphQL modules injector for provider access
|
|
192
|
+
* @param context - GraphQL context with user information
|
|
193
|
+
* @returns Summary of matches made, skipped charges, and errors
|
|
194
|
+
*/
|
|
195
|
+
async autoMatchCharges(context: GraphQLModules.AppContext): Promise<AutoMatchChargesResult> {
|
|
196
|
+
const {
|
|
197
|
+
adminContext: { defaultAdminBusinessId: adminBusinessId },
|
|
198
|
+
injector,
|
|
199
|
+
} = context;
|
|
200
|
+
// Get current user ID from context
|
|
201
|
+
if (!adminBusinessId) {
|
|
202
|
+
throw new Error('Admin business not found in context');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get providers from injector
|
|
206
|
+
const chargesProvider = injector.get(ChargesProvider);
|
|
207
|
+
const transactionsProvider = injector.get(TransactionsProvider);
|
|
208
|
+
const documentsProvider = injector.get(DocumentsProvider);
|
|
209
|
+
|
|
210
|
+
// Step 1: Load all charges for this user
|
|
211
|
+
const allCharges = await chargesProvider.getChargesByFilters({
|
|
212
|
+
ownerIds: [adminBusinessId],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Step 2: Load transactions and documents for all charges
|
|
216
|
+
const chargesWithData: ChargeWithData[] = [];
|
|
217
|
+
const mergedChargeIds = new Set<string>(); // Track merged charges to exclude from processing
|
|
218
|
+
|
|
219
|
+
for (const charge of allCharges) {
|
|
220
|
+
const transactions = (await transactionsProvider.transactionsByChargeIDLoader.load(
|
|
221
|
+
charge.id,
|
|
222
|
+
)) as Transaction[];
|
|
223
|
+
const documents = (await documentsProvider.getDocumentsByChargeIdLoader.load(
|
|
224
|
+
charge.id,
|
|
225
|
+
)) as Document[];
|
|
226
|
+
|
|
227
|
+
chargesWithData.push({
|
|
228
|
+
chargeId: charge.id,
|
|
229
|
+
ownerId: charge.owner_id ?? adminBusinessId,
|
|
230
|
+
type: ChargeType.TRANSACTION_ONLY, // Will be determined by processChargeForAutoMatch
|
|
231
|
+
transactions: transactions || [],
|
|
232
|
+
documents: documents || [],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Step 3: Filter to get only unmatched charges
|
|
237
|
+
const unmatchedCharges = chargesWithData.filter(charge => {
|
|
238
|
+
const hasTx = charge.transactions && charge.transactions.length > 0;
|
|
239
|
+
const hasDocs = charge.documents && charge.documents.length > 0;
|
|
240
|
+
return (hasTx && !hasDocs) || (!hasTx && hasDocs);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Step 4: Process each unmatched charge
|
|
244
|
+
const result: AutoMatchChargesResult = {
|
|
245
|
+
totalMatches: 0,
|
|
246
|
+
mergedCharges: [],
|
|
247
|
+
skippedCharges: [],
|
|
248
|
+
errors: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
for (const sourceCharge of unmatchedCharges) {
|
|
252
|
+
// Skip if this charge was already merged in this run
|
|
253
|
+
if (mergedChargeIds.has(sourceCharge.chargeId)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Get candidates (exclude already merged charges)
|
|
259
|
+
const candidates = chargesWithData.filter(
|
|
260
|
+
c => c.chargeId !== sourceCharge.chargeId && !mergedChargeIds.has(c.chargeId),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Process this charge for auto-match
|
|
264
|
+
const processResult = processChargeForAutoMatch(sourceCharge, candidates, adminBusinessId);
|
|
265
|
+
|
|
266
|
+
if (processResult.status === 'matched' && processResult.match) {
|
|
267
|
+
// Found a single high-confidence match - execute merge
|
|
268
|
+
const matchedChargeId = processResult.match.chargeId;
|
|
269
|
+
const matchedCharge = chargesWithData.find(c => c.chargeId === matchedChargeId);
|
|
270
|
+
|
|
271
|
+
if (!matchedCharge) {
|
|
272
|
+
result.errors.push(
|
|
273
|
+
`Matched charge ${matchedChargeId} not found in charge pool for ${sourceCharge.chargeId}`,
|
|
274
|
+
);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Determine merge direction
|
|
279
|
+
const [sourceToMerge, targetToKeep] = determineMergeDirection(
|
|
280
|
+
sourceCharge,
|
|
281
|
+
matchedCharge,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// Execute merge via existing merge functionality
|
|
286
|
+
await mergeChargesExecutor([sourceToMerge.chargeId], targetToKeep.chargeId, injector);
|
|
287
|
+
|
|
288
|
+
// Track successful merge
|
|
289
|
+
result.totalMatches++;
|
|
290
|
+
result.mergedCharges.push({
|
|
291
|
+
chargeId: sourceToMerge.chargeId,
|
|
292
|
+
confidenceScore: processResult.match.confidenceScore,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Mark both charges as processed (merged away charge and kept charge)
|
|
296
|
+
mergedChargeIds.add(sourceToMerge.chargeId);
|
|
297
|
+
mergedChargeIds.add(targetToKeep.chargeId); // Don't process the kept charge again
|
|
298
|
+
} catch (mergeError) {
|
|
299
|
+
result.errors.push(
|
|
300
|
+
`Failed to merge ${sourceToMerge.chargeId} into ${targetToKeep.chargeId}: ${
|
|
301
|
+
mergeError instanceof Error ? mergeError.message : String(mergeError)
|
|
302
|
+
}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
} else if (processResult.status === 'skipped') {
|
|
306
|
+
// Multiple high-confidence matches - ambiguous
|
|
307
|
+
result.skippedCharges.push(sourceCharge.chargeId);
|
|
308
|
+
}
|
|
309
|
+
// status === 'no-match': do nothing, silently skip
|
|
310
|
+
} catch (error) {
|
|
311
|
+
// Capture error but continue processing other charges
|
|
312
|
+
result.errors.push(
|
|
313
|
+
`Error processing charge ${sourceCharge.chargeId}: ${
|
|
314
|
+
error instanceof Error ? error.message : String(error)
|
|
315
|
+
}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
}
|