@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,279 @@
1
+ # Charges Matcher Module
2
+
3
+ This module implements a transaction-document matching system for the Accounter fullstack
4
+ application. It provides both manual matching suggestions and automatic matching capabilities for
5
+ unmatched charges.
6
+
7
+ ## Overview
8
+
9
+ The charges-matcher module uses a confidence-based scoring algorithm to identify potential matches
10
+ between:
11
+
12
+ - Charges with only transactions (transaction charges)
13
+ - Charges with only accounting documents (document charges)
14
+
15
+ The system provides two main operations:
16
+
17
+ 1. **Single-Match Query**: Find potential matches for a specific unmatched charge
18
+ 2. **Auto-Match Mutation**: Automatically match all unmatched charges above a confidence threshold
19
+
20
+ ## Module Structure
21
+
22
+ ```
23
+ charges-matcher/
24
+ ├── index.ts # Module exports and GraphQL module definition
25
+ ├── types.ts # TypeScript type definitions
26
+ ├── typeDefs/
27
+ │ └── charges-matcher.graphql.ts # GraphQL schema definitions
28
+ ├── resolvers/ # GraphQL resolvers (to be implemented)
29
+ ├── providers/ # Business logic providers (to be implemented)
30
+ ├── helpers/ # Helper functions (to be implemented)
31
+ └── __tests__/
32
+ ├── test-helpers.ts # Test utilities and mock factories
33
+ └── test-infrastructure.spec.ts # Infrastructure tests
34
+ ```
35
+
36
+ ## Type Definitions
37
+
38
+ ### Shared Types
39
+
40
+ The module reuses types from existing modules:
41
+
42
+ - `Transaction`: From `@modules/transactions/types.js` (`IGetTransactionsByIdsResult`)
43
+ - `Document`: From `@modules/documents/types.js` (`IGetAllDocumentsResult`)
44
+ - `Currency`: Re-exported from documents module
45
+ - `DocumentType`: Re-exported from documents module
46
+
47
+ ### GraphQL Response Types
48
+
49
+ - `ChargeMatch`: Single match with charge ID and confidence score
50
+ - `ChargeMatchesResult`: Array of matches (up to 5)
51
+ - `MergedCharge`: Record of a merged charge with confidence score
52
+ - `AutoMatchChargesResult`: Summary of auto-match operation
53
+
54
+ ### Internal Types
55
+
56
+ - `AggregatedTransaction`: Aggregated data from multiple transactions
57
+ - `AggregatedDocument`: Aggregated data from multiple documents
58
+ - `ConfidenceScores`: Individual confidence scores for each matching factor
59
+ - `ConfidenceResult`: Complete confidence calculation result
60
+ - `ChargeType`: Enum for charge classification (TRANSACTION_ONLY, DOCUMENT_ONLY, MATCHED)
61
+ - `ChargeWithData`: Charge with its associated transactions and documents
62
+ - `MatchCandidate`: Candidate charge with aggregated data
63
+
64
+ ## Database Schema
65
+
66
+ The module works with these existing tables:
67
+
68
+ ### charges
69
+
70
+ - `id`: UUID (primary key)
71
+ - `owner_id`: UUID (references businesses)
72
+ - Other fields not directly used in matching logic
73
+
74
+ ### transactions
75
+
76
+ - `id`: UUID
77
+ - `charge_id`: UUID (foreign key to charges)
78
+ - `amount`: numeric (PostgreSQL numeric type)
79
+ - `currency`: enum
80
+ - `business_id`: UUID (nullable)
81
+ - `event_date`: DATE
82
+ - `debit_date`: DATE (nullable)
83
+ - `debit_timestamp`: TIMESTAMP (nullable)
84
+ - `is_fee`: boolean
85
+ - `source_description`: text (nullable)
86
+
87
+ ### documents
88
+
89
+ - `id`: UUID
90
+ - `charge_id`: UUID (foreign key to charges)
91
+ - `total_amount`: double precision (nullable)
92
+ - `currency_code`: enum (nullable)
93
+ - `creditor_id`: UUID (nullable)
94
+ - `debtor_id`: UUID (nullable)
95
+ - `date`: DATE (nullable)
96
+ - `type`: enum (INVOICE, CREDIT_INVOICE, RECEIPT, etc.)
97
+ - `serial_number`: text (nullable)
98
+
99
+ ## Testing Infrastructure
100
+
101
+ ### Mock Factories
102
+
103
+ Test helpers provide factory functions for creating mock data:
104
+
105
+ ```typescript
106
+ import {
107
+ createMockTransaction,
108
+ createMockDocument,
109
+ createMockAggregatedTransaction,
110
+ createMockAggregatedDocument,
111
+ } from './__tests__/test-helpers.js';
112
+
113
+ // Create a transaction with defaults
114
+ const transaction = createMockTransaction();
115
+
116
+ // Create with overrides
117
+ const customTransaction = createMockTransaction({
118
+ amount: '250.00',
119
+ currency: 'USD',
120
+ });
121
+ ```
122
+
123
+ ### Helper Functions
124
+
125
+ ```typescript
126
+ import {
127
+ calculateExpectedConfidence,
128
+ roundConfidence,
129
+ isValidConfidenceScore,
130
+ daysDifference,
131
+ isWithinDays,
132
+ } from './__tests__/test-helpers.js';
133
+
134
+ // Calculate weighted confidence
135
+ const scores = { amount: 0.9, currency: 1.0, business: 0.5, date: 0.8 };
136
+ const confidence = calculateExpectedConfidence(scores); // 0.79
137
+
138
+ // Round to 2 decimal places
139
+ const rounded = roundConfidence(0.956789); // 0.96
140
+
141
+ // Validate score range
142
+ isValidConfidenceScore(0.95); // true
143
+ isValidConfidenceScore(1.5); // false
144
+ ```
145
+
146
+ ## Key Concepts
147
+
148
+ ### Unmatched Charge
149
+
150
+ A charge is considered unmatched if it has:
151
+
152
+ - ≥1 transactions AND 0 accounting documents, OR
153
+ - 0 transactions AND ≥1 accounting documents
154
+
155
+ **Note**: PROFORMA, OTHER, and UNPROCESSED document types don't count toward matched/unmatched
156
+ status.
157
+
158
+ ### Matched Charge
159
+
160
+ A charge is considered matched if it has:
161
+
162
+ - ≥1 transactions AND ≥1 accounting documents
163
+
164
+ ### Accounting Documents
165
+
166
+ Documents with types: INVOICE, CREDIT_INVOICE, RECEIPT, INVOICE_RECEIPT
167
+
168
+ ### Confidence Score
169
+
170
+ A value between 0.00 and 1.00 calculated using:
171
+
172
+ ```
173
+ confidence = (amount × 0.4) + (currency × 0.2) + (business × 0.3) + (date × 0.1)
174
+ ```
175
+
176
+ Where each component score is between 0.0 and 1.0.
177
+
178
+ ### Auto-Match Threshold
179
+
180
+ Charges are automatically matched only when:
181
+
182
+ - Exactly one match has confidence ≥ 0.95
183
+ - Multiple matches ≥ 0.95 result in the charge being skipped
184
+
185
+ ## GraphQL API
186
+
187
+ ### Query: findChargeMatches
188
+
189
+ Find potential matches for a single unmatched charge.
190
+
191
+ ```graphql
192
+ query findChargeMatches($chargeId: UUID!) {
193
+ findChargeMatches(chargeId: $chargeId) {
194
+ matches {
195
+ chargeId
196
+ confidenceScore
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ **Returns**: Up to 5 matches, ordered by confidence score (highest first)
203
+
204
+ ### Mutation: autoMatchCharges
205
+
206
+ Automatically match all unmatched charges above the confidence threshold.
207
+
208
+ ```graphql
209
+ mutation autoMatchCharges {
210
+ autoMatchCharges {
211
+ totalMatches
212
+ mergedCharges {
213
+ chargeId
214
+ confidenceScore
215
+ }
216
+ skippedCharges
217
+ errors
218
+ }
219
+ }
220
+ ```
221
+
222
+ **Returns**: Summary of matches made, skipped charges, and any errors
223
+
224
+ ## Implementation Roadmap
225
+
226
+ ### Step 1: Module Setup (✓ Complete)
227
+
228
+ - [x] Create module structure
229
+ - [x] Define TypeScript types
230
+ - [x] Create GraphQL schema
231
+ - [x] Set up test infrastructure
232
+
233
+ ### Step 2: Core Logic (To Do)
234
+
235
+ - [ ] Implement charge classification
236
+ - [ ] Implement multi-item aggregation
237
+ - [ ] Implement confidence calculation helpers
238
+ - [ ] Implement candidate filtering
239
+
240
+ ### Step 3: GraphQL Integration (To Do)
241
+
242
+ - [ ] Implement findChargeMatches resolver
243
+ - [ ] Implement autoMatchCharges resolver
244
+ - [ ] Implement providers for data access
245
+ - [ ] Add integration tests
246
+
247
+ ### Step 4: UI Integration (To Do)
248
+
249
+ - [ ] Create ChargeMatchingModal component
250
+ - [ ] Create AutoMatchButton component
251
+ - [ ] Add to charge detail screens
252
+ - [ ] Add to charges list view
253
+
254
+ ## Dependencies
255
+
256
+ This module depends on:
257
+
258
+ - `@modules/charges`: For charge data access and merge operations
259
+ - `@modules/transactions`: For transaction data access
260
+ - `@modules/documents`: For document data access
261
+ - `@modules/common`: For error handling and common utilities
262
+ - `@modules/financial-entities`: For business name resolution
263
+
264
+ ## Running Tests
265
+
266
+ ```bash
267
+ # Run all tests
268
+ yarn test
269
+
270
+ # Run specific test file
271
+ yarn test packages/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts
272
+
273
+ # Run tests in watch mode
274
+ yarn test --watch
275
+ ```
276
+
277
+ ## License
278
+
279
+ See the main project LICENSE file.
@@ -0,0 +1,71 @@
1
+ import * as Types from "../../../__generated__/types.js";
2
+ import * as gm from "graphql-modules";
3
+ export namespace ChargesMatcherModule {
4
+ interface DefinedFields {
5
+ Query: 'findChargeMatches';
6
+ Mutation: 'autoMatchCharges';
7
+ ChargeMatchesResult: 'matches';
8
+ ChargeMatch: 'chargeId' | 'confidenceScore';
9
+ AutoMatchChargesResult: 'totalMatches' | 'mergedCharges' | 'skippedCharges' | 'errors';
10
+ MergedCharge: 'chargeId' | 'confidenceScore';
11
+ };
12
+
13
+ export type Query = Pick<Types.Query, DefinedFields['Query']>;
14
+ export type ChargeMatchesResult = Pick<Types.ChargeMatchesResult, DefinedFields['ChargeMatchesResult']>;
15
+ export type UUID = Types.Uuid;
16
+ export type Mutation = Pick<Types.Mutation, DefinedFields['Mutation']>;
17
+ export type AutoMatchChargesResult = Pick<Types.AutoMatchChargesResult, DefinedFields['AutoMatchChargesResult']>;
18
+ export type ChargeMatch = Pick<Types.ChargeMatch, DefinedFields['ChargeMatch']>;
19
+ export type MergedCharge = Pick<Types.MergedCharge, DefinedFields['MergedCharge']>;
20
+
21
+ export type QueryResolvers = Pick<Types.QueryResolvers, DefinedFields['Query']>;
22
+ export type MutationResolvers = Pick<Types.MutationResolvers, DefinedFields['Mutation']>;
23
+ export type ChargeMatchesResultResolvers = Pick<Types.ChargeMatchesResultResolvers, DefinedFields['ChargeMatchesResult']>;
24
+ export type ChargeMatchResolvers = Pick<Types.ChargeMatchResolvers, DefinedFields['ChargeMatch']>;
25
+ export type AutoMatchChargesResultResolvers = Pick<Types.AutoMatchChargesResultResolvers, DefinedFields['AutoMatchChargesResult']>;
26
+ export type MergedChargeResolvers = Pick<Types.MergedChargeResolvers, DefinedFields['MergedCharge']>;
27
+
28
+ export interface Resolvers {
29
+ Query?: QueryResolvers;
30
+ Mutation?: MutationResolvers;
31
+ ChargeMatchesResult?: ChargeMatchesResultResolvers;
32
+ ChargeMatch?: ChargeMatchResolvers;
33
+ AutoMatchChargesResult?: AutoMatchChargesResultResolvers;
34
+ MergedCharge?: MergedChargeResolvers;
35
+ };
36
+
37
+ export interface MiddlewareMap {
38
+ '*'?: {
39
+ '*'?: gm.Middleware[];
40
+ };
41
+ Query?: {
42
+ '*'?: gm.Middleware[];
43
+ findChargeMatches?: gm.Middleware[];
44
+ };
45
+ Mutation?: {
46
+ '*'?: gm.Middleware[];
47
+ autoMatchCharges?: gm.Middleware[];
48
+ };
49
+ ChargeMatchesResult?: {
50
+ '*'?: gm.Middleware[];
51
+ matches?: gm.Middleware[];
52
+ };
53
+ ChargeMatch?: {
54
+ '*'?: gm.Middleware[];
55
+ chargeId?: gm.Middleware[];
56
+ confidenceScore?: gm.Middleware[];
57
+ };
58
+ AutoMatchChargesResult?: {
59
+ '*'?: gm.Middleware[];
60
+ totalMatches?: gm.Middleware[];
61
+ mergedCharges?: gm.Middleware[];
62
+ skippedCharges?: gm.Middleware[];
63
+ errors?: gm.Middleware[];
64
+ };
65
+ MergedCharge?: {
66
+ '*'?: gm.Middleware[];
67
+ chargeId?: gm.Middleware[];
68
+ confidenceScore?: gm.Middleware[];
69
+ };
70
+ };
71
+ }
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { calculateAmountConfidence } from '../helpers/amount-confidence.helper.js';
3
+
4
+ describe('calculateAmountConfidence', () => {
5
+ describe('exact matches', () => {
6
+ it('should return 1.0 for exact match with positive amounts', () => {
7
+ expect(calculateAmountConfidence(100, 100)).toBe(1.0);
8
+ expect(calculateAmountConfidence(50.5, 50.5)).toBe(1.0);
9
+ expect(calculateAmountConfidence(0.01, 0.01)).toBe(1.0);
10
+ });
11
+
12
+ it('should return 1.0 for exact match with negative amounts', () => {
13
+ expect(calculateAmountConfidence(-100, -100)).toBe(1.0);
14
+ expect(calculateAmountConfidence(-50.5, -50.5)).toBe(1.0);
15
+ });
16
+
17
+ it('should return 1.0 for zero amounts', () => {
18
+ expect(calculateAmountConfidence(0, 0)).toBe(1.0);
19
+ });
20
+
21
+ it('should return 1.0 for matching absolute values with different signs', () => {
22
+ // Since we compare absolute values, sign differences shouldn't matter for exact amounts
23
+ expect(calculateAmountConfidence(100, 100)).toBe(1.0);
24
+ expect(calculateAmountConfidence(-100, -100)).toBe(1.0);
25
+ });
26
+ });
27
+
28
+ describe('amounts within 1 unit', () => {
29
+ it('should return 0.9 for amounts within 0.5 units', () => {
30
+ expect(calculateAmountConfidence(100, 100.5)).toBe(0.9);
31
+ expect(calculateAmountConfidence(100.5, 100)).toBe(0.9);
32
+ expect(calculateAmountConfidence(50, 50.3)).toBe(0.9);
33
+ });
34
+
35
+ it('should return 0.9 for amounts at exactly 1 unit difference', () => {
36
+ expect(calculateAmountConfidence(100, 101)).toBe(0.9);
37
+ expect(calculateAmountConfidence(101, 100)).toBe(0.9);
38
+ expect(calculateAmountConfidence(50, 51)).toBe(0.9);
39
+ });
40
+
41
+ it('should return 0.9 for amounts just under 1 unit difference', () => {
42
+ expect(calculateAmountConfidence(100, 100.99)).toBe(0.9);
43
+ expect(calculateAmountConfidence(100.99, 100)).toBe(0.9);
44
+ });
45
+
46
+ it('should return 0.9 for negative amounts within 1 unit', () => {
47
+ expect(calculateAmountConfidence(-100, -100.5)).toBe(0.9);
48
+ expect(calculateAmountConfidence(-100, -101)).toBe(0.9);
49
+ });
50
+ });
51
+
52
+ describe('amounts with differences between 1 unit and 20%', () => {
53
+ it('should return value between 0.0 and 0.7 for 1.5 units difference', () => {
54
+ const confidence = calculateAmountConfidence(100, 101.5);
55
+ expect(confidence).toBeGreaterThan(0.0);
56
+ expect(confidence).toBeLessThan(0.7);
57
+ });
58
+
59
+ it('should return value between 0.0 and 0.7 for 2 units difference', () => {
60
+ const confidence = calculateAmountConfidence(100, 102);
61
+ expect(confidence).toBeGreaterThan(0.0);
62
+ expect(confidence).toBeLessThan(0.7);
63
+ });
64
+
65
+ it('should return value between 0.0 and 0.7 for 5 units difference (on 100)', () => {
66
+ const confidence = calculateAmountConfidence(100, 105);
67
+ expect(confidence).toBeGreaterThan(0.0);
68
+ expect(confidence).toBeLessThan(0.7);
69
+ // 5% difference should be roughly in the middle
70
+ expect(confidence).toBeGreaterThan(0.3);
71
+ });
72
+
73
+ it('should return value between 0.0 and 0.7 for 10 units difference (on 100)', () => {
74
+ const confidence = calculateAmountConfidence(100, 110);
75
+ expect(confidence).toBeGreaterThan(0.0);
76
+ expect(confidence).toBeLessThan(0.7);
77
+ // 10% difference should be lower
78
+ expect(confidence).toBeLessThan(0.4);
79
+ });
80
+
81
+ it('should demonstrate linear degradation', () => {
82
+ // For amounts where 1 unit = 1%, we can test linear degradation more precisely
83
+ // Starting from just over 1% to just under 20%
84
+
85
+ // At around 1% (just over 1 unit on 100): should be close to 0.7
86
+ const conf1 = calculateAmountConfidence(100, 101.5); // 1.5%
87
+
88
+ // At around 10% (middle of range): should be around 0.35
89
+ const conf10 = calculateAmountConfidence(100, 110); // 10%
90
+
91
+ // At around 19% (near end): should be close to 0.0
92
+ const conf19 = calculateAmountConfidence(100, 119); // 19%
93
+
94
+ // Verify degradation order
95
+ expect(conf1).toBeGreaterThan(conf10);
96
+ expect(conf10).toBeGreaterThan(conf19);
97
+ });
98
+ });
99
+
100
+ describe('amounts at exactly 20% difference', () => {
101
+ it('should return 0.0 for exactly 20% difference', () => {
102
+ expect(calculateAmountConfidence(100, 120)).toBe(0.0);
103
+ expect(calculateAmountConfidence(120, 100)).toBe(0.0);
104
+ expect(calculateAmountConfidence(50, 60)).toBe(0.0);
105
+ });
106
+
107
+ it('should return 0.0 for just under 20% difference (19.99%)', () => {
108
+ // Just under 20% should still be very close to 0
109
+ const confidence = calculateAmountConfidence(100, 119.9);
110
+ expect(confidence).toBeLessThanOrEqual(0.01);
111
+ });
112
+ });
113
+
114
+ describe('amounts beyond 20% difference', () => {
115
+ it('should return 0.0 for 25% difference', () => {
116
+ expect(calculateAmountConfidence(100, 125)).toBe(0.0);
117
+ });
118
+
119
+ it('should return 0.0 for 50% difference', () => {
120
+ expect(calculateAmountConfidence(100, 150)).toBe(0.0);
121
+ });
122
+
123
+ it('should return 0.0 for 100% difference', () => {
124
+ expect(calculateAmountConfidence(100, 200)).toBe(0.0);
125
+ });
126
+
127
+ it('should return 0.0 for very large differences', () => {
128
+ expect(calculateAmountConfidence(100, 1000)).toBe(0.0);
129
+ expect(calculateAmountConfidence(10, 500)).toBe(0.0);
130
+ });
131
+ });
132
+
133
+ describe('edge cases with negative amounts', () => {
134
+ it('should handle negative transaction amount', () => {
135
+ expect(calculateAmountConfidence(-100, -100)).toBe(1.0);
136
+ expect(calculateAmountConfidence(-100, -101)).toBe(0.9);
137
+ expect(calculateAmountConfidence(-100, -105)).toBeGreaterThan(0.0);
138
+ expect(calculateAmountConfidence(-100, -120)).toBe(0.0);
139
+ });
140
+
141
+ it('should handle negative document amount', () => {
142
+ expect(calculateAmountConfidence(-100, -100)).toBe(1.0);
143
+ expect(calculateAmountConfidence(-101, -100)).toBe(0.9);
144
+ });
145
+
146
+ it('should handle both amounts negative', () => {
147
+ expect(calculateAmountConfidence(-50, -50.5)).toBe(0.9);
148
+ expect(calculateAmountConfidence(-100, -110)).toBeGreaterThan(0.0);
149
+ });
150
+ });
151
+
152
+ describe('edge cases with very small amounts', () => {
153
+ it('should handle very small amounts correctly', () => {
154
+ expect(calculateAmountConfidence(0.1, 0.1)).toBe(1.0);
155
+ expect(calculateAmountConfidence(0.01, 0.01)).toBe(1.0);
156
+ });
157
+
158
+ it('should return 0.9 for small amounts within 1 unit', () => {
159
+ // Even though 1 unit is huge compared to 0.1, it should still return 0.9
160
+ expect(calculateAmountConfidence(0.1, 1.0)).toBe(0.9);
161
+ expect(calculateAmountConfidence(0.5, 1.5)).toBe(0.9);
162
+ });
163
+
164
+ it('should handle small amounts with percentage differences correctly', () => {
165
+ // 0.1 to 0.12 has a difference of 0.02, which is within 1 unit, so returns 0.9
166
+ expect(calculateAmountConfidence(0.1, 0.12)).toBe(0.9);
167
+ // 0.1 to 0.11 has a difference of 0.01, which is within 1 unit, so returns 0.9
168
+ expect(calculateAmountConfidence(0.1, 0.11)).toBe(0.9);
169
+ // 0.1 to 1.2 has >1 unit difference (1.1) and is also >20%, so returns 0.0
170
+ expect(calculateAmountConfidence(0.1, 1.2)).toBe(0.0);
171
+ });
172
+ });
173
+
174
+ describe('edge cases with zero', () => {
175
+ it('should handle zero in transaction amount', () => {
176
+ expect(calculateAmountConfidence(0, 0)).toBe(1.0);
177
+ });
178
+
179
+ it('should handle zero vs non-zero amounts', () => {
180
+ // 0 to 1 is within 1 unit
181
+ expect(calculateAmountConfidence(0, 1)).toBe(0.9);
182
+ expect(calculateAmountConfidence(1, 0)).toBe(0.9);
183
+
184
+ // 0 to >1 means one amount is 0, which makes percentage calculation undefined
185
+ // The spec requires us to handle this gracefully - since we can't calculate percentage
186
+ // and the difference is >1, we return 0.0
187
+ expect(calculateAmountConfidence(0, 2)).toBe(0.0);
188
+ });
189
+ });
190
+
191
+ describe('formula verification for linear degradation', () => {
192
+ it('should verify the linear formula in the middle range', () => {
193
+ // Using base amount of 100 for easier percentage calculation
194
+ // 1 unit = 1% on 100
195
+ // Range is from 1% to 20%
196
+ // Confidence degrades linearly from 0.7 to 0.0
197
+
198
+ // At 1% (101): should be close to 0.7 (but we're just over 1 unit)
199
+ const conf1 = calculateAmountConfidence(100, 101.01);
200
+ expect(conf1).toBeCloseTo(0.7, 1);
201
+
202
+ // At 10.5% (halfway between 1% and 20%): should be around 0.35
203
+ const conf10_5 = calculateAmountConfidence(100, 110.5);
204
+ expect(conf10_5).toBeCloseTo(0.35, 1);
205
+
206
+ // At 19.9% (just before 20%): should be close to 0.0
207
+ const conf19_9 = calculateAmountConfidence(100, 119.9);
208
+ expect(conf19_9).toBeCloseTo(0.0, 1);
209
+ });
210
+
211
+ it('should verify degradation is proportional across the range', () => {
212
+ // Calculate several points and verify linear relationship
213
+ const base = 100;
214
+
215
+ // Points at different percentages in the degradation range
216
+ const conf_2pct = calculateAmountConfidence(base, 102); // ~2%
217
+ const conf_5pct = calculateAmountConfidence(base, 105); // 5%
218
+ const conf_10pct = calculateAmountConfidence(base, 110); // 10%
219
+ const conf_15pct = calculateAmountConfidence(base, 115); // 15%
220
+
221
+ // Each should be progressively smaller
222
+ expect(conf_2pct).toBeGreaterThan(conf_5pct);
223
+ expect(conf_5pct).toBeGreaterThan(conf_10pct);
224
+ expect(conf_10pct).toBeGreaterThan(conf_15pct);
225
+ });
226
+ });
227
+
228
+ describe('return value precision', () => {
229
+ it('should return values rounded to 2 decimal places', () => {
230
+ const confidence = calculateAmountConfidence(100, 105);
231
+ // Check that the value has at most 2 decimal places
232
+ const decimalPlaces = (confidence.toString().split('.')[1] || '').length;
233
+ expect(decimalPlaces).toBeLessThanOrEqual(2);
234
+ });
235
+
236
+ it('should handle rounding correctly for edge values', () => {
237
+ // Test various amounts that might produce values needing rounding
238
+ const amounts = [
239
+ [100, 102.5],
240
+ [100, 107.3],
241
+ [100, 113.7],
242
+ [50, 53.3],
243
+ ];
244
+
245
+ amounts.forEach(([amt1, amt2]) => {
246
+ const confidence = calculateAmountConfidence(amt1, amt2);
247
+ const decimalPlaces = (confidence.toString().split('.')[1] || '').length;
248
+ expect(decimalPlaces).toBeLessThanOrEqual(2);
249
+ });
250
+ });
251
+ });
252
+
253
+ describe('symmetry', () => {
254
+ it('should return same confidence regardless of parameter order', () => {
255
+ expect(calculateAmountConfidence(100, 105)).toBe(calculateAmountConfidence(105, 100));
256
+ expect(calculateAmountConfidence(50, 60)).toBe(calculateAmountConfidence(60, 50));
257
+ expect(calculateAmountConfidence(100, 101)).toBe(calculateAmountConfidence(101, 100));
258
+ });
259
+ });
260
+ });