@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,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate the absolute difference between two numbers as a percentage of the smaller absolute value
|
|
3
|
+
* @param amount1 - First amount
|
|
4
|
+
* @param amount2 - Second amount
|
|
5
|
+
* @returns Percentage difference (0-100+)
|
|
6
|
+
*/
|
|
7
|
+
function calculatePercentageDiff(amount1: number, amount2: number): number {
|
|
8
|
+
const abs1 = Math.abs(amount1);
|
|
9
|
+
const abs2 = Math.abs(amount2);
|
|
10
|
+
const smallerAmount = Math.min(abs1, abs2);
|
|
11
|
+
|
|
12
|
+
// Handle zero amounts - if one is zero, we can't calculate percentage
|
|
13
|
+
// In this case, treat any difference > 1 as beyond 20%
|
|
14
|
+
if (smallerAmount === 0) {
|
|
15
|
+
return 100; // Return a high percentage to indicate no match
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const diff = Math.abs(abs1 - abs2);
|
|
19
|
+
return (diff / smallerAmount) * 100;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate confidence score based on amount similarity
|
|
24
|
+
* @param transactionAmount - The transaction amount (signed number)
|
|
25
|
+
* @param documentAmount - The normalized document amount (signed number)
|
|
26
|
+
* @returns Confidence score between 0.0 and 1.0
|
|
27
|
+
*/
|
|
28
|
+
export function calculateAmountConfidence(
|
|
29
|
+
transactionAmount: number,
|
|
30
|
+
documentAmount: number,
|
|
31
|
+
): number {
|
|
32
|
+
// Calculate absolute difference
|
|
33
|
+
const absoluteDiff = Math.abs(Math.abs(transactionAmount) - Math.abs(documentAmount));
|
|
34
|
+
|
|
35
|
+
// Exact match (0% difference): 1.0
|
|
36
|
+
if (absoluteDiff === 0) {
|
|
37
|
+
return 1.0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Within 1 currency unit: 0.9
|
|
41
|
+
if (absoluteDiff <= 1) {
|
|
42
|
+
return 0.9;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Calculate percentage difference for amounts beyond 1 unit
|
|
46
|
+
const percentageDiff = calculatePercentageDiff(transactionAmount, documentAmount);
|
|
47
|
+
|
|
48
|
+
// 20%+ difference: 0.0
|
|
49
|
+
if (percentageDiff >= 20) {
|
|
50
|
+
return 0.0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Between 1 unit and 20% difference: Linear degradation from 0.7 to 0.0
|
|
54
|
+
// We need to determine which threshold is reached first:
|
|
55
|
+
// - The absolute difference exceeds 1 unit (already true at this point)
|
|
56
|
+
// - The percentage difference reaches 20%
|
|
57
|
+
|
|
58
|
+
// Linear interpolation from 0.7 (at percentage start) to 0.0 (at 20%)
|
|
59
|
+
// We need to find where we are in the range
|
|
60
|
+
|
|
61
|
+
// Calculate what percentage corresponds to 1 unit difference
|
|
62
|
+
const abs1 = Math.abs(transactionAmount);
|
|
63
|
+
const abs2 = Math.abs(documentAmount);
|
|
64
|
+
const smallerAmount = Math.min(abs1, abs2);
|
|
65
|
+
|
|
66
|
+
const oneUnitPercentage = (1 / smallerAmount) * 100;
|
|
67
|
+
|
|
68
|
+
// If we're still within the percentage range that corresponds to 1 unit,
|
|
69
|
+
// this shouldn't happen since we already checked absoluteDiff <= 1 above
|
|
70
|
+
// But just in case of floating point issues
|
|
71
|
+
if (percentageDiff <= oneUnitPercentage) {
|
|
72
|
+
return 0.9;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Linear degradation from 0.7 to 0.0 over the range [oneUnitPercentage, 20%]
|
|
76
|
+
const rangeStart = oneUnitPercentage;
|
|
77
|
+
const rangeEnd = 20;
|
|
78
|
+
const rangeSpan = rangeEnd - rangeStart;
|
|
79
|
+
|
|
80
|
+
// How far are we into this range? (0.0 = at start, 1.0 = at end)
|
|
81
|
+
const position = (percentageDiff - rangeStart) / rangeSpan;
|
|
82
|
+
|
|
83
|
+
// Linear degradation from 0.7 to 0.0
|
|
84
|
+
const confidence = 0.7 * (1 - position);
|
|
85
|
+
|
|
86
|
+
// Round to 2 decimal places
|
|
87
|
+
return Math.round(confidence * 100) / 100;
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate confidence score based on business ID match
|
|
3
|
+
* @param transactionBusinessId - Business ID from transaction (can be null)
|
|
4
|
+
* @param documentBusinessId - Business ID from document (can be null)
|
|
5
|
+
* @returns Confidence score: 1.0 (exact match), 0.5 (one or both null), 0.2 (mismatch)
|
|
6
|
+
*/
|
|
7
|
+
export function calculateBusinessConfidence(
|
|
8
|
+
transactionBusinessId: string | null,
|
|
9
|
+
documentBusinessId: string | null,
|
|
10
|
+
): number {
|
|
11
|
+
// If either or both are null, return 0.5
|
|
12
|
+
if (transactionBusinessId === null || documentBusinessId === null) {
|
|
13
|
+
return 0.5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Both are non-null, check if they match
|
|
17
|
+
if (transactionBusinessId === documentBusinessId) {
|
|
18
|
+
return 1.0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Both are non-null but don't match
|
|
22
|
+
return 0.2;
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Document, Transaction } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filter transactions that should be excluded from matching
|
|
5
|
+
* Excludes transactions marked as fees (is_fee = true)
|
|
6
|
+
* @param transaction - Transaction to check
|
|
7
|
+
* @returns true if transaction should be included in matching
|
|
8
|
+
*/
|
|
9
|
+
export function isValidTransactionForMatching(transaction: Transaction): boolean {
|
|
10
|
+
// Exclude fee transactions
|
|
11
|
+
return !transaction.is_fee;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Filter documents that should be excluded from matching
|
|
16
|
+
* Excludes documents with null mandatory fields (total_amount or currency_code)
|
|
17
|
+
* @param document - Document to check
|
|
18
|
+
* @returns true if document should be included in matching
|
|
19
|
+
*/
|
|
20
|
+
export function isValidDocumentForMatching(document: Document): boolean {
|
|
21
|
+
// Document must have total_amount (can be zero, but not null)
|
|
22
|
+
if (document.total_amount === null || document.total_amount === undefined) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Document must have currency_code
|
|
27
|
+
if (!document.currency_code) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a date falls within the matching window
|
|
36
|
+
* Window is calculated as ±windowMonths from the reference date
|
|
37
|
+
* @param candidateDate - Date to check
|
|
38
|
+
* @param referenceDate - Center point of the window
|
|
39
|
+
* @param windowMonths - Number of months before/after (default 12)
|
|
40
|
+
* @returns true if within window
|
|
41
|
+
*/
|
|
42
|
+
export function isWithinDateWindow(
|
|
43
|
+
candidateDate: Date,
|
|
44
|
+
referenceDate: Date,
|
|
45
|
+
windowMonths: number = 12,
|
|
46
|
+
): boolean {
|
|
47
|
+
// Calculate the date range
|
|
48
|
+
const minDate = new Date(referenceDate);
|
|
49
|
+
minDate.setMonth(minDate.getMonth() - windowMonths);
|
|
50
|
+
|
|
51
|
+
const maxDate = new Date(referenceDate);
|
|
52
|
+
maxDate.setMonth(maxDate.getMonth() + windowMonths);
|
|
53
|
+
|
|
54
|
+
// Check if candidate is within range (inclusive)
|
|
55
|
+
return candidateDate >= minDate && candidateDate <= maxDate;
|
|
56
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Document, DocumentType, Transaction } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a charge with its associated transactions and documents
|
|
5
|
+
*/
|
|
6
|
+
interface Charge {
|
|
7
|
+
id: string;
|
|
8
|
+
owner_id: string | null;
|
|
9
|
+
transactions?: Transaction[];
|
|
10
|
+
documents?: Document[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Accounting document types that count toward matched/unmatched status
|
|
15
|
+
*/
|
|
16
|
+
const ACCOUNTING_DOC_TYPES: DocumentType[] = [
|
|
17
|
+
'INVOICE',
|
|
18
|
+
'CREDIT_INVOICE',
|
|
19
|
+
'RECEIPT',
|
|
20
|
+
'INVOICE_RECEIPT',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate a charge is properly formed for matching
|
|
25
|
+
* @throws Error with descriptive message if invalid
|
|
26
|
+
*/
|
|
27
|
+
export function validateChargeForMatching(charge: Charge): void {
|
|
28
|
+
if (!charge) {
|
|
29
|
+
throw new Error('Charge is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!charge.id) {
|
|
33
|
+
throw new Error('Charge must have an ID');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!charge.owner_id) {
|
|
37
|
+
throw new Error(`Charge ${charge.id} must have an owner_id`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if charge has data
|
|
41
|
+
const hasTransactions = charge.transactions && charge.transactions.length > 0;
|
|
42
|
+
const hasDocuments = charge.documents && charge.documents.length > 0;
|
|
43
|
+
|
|
44
|
+
if (!hasTransactions && !hasDocuments) {
|
|
45
|
+
throw new Error(`Charge ${charge.id} has no transactions or documents - cannot be matched`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if charge is matched (has both transactions and accounting documents)
|
|
51
|
+
*/
|
|
52
|
+
export function isChargeMatched(charge: Charge): boolean {
|
|
53
|
+
const hasTransactions = charge.transactions && charge.transactions.length > 0;
|
|
54
|
+
|
|
55
|
+
const hasAccountingDocs = charge.documents?.some(doc =>
|
|
56
|
+
ACCOUNTING_DOC_TYPES.includes(doc.type as DocumentType),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return !!hasTransactions && !!hasAccountingDocs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if charge has only transactions (no accounting documents)
|
|
64
|
+
*/
|
|
65
|
+
export function hasOnlyTransactions(charge: Charge): boolean {
|
|
66
|
+
const hasTransactions = charge.transactions && charge.transactions.length > 0;
|
|
67
|
+
|
|
68
|
+
const hasAccountingDocs = charge.documents?.some(doc =>
|
|
69
|
+
ACCOUNTING_DOC_TYPES.includes(doc.type as DocumentType),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return !!hasTransactions && !hasAccountingDocs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if charge has only accounting documents (no transactions)
|
|
77
|
+
*/
|
|
78
|
+
export function hasOnlyDocuments(charge: Charge): boolean {
|
|
79
|
+
const hasTransactions = charge.transactions && charge.transactions.length > 0;
|
|
80
|
+
|
|
81
|
+
const hasAccountingDocs = charge.documents?.some(doc =>
|
|
82
|
+
ACCOUNTING_DOC_TYPES.includes(doc.type as DocumentType),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return !hasTransactions && !!hasAccountingDocs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate that a charge is unmatched (for matching operations)
|
|
90
|
+
* @throws Error if charge is already matched
|
|
91
|
+
*/
|
|
92
|
+
export function validateChargeIsUnmatched(charge: Charge): void {
|
|
93
|
+
validateChargeForMatching(charge);
|
|
94
|
+
|
|
95
|
+
if (isChargeMatched(charge)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Charge ${charge.id} is already matched (has both transactions and accounting documents)`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate confidence score based on currency match
|
|
3
|
+
* @param transactionCurrency - Currency code from transaction
|
|
4
|
+
* @param documentCurrency - Currency code from document
|
|
5
|
+
* @returns 1.0 if same, 0.2 if one/both missing, 0.0 if different
|
|
6
|
+
*/
|
|
7
|
+
export function calculateCurrencyConfidence(
|
|
8
|
+
transactionCurrency: string | null | undefined,
|
|
9
|
+
documentCurrency: string | null | undefined,
|
|
10
|
+
): number {
|
|
11
|
+
// Handle null/undefined/empty cases
|
|
12
|
+
if (!transactionCurrency || !documentCurrency) {
|
|
13
|
+
return 0.2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Case-insensitive comparison
|
|
17
|
+
if (transactionCurrency.toUpperCase() === documentCurrency.toUpperCase()) {
|
|
18
|
+
return 1.0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return 0.0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate the absolute difference in days between two dates
|
|
3
|
+
* Ignores time components by comparing only the date parts
|
|
4
|
+
* @param date1 - First date
|
|
5
|
+
* @param date2 - Second date
|
|
6
|
+
* @returns Absolute difference in days
|
|
7
|
+
*/
|
|
8
|
+
function calculateDaysDifference(date1: Date, date2: Date): number {
|
|
9
|
+
// Create new dates with time set to midnight to ignore time components
|
|
10
|
+
const d1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
|
|
11
|
+
const d2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
|
|
12
|
+
|
|
13
|
+
// Calculate difference in milliseconds
|
|
14
|
+
const diffMs = Math.abs(d1.getTime() - d2.getTime());
|
|
15
|
+
|
|
16
|
+
// Convert to days
|
|
17
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
18
|
+
|
|
19
|
+
return diffDays;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate confidence score based on date proximity
|
|
24
|
+
* @param date1 - First date
|
|
25
|
+
* @param date2 - Second date
|
|
26
|
+
* @returns Confidence score from 0.0 (30+ days) to 1.0 (same day)
|
|
27
|
+
*/
|
|
28
|
+
export function calculateDateConfidence(date1: Date, date2: Date): number {
|
|
29
|
+
const daysDiff = calculateDaysDifference(date1, date2);
|
|
30
|
+
|
|
31
|
+
// 30 or more days: return 0.0
|
|
32
|
+
if (daysDiff >= 30) {
|
|
33
|
+
return 0.0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Linear degradation: 1.0 - (days_diff / 30)
|
|
37
|
+
const confidence = 1.0 - daysDiff / 30;
|
|
38
|
+
|
|
39
|
+
// Round to 2 decimal places
|
|
40
|
+
return Math.round(confidence * 100) / 100;
|
|
41
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Amount Normalization
|
|
3
|
+
*
|
|
4
|
+
* Normalizes document amounts for comparison with transaction amounts.
|
|
5
|
+
* Per specification (section 4.3.1):
|
|
6
|
+
* 1. Start with absolute value of total_amount
|
|
7
|
+
* 2. If business is creditor: negate
|
|
8
|
+
* 3. If document type is CREDIT_INVOICE: negate
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Document types from database schema
|
|
13
|
+
*/
|
|
14
|
+
export type DocumentType =
|
|
15
|
+
| 'CREDIT_INVOICE'
|
|
16
|
+
| 'INVOICE'
|
|
17
|
+
| 'INVOICE_RECEIPT'
|
|
18
|
+
| 'OTHER'
|
|
19
|
+
| 'PROFORMA'
|
|
20
|
+
| 'RECEIPT'
|
|
21
|
+
| 'UNPROCESSED';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalize document amount for comparison with transaction amount
|
|
25
|
+
*
|
|
26
|
+
* Per specification:
|
|
27
|
+
* - Start with absolute value of total_amount
|
|
28
|
+
* - If business is creditor (debtor is user): negate
|
|
29
|
+
* - If document type is CREDIT_INVOICE: negate
|
|
30
|
+
*
|
|
31
|
+
* Examples:
|
|
32
|
+
* - INVOICE, business debtor (creditor is user): |100| = 100
|
|
33
|
+
* - INVOICE, business creditor (debtor is user): |100| * -1 = -100
|
|
34
|
+
* - CREDIT_INVOICE, business debtor: |100| * -1 = -100
|
|
35
|
+
* - CREDIT_INVOICE, business creditor: |100| * -1 * -1 = 100 (double negation)
|
|
36
|
+
*
|
|
37
|
+
* @param totalAmount - Raw total_amount from document (can be positive or negative)
|
|
38
|
+
* @param isBusinessCreditor - Whether the business is the creditor (from business extraction)
|
|
39
|
+
* @param documentType - Type of document
|
|
40
|
+
* @returns Normalized amount (signed) for comparison with transaction amount
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Regular invoice where user is creditor (business owes user)
|
|
44
|
+
* normalizeDocumentAmount(100, false, 'INVOICE') // Returns 100
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Regular invoice where user is debtor (user owes business)
|
|
48
|
+
* normalizeDocumentAmount(100, true, 'INVOICE') // Returns -100
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // Credit invoice where user is creditor (user owes business a refund)
|
|
52
|
+
* normalizeDocumentAmount(100, false, 'CREDIT_INVOICE') // Returns -100
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // Credit invoice where user is debtor (business owes user a refund)
|
|
56
|
+
* normalizeDocumentAmount(100, true, 'CREDIT_INVOICE') // Returns 100 (double negation)
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeDocumentAmount(
|
|
59
|
+
totalAmount: number,
|
|
60
|
+
isBusinessCreditor: boolean,
|
|
61
|
+
documentType: DocumentType,
|
|
62
|
+
): number {
|
|
63
|
+
// Step 1: Start with absolute value
|
|
64
|
+
let normalizedAmount = Math.abs(totalAmount);
|
|
65
|
+
|
|
66
|
+
// Step 2: If business is creditor (debtor is user), negate
|
|
67
|
+
if (isBusinessCreditor) {
|
|
68
|
+
normalizedAmount = -normalizedAmount;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 3: If document type is CREDIT_INVOICE, negate
|
|
72
|
+
if (documentType === 'CREDIT_INVOICE') {
|
|
73
|
+
normalizedAmount = -normalizedAmount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return normalizedAmount;
|
|
77
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface DocumentBusinessInfo {
|
|
2
|
+
businessId: string | null;
|
|
3
|
+
isBusinessCreditor: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract business information from a document
|
|
8
|
+
* @param creditorId - Document's creditor_id
|
|
9
|
+
* @param debtorId - Document's debtor_id
|
|
10
|
+
* @param adminBusinessId - Current user's ID
|
|
11
|
+
* @returns Business ID and whether business is the creditor
|
|
12
|
+
* @throws Error if both or neither IDs match adminBusinessId
|
|
13
|
+
*/
|
|
14
|
+
export function extractDocumentBusiness(
|
|
15
|
+
creditorId: string | null,
|
|
16
|
+
debtorId: string | null,
|
|
17
|
+
adminBusinessId: string,
|
|
18
|
+
): DocumentBusinessInfo {
|
|
19
|
+
// Check if both are null
|
|
20
|
+
if (creditorId === null && debtorId === null) {
|
|
21
|
+
throw new Error('Document has both creditor_id and debtor_id as null - invalid document state');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isCreditorUser = creditorId === adminBusinessId;
|
|
25
|
+
const isDebtorUser = debtorId === adminBusinessId;
|
|
26
|
+
|
|
27
|
+
// Both sides are user
|
|
28
|
+
if (isCreditorUser && isDebtorUser) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'Document has both creditor_id and debtor_id equal to user ID - invalid document state',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Neither side is user
|
|
35
|
+
if (!isCreditorUser && !isDebtorUser) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Document has neither creditor_id nor debtor_id equal to user ID - document does not belong to user',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// User is debtor, business is creditor (or creditor is null)
|
|
42
|
+
if (isDebtorUser) {
|
|
43
|
+
return {
|
|
44
|
+
businessId: creditorId,
|
|
45
|
+
isBusinessCreditor: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// User is creditor, business is debtor (or debtor is null)
|
|
50
|
+
return {
|
|
51
|
+
businessId: debtorId,
|
|
52
|
+
isBusinessCreditor: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overall Confidence Calculator
|
|
3
|
+
*
|
|
4
|
+
* Combines individual confidence scores (amount, currency, business, date)
|
|
5
|
+
* into a single weighted overall confidence score.
|
|
6
|
+
*
|
|
7
|
+
* Formula: (amount × 0.4) + (currency × 0.2) + (business × 0.3) + (date × 0.1)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ConfidenceComponents {
|
|
11
|
+
amount: number;
|
|
12
|
+
currency: number;
|
|
13
|
+
business: number;
|
|
14
|
+
date: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Weights for each confidence component
|
|
19
|
+
*/
|
|
20
|
+
const CONFIDENCE_WEIGHTS = {
|
|
21
|
+
amount: 0.4,
|
|
22
|
+
currency: 0.2,
|
|
23
|
+
business: 0.3,
|
|
24
|
+
date: 0.1,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate overall confidence score from individual components
|
|
29
|
+
*
|
|
30
|
+
* @param components - Individual confidence scores (each 0.0 to 1.0)
|
|
31
|
+
* @returns Weighted overall confidence score (0.0 to 1.0, rounded to 2 decimals)
|
|
32
|
+
*
|
|
33
|
+
* @throws {Error} If any component is null or undefined
|
|
34
|
+
* @throws {Error} If any component is outside the valid range [0.0, 1.0]
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Perfect match
|
|
38
|
+
* calculateOverallConfidence({
|
|
39
|
+
* amount: 1.0,
|
|
40
|
+
* currency: 1.0,
|
|
41
|
+
* business: 1.0,
|
|
42
|
+
* date: 1.0
|
|
43
|
+
* }) // Returns 1.0
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Mixed confidence
|
|
47
|
+
* calculateOverallConfidence({
|
|
48
|
+
* amount: 0.9,
|
|
49
|
+
* currency: 1.0,
|
|
50
|
+
* business: 0.5,
|
|
51
|
+
* date: 0.8
|
|
52
|
+
* }) // Returns 0.79
|
|
53
|
+
*/
|
|
54
|
+
export function calculateOverallConfidence(components: ConfidenceComponents): number {
|
|
55
|
+
// Validate all components are present
|
|
56
|
+
if (components.amount == null) {
|
|
57
|
+
throw new Error('Amount confidence is required');
|
|
58
|
+
}
|
|
59
|
+
if (components.currency == null) {
|
|
60
|
+
throw new Error('Currency confidence is required');
|
|
61
|
+
}
|
|
62
|
+
if (components.business == null) {
|
|
63
|
+
throw new Error('Business confidence is required');
|
|
64
|
+
}
|
|
65
|
+
if (components.date == null) {
|
|
66
|
+
throw new Error('Date confidence is required');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate all components are in valid range
|
|
70
|
+
const validateRange = (value: number, name: string): void => {
|
|
71
|
+
if (value < 0.0 || value > 1.0) {
|
|
72
|
+
throw new Error(`${name} confidence must be between 0.0 and 1.0, got ${value}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
validateRange(components.amount, 'Amount');
|
|
77
|
+
validateRange(components.currency, 'Currency');
|
|
78
|
+
validateRange(components.business, 'Business');
|
|
79
|
+
validateRange(components.date, 'Date');
|
|
80
|
+
|
|
81
|
+
// Calculate weighted sum
|
|
82
|
+
const weightedSum =
|
|
83
|
+
components.amount * CONFIDENCE_WEIGHTS.amount +
|
|
84
|
+
components.currency * CONFIDENCE_WEIGHTS.currency +
|
|
85
|
+
components.business * CONFIDENCE_WEIGHTS.business +
|
|
86
|
+
components.date * CONFIDENCE_WEIGHTS.date;
|
|
87
|
+
|
|
88
|
+
// Round to 2 decimal places
|
|
89
|
+
return Math.round(weightedSum * 100) / 100;
|
|
90
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import chargesMatcherTypeDefs from './typeDefs/charges-matcher.graphql.js';
|
|
2
|
+
import { createModule } from 'graphql-modules';
|
|
3
|
+
import { ChargesMatcherProvider } from './providers/charges-matcher.provider.js';
|
|
4
|
+
import { chargesMatcherResolvers } from './resolvers/index.js';
|
|
5
|
+
|
|
6
|
+
const __dirname = new URL('.', import.meta.url).pathname;
|
|
7
|
+
|
|
8
|
+
export const chargesMatcherModule = createModule({
|
|
9
|
+
id: 'charges-matcher',
|
|
10
|
+
dirname: __dirname,
|
|
11
|
+
typeDefs: [chargesMatcherTypeDefs],
|
|
12
|
+
resolvers: [chargesMatcherResolvers],
|
|
13
|
+
providers: [ChargesMatcherProvider],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export { ChargesMatcherProvider } from './providers/charges-matcher.provider.js';
|
|
17
|
+
export * as ChargesMatcherTypes from './types.js';
|