@accounter/server 0.0.9-alpha-20251217093036-7168648b507d62946aa287af4ea690b73b077b2d → 0.0.9-alpha-20251217131153-65f961a4072436d7f1042ea8ea4d96534cb3650e

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 (69) hide show
  1. package/CHANGELOG.md +16 -8
  2. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +45 -122
  3. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -1
  4. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +45 -29
  5. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -1
  6. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +2 -11
  7. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -1
  8. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +25 -0
  9. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -1
  10. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -1
  11. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +65 -64
  12. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -1
  13. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +494 -60
  14. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -1
  15. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +34 -98
  16. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
  17. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +79 -59
  18. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -1
  19. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +6 -4
  20. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -1
  21. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +9 -2
  22. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +24 -2
  23. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -1
  24. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +1 -4
  25. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +2 -1
  26. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -1
  27. package/dist/server/src/modules/charges-matcher/index.d.ts +0 -1
  28. package/dist/server/src/modules/charges-matcher/index.js +0 -1
  29. package/dist/server/src/modules/charges-matcher/index.js.map +1 -1
  30. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +2 -1
  31. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +2 -2
  32. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -1
  33. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +2 -2
  34. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -1
  35. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +4 -5
  36. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +5 -4
  37. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -1
  38. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +5 -3
  39. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +70 -13
  40. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -1
  41. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +4 -2
  42. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +15 -7
  43. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -1
  44. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +1 -1
  45. package/dist/server/src/modules/charges-matcher/types.d.ts +2 -4
  46. package/dist/server/src/modules/charges-matcher/types.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/modules/charges-matcher/README.md +14 -3
  49. package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +52 -100
  50. package/src/modules/charges-matcher/__tests__/auto-match.test.ts +51 -29
  51. package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +2 -13
  52. package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +29 -0
  53. package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +0 -1
  54. package/src/modules/charges-matcher/__tests__/document-amount.test.ts +66 -65
  55. package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +552 -60
  56. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +43 -73
  57. package/src/modules/charges-matcher/__tests__/single-match.test.ts +81 -59
  58. package/src/modules/charges-matcher/documentation/SPEC.md +276 -4
  59. package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +7 -5
  60. package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +32 -2
  61. package/src/modules/charges-matcher/helpers/document-amount.helper.ts +2 -12
  62. package/src/modules/charges-matcher/index.ts +0 -1
  63. package/src/modules/charges-matcher/providers/auto-match.provider.ts +5 -3
  64. package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +8 -2
  65. package/src/modules/charges-matcher/providers/document-aggregator.ts +12 -11
  66. package/src/modules/charges-matcher/providers/match-scorer.provider.ts +97 -17
  67. package/src/modules/charges-matcher/providers/single-match.provider.ts +21 -8
  68. package/src/modules/charges-matcher/providers/transaction-aggregator.ts +1 -1
  69. package/src/modules/charges-matcher/types.ts +2 -5
@@ -1,4 +1,8 @@
1
- import type { document_type } from '../../documents/types.js';
1
+ import type { Injector } from 'graphql-modules';
2
+ import { DocumentType } from '../../../shared/enums.js';
3
+ import type { IGetIssuedDocumentsStatusByChargeIdsResult } from '../../documents/__generated__/issued-documents.types.js';
4
+ import { IssuedDocumentsProvider } from '../../documents/providers/issued-documents.provider.js';
5
+ import { ClientsProvider } from '../../financial-entities/providers/clients.provider.js';
2
6
  import { calculateAmountConfidence } from '../helpers/amount-confidence.helper.js';
3
7
  import { calculateBusinessConfidence } from '../helpers/business-confidence.helper.js';
4
8
  import { calculateCurrencyConfidence } from '../helpers/currency-confidence.helper.js';
@@ -28,19 +32,19 @@ import { aggregateTransactions } from './transaction-aggregator.js';
28
32
  */
29
33
  export function selectTransactionDate(
30
34
  transaction: AggregatedTransaction,
31
- documentType: document_type,
35
+ documentType: DocumentType,
32
36
  ): Date {
33
37
  switch (documentType) {
34
- case 'INVOICE':
35
- case 'CREDIT_INVOICE':
36
- case 'RECEIPT':
37
- case 'INVOICE_RECEIPT':
38
+ case DocumentType.Invoice:
39
+ case DocumentType.CreditInvoice:
40
+ case DocumentType.Receipt:
41
+ case DocumentType.InvoiceReceipt:
38
42
  // For invoices, use event_date
39
43
  return transaction.date;
40
44
 
41
- case 'OTHER':
42
- case 'PROFORMA':
43
- case 'UNPROCESSED':
45
+ case DocumentType.Other:
46
+ case DocumentType.Proforma:
47
+ case DocumentType.Unprocessed:
44
48
  // For flexible types, use event_date as default
45
49
  // (caller should calculate both and use better score)
46
50
  return transaction.date;
@@ -58,14 +62,16 @@ export function selectTransactionDate(
58
62
  * @param txCharge - Transaction charge to match
59
63
  * @param docCharge - Document charge candidate
60
64
  * @param userId - Current user UUID for business extraction
65
+ * @param injector - Optional GraphQL modules injector for provider access (for client matching)
61
66
  * @returns Match score with confidence and component breakdown
62
67
  * @throws Error if aggregation fails (mixed currencies, multiple businesses, etc.)
63
68
  */
64
- export function scoreMatch(
69
+ export async function scoreMatch(
65
70
  txCharge: TransactionCharge,
66
71
  docCharge: DocumentCharge,
67
72
  userId: string,
68
- ): MatchScore {
73
+ injector: Injector,
74
+ ): Promise<MatchScore> {
69
75
  // Aggregate transaction data
70
76
  const aggregatedTransaction = aggregateTransactions(txCharge.transactions);
71
77
 
@@ -79,20 +85,22 @@ export function scoreMatch(
79
85
  // aggregatedDocument.type === 'UNPROCESSED'
80
86
  // ) {
81
87
  // // Calculate score with event_date
82
- // const scoreWithEventDate = calculateScoreWithDate(
88
+ // const scoreWithEventDate = await calculateScoreWithDate(
83
89
  // aggregatedTransaction,
84
90
  // aggregatedDocument,
85
91
  // aggregatedTransaction.date,
86
92
  // docCharge.chargeId,
93
+ // injector,
87
94
  // );
88
95
 
89
96
  // // Calculate score with debit_date (if available)
90
97
  // if (aggregatedTransaction.debitDate) {
91
- // const scoreWithDebitDate = calculateScoreWithDate(
98
+ // const scoreWithDebitDate = await calculateScoreWithDate(
92
99
  // aggregatedTransaction,
93
100
  // aggregatedDocument,
94
101
  // aggregatedTransaction.debitDate,
95
102
  // docCharge.chargeId,
103
+ // injector,
96
104
  // );
97
105
 
98
106
  // // Return the better score
@@ -105,13 +113,17 @@ export function scoreMatch(
105
113
  // }
106
114
 
107
115
  // For specific document types, use the appropriate date
108
- const transactionDate = selectTransactionDate(aggregatedTransaction, aggregatedDocument.type);
116
+ const transactionDate = selectTransactionDate(
117
+ aggregatedTransaction,
118
+ aggregatedDocument.type as DocumentType,
119
+ );
109
120
 
110
121
  return calculateScoreWithDate(
111
122
  aggregatedTransaction,
112
123
  aggregatedDocument,
113
124
  transactionDate,
114
125
  docCharge.chargeId,
126
+ injector,
115
127
  );
116
128
  }
117
129
 
@@ -121,19 +133,84 @@ export function scoreMatch(
121
133
  * @param document - Aggregated document
122
134
  * @param transactionDate - Date to use from transaction
123
135
  * @param chargeId - Document charge ID
136
+ * @param injector - Optional GraphQL modules injector for provider access
124
137
  * @returns Match score
125
138
  */
126
- function calculateScoreWithDate(
139
+ async function calculateScoreWithDate(
127
140
  transaction: Omit<AggregatedTransaction, 'debitDate'>,
128
141
  document: Omit<AggregatedDocument, 'businessIsCreditor'>,
129
142
  transactionDate: Date,
130
143
  chargeId: string,
131
- ): MatchScore {
144
+ injector: Injector,
145
+ ): Promise<MatchScore> {
146
+ // Check if transaction and document share the same business entity
147
+ const businessesMatch =
148
+ transaction.businessId != null && transaction.businessId === document.businessId;
149
+
150
+ // If businesses match, verify if it's a registered CLIENT
151
+ let isClientMatch = false;
152
+ if (businessesMatch && transaction.businessId) {
153
+ try {
154
+ const client = await injector
155
+ .get(ClientsProvider)
156
+ .getClientByIdLoader.load(transaction.businessId);
157
+ // isClientMatch is true only if business is a registered client
158
+ isClientMatch = client != null;
159
+ } catch (error) {
160
+ console.error(`Error looking up client for business ID ${transaction.businessId}:`, error);
161
+ isClientMatch = false;
162
+ }
163
+ }
164
+
165
+ // Determine gentle eligibility for client matches
166
+ // Conditions:
167
+ // - Same business and registered client
168
+ // - Document type is INVOICE or PROFORMA
169
+ // - Document status is OPEN (via issued documents status by charge id)
170
+ // - Document date <= transaction date (date-only compare)
171
+ let isGentleEligible = false;
172
+ if (isClientMatch) {
173
+ // Type gating
174
+ const typeIsEligible =
175
+ document.type === DocumentType.Invoice || document.type === DocumentType.Proforma;
176
+
177
+ // Date-only comparison: doc.date <= transactionDate
178
+ const docDate = new Date(
179
+ document.date.getFullYear(),
180
+ document.date.getMonth(),
181
+ document.date.getDate(),
182
+ );
183
+ const txDate = new Date(
184
+ transactionDate.getFullYear(),
185
+ transactionDate.getMonth(),
186
+ transactionDate.getDate(),
187
+ );
188
+ const dateIsEligible = docDate.getTime() <= txDate.getTime();
189
+
190
+ // Status gating via DataLoader
191
+ let statusIsEligible = false;
192
+ try {
193
+ const status = (await injector
194
+ .get(IssuedDocumentsProvider)
195
+ .getIssuedDocumentsStatusByChargeIdLoader.load(
196
+ chargeId,
197
+ )) as IGetIssuedDocumentsStatusByChargeIdsResult | null;
198
+ // Expect shape with open_docs_flag boolean
199
+ statusIsEligible = !!(status && status.open_docs_flag === true);
200
+ } catch {
201
+ // If status lookup fails, treat as ineligible (do not throw)
202
+ statusIsEligible = false;
203
+ }
204
+
205
+ isGentleEligible = typeIsEligible && dateIsEligible && statusIsEligible;
206
+ }
207
+
132
208
  // Calculate individual confidence scores
133
209
  const amountScore = calculateAmountConfidence(transaction.amount, document.amount);
134
210
  const currencyScore = calculateCurrencyConfidence(transaction.currency, document.currency);
135
211
  const businessScore = calculateBusinessConfidence(transaction.businessId, document.businessId);
136
- const dateScore = calculateDateConfidence(transactionDate, document.date);
212
+ // Use gentle eligibility to compute date confidence
213
+ const dateScore = calculateDateConfidence(transactionDate, document.date, isGentleEligible);
137
214
 
138
215
  // Create components object
139
216
  const components: ConfidenceScores = {
@@ -150,5 +227,8 @@ function calculateScoreWithDate(
150
227
  chargeId,
151
228
  confidenceScore,
152
229
  components,
230
+ // Expose gentle flag for tie-breaker handling upstream
231
+ // (optional property tolerated by consumer types)
232
+ gentleMode: isGentleEligible,
153
233
  };
154
234
  }
@@ -5,6 +5,7 @@
5
5
  * This is a pure function implementation without database dependencies.
6
6
  */
7
7
 
8
+ import type { Injector } from 'graphql-modules';
8
9
  import { isWithinDateWindow } from '../helpers/candidate-filter.helper.js';
9
10
  import type { DocumentCharge, MatchScore, TransactionCharge } from '../types.js';
10
11
  import { aggregateDocuments } from './document-aggregator.js';
@@ -24,6 +25,7 @@ export interface MatchResult {
24
25
  date: number;
25
26
  };
26
27
  dateProximity?: number; // Days between earliest tx date and latest doc date (for tie-breaking)
28
+ gentleMode?: boolean; // Whether gentle client scoring applied
27
29
  }
28
30
 
29
31
  /**
@@ -119,16 +121,17 @@ function calculateDateProximity(txCharge: TransactionCharge, docCharge: Document
119
121
  * @param sourceCharge - The unmatched charge (transactions OR documents)
120
122
  * @param candidateCharges - All potential match candidates
121
123
  * @param userId - Current user ID
122
- * @param options - Optional configuration (maxMatches, dateWindowMonths)
124
+ * @param options - Optional configuration (maxMatches, dateWindowMonths, injector)
123
125
  * @returns Top matches sorted by confidence
124
126
  * @throws Error if source charge is matched or has validation issues
125
127
  */
126
- export function findMatches(
128
+ export async function findMatches(
127
129
  sourceCharge: TransactionCharge | DocumentCharge,
128
130
  candidateCharges: Array<TransactionCharge | DocumentCharge>,
129
131
  userId: string,
132
+ injector: Injector,
130
133
  options?: FindMatchesOptions,
131
- ): MatchResult[] {
134
+ ): Promise<MatchResult[]> {
132
135
  const maxMatches = options?.maxMatches ?? 5;
133
136
  const dateWindowMonths = options?.dateWindowMonths ?? 12;
134
137
 
@@ -208,11 +211,11 @@ export function findMatches(
208
211
  if (isSourceTransaction) {
209
212
  txCharge = sourceCharge;
210
213
  docCharge = candidate as DocumentCharge;
211
- matchScore = scoreMatch(txCharge, docCharge, userId);
214
+ matchScore = await scoreMatch(txCharge, docCharge, userId, injector);
212
215
  } else {
213
216
  txCharge = candidate as TransactionCharge;
214
217
  docCharge = sourceCharge;
215
- matchScore = scoreMatch(txCharge, docCharge, userId);
218
+ matchScore = await scoreMatch(txCharge, docCharge, userId, injector);
216
219
  }
217
220
 
218
221
  // Calculate date proximity for tie-breaking
@@ -223,6 +226,7 @@ export function findMatches(
223
226
  confidenceScore: matchScore.confidenceScore,
224
227
  components: matchScore.components,
225
228
  dateProximity,
229
+ gentleMode: matchScore.gentleMode === true,
226
230
  _txCharge: txCharge,
227
231
  _docCharge: docCharge,
228
232
  });
@@ -233,15 +237,24 @@ export function findMatches(
233
237
  }
234
238
  }
235
239
 
236
- // Step 9: Sort by confidence descending, then by date proximity ascending (tie-breaker)
240
+ // Step 9: Sort by confidence descending, then by date proximity tie-breaker
237
241
  scoredCandidates.sort((a, b) => {
238
242
  // Primary: confidence score (descending)
239
243
  if (a.confidenceScore !== b.confidenceScore) {
240
244
  return b.confidenceScore - a.confidenceScore;
241
245
  }
242
246
 
243
- // Tie-breaker: date proximity (ascending - closer dates win)
244
- return (a.dateProximity ?? Infinity) - (b.dateProximity ?? Infinity);
247
+ // Tie-breaker:
248
+ // - If both in gentle mode: prefer earlier document (larger proximity)
249
+ // - Otherwise: prefer closer dates (smaller proximity)
250
+ const aProx = a.dateProximity ?? Infinity;
251
+ const bProx = b.dateProximity ?? Infinity;
252
+
253
+ if (a.gentleMode && b.gentleMode) {
254
+ return bProx - aProx;
255
+ }
256
+
257
+ return aProx - bProx;
245
258
  });
246
259
 
247
260
  // Step 10: Return top N matches
@@ -6,7 +6,7 @@
6
6
  * validation, amount summation, date selection, and description concatenation.
7
7
  */
8
8
 
9
- import type { currency } from '../../documents/types.js';
9
+ import type { currency } from '../../transactions/types.js';
10
10
  import type { AggregatedTransaction } from '../types.js';
11
11
 
12
12
  /**
@@ -1,11 +1,6 @@
1
1
  import type { currency, document_type, IGetAllDocumentsResult } from '../documents/types.js';
2
2
  import type { IGetTransactionsByIdsResult } from '../transactions/types.js';
3
3
 
4
- /**
5
- * Re-export shared types from other modules
6
- */
7
- export type { currency as Currency, document_type as DocumentType } from '../documents/types.js';
8
-
9
4
  /**
10
5
  * Transaction interface matching the database schema
11
6
  * Uses the complete type from transactions module
@@ -174,6 +169,8 @@ export interface MatchScore {
174
169
  confidenceScore: number;
175
170
  /** Individual confidence component scores */
176
171
  components: ConfidenceScores;
172
+ /** Whether client gentle scoring was applied (for tie-breakers) */
173
+ gentleMode?: boolean;
177
174
  }
178
175
 
179
176
  /**