Phase 3: Bank reconciliation system

Added complete bank statement reconciliation workflow:

Database Schema:
- BankStatement: metadata for uploaded statements
- BankStatementLine: individual transactions from statements
- BankReconciliation: links system transactions to bank lines

Features:
- CSV import with flexible format detection (DD/MM/YYYY, YYYY-MM-DD dates)
- Auto-matching algorithm: exact amount+date = 100% confidence, fuzzy matching up to 7 days
- Manual matching UI for unmatched transactions
- Discrepancy detection (amount mismatches)
- Reconciliation dashboard with summary stats
- Statement history view with status tracking

Pages:
- /admin/accounts/reconciliation: Dashboard and statement list
- /admin/accounts/reconciliation?id=X: Statement detail with matching UI
- /admin/accounts/reconciliation/upload: CSV upload form

Server Actions:
- uploadBankStatement: CSV parsing and auto-matching
- manualMatch: User-initiated transaction linking
- unmatchBankLine: Remove incorrect match
- archiveStatement: Mark statement as complete
- markReconciled: Finalize reconciliation

Utils:
- parseCSV: Flexible bank statement parser
- autoMatchTransactions: Fuzzy matching algorithm
- getReconciliationSummary: Stats calculation

UI Components:
- ReconciliationClient: Interactive matching interface with tabs
- Manual match form: User-friendly transaction linking

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-17 15:58:23 +01:00
co-authored by Claude Haiku 4.5
parent 2d55203c1c
commit 7f88379d3b
7 changed files with 1154 additions and 0 deletions
+55
View File
@@ -417,3 +417,58 @@ model FinancialAuditLog {
changedBy String? // "SYSTEM" or IP address if admin
reason String? // why was this changed
}
// Bank statement uploaded for reconciliation
model BankStatement {
id String @id @default(cuid())
filename String // original filename
bankName String? // e.g., "HSBC", "Barclays"
accountNumber String? // last 4 digits for safety
statementMonth DateTime // the month this statement covers
startDate DateTime // first transaction date in statement
endDate DateTime // last transaction date in statement
totalTransactions Int // count of lines in statement
totalAmount Int? // cents, total of all transactions (for verification)
status String @default("PENDING") // PENDING, IN_PROGRESS, RECONCILED, ARCHIVED
uploadedAt DateTime @default(now())
reconciliedAt DateTime? // when reconciliation was completed
lines BankStatementLine[]
reconciliations BankReconciliation[]
}
// Individual transaction line from a bank statement
model BankStatementLine {
id String @id @default(cuid())
statementId String
statement BankStatement @relation(fields: [statementId], references: [id], onDelete: Cascade)
date DateTime // transaction date
description String // e.g., "STRIPE DEPOSIT", "TRANSFER TO SAVINGS"
amount Int // cents, positive for credit, negative for debit
balance Int? // account balance after this transaction (if provided)
reference String? // transaction reference from bank
matched Boolean @default(false) // whether this line has been reconciled
reconciliation BankReconciliation? // the matched transaction, if any
createdAt DateTime @default(now())
}
// Link between system transaction (order/sale/expense) and bank statement line
model BankReconciliation {
id String @id @default(cuid())
statementId String
statement BankStatement @relation(fields: [statementId], references: [id], onDelete: Cascade)
bankLineId String @unique
bankLine BankStatementLine @relation(fields: [bankLineId], references: [id], onDelete: Cascade)
// Which system transaction this was matched to
transactionType String // "ORDER", "MANUAL_SALE", "REFUND", "EXPENSE", "TRANSFER_OUT", "FEE", "OTHER"
transactionId String? // ID of the order/sale/expense if applicable
description String // what the match was (e.g., "Order #12345", "Stripe fee")
systemAmount Int // cents, amount in our system
bankAmount Int // cents, amount on bank statement
amountMatches Boolean // whether system and bank amounts are equal
matchedAt DateTime @default(now())
matchedBy String? // IP/admin who manually matched this (null if auto-matched)
confidence Int @default(100) // 0-100, how confident the match is (100=exact amount+date, <100=fuzzy match)
notes String? // manual notes about the match
}