@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.
Files changed (185) hide show
  1. package/CHANGELOG.md +47 -7
  2. package/dist/green-invoice-graphql/src/mesh-artifacts/index.d.ts +7 -7
  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/green-invoice/helpers/contract-to-draft.helper.js +1 -1
  128. package/dist/server/src/modules/green-invoice/helpers/contract-to-draft.helper.js.map +1 -1
  129. package/dist/server/src/modules/green-invoice/helpers/green-invoice.helper.d.ts +1 -1
  130. package/dist/server/src/modules/green-invoice/helpers/green-invoice.helper.js +1 -1
  131. package/dist/server/src/modules/green-invoice/helpers/green-invoice.helper.js.map +1 -1
  132. package/dist/server/src/modules-app.js +2 -0
  133. package/dist/server/src/modules-app.js.map +1 -1
  134. package/dist/server/src/shared/types/index.d.ts +1 -1
  135. package/package.json +4 -4
  136. package/src/__generated__/types.ts +87 -0
  137. package/src/modules/charges-matcher/README.md +279 -0
  138. package/src/modules/charges-matcher/__generated__/types.ts +71 -0
  139. package/src/modules/charges-matcher/__tests__/amount-confidence.test.ts +260 -0
  140. package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +714 -0
  141. package/src/modules/charges-matcher/__tests__/auto-match.test.ts +621 -0
  142. package/src/modules/charges-matcher/__tests__/business-confidence.test.ts +177 -0
  143. package/src/modules/charges-matcher/__tests__/candidate-filter.test.ts +238 -0
  144. package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +374 -0
  145. package/src/modules/charges-matcher/__tests__/currency-confidence.test.ts +164 -0
  146. package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +291 -0
  147. package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +614 -0
  148. package/src/modules/charges-matcher/__tests__/document-amount.test.ts +352 -0
  149. package/src/modules/charges-matcher/__tests__/document-business.test.ts +192 -0
  150. package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +659 -0
  151. package/src/modules/charges-matcher/__tests__/overall-confidence.test.ts +502 -0
  152. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +556 -0
  153. package/src/modules/charges-matcher/__tests__/single-match.test.ts +608 -0
  154. package/src/modules/charges-matcher/__tests__/test-helpers.ts +174 -0
  155. package/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts +177 -0
  156. package/src/modules/charges-matcher/__tests__/transaction-aggregator.test.ts +547 -0
  157. package/src/modules/charges-matcher/documentation/README.md +331 -0
  158. package/src/modules/charges-matcher/documentation/SPEC.md +1503 -0
  159. package/src/modules/charges-matcher/documentation/TODO.md +799 -0
  160. package/src/modules/charges-matcher/helpers/amount-confidence.helper.ts +88 -0
  161. package/src/modules/charges-matcher/helpers/business-confidence.helper.ts +23 -0
  162. package/src/modules/charges-matcher/helpers/candidate-filter.helper.ts +56 -0
  163. package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +100 -0
  164. package/src/modules/charges-matcher/helpers/currency-confidence.helper.ts +22 -0
  165. package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +41 -0
  166. package/src/modules/charges-matcher/helpers/document-amount.helper.ts +77 -0
  167. package/src/modules/charges-matcher/helpers/document-business.helper.ts +54 -0
  168. package/src/modules/charges-matcher/helpers/overall-confidence.helper.ts +90 -0
  169. package/src/modules/charges-matcher/index.ts +17 -0
  170. package/src/modules/charges-matcher/providers/auto-match.provider.ts +176 -0
  171. package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +322 -0
  172. package/src/modules/charges-matcher/providers/document-aggregator.ts +211 -0
  173. package/src/modules/charges-matcher/providers/match-scorer.provider.ts +154 -0
  174. package/src/modules/charges-matcher/providers/single-match.provider.ts +252 -0
  175. package/src/modules/charges-matcher/providers/transaction-aggregator.ts +131 -0
  176. package/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.ts +23 -0
  177. package/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.ts +25 -0
  178. package/src/modules/charges-matcher/resolvers/index.ts +12 -0
  179. package/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.ts +47 -0
  180. package/src/modules/charges-matcher/types.ts +200 -0
  181. package/src/modules/documents/resolvers/document-suggestions.resolver.ts +2 -2
  182. package/src/modules/green-invoice/helpers/contract-to-draft.helper.ts +1 -1
  183. package/src/modules/green-invoice/helpers/green-invoice.helper.ts +1 -1
  184. package/src/modules-app.ts +2 -0
  185. 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
+ }