UNPKG

@accounter/server

Version:
291 lines (207 loc) 8.08 kB
# 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.