@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +47 -7
  2. package/dist/green-invoice-graphql/src/mesh-artifacts/index.d.ts +1 -1
  3. package/dist/server/src/__generated__/types.d.ts +77 -0
  4. package/dist/server/src/__generated__/types.js.map +1 -1
  5. package/dist/server/src/modules/charges-matcher/__generated__/types.d.ts +68 -0
  6. package/dist/server/src/modules/charges-matcher/__generated__/types.js +7 -0
  7. package/dist/server/src/modules/charges-matcher/__generated__/types.js.map +1 -0
  8. package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.d.ts +1 -0
  9. package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js +218 -0
  10. package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js.map +1 -0
  11. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.d.ts +1 -0
  12. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +645 -0
  13. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -0
  14. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.d.ts +1 -0
  15. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +530 -0
  16. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -0
  17. package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.d.ts +1 -0
  18. package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js +143 -0
  19. package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js.map +1 -0
  20. package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.d.ts +1 -0
  21. package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js +186 -0
  22. package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js.map +1 -0
  23. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.d.ts +1 -0
  24. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +301 -0
  25. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -0
  26. package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.d.ts +1 -0
  27. package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js +127 -0
  28. package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js.map +1 -0
  29. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.d.ts +1 -0
  30. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +246 -0
  31. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -0
  32. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.d.ts +1 -0
  33. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js +475 -0
  34. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -0
  35. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.d.ts +1 -0
  36. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +287 -0
  37. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -0
  38. package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.d.ts +1 -0
  39. package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js +151 -0
  40. package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js.map +1 -0
  41. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.d.ts +1 -0
  42. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +550 -0
  43. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -0
  44. package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.d.ts +1 -0
  45. package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js +410 -0
  46. package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js.map +1 -0
  47. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.d.ts +1 -0
  48. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +504 -0
  49. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -0
  50. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.d.ts +1 -0
  51. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +483 -0
  52. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -0
  53. package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.d.ts +46 -0
  54. package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js +143 -0
  55. package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js.map +1 -0
  56. package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.d.ts +1 -0
  57. package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js +137 -0
  58. package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js.map +1 -0
  59. package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.d.ts +1 -0
  60. package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js +415 -0
  61. package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js.map +1 -0
  62. package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.d.ts +7 -0
  63. package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js +70 -0
  64. package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js.map +1 -0
  65. package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.d.ts +7 -0
  66. package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js +19 -0
  67. package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js.map +1 -0
  68. package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.d.ts +24 -0
  69. package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js +45 -0
  70. package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js.map +1 -0
  71. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.d.ts +33 -0
  72. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +65 -0
  73. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -0
  74. package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.d.ts +7 -0
  75. package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js +18 -0
  76. package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js.map +1 -0
  77. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +7 -0
  78. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +35 -0
  79. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -0
  80. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +49 -0
  81. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +58 -0
  82. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -0
  83. package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.d.ts +13 -0
  84. package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js +37 -0
  85. package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js.map +1 -0
  86. package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.d.ts +42 -0
  87. package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js +77 -0
  88. package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js.map +1 -0
  89. package/dist/server/src/modules/charges-matcher/index.d.ts +3 -0
  90. package/dist/server/src/modules/charges-matcher/index.js +15 -0
  91. package/dist/server/src/modules/charges-matcher/index.js.map +1 -0
  92. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +48 -0
  93. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +133 -0
  94. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -0
  95. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.d.ts +38 -0
  96. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +248 -0
  97. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -0
  98. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +61 -0
  99. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +153 -0
  100. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -0
  101. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +25 -0
  102. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +114 -0
  103. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -0
  104. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +39 -0
  105. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +189 -0
  106. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -0
  107. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +54 -0
  108. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js +93 -0
  109. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js.map +1 -0
  110. package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.d.ts +2 -0
  111. package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js +22 -0
  112. package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js.map +1 -0
  113. package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.d.ts +2 -0
  114. package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js +24 -0
  115. package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js.map +1 -0
  116. package/dist/server/src/modules/charges-matcher/resolvers/index.d.ts +2 -0
  117. package/dist/server/src/modules/charges-matcher/resolvers/index.js +11 -0
  118. package/dist/server/src/modules/charges-matcher/resolvers/index.js.map +1 -0
  119. package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.d.ts +2 -0
  120. package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js +47 -0
  121. package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js.map +1 -0
  122. package/dist/server/src/modules/charges-matcher/types.d.ts +179 -0
  123. package/dist/server/src/modules/charges-matcher/types.js +14 -0
  124. package/dist/server/src/modules/charges-matcher/types.js.map +1 -0
  125. package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js +2 -2
  126. package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js.map +1 -1
  127. package/dist/server/src/modules-app.js +2 -0
  128. package/dist/server/src/modules-app.js.map +1 -1
  129. package/dist/server/src/shared/types/index.d.ts +1 -1
  130. package/package.json +4 -4
  131. package/src/__generated__/types.ts +87 -0
  132. package/src/modules/charges-matcher/README.md +279 -0
  133. package/src/modules/charges-matcher/__generated__/types.ts +71 -0
  134. package/src/modules/charges-matcher/__tests__/amount-confidence.test.ts +260 -0
  135. package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +714 -0
  136. package/src/modules/charges-matcher/__tests__/auto-match.test.ts +621 -0
  137. package/src/modules/charges-matcher/__tests__/business-confidence.test.ts +177 -0
  138. package/src/modules/charges-matcher/__tests__/candidate-filter.test.ts +238 -0
  139. package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +374 -0
  140. package/src/modules/charges-matcher/__tests__/currency-confidence.test.ts +164 -0
  141. package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +291 -0
  142. package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +614 -0
  143. package/src/modules/charges-matcher/__tests__/document-amount.test.ts +352 -0
  144. package/src/modules/charges-matcher/__tests__/document-business.test.ts +192 -0
  145. package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +659 -0
  146. package/src/modules/charges-matcher/__tests__/overall-confidence.test.ts +502 -0
  147. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +556 -0
  148. package/src/modules/charges-matcher/__tests__/single-match.test.ts +608 -0
  149. package/src/modules/charges-matcher/__tests__/test-helpers.ts +174 -0
  150. package/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts +177 -0
  151. package/src/modules/charges-matcher/__tests__/transaction-aggregator.test.ts +547 -0
  152. package/src/modules/charges-matcher/documentation/README.md +331 -0
  153. package/src/modules/charges-matcher/documentation/SPEC.md +1503 -0
  154. package/src/modules/charges-matcher/documentation/TODO.md +799 -0
  155. package/src/modules/charges-matcher/helpers/amount-confidence.helper.ts +88 -0
  156. package/src/modules/charges-matcher/helpers/business-confidence.helper.ts +23 -0
  157. package/src/modules/charges-matcher/helpers/candidate-filter.helper.ts +56 -0
  158. package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +100 -0
  159. package/src/modules/charges-matcher/helpers/currency-confidence.helper.ts +22 -0
  160. package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +41 -0
  161. package/src/modules/charges-matcher/helpers/document-amount.helper.ts +77 -0
  162. package/src/modules/charges-matcher/helpers/document-business.helper.ts +54 -0
  163. package/src/modules/charges-matcher/helpers/overall-confidence.helper.ts +90 -0
  164. package/src/modules/charges-matcher/index.ts +17 -0
  165. package/src/modules/charges-matcher/providers/auto-match.provider.ts +176 -0
  166. package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +322 -0
  167. package/src/modules/charges-matcher/providers/document-aggregator.ts +211 -0
  168. package/src/modules/charges-matcher/providers/match-scorer.provider.ts +154 -0
  169. package/src/modules/charges-matcher/providers/single-match.provider.ts +252 -0
  170. package/src/modules/charges-matcher/providers/transaction-aggregator.ts +131 -0
  171. package/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.ts +23 -0
  172. package/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.ts +25 -0
  173. package/src/modules/charges-matcher/resolvers/index.ts +12 -0
  174. package/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.ts +47 -0
  175. package/src/modules/charges-matcher/types.ts +200 -0
  176. package/src/modules/documents/resolvers/document-suggestions.resolver.ts +2 -2
  177. package/src/modules-app.ts +2 -0
  178. 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';