@accounter/server 0.0.8-alpha-20251102200443-d7162b8ce1dfc629b8b454df17dcec9ed005a052 → 0.0.8-alpha-20251102213150-c9d936f545d5351df0dc5326c2623266f1ad1f46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -7
- package/dist/green-invoice-graphql/src/mesh-artifacts/index.d.ts +1 -1
- package/dist/server/src/__generated__/types.d.ts +77 -0
- package/dist/server/src/__generated__/types.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__generated__/types.d.ts +68 -0
- package/dist/server/src/modules/charges-matcher/__generated__/types.js +7 -0
- package/dist/server/src/modules/charges-matcher/__generated__/types.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js +218 -0
- package/dist/server/src/modules/charges-matcher/__tests__/amount-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +645 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +530 -0
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js +143 -0
- package/dist/server/src/modules/charges-matcher/__tests__/business-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js +186 -0
- package/dist/server/src/modules/charges-matcher/__tests__/candidate-filter.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +301 -0
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js +127 -0
- package/dist/server/src/modules/charges-matcher/__tests__/currency-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +246 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js +475 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +287 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js +151 -0
- package/dist/server/src/modules/charges-matcher/__tests__/document-business.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +550 -0
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js +410 -0
- package/dist/server/src/modules/charges-matcher/__tests__/overall-confidence.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +504 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +483 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.d.ts +46 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js +143 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-helpers.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js +137 -0
- package/dist/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.d.ts +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js +415 -0
- package/dist/server/src/modules/charges-matcher/__tests__/transaction-aggregator.test.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js +70 -0
- package/dist/server/src/modules/charges-matcher/helpers/amount-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js +19 -0
- package/dist/server/src/modules/charges-matcher/helpers/business-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.d.ts +24 -0
- package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js +45 -0
- package/dist/server/src/modules/charges-matcher/helpers/candidate-filter.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.d.ts +33 -0
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +65 -0
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js +18 -0
- package/dist/server/src/modules/charges-matcher/helpers/currency-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +7 -0
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +35 -0
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +49 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +58 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.d.ts +13 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js +37 -0
- package/dist/server/src/modules/charges-matcher/helpers/document-business.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.d.ts +42 -0
- package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js +77 -0
- package/dist/server/src/modules/charges-matcher/helpers/overall-confidence.helper.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/index.d.ts +3 -0
- package/dist/server/src/modules/charges-matcher/index.js +15 -0
- package/dist/server/src/modules/charges-matcher/index.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +48 -0
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +133 -0
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.d.ts +38 -0
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +248 -0
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +61 -0
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +153 -0
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +25 -0
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +114 -0
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +39 -0
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +189 -0
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +54 -0
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js +93 -0
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js +22 -0
- package/dist/server/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js +24 -0
- package/dist/server/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/resolvers/index.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/resolvers/index.js +11 -0
- package/dist/server/src/modules/charges-matcher/resolvers/index.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.d.ts +2 -0
- package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js +47 -0
- package/dist/server/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/types.d.ts +179 -0
- package/dist/server/src/modules/charges-matcher/types.js +14 -0
- package/dist/server/src/modules/charges-matcher/types.js.map +1 -0
- package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js +2 -2
- package/dist/server/src/modules/documents/resolvers/document-suggestions.resolver.js.map +1 -1
- package/dist/server/src/modules-app.js +2 -0
- package/dist/server/src/modules-app.js.map +1 -1
- package/dist/server/src/shared/types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/__generated__/types.ts +87 -0
- package/src/modules/charges-matcher/README.md +279 -0
- package/src/modules/charges-matcher/__generated__/types.ts +71 -0
- package/src/modules/charges-matcher/__tests__/amount-confidence.test.ts +260 -0
- package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +714 -0
- package/src/modules/charges-matcher/__tests__/auto-match.test.ts +621 -0
- package/src/modules/charges-matcher/__tests__/business-confidence.test.ts +177 -0
- package/src/modules/charges-matcher/__tests__/candidate-filter.test.ts +238 -0
- package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +374 -0
- package/src/modules/charges-matcher/__tests__/currency-confidence.test.ts +164 -0
- package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +291 -0
- package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +614 -0
- package/src/modules/charges-matcher/__tests__/document-amount.test.ts +352 -0
- package/src/modules/charges-matcher/__tests__/document-business.test.ts +192 -0
- package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +659 -0
- package/src/modules/charges-matcher/__tests__/overall-confidence.test.ts +502 -0
- package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +556 -0
- package/src/modules/charges-matcher/__tests__/single-match.test.ts +608 -0
- package/src/modules/charges-matcher/__tests__/test-helpers.ts +174 -0
- package/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts +177 -0
- package/src/modules/charges-matcher/__tests__/transaction-aggregator.test.ts +547 -0
- package/src/modules/charges-matcher/documentation/README.md +331 -0
- package/src/modules/charges-matcher/documentation/SPEC.md +1503 -0
- package/src/modules/charges-matcher/documentation/TODO.md +799 -0
- package/src/modules/charges-matcher/helpers/amount-confidence.helper.ts +88 -0
- package/src/modules/charges-matcher/helpers/business-confidence.helper.ts +23 -0
- package/src/modules/charges-matcher/helpers/candidate-filter.helper.ts +56 -0
- package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +100 -0
- package/src/modules/charges-matcher/helpers/currency-confidence.helper.ts +22 -0
- package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +41 -0
- package/src/modules/charges-matcher/helpers/document-amount.helper.ts +77 -0
- package/src/modules/charges-matcher/helpers/document-business.helper.ts +54 -0
- package/src/modules/charges-matcher/helpers/overall-confidence.helper.ts +90 -0
- package/src/modules/charges-matcher/index.ts +17 -0
- package/src/modules/charges-matcher/providers/auto-match.provider.ts +176 -0
- package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +322 -0
- package/src/modules/charges-matcher/providers/document-aggregator.ts +211 -0
- package/src/modules/charges-matcher/providers/match-scorer.provider.ts +154 -0
- package/src/modules/charges-matcher/providers/single-match.provider.ts +252 -0
- package/src/modules/charges-matcher/providers/transaction-aggregator.ts +131 -0
- package/src/modules/charges-matcher/resolvers/auto-match-charges.resolver.ts +23 -0
- package/src/modules/charges-matcher/resolvers/find-charge-matches.resolver.ts +25 -0
- package/src/modules/charges-matcher/resolvers/index.ts +12 -0
- package/src/modules/charges-matcher/typeDefs/charges-matcher.graphql.ts +47 -0
- package/src/modules/charges-matcher/types.ts +200 -0
- package/src/modules/documents/resolvers/document-suggestions.resolver.ts +2 -2
- package/src/modules-app.ts +2 -0
- package/src/shared/types/index.ts +1 -1
|
@@ -0,0 +1,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
|
+
});
|