@accounter/server
Version:
Accounter GraphQL server
291 lines (207 loc) • 8.08 kB
Markdown
# Charges Matcher Module
This module implements a transaction-document matching system for the Accounter fullstack
application. It provides both manual matching suggestions and automatic matching capabilities for
unmatched charges.
## Overview
The charges-matcher module uses a confidence-based scoring algorithm to identify potential matches
between:
- Charges with only transactions (transaction charges)
- Charges with only accounting documents (document charges)
The system provides two main operations:
1. **Single-Match Query**: Find potential matches for a specific unmatched charge
2. **Auto-Match Mutation**: Automatically match all unmatched charges above a confidence threshold
## Module Structure
```
charges-matcher/
├── index.ts # Module exports and GraphQL module definition
├── types.ts # TypeScript type definitions
├── typeDefs/
│ └── charges-matcher.graphql.ts # GraphQL schema definitions
├── resolvers/ # GraphQL resolvers (to be implemented)
├── providers/ # Business logic providers (to be implemented)
├── helpers/ # Helper functions (to be implemented)
└── __tests__/
├── test-helpers.ts # Test utilities and mock factories
└── test-infrastructure.spec.ts # Infrastructure tests
```
## Type Definitions
### Shared Types
The module reuses types from existing modules:
- `Transaction`: From `@modules/transactions/types.js` (`IGetTransactionsByIdsResult`)
- `Document`: From `@modules/documents/types.js` (`IGetAllDocumentsResult`)
- `Currency`: Re-exported from documents module
- `DocumentType`: Re-exported from documents module
### GraphQL Response Types
- `ChargeMatch`: Single match with charge ID and confidence score
- `ChargeMatchesResult`: Array of matches (up to 5)
- `MergedCharge`: Record of a merged charge with confidence score
- `AutoMatchChargesResult`: Summary of auto-match operation
### Internal Types
- `AggregatedTransaction`: Aggregated data from multiple transactions
- `AggregatedDocument`: Aggregated data from multiple documents
- `ConfidenceScores`: Individual confidence scores for each matching factor
- `ConfidenceResult`: Complete confidence calculation result
- `ChargeType`: Enum for charge classification (TRANSACTION_ONLY, DOCUMENT_ONLY, MATCHED)
- `ChargeWithData`: Charge with its associated transactions and documents
- `MatchCandidate`: Candidate charge with aggregated data
## Database Schema
The module works with these existing tables:
### charges
- `id`: UUID (primary key)
- `owner_id`: UUID (references businesses)
- Other fields not directly used in matching logic
### transactions
- `id`: UUID
- `charge_id`: UUID (foreign key to charges)
- `amount`: numeric (PostgreSQL numeric type)
- `currency`: enum
- `business_id`: UUID (nullable)
- `event_date`: DATE
- `debit_date`: DATE (nullable)
- `debit_timestamp`: TIMESTAMP (nullable)
- `is_fee`: boolean
- `source_description`: text (nullable)
### documents
- `id`: UUID
- `charge_id`: UUID (foreign key to charges)
- `total_amount`: double precision (nullable)
- `currency_code`: enum (nullable)
- `creditor_id`: UUID (nullable)
- `debtor_id`: UUID (nullable)
- `date`: DATE (nullable)
- `type`: enum (INVOICE, CREDIT_INVOICE, RECEIPT, etc.)
- `serial_number`: text (nullable)
## Testing Infrastructure
### Mock Factories
Test helpers provide factory functions for creating mock data:
```typescript
import {
createMockTransaction,
createMockDocument,
createMockAggregatedTransaction,
createMockAggregatedDocument
} from './__tests__/test-helpers.js'
// Create a transaction with defaults
const transaction = createMockTransaction()
// Create with overrides
const customTransaction = createMockTransaction({
amount: '250.00',
currency: 'USD'
})
```
### Helper Functions
```typescript
import {
calculateExpectedConfidence,
roundConfidence,
isValidConfidenceScore,
daysDifference,
isWithinDays
} from './__tests__/test-helpers.js'
// Calculate weighted confidence
const scores = { amount: 0.9, currency: 1.0, business: 0.5, date: 0.8 }
const confidence = calculateExpectedConfidence(scores) // 0.79
// Round to 2 decimal places
const rounded = roundConfidence(0.956789) // 0.96
// Validate score range
isValidConfidenceScore(0.95) // true
isValidConfidenceScore(1.5) // false
```
## Key Concepts
### Unmatched Charge
A charge is considered unmatched if it has:
- ≥1 transactions AND 0 accounting documents, OR
- 0 transactions AND ≥1 accounting documents
**Note**: OTHER, and UNPROCESSED document types don't count toward matched/unmatched status.
### Matched Charge
A charge is considered matched if it has:
- ≥1 transactions AND ≥1 accounting documents
### Accounting Documents
Documents with types: INVOICE, CREDIT_INVOICE, RECEIPT, INVOICE_RECEIPT, PROFORMA
### Confidence Score
A value between 0.00 and 1.00 calculated using:
```
confidence = (amount × 0.4) + (currency × 0.2) + (business × 0.3) + (date × 0.1)
```
Where each component score is between 0.0 and 1.0.
**Date Component Enhancement (v3.0 - Gentle Scoring):**
The date confidence uses "gentle scoring" for eligible client invoices:
- **Gentle Eligible:** Client same-business matches with OPEN INVOICE/PROFORMA documents dated on or
before the transaction date (within 365 days) receive near-maximum confidence (~1.00) with a
microscopic preference for earlier invoices
- **Standard:** All other scenarios use linear degradation from 1.0 (same day) to 0.0 (30+ days)
- **Tie-Breaker:** When scores are equal under gentle mode, earlier invoices are preferred
This ensures recurring client payments match to the earliest eligible open invoice.
### Auto-Match Threshold
Charges are automatically matched only when:
- Exactly one match has confidence ≥ 0.95
- Multiple matches ≥ 0.95 result in the charge being skipped
## GraphQL API
### Query: findChargeMatches
Find potential matches for a single unmatched charge.
```graphql
query findChargeMatches($chargeId: UUID!) {
findChargeMatches(chargeId: $chargeId) {
matches {
chargeId
confidenceScore
}
}
}
```
**Returns**: Up to 5 matches, ordered by confidence score (highest first)
### Mutation: autoMatchCharges
Automatically match all unmatched charges above the confidence threshold.
```graphql
mutation autoMatchCharges {
autoMatchCharges {
totalMatches
mergedCharges {
chargeId
confidenceScore
}
skippedCharges
errors
}
}
```
**Returns**: Summary of matches made, skipped charges, and any errors
## Implementation Roadmap
### Step 1: Module Setup (✓ Complete)
- [x] Create module structure
- [x] Define TypeScript types
- [x] Create GraphQL schema
- [x] Set up test infrastructure
### Step 2: Core Logic (To Do)
- [ ] Implement charge classification
- [ ] Implement multi-item aggregation
- [ ] Implement confidence calculation helpers
- [ ] Implement candidate filtering
### Step 3: GraphQL Integration (To Do)
- [ ] Implement findChargeMatches resolver
- [ ] Implement autoMatchCharges resolver
- [ ] Implement providers for data access
- [ ] Add integration tests
### Step 4: UI Integration (To Do)
- [ ] Create ChargeMatchingModal component
- [ ] Create AutoMatchButton component
- [ ] Add to charge detail screens
- [ ] Add to charges list view
## Dependencies
This module depends on:
- `@modules/charges`: For charge data access and merge operations
- `@modules/transactions`: For transaction data access
- `@modules/documents`: For document data access
- `@modules/common`: For error handling and common utilities
- `@modules/financial-entities`: For business name resolution
## Running Tests
```bash
# Run all tests
yarn test
# Run specific test file
yarn test packages/server/src/modules/charges-matcher/__tests__/test-infrastructure.spec.ts
# Run tests in watch mode
yarn test --watch
```
## License
See the main project LICENSE file.