Restructure documentation for DRY principle
Split documentation into three files to eliminate duplication: - ARCHITECTURE.md: Shared technical documentation (domain models, services, database schema, workflows, patterns) - CLAUDE.md: Claude Code-specific context referencing ARCHITECTURE.md - AGENTS.md: Codex agent context referencing ARCHITECTURE.md This allows both Claude Code and Codex CLI to share the same source of truth for architecture details while maintaining tool-specific configuration in separate files. Benefits: - Single source of truth for technical details - Easier maintenance (update once, not twice) - Consistent documentation across tools - Clear separation of concerns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
951
AGENTS.md
951
AGENTS.md
@@ -1,855 +1,96 @@
|
||||
# MoneyMap Architecture Documentation
|
||||
|
||||
## Project Overview
|
||||
|
||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application designed for personal finance tracking. It allows users to import bank transaction CSV files, categorize expenses, attach receipt images/PDFs, and parse receipts using AI (OpenAI GPT-4o-mini Vision API).
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: ASP.NET Core 8.0 (Razor Pages)
|
||||
- **Database**: SQL Server with Entity Framework Core 9.0
|
||||
- **Libraries**:
|
||||
- CsvHelper (33.1.0) - CSV parsing
|
||||
- Magick.NET (14.8.2) - PDF to image conversion
|
||||
- PdfPig (0.1.11) - PDF processing
|
||||
- OpenAI API - Receipt OCR and parsing
|
||||
|
||||
## Architecture
|
||||
|
||||
MoneyMap follows a clean, service-oriented architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Razor Pages (UI Layer) │
|
||||
│ Index, Upload, Transactions, ViewReceipt │
|
||||
│ EditTransaction, CategoryMappings, etc. │
|
||||
└────────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────────────────┐
|
||||
│ Service Layer (Business Logic) │
|
||||
│ - TransactionImporter │
|
||||
│ - CardResolver │
|
||||
│ - TransactionCategorizer │
|
||||
│ - DashboardService │
|
||||
│ - ReceiptManager │
|
||||
│ - OpenAIReceiptParser │
|
||||
└────────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────────────────┐
|
||||
│ Data Access Layer (EF Core) │
|
||||
│ MoneyMapContext │
|
||||
└────────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────────────────┐
|
||||
│ SQL Server Database │
|
||||
│ Cards, Transactions, Receipts, │
|
||||
│ ReceiptLineItems, ReceiptParseLogs, │
|
||||
│ CategoryMappings │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Core Domain Models
|
||||
|
||||
### Card (Models/Card.cs)
|
||||
Represents a payment card (credit/debit).
|
||||
|
||||
**Properties:**
|
||||
- `Id` (int) - Primary key
|
||||
- `Issuer` (string, 100) - Card issuer (VISA, MC, etc.)
|
||||
- `Last4` (string, 4) - Last 4 digits of card number
|
||||
- `Owner` (string, 100) - Cardholder name (optional)
|
||||
- `Transactions` - Navigation property to transactions
|
||||
|
||||
### Transaction (Models/Transaction.cs)
|
||||
Core entity representing a bank transaction.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `Date` (DateTime) - Transaction date
|
||||
- `TransactionType` (string, 20) - "DEBIT"/"CREDIT"
|
||||
- `Name` (string, 200) - Merchant/payee name
|
||||
- `Memo` (string, 500) - Transaction description
|
||||
- `Amount` (decimal) - Amount (negative = debit, positive = credit)
|
||||
- `Category` (string, 100) - Expense category
|
||||
- `Notes` (string) - User notes
|
||||
- `CardId` (int) - Foreign key to Card
|
||||
- `CardLast4` (string, 4) - Cached last 4 digits from memo
|
||||
- `Receipts` - Collection of attached receipts
|
||||
|
||||
**Unique Index:** Date + Amount + Name + Memo + CardId (prevents duplicates)
|
||||
|
||||
**Computed Properties:**
|
||||
- `IsCredit` - Returns true if Amount > 0
|
||||
- `IsDebit` - Returns true if Amount < 0
|
||||
|
||||
### Receipt (Models/Receipt.cs)
|
||||
Stores uploaded receipt files (images/PDFs) linked to transactions.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `TransactionId` (long) - Foreign key to Transaction
|
||||
- `FileName` (string, 260) - Original file name (sanitized)
|
||||
- `ContentType` (string, 100) - MIME type
|
||||
- `StoragePath` (string, 1024) - Relative path in wwwroot
|
||||
- `FileSizeBytes` (long) - File size
|
||||
- `FileHashSha256` (string, 64) - SHA256 hash for deduplication
|
||||
- `UploadedAtUtc` (DateTime) - Upload timestamp
|
||||
|
||||
**Parsed Fields (populated by AI parser):**
|
||||
- `Merchant` (string, 200) - Merchant name extracted from receipt
|
||||
- `ReceiptDate` (DateTime?) - Date on receipt
|
||||
- `Subtotal`, `Tax`, `Total` (decimal?) - Monetary amounts
|
||||
- `Currency` (string, 8) - Currency code
|
||||
|
||||
**Navigation Properties:**
|
||||
- `ParseLogs` - Collection of parse attempts
|
||||
- `LineItems` - Collection of receipt line items
|
||||
|
||||
**Unique Index:** TransactionId + FileHashSha256 (prevents duplicate uploads)
|
||||
|
||||
### ReceiptLineItem (Models/ReceiptLineItem.cs)
|
||||
Individual line items extracted from receipts.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `ReceiptId` (long) - Foreign key to Receipt
|
||||
- `LineNumber` (int) - Sequential line number
|
||||
- `Description` (string, 300) - Item description
|
||||
- `Quantity` (decimal?) - Quantity purchased (null for services/fees)
|
||||
- `UnitPrice` (decimal?) - Price per unit
|
||||
- `LineTotal` (decimal) - Total for this line
|
||||
|
||||
### ReceiptParseLog (Models/ReceiptParseLog.cs)
|
||||
Logs each receipt parsing attempt for auditing.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `ReceiptId` (long) - Foreign key to Receipt
|
||||
- `Provider` (string, 50) - "OpenAI"
|
||||
- `Model` (string, 100) - Model used (e.g., "gpt-4o-mini")
|
||||
- `StartedAtUtc`, `CompletedAtUtc` (DateTime)
|
||||
- `Success` (bool) - Parse success status
|
||||
- `Confidence` (decimal?) - AI confidence score
|
||||
- `Error` (string?) - Error message if failed
|
||||
- `RawProviderPayloadJson` (string?) - Full API response
|
||||
|
||||
### CategoryMapping (Services/TransactionCategorizer.cs)
|
||||
Pattern-based rules for auto-categorization.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (int) - Primary key
|
||||
- `Category` (string) - Category name
|
||||
- `Pattern` (string) - Merchant name pattern to match
|
||||
- `Priority` (int) - Higher priority checked first (default 0)
|
||||
|
||||
## Service Layer
|
||||
|
||||
### TransactionImporter (Pages/Upload.cshtml.cs)
|
||||
**Interface:** `ITransactionImporter`
|
||||
|
||||
**Responsibility:** Import transactions from CSV files.
|
||||
|
||||
**Key Methods:**
|
||||
- `ImportAsync(Stream csvStream, ImportContext context)`
|
||||
- Parses CSV using CsvHelper
|
||||
- Resolves card for each transaction (auto or manual)
|
||||
- Deduplicates against database and current batch
|
||||
- Returns `ImportOperationResult` with stats
|
||||
|
||||
**Workflow:**
|
||||
1. Read CSV with flexible header mapping
|
||||
2. For each row:
|
||||
- Resolve card using CardResolver
|
||||
- Map CSV row to Transaction entity
|
||||
- Check for duplicates (database + in-memory batch)
|
||||
- Add non-duplicates to batch
|
||||
3. Bulk save to database
|
||||
4. Return import statistics
|
||||
|
||||
**Location:** Pages/Upload.cshtml.cs:92-180
|
||||
|
||||
### CardResolver (Pages/Upload.cshtml.cs)
|
||||
**Interface:** `ICardResolver`
|
||||
|
||||
**Responsibility:** Determine which card a transaction belongs to.
|
||||
|
||||
**Key Methods:**
|
||||
- `ResolveCardAsync(string? memo, ImportContext context)`
|
||||
- Auto mode: Extract last 4 digits from memo or filename
|
||||
- Manual mode: Use user-selected card
|
||||
- Auto-creates cards if not found
|
||||
- Returns `CardResolutionResult`
|
||||
|
||||
**Card Extraction Logic:**
|
||||
- From memo: Regex pattern `\b(?:\.|\s)(\d{4,6})\b` extracts last 4 digits
|
||||
- From filename: Split by `-`, `_`, or space; find 4-digit numeric segment
|
||||
- Auto-create: If card doesn't exist, creates new Card with Owner="Unknown"
|
||||
|
||||
**Location:** Pages/Upload.cshtml.cs:190-248
|
||||
|
||||
### TransactionCategorizer (Services/TransactionCategorizer.cs)
|
||||
**Interface:** `ITransactionCategorizer`
|
||||
|
||||
**Responsibility:** Auto-categorize transactions based on merchant name patterns.
|
||||
|
||||
**Key Methods:**
|
||||
- `CategorizeAsync(string merchantName, decimal? amount = null)`
|
||||
- Matches merchant name against CategoryMapping patterns (case-insensitive)
|
||||
- Special logic: Gas stations with small purchases (<-$20) → "Convenience Store"
|
||||
- Returns category string or empty if no match
|
||||
- `GetAllMappingsAsync()` - Retrieve all mappings for management UI
|
||||
- `SeedDefaultMappingsAsync()` - One-time seed of default category rules
|
||||
|
||||
**Default Categories (200+ patterns):**
|
||||
- Online shopping (Amazon, Sephora, Kohls, etc.)
|
||||
- Walmart (separate: Online, Pickup/Grocery)
|
||||
- Pizza chains (Domino's, Papa John's, etc.)
|
||||
- Brick/mortar stores (Dollar General, Kroger, Target, etc.)
|
||||
- Restaurants (McDonald's, Starbucks, Olive Garden, etc.)
|
||||
- School expenses
|
||||
- Health (pharmacies, clinics, dental)
|
||||
- Gas & Auto (high priority, 100)
|
||||
- Utilities/Services (Verizon, Comcast, etc.)
|
||||
- Entertainment (Netflix, Hulu, Steam, etc.)
|
||||
- Banking fees (highest priority, 200)
|
||||
- Mortgage, Car Payment, Insurance
|
||||
- Credit Card payments
|
||||
- Ice Cream/Treats
|
||||
- Government/DMV
|
||||
- Home Improvement
|
||||
- Software/Subscriptions
|
||||
|
||||
**Location:** Services/TransactionCategorizer.cs:31-231
|
||||
|
||||
### DashboardService (Pages/Index.cshtml.cs)
|
||||
**Interface:** `IDashboardService`
|
||||
|
||||
**Responsibility:** Aggregate dashboard statistics and data.
|
||||
|
||||
**Key Methods:**
|
||||
- `GetDashboardDataAsync(int topCategoriesCount, int recentTransactionsCount)`
|
||||
- Orchestrates calls to specialized providers
|
||||
- Returns `DashboardData` DTO
|
||||
|
||||
**Sub-Services:**
|
||||
|
||||
#### IDashboardStatsCalculator
|
||||
- `CalculateAsync()` - Compute aggregate stats:
|
||||
- Total transactions, credits, debits, uncategorized count
|
||||
- Total receipts, total cards
|
||||
|
||||
#### ITopCategoriesProvider
|
||||
- `GetTopCategoriesAsync(int count, int lastDays)`
|
||||
- Aggregates spending by category for last N days (default 90)
|
||||
- Returns top N categories by total spend
|
||||
- Includes transaction count per category
|
||||
|
||||
#### IRecentTransactionsProvider
|
||||
- `GetRecentTransactionsAsync(int count)`
|
||||
- Fetches most recent N transactions (default 20)
|
||||
- Includes card label, receipt count
|
||||
- Sorted by date descending, then ID descending
|
||||
|
||||
**Location:** Pages/Index.cshtml.cs:63-250
|
||||
|
||||
### ReceiptManager (Services/ReceiptManager.cs)
|
||||
**Interface:** `IReceiptManager`
|
||||
|
||||
**Responsibility:** Handle receipt file uploads and storage.
|
||||
|
||||
**Key Methods:**
|
||||
- `UploadReceiptAsync(long transactionId, IFormFile file)`
|
||||
- Validates file (10MB max, allowed extensions: jpg, jpeg, png, pdf, gif, heic)
|
||||
- Computes SHA256 hash for deduplication
|
||||
- Saves file to `wwwroot/receipts/{transactionId}_{guid}{ext}`
|
||||
- Sanitizes filename (removes non-ASCII like ®, ™, ©)
|
||||
- Creates Receipt entity in database
|
||||
- Returns `ReceiptUploadResult`
|
||||
|
||||
- `DeleteReceiptAsync(long receiptId)`
|
||||
- Deletes physical file from disk
|
||||
- Removes database record (cascades to ParseLogs and LineItems)
|
||||
|
||||
- `GetReceiptPhysicalPath(Receipt receipt)` - Resolves storage path
|
||||
- `GetReceiptAsync(long receiptId)` - Retrieves receipt with transaction
|
||||
|
||||
**Security Features:**
|
||||
- File size validation (10MB limit)
|
||||
- Extension whitelist
|
||||
- Filename sanitization
|
||||
- SHA256 deduplication
|
||||
|
||||
**Location:** Services/ReceiptManager.cs:23-199
|
||||
|
||||
### OpenAIReceiptParser (Services/OpenAIReceiptParser.cs)
|
||||
**Interface:** `IReceiptParser`
|
||||
|
||||
**Responsibility:** Parse receipts using OpenAI GPT-4o-mini Vision API.
|
||||
|
||||
**Key Methods:**
|
||||
- `ParseReceiptAsync(long receiptId)`
|
||||
- Loads receipt file from disk
|
||||
- Converts PDFs to PNG images using ImageMagick (220 DPI)
|
||||
- Calls OpenAI Vision API with structured prompt
|
||||
- Parses JSON response (merchant, date, amounts, line items)
|
||||
- Updates Receipt entity with extracted data
|
||||
- Replaces existing line items
|
||||
- Logs parse attempt in ReceiptParseLog
|
||||
- Returns `ReceiptParseResult`
|
||||
|
||||
**API Configuration:**
|
||||
- Model: `gpt-4o-mini`
|
||||
- Temperature: 0.1 (deterministic)
|
||||
- Max tokens: 2000
|
||||
- API key: Environment variable `OPENAI_API_KEY` or config `OpenAI:ApiKey`
|
||||
|
||||
**Prompt Strategy:**
|
||||
- Structured JSON request with schema example
|
||||
- Extracts: merchant, date, subtotal, tax, total, confidence
|
||||
- Line items with: description, quantity, unitPrice, lineTotal
|
||||
- Special handling: Services/fees have null quantity (not products)
|
||||
|
||||
**PDF Handling:**
|
||||
- ImageMagick converts first page to PNG at 220 DPI
|
||||
- Flattens alpha channel (white background)
|
||||
- TrueColor 8-bit RGB output
|
||||
|
||||
**Location:** Services/OpenAIReceiptParser.cs:23-302
|
||||
|
||||
## Data Access Layer
|
||||
|
||||
### MoneyMapContext (Data/MoneyMapContext.cs)
|
||||
EF Core DbContext managing all database entities.
|
||||
|
||||
**DbSets:**
|
||||
- `Cards` - Payment cards
|
||||
- `Transactions` - Bank transactions
|
||||
- `Receipts` - Receipt files
|
||||
- `ReceiptParseLogs` - Parse attempt logs
|
||||
- `ReceiptLineItems` - Receipt line items
|
||||
- `CategoryMappings` - Categorization rules
|
||||
|
||||
**Key Configuration:**
|
||||
- **Transactions ↔ Cards**: Restrict delete (can't delete card with transactions)
|
||||
- **Receipts ↔ Transactions**: Cascade delete (deleting transaction removes receipts)
|
||||
- **ReceiptParseLogs ↔ Receipts**: Cascade delete
|
||||
- **ReceiptLineItems ↔ Receipts**: Cascade delete
|
||||
|
||||
**Performance Indexes:**
|
||||
- Transaction.Date, Transaction.Amount, Transaction.Category (filtering)
|
||||
- Card: (Issuer, Last4, Owner) composite
|
||||
- Transaction: (Date, Amount, Name, Memo, CardId) unique constraint
|
||||
- Receipt: (TransactionId, FileHashSha256) unique constraint
|
||||
|
||||
**Location:** Data/MoneyMapContext.cs:9-107
|
||||
|
||||
## Razor Pages (UI Layer)
|
||||
|
||||
### Index.cshtml / IndexModel (Dashboard)
|
||||
**Route:** `/`
|
||||
|
||||
**Purpose:** Main dashboard showing financial overview.
|
||||
|
||||
**Displays:**
|
||||
- Aggregate stats (total transactions, credits, debits, uncategorized, receipts, cards)
|
||||
- Top 8 spending categories (last 90 days)
|
||||
- Recent 20 transactions with card labels and receipt counts
|
||||
|
||||
**Dependencies:** `IDashboardService`
|
||||
|
||||
**Location:** Pages/Index.cshtml.cs:12-250
|
||||
|
||||
### Upload.cshtml / UploadModel (CSV Import)
|
||||
**Route:** `/Upload`
|
||||
|
||||
**Purpose:** Import bank transactions from CSV files.
|
||||
|
||||
**Features:**
|
||||
- Card selection mode: Auto (extract from memo/filename) or Manual (user selects)
|
||||
- Duplicate detection (unique constraint on Date+Amount+Name+Memo+CardId)
|
||||
- Auto-creates cards if not found in Auto mode
|
||||
- Displays import stats (total, inserted, skipped)
|
||||
|
||||
**Form Inputs:**
|
||||
- CSV file
|
||||
- Card mode (radio: Auto/Manual)
|
||||
- Card dropdown (if Manual mode)
|
||||
|
||||
**Dependencies:** `ITransactionImporter`
|
||||
|
||||
**Location:** Pages/Upload.cshtml.cs:17-333
|
||||
|
||||
### Transactions.cshtml / TransactionsModel
|
||||
**Route:** `/Transactions`
|
||||
|
||||
**Purpose:** View and search all transactions.
|
||||
|
||||
**Features:**
|
||||
- Search by name or memo
|
||||
- Filter by card, category, date range
|
||||
- Sort by date, amount, name, category
|
||||
- Pagination
|
||||
- Receipt count indicator
|
||||
- Edit transaction link
|
||||
|
||||
**Location:** Pages/Transactions.cshtml.cs (estimated)
|
||||
|
||||
### EditTransaction.cshtml / EditTransactionModel
|
||||
**Route:** `/EditTransaction?id={id}`
|
||||
|
||||
**Purpose:** Edit transaction details.
|
||||
|
||||
**Features:**
|
||||
- Update category, notes, date, amount, name, memo
|
||||
- Upload/delete receipts
|
||||
- View receipt thumbnails
|
||||
- Trigger AI receipt parsing
|
||||
|
||||
**Location:** Pages/EditTransaction.cshtml.cs:27-202
|
||||
|
||||
### ViewReceipt.cshtml / ViewReceiptModel
|
||||
**Route:** `/ViewReceipt?id={id}`
|
||||
|
||||
**Purpose:** View receipt details and parsed data.
|
||||
|
||||
**Displays:**
|
||||
- Receipt image/PDF preview
|
||||
- File metadata (name, size, upload date)
|
||||
- Parsed merchant, date, amounts
|
||||
- Line items table (description, quantity, unit price, total)
|
||||
- Parse logs (provider, model, confidence, timestamp, errors)
|
||||
- Actions: Re-parse, Delete
|
||||
|
||||
**Dependencies:** `IReceiptManager`, `IReceiptParser`
|
||||
|
||||
**Location:** Pages/ViewReceipt.cshtml.cs:16-126
|
||||
|
||||
### CategoryMappings.cshtml / CategoryMappingsModel
|
||||
**Route:** `/CategoryMappings`
|
||||
|
||||
**Purpose:** Manage category mapping rules.
|
||||
|
||||
**Features:**
|
||||
- View all category patterns
|
||||
- Add/edit/delete mappings
|
||||
- Priority management (higher = checked first)
|
||||
- Seed default mappings
|
||||
|
||||
**Dependencies:** `ITransactionCategorizer`
|
||||
|
||||
**Location:** Pages/CategoryMappings.cshtml.cs:16-91
|
||||
|
||||
### Recategorize.cshtml / RecategorizeModel
|
||||
**Route:** `/Recategorize`
|
||||
|
||||
**Purpose:** Bulk recategorize uncategorized transactions.
|
||||
|
||||
**Features:**
|
||||
- Lists all transactions without a category
|
||||
- Applies TransactionCategorizer rules
|
||||
- Updates matching transactions
|
||||
- Reports counts (total, categorized, still uncategorized)
|
||||
|
||||
**Dependencies:** `ITransactionCategorizer`
|
||||
|
||||
**Location:** Pages/Recategorize.cshtml.cs:16-87
|
||||
|
||||
## Configuration
|
||||
|
||||
### appsettings.json
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"MoneyMapDb": "Server=...;Database=MoneyMap;..."
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "sk-..." // Optional, can use OPENAI_API_KEY env var
|
||||
},
|
||||
"Receipts": {
|
||||
"StoragePath": "receipts" // Relative to wwwroot
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Program.cs Dependency Injection
|
||||
```csharp
|
||||
// Database
|
||||
builder.Services.AddDbContext<MoneyMapContext>(options =>
|
||||
options.UseSqlServer(...));
|
||||
|
||||
// Transaction Services
|
||||
builder.Services.AddScoped<ITransactionImporter, TransactionImporter>();
|
||||
builder.Services.AddScoped<ICardResolver, CardResolver>();
|
||||
builder.Services.AddScoped<ITransactionCategorizer, TransactionCategorizer>();
|
||||
|
||||
// Dashboard Services
|
||||
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
||||
builder.Services.AddScoped<IDashboardStatsCalculator, DashboardStatsCalculator>();
|
||||
builder.Services.AddScoped<ITopCategoriesProvider, TopCategoriesProvider>();
|
||||
builder.Services.AddScoped<IRecentTransactionsProvider, RecentTransactionsProvider>();
|
||||
|
||||
// Receipt Services
|
||||
builder.Services.AddScoped<IReceiptManager, ReceiptManager>();
|
||||
builder.Services.AddHttpClient<IReceiptParser, OpenAIReceiptParser>();
|
||||
```
|
||||
|
||||
**Location:** Program.cs:1-44
|
||||
|
||||
## Database Schema (SQL Server)
|
||||
|
||||
### Cards Table
|
||||
```sql
|
||||
Id (int, PK)
|
||||
Issuer (nvarchar(100), NOT NULL)
|
||||
Last4 (nvarchar(4), NOT NULL)
|
||||
Owner (nvarchar(100))
|
||||
INDEX: (Issuer, Last4, Owner)
|
||||
```
|
||||
|
||||
### Transactions Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
Date (datetime2, NOT NULL)
|
||||
TransactionType (nvarchar(20))
|
||||
Name (nvarchar(200), NOT NULL)
|
||||
Memo (nvarchar(500))
|
||||
Amount (decimal(18,2), NOT NULL)
|
||||
Category (nvarchar(100))
|
||||
Notes (nvarchar(max))
|
||||
CardId (int, FK → Cards.Id, RESTRICT)
|
||||
CardLast4 (nvarchar(4))
|
||||
UNIQUE INDEX: (Date, Amount, Name, Memo, CardId)
|
||||
INDEX: Date, Amount, Category
|
||||
```
|
||||
|
||||
### Receipts Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
TransactionId (bigint, FK → Transactions.Id, CASCADE)
|
||||
FileName (nvarchar(260), NOT NULL)
|
||||
ContentType (nvarchar(100), DEFAULT 'application/octet-stream')
|
||||
StoragePath (nvarchar(1024), NOT NULL)
|
||||
FileSizeBytes (bigint, NOT NULL)
|
||||
FileHashSha256 (nvarchar(64), NOT NULL)
|
||||
UploadedAtUtc (datetime2, NOT NULL)
|
||||
Merchant (nvarchar(200))
|
||||
ReceiptDate (datetime2)
|
||||
Subtotal (decimal(18,2))
|
||||
Tax (decimal(18,2))
|
||||
Total (decimal(18,2))
|
||||
Currency (nvarchar(8))
|
||||
UNIQUE INDEX: (TransactionId, FileHashSha256)
|
||||
```
|
||||
|
||||
### ReceiptParseLogs Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
ReceiptId (bigint, FK → Receipts.Id, CASCADE)
|
||||
Provider (nvarchar(50), NOT NULL)
|
||||
Model (nvarchar(100), NOT NULL)
|
||||
StartedAtUtc (datetime2, NOT NULL)
|
||||
CompletedAtUtc (datetime2)
|
||||
Success (bit, NOT NULL)
|
||||
Confidence (decimal(18,2))
|
||||
Error (nvarchar(max))
|
||||
ProviderJobId (nvarchar(100))
|
||||
ExtractedTextPath (nvarchar(1024))
|
||||
RawProviderPayloadJson (nvarchar(max))
|
||||
```
|
||||
|
||||
### ReceiptLineItems Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
ReceiptId (bigint, FK → Receipts.Id, CASCADE)
|
||||
LineNumber (int, NOT NULL)
|
||||
Description (nvarchar(300), NOT NULL)
|
||||
Quantity (decimal(18,4))
|
||||
UnitPrice (decimal(18,4))
|
||||
LineTotal (decimal(18,2), NOT NULL)
|
||||
Unit (nvarchar(16))
|
||||
Sku (nvarchar(64))
|
||||
Category (nvarchar(100))
|
||||
```
|
||||
|
||||
### CategoryMappings Table
|
||||
```sql
|
||||
Id (int, PK)
|
||||
Category (nvarchar(max), NOT NULL)
|
||||
Pattern (nvarchar(max), NOT NULL)
|
||||
Priority (int, NOT NULL, DEFAULT 0)
|
||||
```
|
||||
|
||||
## Key Workflows
|
||||
|
||||
### 1. Import Transactions from CSV
|
||||
|
||||
```
|
||||
User uploads CSV file
|
||||
↓
|
||||
UploadModel.OnPostAsync()
|
||||
↓
|
||||
TransactionImporter.ImportAsync()
|
||||
↓
|
||||
For each CSV row:
|
||||
CardResolver.ResolveCardAsync() → Get/Create Card
|
||||
Check for duplicates (DB + batch)
|
||||
Add to batch if unique
|
||||
↓
|
||||
DbContext.SaveChangesAsync()
|
||||
↓
|
||||
Display import stats (inserted, skipped)
|
||||
```
|
||||
|
||||
### 2. Auto-Categorize Transaction
|
||||
|
||||
```
|
||||
Transaction inserted with Name="KROGER #123"
|
||||
↓
|
||||
TransactionCategorizer.CategorizeAsync("KROGER #123")
|
||||
↓
|
||||
Load CategoryMappings (ordered by Priority DESC)
|
||||
↓
|
||||
Check special case: Gas station with small purchase?
|
||||
↓
|
||||
Iterate mappings:
|
||||
If "KROGER".Contains(pattern.ToUpper()) → Match!
|
||||
Return "Brick/mortar store"
|
||||
↓
|
||||
Update Transaction.Category
|
||||
```
|
||||
|
||||
### 3. Upload and Parse Receipt
|
||||
|
||||
```
|
||||
User attaches receipt to transaction
|
||||
↓
|
||||
EditTransactionModel.OnPostUploadReceiptAsync()
|
||||
↓
|
||||
ReceiptManager.UploadReceiptAsync()
|
||||
- Validate file (size, extension)
|
||||
- Compute SHA256 hash
|
||||
- Check for duplicate (TransactionId + Hash)
|
||||
- Save to wwwroot/receipts/
|
||||
- Create Receipt entity
|
||||
↓
|
||||
User clicks "Parse Receipt"
|
||||
↓
|
||||
ViewReceiptModel.OnPostParseAsync()
|
||||
↓
|
||||
OpenAIReceiptParser.ParseReceiptAsync()
|
||||
- Load receipt file
|
||||
- If PDF: Convert to PNG with ImageMagick
|
||||
- Encode as base64
|
||||
- Call OpenAI Vision API with structured prompt
|
||||
- Parse JSON response
|
||||
- Update Receipt (merchant, date, amounts)
|
||||
- Replace ReceiptLineItems
|
||||
- Create ReceiptParseLog
|
||||
↓
|
||||
Display parsed data
|
||||
```
|
||||
|
||||
### 4. Dashboard Aggregation
|
||||
|
||||
```
|
||||
User visits homepage (/)
|
||||
↓
|
||||
IndexModel.OnGet()
|
||||
↓
|
||||
DashboardService.GetDashboardDataAsync()
|
||||
↓
|
||||
Parallel data fetching:
|
||||
- DashboardStatsCalculator → Total txns, credits, debits, uncategorized, receipts, cards
|
||||
- TopCategoriesProvider → Top 8 categories by spend (last 90 days)
|
||||
- RecentTransactionsProvider → Latest 20 transactions
|
||||
↓
|
||||
Return DashboardData DTO
|
||||
↓
|
||||
Render dashboard view
|
||||
```
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### 1. Service Layer Pattern
|
||||
All business logic isolated in service interfaces/implementations:
|
||||
- Testable (mock services in unit tests)
|
||||
- Reusable across pages
|
||||
- Single Responsibility Principle
|
||||
|
||||
### 2. Repository Pattern (via EF Core DbContext)
|
||||
- MoneyMapContext encapsulates data access
|
||||
- LINQ queries in service layer
|
||||
- Change tracking and unit of work handled by EF Core
|
||||
|
||||
### 3. Result Pattern
|
||||
Services return result objects instead of throwing exceptions:
|
||||
- `ImportOperationResult` (success/failure with data or error message)
|
||||
- `CardResolutionResult`
|
||||
- `ReceiptUploadResult`
|
||||
- `ReceiptParseResult`
|
||||
|
||||
Benefits: Explicit error handling, no try-catch clutter, clear success/failure paths
|
||||
|
||||
### 4. Dependency Injection
|
||||
All services registered in Program.cs:
|
||||
- Scoped lifetime for per-request services
|
||||
- HttpClient factory for IReceiptParser
|
||||
- Testability via interface injection
|
||||
|
||||
### 5. Data Transfer Objects (DTOs)
|
||||
Separate DTOs for data transfer between layers:
|
||||
- `DashboardData` (aggregates stats + categories + recent transactions)
|
||||
- `ImportContext` (encapsulates import parameters)
|
||||
- `TransactionKey` (value object for deduplication)
|
||||
- Page model record types (TopCategoryRow, RecentTxnRow, etc.)
|
||||
|
||||
### 6. Strategy Pattern
|
||||
CardResolver uses strategy based on CardSelectMode:
|
||||
- Auto strategy: Extract from memo/filename
|
||||
- Manual strategy: Use user selection
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. File Upload Security
|
||||
- File size limit: 10MB
|
||||
- Extension whitelist: jpg, jpeg, png, pdf, gif, heic
|
||||
- Filename sanitization: Remove non-ASCII characters (®, ™, ©)
|
||||
- SHA256 hash validation for deduplication
|
||||
- Files stored outside database (wwwroot/receipts/)
|
||||
|
||||
### 2. SQL Injection Prevention
|
||||
- Parameterized queries via EF Core
|
||||
- LINQ expressions compile to safe SQL
|
||||
|
||||
### 3. API Key Management
|
||||
- OpenAI API key from environment variable (preferred) or config
|
||||
- Not hardcoded in source
|
||||
|
||||
### 4. Delete Constraints
|
||||
- Restrict delete on Cards (can't delete if transactions exist)
|
||||
- Cascade delete on Receipts (deleting transaction removes receipts)
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Database Indexes
|
||||
- Transactions: Date, Amount, Category (for filtering/sorting)
|
||||
- Unique indexes on duplicate detection columns
|
||||
- Composite index on Cards (Issuer, Last4, Owner)
|
||||
|
||||
### 2. Batch Processing
|
||||
- CSV import: Bulk insert with single SaveChangesAsync()
|
||||
- In-memory duplicate checking (HashSet) avoids N+1 queries
|
||||
|
||||
### 3. Pagination
|
||||
- Transactions list uses pagination to limit result sets
|
||||
- Dashboard limits top categories (8) and recent transactions (20)
|
||||
|
||||
### 4. AsNoTracking
|
||||
- Read-only queries use `.AsNoTracking()` to reduce EF overhead
|
||||
- TopCategoriesProvider, RecentTransactionsProvider
|
||||
|
||||
### 5. PDF Conversion
|
||||
- ImageMagick runs on ThreadPool (Task.Run) to avoid blocking
|
||||
- First page only for multi-page PDFs
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
### Unit Testing Strategy
|
||||
All service interfaces are mockable:
|
||||
```csharp
|
||||
// Example: Test TransactionImporter
|
||||
var mockCardResolver = new Mock<ICardResolver>();
|
||||
var mockDb = new Mock<MoneyMapContext>();
|
||||
var importer = new TransactionImporter(mockDb.Object, mockCardResolver.Object);
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
- In-memory database for DbContext tests
|
||||
- Test CSV import end-to-end
|
||||
- Validate categorization rules
|
||||
|
||||
### External Dependencies
|
||||
- OpenAI API: Mock IReceiptParser for tests
|
||||
- File system: Mock IWebHostEnvironment
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Features
|
||||
1. **Multi-user support**: Add authentication/authorization
|
||||
2. **Budget tracking**: Monthly budget limits per category
|
||||
3. **Recurring transactions**: Auto-detect and predict recurring expenses
|
||||
4. **Data export**: Export transactions to Excel/CSV
|
||||
5. **Charts/graphs**: Spending trends over time
|
||||
6. **Mobile app**: React Native or .NET MAUI
|
||||
7. **Bank API integration**: Direct import from bank APIs (Plaid, Yodlee)
|
||||
8. **Receipt search**: Full-text search on parsed line items
|
||||
9. **Split transactions**: Divide single transaction across categories
|
||||
10. **Tags**: Tag transactions with multiple labels
|
||||
|
||||
### Technical Debt
|
||||
1. Move inline service implementations to separate files (Upload.cshtml.cs is 333 lines)
|
||||
2. Add logging (ILogger) to all services
|
||||
3. Add retry logic for OpenAI API calls (Polly)
|
||||
4. Implement background job processing for receipt parsing (Hangfire)
|
||||
5. Add integration tests for critical workflows
|
||||
6. Implement caching for CategoryMappings (IMemoryCache)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Duplicate Transaction Errors**
|
||||
- **Cause**: Unique constraint violation (Date + Amount + Name + Memo + CardId)
|
||||
- **Solution**: Transactions are skipped automatically during import
|
||||
|
||||
**2. Receipt Parsing Failures**
|
||||
- **Cause**: OpenAI API key missing or invalid
|
||||
- **Solution**: Set `OPENAI_API_KEY` environment variable or `OpenAI:ApiKey` in appsettings.json
|
||||
- **Cause**: PDF conversion error
|
||||
- **Solution**: Ensure Magick.NET is properly installed
|
||||
|
||||
**3. Card Not Found**
|
||||
- **Cause**: Auto mode can't extract last 4 digits from memo or filename
|
||||
- **Solution**: Use Manual mode and select card from dropdown, or rename CSV file to include card digits (e.g., `transactions-1234.csv`)
|
||||
|
||||
**4. Slow Dashboard Load**
|
||||
- **Cause**: Large transaction dataset without indexes
|
||||
- **Solution**: Ensure migrations have run (indexes on Date, Amount, Category)
|
||||
|
||||
**5. File Upload Fails**
|
||||
- **Cause**: File exceeds 10MB or unsupported format
|
||||
- **Solution**: Resize/compress image, or use supported format
|
||||
|
||||
## Deployment
|
||||
|
||||
### Database Migration
|
||||
```bash
|
||||
# Create migration
|
||||
dotnet ef migrations add MigrationName
|
||||
|
||||
# Update database
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
1. Update connection string in appsettings.json (Azure SQL, AWS RDS, etc.)
|
||||
2. Set `OPENAI_API_KEY` environment variable
|
||||
3. Configure `Receipts:StoragePath` (consider Azure Blob Storage)
|
||||
4. Enable HTTPS and HSTS
|
||||
5. Set `ASPNETCORE_ENVIRONMENT=Production`
|
||||
|
||||
### File Storage Considerations
|
||||
- **Local**: wwwroot/receipts (current implementation)
|
||||
- **Production**: Azure Blob Storage, AWS S3, or network share
|
||||
- Modify ReceiptManager to use cloud storage SDK
|
||||
- Update StoragePath format (blob URL instead of relative path)
|
||||
|
||||
## Conclusion
|
||||
|
||||
MoneyMap demonstrates a well-architected ASP.NET Core application with clear separation of concerns, testable services, and modern AI integration. The service layer pattern allows for easy maintenance and extension, while the Result pattern provides explicit error handling. The OpenAI receipt parsing feature showcases practical LLM integration for automating tedious data entry tasks.
|
||||
|
||||
---
|
||||
|
||||
**Generated:** 2025-10-09
|
||||
**Version:** 1.0
|
||||
**Framework:** ASP.NET Core 8.0 / EF Core 9.0
|
||||
# MoneyMap - Codex Agent Context
|
||||
|
||||
## Quick Overview
|
||||
|
||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application for personal finance tracking. Users can import bank transaction CSVs, categorize expenses, attach receipt images/PDFs, and parse receipts using OpenAI's Vision API.
|
||||
|
||||
## Architecture Documentation
|
||||
|
||||
**For complete technical documentation, see [ARCHITECTURE.md](./ARCHITECTURE.md)**
|
||||
|
||||
The shared architecture document provides:
|
||||
- Complete technology stack and dependencies
|
||||
- Core domain models and relationships
|
||||
- Service layer implementations
|
||||
- Database schema with all tables and relationships
|
||||
- Key workflows (CSV import, categorization, receipt parsing, etc.)
|
||||
- Design patterns and best practices
|
||||
- Security considerations
|
||||
- Performance optimizations
|
||||
- Troubleshooting guide
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
This file is used by Codex CLI for agent context. Claude Code users should reference [CLAUDE.md](./CLAUDE.md) instead.
|
||||
|
||||
## Key Features for Agents
|
||||
|
||||
### Transaction Management
|
||||
- CSV import with duplicate detection
|
||||
- Auto-categorization based on merchant patterns
|
||||
- Manual category and merchant assignment
|
||||
- Transfer detection between accounts
|
||||
|
||||
### Receipt Processing
|
||||
- File upload with SHA256 deduplication
|
||||
- OpenAI Vision API integration for parsing
|
||||
- Line item extraction
|
||||
- Parse logging and confidence tracking
|
||||
|
||||
### Data Organization
|
||||
- Account and Card hierarchy
|
||||
- Merchant normalization
|
||||
- Category mapping rules with priority
|
||||
- Relationship tracking (transactions ↔ receipts ↔ line items)
|
||||
|
||||
## Common Agent Tasks
|
||||
|
||||
### Code Analysis
|
||||
When analyzing code, refer to [ARCHITECTURE.md](./ARCHITECTURE.md) for:
|
||||
- Service interfaces and their responsibilities
|
||||
- Domain model relationships
|
||||
- Database constraints and cascade rules
|
||||
- Performance considerations (indexes, AsNoTracking)
|
||||
|
||||
### Feature Implementation
|
||||
1. Check [ARCHITECTURE.md](./ARCHITECTURE.md) for existing patterns
|
||||
2. Follow Service Layer Pattern (business logic in services)
|
||||
3. Use Result Pattern for error handling
|
||||
4. Register new services in Program.cs
|
||||
5. Create migrations for schema changes
|
||||
|
||||
### Bug Investigation
|
||||
1. Review relevant service in [ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||
2. Check database constraints and relationships
|
||||
3. Review cascade delete rules
|
||||
4. Check unique indexes for duplicate constraints
|
||||
|
||||
### Refactoring
|
||||
1. Maintain service layer separation
|
||||
2. Keep interfaces for testability
|
||||
3. Follow existing patterns (DI, Result Pattern, DTOs)
|
||||
4. Update [ARCHITECTURE.md](./ARCHITECTURE.md) if making significant changes
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- **Unique Transactions**: (Date, Amount, Name, Memo, AccountId, CardId)
|
||||
- **Cascade Deletes**: Transaction → Receipts → ParseLogs/LineItems
|
||||
- **Restrict Deletes**: Can't delete Account/Card with existing transactions
|
||||
- **File Limits**: Receipts max 10MB, whitelist extensions only
|
||||
- **API Requirements**: OpenAI API key required for receipt parsing
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
1. **Always** check [ARCHITECTURE.md](./ARCHITECTURE.md) before making changes
|
||||
2. **Use** existing service patterns and interfaces
|
||||
3. **Follow** Single Responsibility Principle
|
||||
4. **Test** with mockable interfaces
|
||||
5. **Update** documentation when adding major features
|
||||
|
||||
## Reference Links
|
||||
|
||||
- Technical Details: [ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||
- Claude Code Context: [CLAUDE.md](./CLAUDE.md)
|
||||
- Service Layer: See ARCHITECTURE.md § Service Layer
|
||||
- Database Schema: See ARCHITECTURE.md § Database Schema
|
||||
- Workflows: See ARCHITECTURE.md § Key Workflows
|
||||
|
||||
928
ARCHITECTURE.md
Normal file
928
ARCHITECTURE.md
Normal file
@@ -0,0 +1,928 @@
|
||||
# MoneyMap Architecture Documentation
|
||||
|
||||
## Project Overview
|
||||
|
||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application designed for personal finance tracking. It allows users to import bank transaction CSV files, categorize expenses, attach receipt images/PDFs, and parse receipts using AI (OpenAI GPT-4o-mini Vision API).
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: ASP.NET Core 8.0 (Razor Pages)
|
||||
- **Database**: SQL Server with Entity Framework Core 9.0
|
||||
- **Libraries**:
|
||||
- CsvHelper (33.1.0) - CSV parsing
|
||||
- Magick.NET (14.8.2) - PDF to image conversion
|
||||
- PdfPig (0.1.11) - PDF processing
|
||||
- OpenAI API - Receipt OCR and parsing
|
||||
|
||||
## Architecture
|
||||
|
||||
MoneyMap follows a clean, service-oriented architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Razor Pages (UI Layer) │
|
||||
│ Index, Upload, Transactions, ViewReceipt │
|
||||
│ EditTransaction, CategoryMappings, etc. │
|
||||
└────────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────────────────┐
|
||||
│ Service Layer (Business Logic) │
|
||||
│ - TransactionImporter │
|
||||
│ - CardResolver │
|
||||
│ - TransactionCategorizer │
|
||||
│ - DashboardService │
|
||||
│ - ReceiptManager │
|
||||
│ - OpenAIReceiptParser │
|
||||
└────────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────────────────┐
|
||||
│ Data Access Layer (EF Core) │
|
||||
│ MoneyMapContext │
|
||||
└────────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────────────────┐
|
||||
│ SQL Server Database │
|
||||
│ Cards, Accounts, Transactions, Receipts, │
|
||||
│ ReceiptLineItems, ReceiptParseLogs, │
|
||||
│ CategoryMappings, Merchants │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Core Domain Models
|
||||
|
||||
### Account (Models/Account.cs)
|
||||
Represents a bank account (checking, savings, credit card account).
|
||||
|
||||
**Properties:**
|
||||
- `Id` (int) - Primary key
|
||||
- `Issuer` (string, 100) - Institution name
|
||||
- `Last4` (string, 4) - Last 4 digits of account number
|
||||
- `Owner` (string, 100) - Account owner name (optional)
|
||||
- `DisplayLabel` (string, computed) - Formatted label for display
|
||||
- `Cards` - Collection of cards linked to this account
|
||||
- `Transactions` - Collection of transactions for this account
|
||||
|
||||
### Card (Models/Card.cs)
|
||||
Represents a payment card (credit/debit) linked to an account.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (int) - Primary key
|
||||
- `AccountId` (int) - Foreign key to Account
|
||||
- `Issuer` (string, 100) - Card issuer (VISA, MC, etc.)
|
||||
- `Last4` (string, 4) - Last 4 digits of card number
|
||||
- `Owner` (string, 100) - Cardholder name (optional)
|
||||
- `DisplayLabel` (string, computed) - Formatted label for display
|
||||
- `Transactions` - Navigation property to transactions
|
||||
|
||||
### Merchant (Models/Merchant.cs)
|
||||
Represents a merchant/vendor where transactions occur.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (int) - Primary key
|
||||
- `Name` (string, 100) - Merchant name
|
||||
- `Transactions` - Collection of transactions with this merchant
|
||||
- `CategoryMappings` - Collection of category mapping rules
|
||||
|
||||
### Transaction (Models/Transaction.cs)
|
||||
Core entity representing a bank transaction.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `Date` (DateTime) - Transaction date
|
||||
- `TransactionType` (string, 20) - "DEBIT"/"CREDIT"
|
||||
- `Name` (string, 200) - Transaction name from CSV
|
||||
- `Memo` (string, 500) - Transaction description
|
||||
- `Amount` (decimal) - Amount (negative = debit, positive = credit)
|
||||
- `Category` (string, 100) - Expense category
|
||||
- `MerchantId` (int?) - Foreign key to Merchant
|
||||
- `Notes` (string) - User notes
|
||||
- `AccountId` (int) - Foreign key to Account (required)
|
||||
- `CardId` (int?) - Foreign key to Card (optional)
|
||||
- `TransferToAccountId` (int?) - Foreign key for transfers
|
||||
- `Last4` (string, 4) - Cached last 4 digits from memo
|
||||
- `Receipts` - Collection of attached receipts
|
||||
|
||||
**Unique Index:** Date + Amount + Name + Memo + AccountId + CardId (prevents duplicates)
|
||||
|
||||
**Computed Properties:**
|
||||
- `IsCredit` - Returns true if Amount > 0
|
||||
- `IsDebit` - Returns true if Amount < 0
|
||||
- `IsTransfer` - Returns true if TransferToAccountId is set
|
||||
- `PaymentMethodLabel` - Formatted payment method string
|
||||
|
||||
### Receipt (Models/Receipt.cs)
|
||||
Stores uploaded receipt files (images/PDFs) linked to transactions.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `TransactionId` (long) - Foreign key to Transaction
|
||||
- `FileName` (string, 260) - Original file name (sanitized)
|
||||
- `ContentType` (string, 100) - MIME type
|
||||
- `StoragePath` (string, 1024) - Relative path in wwwroot
|
||||
- `FileSizeBytes` (long) - File size
|
||||
- `FileHashSha256` (string, 64) - SHA256 hash for deduplication
|
||||
- `UploadedAtUtc` (DateTime) - Upload timestamp
|
||||
|
||||
**Parsed Fields (populated by AI parser):**
|
||||
- `Merchant` (string, 200) - Merchant name extracted from receipt
|
||||
- `ReceiptDate` (DateTime?) - Date on receipt
|
||||
- `Subtotal`, `Tax`, `Total` (decimal?) - Monetary amounts
|
||||
- `Currency` (string, 8) - Currency code
|
||||
|
||||
**Navigation Properties:**
|
||||
- `ParseLogs` - Collection of parse attempts
|
||||
- `LineItems` - Collection of receipt line items
|
||||
|
||||
**Unique Index:** TransactionId + FileHashSha256 (prevents duplicate uploads)
|
||||
|
||||
### ReceiptLineItem (Models/ReceiptLineItem.cs)
|
||||
Individual line items extracted from receipts.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `ReceiptId` (long) - Foreign key to Receipt
|
||||
- `LineNumber` (int) - Sequential line number
|
||||
- `Description` (string, 300) - Item description
|
||||
- `Quantity` (decimal?) - Quantity purchased (null for services/fees)
|
||||
- `UnitPrice` (decimal?) - Price per unit
|
||||
- `LineTotal` (decimal) - Total for this line
|
||||
|
||||
### ReceiptParseLog (Models/ReceiptParseLog.cs)
|
||||
Logs each receipt parsing attempt for auditing.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (long) - Primary key
|
||||
- `ReceiptId` (long) - Foreign key to Receipt
|
||||
- `Provider` (string, 50) - "OpenAI"
|
||||
- `Model` (string, 100) - Model used (e.g., "gpt-4o-mini")
|
||||
- `StartedAtUtc`, `CompletedAtUtc` (DateTime)
|
||||
- `Success` (bool) - Parse success status
|
||||
- `Confidence` (decimal?) - AI confidence score
|
||||
- `Error` (string?) - Error message if failed
|
||||
- `RawProviderPayloadJson` (string?) - Full API response
|
||||
|
||||
### CategoryMapping (Services/TransactionCategorizer.cs)
|
||||
Pattern-based rules for auto-categorization with merchant linking.
|
||||
|
||||
**Properties:**
|
||||
- `Id` (int) - Primary key
|
||||
- `Category` (string) - Category name
|
||||
- `Pattern` (string) - Merchant name pattern to match
|
||||
- `MerchantId` (int?) - Foreign key to Merchant (optional)
|
||||
- `Priority` (int) - Higher priority checked first (default 0)
|
||||
|
||||
## Service Layer
|
||||
|
||||
### TransactionImporter (Pages/Upload.cshtml.cs)
|
||||
**Interface:** `ITransactionImporter`
|
||||
|
||||
**Responsibility:** Import transactions from CSV files.
|
||||
|
||||
**Key Methods:**
|
||||
- `ImportAsync(Stream csvStream, ImportContext context)`
|
||||
- Parses CSV using CsvHelper
|
||||
- Resolves card for each transaction (auto or manual)
|
||||
- Deduplicates against database and current batch
|
||||
- Returns `ImportOperationResult` with stats
|
||||
|
||||
**Workflow:**
|
||||
1. Read CSV with flexible header mapping
|
||||
2. For each row:
|
||||
- Resolve card using CardResolver
|
||||
- Map CSV row to Transaction entity
|
||||
- Check for duplicates (database + in-memory batch)
|
||||
- Add non-duplicates to batch
|
||||
3. Bulk save to database
|
||||
4. Return import statistics
|
||||
|
||||
**Location:** Pages/Upload.cshtml.cs:92-180
|
||||
|
||||
### CardResolver (Pages/Upload.cshtml.cs)
|
||||
**Interface:** `ICardResolver`
|
||||
|
||||
**Responsibility:** Determine which card a transaction belongs to.
|
||||
|
||||
**Key Methods:**
|
||||
- `ResolveCardAsync(string? memo, ImportContext context)`
|
||||
- Auto mode: Extract last 4 digits from memo or filename
|
||||
- Manual mode: Use user-selected card
|
||||
- Auto-creates cards if not found
|
||||
- Returns `CardResolutionResult`
|
||||
|
||||
**Card Extraction Logic:**
|
||||
- From memo: Regex pattern `\b(?:\.|\s)(\d{4,6})\b` extracts last 4 digits
|
||||
- From filename: Split by `-`, `_`, or space; find 4-digit numeric segment
|
||||
- Auto-create: If card doesn't exist, creates new Card with Owner="Unknown"
|
||||
|
||||
**Location:** Pages/Upload.cshtml.cs:190-248
|
||||
|
||||
### TransactionCategorizer (Services/TransactionCategorizer.cs)
|
||||
**Interface:** `ITransactionCategorizer`
|
||||
|
||||
**Responsibility:** Auto-categorize transactions based on merchant name patterns.
|
||||
|
||||
**Key Methods:**
|
||||
- `CategorizeAsync(string merchantName, decimal? amount = null)`
|
||||
- Matches merchant name against CategoryMapping patterns (case-insensitive)
|
||||
- Special logic: Gas stations with small purchases (<-$20) → "Convenience Store"
|
||||
- Assigns merchant to transaction if mapping has MerchantId
|
||||
- Returns category string or empty if no match
|
||||
- `GetAllMappingsAsync()` - Retrieve all mappings for management UI
|
||||
- `SeedDefaultMappingsAsync()` - One-time seed of default category rules
|
||||
|
||||
**Default Categories (200+ patterns):**
|
||||
- Online shopping (Amazon, Sephora, Kohls, etc.)
|
||||
- Walmart (separate: Online, Pickup/Grocery)
|
||||
- Pizza chains (Domino's, Papa John's, etc.)
|
||||
- Brick/mortar stores (Dollar General, Kroger, Target, etc.)
|
||||
- Restaurants (McDonald's, Starbucks, Olive Garden, etc.)
|
||||
- School expenses
|
||||
- Health (pharmacies, clinics, dental)
|
||||
- Gas & Auto (high priority, 100)
|
||||
- Utilities/Services (Verizon, Comcast, etc.)
|
||||
- Entertainment (Netflix, Hulu, Steam, etc.)
|
||||
- Banking fees (highest priority, 200)
|
||||
- Mortgage, Car Payment, Insurance
|
||||
- Credit Card payments
|
||||
- Ice Cream/Treats
|
||||
- Government/DMV
|
||||
- Home Improvement
|
||||
- Software/Subscriptions
|
||||
|
||||
**Location:** Services/TransactionCategorizer.cs:31-231
|
||||
|
||||
### DashboardService (Pages/Index.cshtml.cs)
|
||||
**Interface:** `IDashboardService`
|
||||
|
||||
**Responsibility:** Aggregate dashboard statistics and data.
|
||||
|
||||
**Key Methods:**
|
||||
- `GetDashboardDataAsync(int topCategoriesCount, int recentTransactionsCount)`
|
||||
- Orchestrates calls to specialized providers
|
||||
- Returns `DashboardData` DTO
|
||||
|
||||
**Sub-Services:**
|
||||
|
||||
#### IDashboardStatsCalculator
|
||||
- `CalculateAsync()` - Compute aggregate stats:
|
||||
- Total transactions, credits, debits, uncategorized count
|
||||
- Total receipts, total cards
|
||||
|
||||
#### ITopCategoriesProvider
|
||||
- `GetTopCategoriesAsync(int count, int lastDays)`
|
||||
- Aggregates spending by category for last N days (default 90)
|
||||
- Returns top N categories by total spend
|
||||
- Includes transaction count and average per transaction
|
||||
- Excludes transfers
|
||||
|
||||
#### IRecentTransactionsProvider
|
||||
- `GetRecentTransactionsAsync(int count)`
|
||||
- Fetches most recent N transactions (default 20)
|
||||
- Includes card label, receipt count
|
||||
- Sorted by date descending, then ID descending
|
||||
|
||||
**Location:** Pages/Index.cshtml.cs:63-250
|
||||
|
||||
### ReceiptManager (Services/ReceiptManager.cs)
|
||||
**Interface:** `IReceiptManager`
|
||||
|
||||
**Responsibility:** Handle receipt file uploads and storage.
|
||||
|
||||
**Key Methods:**
|
||||
- `UploadReceiptAsync(long transactionId, IFormFile file)`
|
||||
- Validates file (10MB max, allowed extensions: jpg, jpeg, png, pdf, gif, heic)
|
||||
- Computes SHA256 hash for deduplication
|
||||
- Saves file to `wwwroot/receipts/{transactionId}_{guid}{ext}`
|
||||
- Sanitizes filename (removes non-ASCII like ®, ™, ©)
|
||||
- Creates Receipt entity in database
|
||||
- Returns `ReceiptUploadResult`
|
||||
|
||||
- `DeleteReceiptAsync(long receiptId)`
|
||||
- Deletes physical file from disk
|
||||
- Removes database record (cascades to ParseLogs and LineItems)
|
||||
|
||||
- `GetReceiptPhysicalPath(Receipt receipt)` - Resolves storage path
|
||||
- `GetReceiptAsync(long receiptId)` - Retrieves receipt with transaction
|
||||
|
||||
**Security Features:**
|
||||
- File size validation (10MB limit)
|
||||
- Extension whitelist
|
||||
- Filename sanitization
|
||||
- SHA256 deduplication
|
||||
|
||||
**Location:** Services/ReceiptManager.cs:23-199
|
||||
|
||||
### OpenAIReceiptParser (Services/OpenAIReceiptParser.cs)
|
||||
**Interface:** `IReceiptParser`
|
||||
|
||||
**Responsibility:** Parse receipts using OpenAI GPT-4o-mini Vision API.
|
||||
|
||||
**Key Methods:**
|
||||
- `ParseReceiptAsync(long receiptId)`
|
||||
- Loads receipt file from disk
|
||||
- Converts PDFs to PNG images using ImageMagick (220 DPI)
|
||||
- Calls OpenAI Vision API with structured prompt
|
||||
- Parses JSON response (merchant, date, amounts, line items)
|
||||
- Updates Receipt entity with extracted data
|
||||
- Replaces existing line items
|
||||
- Logs parse attempt in ReceiptParseLog
|
||||
- Returns `ReceiptParseResult`
|
||||
|
||||
**API Configuration:**
|
||||
- Model: `gpt-4o-mini`
|
||||
- Temperature: 0.1 (deterministic)
|
||||
- Max tokens: 2000
|
||||
- API key: Environment variable `OPENAI_API_KEY` or config `OpenAI:ApiKey`
|
||||
|
||||
**Prompt Strategy:**
|
||||
- Structured JSON request with schema example
|
||||
- Extracts: merchant, date, subtotal, tax, total, confidence
|
||||
- Line items with: description, quantity, unitPrice, lineTotal
|
||||
- Special handling: Services/fees have null quantity (not products)
|
||||
|
||||
**PDF Handling:**
|
||||
- ImageMagick converts first page to PNG at 220 DPI
|
||||
- Flattens alpha channel (white background)
|
||||
- TrueColor 8-bit RGB output
|
||||
|
||||
**Location:** Services/OpenAIReceiptParser.cs:23-302
|
||||
|
||||
## Data Access Layer
|
||||
|
||||
### MoneyMapContext (Data/MoneyMapContext.cs)
|
||||
EF Core DbContext managing all database entities.
|
||||
|
||||
**DbSets:**
|
||||
- `Accounts` - Bank accounts
|
||||
- `Cards` - Payment cards
|
||||
- `Merchants` - Merchants/vendors
|
||||
- `Transactions` - Bank transactions
|
||||
- `Receipts` - Receipt files
|
||||
- `ReceiptParseLogs` - Parse attempt logs
|
||||
- `ReceiptLineItems` - Receipt line items
|
||||
- `CategoryMappings` - Categorization rules
|
||||
|
||||
**Key Configuration:**
|
||||
- **Transactions ↔ Cards**: Restrict delete (can't delete card with transactions)
|
||||
- **Transactions ↔ Accounts**: Restrict delete (can't delete account with transactions)
|
||||
- **Receipts ↔ Transactions**: Cascade delete (deleting transaction removes receipts)
|
||||
- **ReceiptParseLogs ↔ Receipts**: Cascade delete
|
||||
- **ReceiptLineItems ↔ Receipts**: Cascade delete
|
||||
|
||||
**Performance Indexes:**
|
||||
- Transaction.Date, Transaction.Amount, Transaction.Category (filtering)
|
||||
- Card: (Issuer, Last4, Owner) composite
|
||||
- Transaction: (Date, Amount, Name, Memo, AccountId, CardId) unique constraint
|
||||
- Receipt: (TransactionId, FileHashSha256) unique constraint
|
||||
|
||||
**Location:** Data/MoneyMapContext.cs:9-107
|
||||
|
||||
## Razor Pages (UI Layer)
|
||||
|
||||
### Index.cshtml / IndexModel (Dashboard)
|
||||
**Route:** `/`
|
||||
|
||||
**Purpose:** Main dashboard showing financial overview.
|
||||
|
||||
**Displays:**
|
||||
- Aggregate stats (total transactions, credits, debits, uncategorized, receipts, cards)
|
||||
- Top 8 spending categories (last 90 days) with average per transaction
|
||||
- Recent 20 transactions with card labels and receipt counts
|
||||
|
||||
**Dependencies:** `IDashboardService`
|
||||
|
||||
**Location:** Pages/Index.cshtml.cs:12-250
|
||||
|
||||
### Upload.cshtml / UploadModel (CSV Import)
|
||||
**Route:** `/Upload`
|
||||
|
||||
**Purpose:** Import bank transactions from CSV files.
|
||||
|
||||
**Features:**
|
||||
- Card selection mode: Auto (extract from memo/filename) or Manual (user selects)
|
||||
- Duplicate detection (unique constraint on Date+Amount+Name+Memo+AccountId+CardId)
|
||||
- Auto-creates cards if not found in Auto mode
|
||||
- Displays import stats (total, inserted, skipped)
|
||||
|
||||
**Form Inputs:**
|
||||
- CSV file
|
||||
- Card mode (radio: Auto/Manual)
|
||||
- Card dropdown (if Manual mode)
|
||||
|
||||
**Dependencies:** `ITransactionImporter`
|
||||
|
||||
**Location:** Pages/Upload.cshtml.cs:17-333
|
||||
|
||||
### Transactions.cshtml / TransactionsModel
|
||||
**Route:** `/Transactions`
|
||||
|
||||
**Purpose:** View and search all transactions.
|
||||
|
||||
**Features:**
|
||||
- Search by name or memo
|
||||
- Filter by card, category, date range
|
||||
- Sort by date, amount, name, category
|
||||
- Pagination
|
||||
- Receipt count indicator
|
||||
- Edit transaction link
|
||||
|
||||
**Location:** Pages/Transactions.cshtml.cs
|
||||
|
||||
### EditTransaction.cshtml / EditTransactionModel
|
||||
**Route:** `/EditTransaction/{id}`
|
||||
|
||||
**Purpose:** Edit transaction details.
|
||||
|
||||
**Features:**
|
||||
- Update category (select existing or create new)
|
||||
- Update merchant (select existing or create new)
|
||||
- Update notes
|
||||
- Upload/delete receipts
|
||||
- View receipt thumbnails with parsed data
|
||||
- Trigger AI receipt parsing
|
||||
|
||||
**Location:** Pages/EditTransaction.cshtml.cs:27-202
|
||||
|
||||
### ViewReceipt.cshtml / ViewReceiptModel
|
||||
**Route:** `/ViewReceipt/{id}`
|
||||
|
||||
**Purpose:** View receipt details and parsed data.
|
||||
|
||||
**Displays:**
|
||||
- Receipt image/PDF preview
|
||||
- File metadata (name, size, upload date)
|
||||
- Parsed merchant, date, amounts
|
||||
- Line items table (description, quantity, unit price, total)
|
||||
- Parse logs (provider, model, confidence, timestamp, errors)
|
||||
- Actions: Re-parse, Delete
|
||||
|
||||
**Dependencies:** `IReceiptManager`, `IReceiptParser`
|
||||
|
||||
**Location:** Pages/ViewReceipt.cshtml.cs:16-126
|
||||
|
||||
### CategoryMappings.cshtml / CategoryMappingsModel
|
||||
**Route:** `/CategoryMappings`
|
||||
|
||||
**Purpose:** Manage category mapping rules.
|
||||
|
||||
**Features:**
|
||||
- View all category patterns grouped by category
|
||||
- Add/edit/delete mappings
|
||||
- Assign merchant to mapping (creates normalized merchant name)
|
||||
- Priority management (higher = checked first)
|
||||
- Seed default mappings
|
||||
- Export/import mappings as JSON
|
||||
|
||||
**Dependencies:** `ITransactionCategorizer`
|
||||
|
||||
**Location:** Pages/CategoryMappings.cshtml.cs:16-91
|
||||
|
||||
### Merchants.cshtml / MerchantsModel
|
||||
**Route:** `/Merchants`
|
||||
|
||||
**Purpose:** Manage merchant entities.
|
||||
|
||||
**Features:**
|
||||
- View all merchants
|
||||
- Edit merchant names
|
||||
- Delete merchants (with confirmation)
|
||||
- See transaction count per merchant
|
||||
|
||||
**Location:** Pages/Merchants.cshtml.cs
|
||||
|
||||
### Recategorize.cshtml / RecategorizeModel
|
||||
**Route:** `/Recategorize`
|
||||
|
||||
**Purpose:** Bulk recategorize uncategorized transactions.
|
||||
|
||||
**Features:**
|
||||
- Lists all transactions without a category
|
||||
- Applies TransactionCategorizer rules
|
||||
- Updates matching transactions
|
||||
- Reports counts (total, categorized, still uncategorized)
|
||||
|
||||
**Dependencies:** `ITransactionCategorizer`
|
||||
|
||||
**Location:** Pages/Recategorize.cshtml.cs:16-87
|
||||
|
||||
## Configuration
|
||||
|
||||
### appsettings.json
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"MoneyMapDb": "Server=...;Database=MoneyMap;..."
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "sk-..." // Optional, can use OPENAI_API_KEY env var
|
||||
},
|
||||
"Receipts": {
|
||||
"StoragePath": "receipts" // Relative to wwwroot
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Program.cs Dependency Injection
|
||||
```csharp
|
||||
// Database
|
||||
builder.Services.AddDbContext<MoneyMapContext>(options =>
|
||||
options.UseSqlServer(...));
|
||||
|
||||
// Transaction Services
|
||||
builder.Services.AddScoped<ITransactionImporter, TransactionImporter>();
|
||||
builder.Services.AddScoped<ICardResolver, CardResolver>();
|
||||
builder.Services.AddScoped<ITransactionCategorizer, TransactionCategorizer>();
|
||||
|
||||
// Dashboard Services
|
||||
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
||||
builder.Services.AddScoped<IDashboardStatsCalculator, DashboardStatsCalculator>();
|
||||
builder.Services.AddScoped<ITopCategoriesProvider, TopCategoriesProvider>();
|
||||
builder.Services.AddScoped<IRecentTransactionsProvider, RecentTransactionsProvider>();
|
||||
|
||||
// Receipt Services
|
||||
builder.Services.AddScoped<IReceiptManager, ReceiptManager>();
|
||||
builder.Services.AddHttpClient<IReceiptParser, OpenAIReceiptParser>();
|
||||
```
|
||||
|
||||
**Location:** Program.cs:1-44
|
||||
|
||||
## Database Schema (SQL Server)
|
||||
|
||||
### Accounts Table
|
||||
```sql
|
||||
Id (int, PK)
|
||||
Issuer (nvarchar(100), NOT NULL)
|
||||
Last4 (nvarchar(4), NOT NULL)
|
||||
Owner (nvarchar(100))
|
||||
INDEX: (Issuer, Last4, Owner)
|
||||
```
|
||||
|
||||
### Cards Table
|
||||
```sql
|
||||
Id (int, PK)
|
||||
AccountId (int, FK → Accounts.Id, RESTRICT)
|
||||
Issuer (nvarchar(100), NOT NULL)
|
||||
Last4 (nvarchar(4), NOT NULL)
|
||||
Owner (nvarchar(100))
|
||||
INDEX: (Issuer, Last4, Owner)
|
||||
```
|
||||
|
||||
### Merchants Table
|
||||
```sql
|
||||
Id (int, PK)
|
||||
Name (nvarchar(100), NOT NULL)
|
||||
```
|
||||
|
||||
### Transactions Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
Date (datetime2, NOT NULL)
|
||||
TransactionType (nvarchar(20))
|
||||
Name (nvarchar(200), NOT NULL)
|
||||
Memo (nvarchar(500))
|
||||
Amount (decimal(18,2), NOT NULL)
|
||||
Category (nvarchar(100))
|
||||
MerchantId (int, FK → Merchants.Id, SET NULL)
|
||||
Notes (nvarchar(max))
|
||||
AccountId (int, FK → Accounts.Id, RESTRICT)
|
||||
CardId (int, FK → Cards.Id, RESTRICT)
|
||||
TransferToAccountId (int, FK → Accounts.Id, RESTRICT)
|
||||
Last4 (nvarchar(4))
|
||||
UNIQUE INDEX: (Date, Amount, Name, Memo, AccountId, CardId)
|
||||
INDEX: Date, Amount, Category
|
||||
```
|
||||
|
||||
### Receipts Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
TransactionId (bigint, FK → Transactions.Id, CASCADE)
|
||||
FileName (nvarchar(260), NOT NULL)
|
||||
ContentType (nvarchar(100), DEFAULT 'application/octet-stream')
|
||||
StoragePath (nvarchar(1024), NOT NULL)
|
||||
FileSizeBytes (bigint, NOT NULL)
|
||||
FileHashSha256 (nvarchar(64), NOT NULL)
|
||||
UploadedAtUtc (datetime2, NOT NULL)
|
||||
Merchant (nvarchar(200))
|
||||
ReceiptDate (datetime2)
|
||||
Subtotal (decimal(18,2))
|
||||
Tax (decimal(18,2))
|
||||
Total (decimal(18,2))
|
||||
Currency (nvarchar(8))
|
||||
UNIQUE INDEX: (TransactionId, FileHashSha256)
|
||||
```
|
||||
|
||||
### ReceiptParseLogs Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
ReceiptId (bigint, FK → Receipts.Id, CASCADE)
|
||||
Provider (nvarchar(50), NOT NULL)
|
||||
Model (nvarchar(100), NOT NULL)
|
||||
StartedAtUtc (datetime2, NOT NULL)
|
||||
CompletedAtUtc (datetime2)
|
||||
Success (bit, NOT NULL)
|
||||
Confidence (decimal(18,2))
|
||||
Error (nvarchar(max))
|
||||
ProviderJobId (nvarchar(100))
|
||||
ExtractedTextPath (nvarchar(1024))
|
||||
RawProviderPayloadJson (nvarchar(max))
|
||||
```
|
||||
|
||||
### ReceiptLineItems Table
|
||||
```sql
|
||||
Id (bigint, PK)
|
||||
ReceiptId (bigint, FK → Receipts.Id, CASCADE)
|
||||
LineNumber (int, NOT NULL)
|
||||
Description (nvarchar(300), NOT NULL)
|
||||
Quantity (decimal(18,4))
|
||||
UnitPrice (decimal(18,4))
|
||||
LineTotal (decimal(18,2), NOT NULL)
|
||||
Unit (nvarchar(16))
|
||||
Sku (nvarchar(64))
|
||||
Category (nvarchar(100))
|
||||
```
|
||||
|
||||
### CategoryMappings Table
|
||||
```sql
|
||||
Id (int, PK)
|
||||
Category (nvarchar(max), NOT NULL)
|
||||
Pattern (nvarchar(max), NOT NULL)
|
||||
MerchantId (int, FK → Merchants.Id, SET NULL)
|
||||
Priority (int, NOT NULL, DEFAULT 0)
|
||||
```
|
||||
|
||||
## Key Workflows
|
||||
|
||||
### 1. Import Transactions from CSV
|
||||
|
||||
```
|
||||
User uploads CSV file
|
||||
↓
|
||||
UploadModel.OnPostAsync()
|
||||
↓
|
||||
TransactionImporter.ImportAsync()
|
||||
↓
|
||||
For each CSV row:
|
||||
CardResolver.ResolveCardAsync() → Get/Create Card
|
||||
Check for duplicates (DB + batch)
|
||||
Add to batch if unique
|
||||
↓
|
||||
DbContext.SaveChangesAsync()
|
||||
↓
|
||||
Display import stats (inserted, skipped)
|
||||
```
|
||||
|
||||
### 2. Auto-Categorize Transaction
|
||||
|
||||
```
|
||||
Transaction inserted with Name="KROGER #123"
|
||||
↓
|
||||
TransactionCategorizer.CategorizeAsync("KROGER #123")
|
||||
↓
|
||||
Load CategoryMappings (ordered by Priority DESC)
|
||||
↓
|
||||
Check special case: Gas station with small purchase?
|
||||
↓
|
||||
Iterate mappings:
|
||||
If "KROGER".Contains(pattern.ToUpper()) → Match!
|
||||
Return "Brick/mortar store"
|
||||
If mapping has MerchantId → Assign to transaction
|
||||
↓
|
||||
Update Transaction.Category and Transaction.MerchantId
|
||||
```
|
||||
|
||||
### 3. Upload and Parse Receipt
|
||||
|
||||
```
|
||||
User attaches receipt to transaction
|
||||
↓
|
||||
EditTransactionModel.OnPostUploadReceiptAsync()
|
||||
↓
|
||||
ReceiptManager.UploadReceiptAsync()
|
||||
- Validate file (size, extension)
|
||||
- Compute SHA256 hash
|
||||
- Check for duplicate (TransactionId + Hash)
|
||||
- Save to wwwroot/receipts/
|
||||
- Create Receipt entity
|
||||
↓
|
||||
User clicks "Parse Receipt"
|
||||
↓
|
||||
ViewReceiptModel.OnPostParseAsync()
|
||||
↓
|
||||
OpenAIReceiptParser.ParseReceiptAsync()
|
||||
- Load receipt file
|
||||
- If PDF: Convert to PNG with ImageMagick
|
||||
- Encode as base64
|
||||
- Call OpenAI Vision API with structured prompt
|
||||
- Parse JSON response
|
||||
- Update Receipt (merchant, date, amounts)
|
||||
- Replace ReceiptLineItems
|
||||
- Create ReceiptParseLog
|
||||
↓
|
||||
Display parsed data
|
||||
```
|
||||
|
||||
### 4. Dashboard Aggregation
|
||||
|
||||
```
|
||||
User visits homepage (/)
|
||||
↓
|
||||
IndexModel.OnGet()
|
||||
↓
|
||||
DashboardService.GetDashboardDataAsync()
|
||||
↓
|
||||
Parallel data fetching:
|
||||
- DashboardStatsCalculator → Total txns, credits, debits, uncategorized, receipts, cards
|
||||
- TopCategoriesProvider → Top 8 categories by spend (last 90 days) with average per transaction
|
||||
- RecentTransactionsProvider → Latest 20 transactions
|
||||
↓
|
||||
Return DashboardData DTO
|
||||
↓
|
||||
Render dashboard view
|
||||
```
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### 1. Service Layer Pattern
|
||||
All business logic isolated in service interfaces/implementations:
|
||||
- Testable (mock services in unit tests)
|
||||
- Reusable across pages
|
||||
- Single Responsibility Principle
|
||||
|
||||
### 2. Repository Pattern (via EF Core DbContext)
|
||||
- MoneyMapContext encapsulates data access
|
||||
- LINQ queries in service layer
|
||||
- Change tracking and unit of work handled by EF Core
|
||||
|
||||
### 3. Result Pattern
|
||||
Services return result objects instead of throwing exceptions:
|
||||
- `ImportOperationResult` (success/failure with data or error message)
|
||||
- `CardResolutionResult`
|
||||
- `ReceiptUploadResult`
|
||||
- `ReceiptParseResult`
|
||||
|
||||
Benefits: Explicit error handling, no try-catch clutter, clear success/failure paths
|
||||
|
||||
### 4. Dependency Injection
|
||||
All services registered in Program.cs:
|
||||
- Scoped lifetime for per-request services
|
||||
- HttpClient factory for IReceiptParser
|
||||
- Testability via interface injection
|
||||
|
||||
### 5. Data Transfer Objects (DTOs)
|
||||
Separate DTOs for data transfer between layers:
|
||||
- `DashboardData` (aggregates stats + categories + recent transactions)
|
||||
- `ImportContext` (encapsulates import parameters)
|
||||
- `TransactionKey` (value object for deduplication)
|
||||
- Page model record types (TopCategoryRow, RecentTxnRow, etc.)
|
||||
|
||||
### 6. Strategy Pattern
|
||||
CardResolver uses strategy based on CardSelectMode:
|
||||
- Auto strategy: Extract from memo/filename
|
||||
- Manual strategy: Use user selection
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. File Upload Security
|
||||
- File size limit: 10MB
|
||||
- Extension whitelist: jpg, jpeg, png, pdf, gif, heic
|
||||
- Filename sanitization: Remove non-ASCII characters (®, ™, ©)
|
||||
- SHA256 hash validation for deduplication
|
||||
- Files stored outside database (wwwroot/receipts/)
|
||||
|
||||
### 2. SQL Injection Prevention
|
||||
- Parameterized queries via EF Core
|
||||
- LINQ expressions compile to safe SQL
|
||||
|
||||
### 3. API Key Management
|
||||
- OpenAI API key from environment variable (preferred) or config
|
||||
- Not hardcoded in source
|
||||
|
||||
### 4. Delete Constraints
|
||||
- Restrict delete on Cards (can't delete if transactions exist)
|
||||
- Restrict delete on Accounts (can't delete if transactions exist)
|
||||
- Cascade delete on Receipts (deleting transaction removes receipts)
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Database Indexes
|
||||
- Transactions: Date, Amount, Category (for filtering/sorting)
|
||||
- Unique indexes on duplicate detection columns
|
||||
- Composite index on Cards (Issuer, Last4, Owner)
|
||||
|
||||
### 2. Batch Processing
|
||||
- CSV import: Bulk insert with single SaveChangesAsync()
|
||||
- In-memory duplicate checking (HashSet) avoids N+1 queries
|
||||
|
||||
### 3. Pagination
|
||||
- Transactions list uses pagination to limit result sets
|
||||
- Dashboard limits top categories (8) and recent transactions (20)
|
||||
|
||||
### 4. AsNoTracking
|
||||
- Read-only queries use `.AsNoTracking()` to reduce EF overhead
|
||||
- TopCategoriesProvider, RecentTransactionsProvider
|
||||
|
||||
### 5. PDF Conversion
|
||||
- ImageMagick runs on ThreadPool (Task.Run) to avoid blocking
|
||||
- First page only for multi-page PDFs
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
### Unit Testing Strategy
|
||||
All service interfaces are mockable:
|
||||
```csharp
|
||||
// Example: Test TransactionImporter
|
||||
var mockCardResolver = new Mock<ICardResolver>();
|
||||
var mockDb = new Mock<MoneyMapContext>();
|
||||
var importer = new TransactionImporter(mockDb.Object, mockCardResolver.Object);
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
- In-memory database for DbContext tests
|
||||
- Test CSV import end-to-end
|
||||
- Validate categorization rules
|
||||
|
||||
### External Dependencies
|
||||
- OpenAI API: Mock IReceiptParser for tests
|
||||
- File system: Mock IWebHostEnvironment
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Features
|
||||
1. **Multi-user support**: Add authentication/authorization
|
||||
2. **Budget tracking**: Monthly budget limits per category
|
||||
3. **Recurring transactions**: Auto-detect and predict recurring expenses
|
||||
4. **Data export**: Export transactions to Excel/CSV
|
||||
5. **Charts/graphs**: Spending trends over time
|
||||
6. **Mobile app**: React Native or .NET MAUI
|
||||
7. **Bank API integration**: Direct import from bank APIs (Plaid, Yodlee)
|
||||
8. **Receipt search**: Full-text search on parsed line items
|
||||
9. **Split transactions**: Divide single transaction across categories
|
||||
10. **Tags**: Tag transactions with multiple labels
|
||||
|
||||
### Technical Debt
|
||||
1. Move inline service implementations to separate files (Upload.cshtml.cs is 333 lines)
|
||||
2. Add logging (ILogger) to all services
|
||||
3. Add retry logic for OpenAI API calls (Polly)
|
||||
4. Implement background job processing for receipt parsing (Hangfire)
|
||||
5. Add integration tests for critical workflows
|
||||
6. Implement caching for CategoryMappings (IMemoryCache)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Duplicate Transaction Errors**
|
||||
- **Cause**: Unique constraint violation (Date + Amount + Name + Memo + AccountId + CardId)
|
||||
- **Solution**: Transactions are skipped automatically during import
|
||||
|
||||
**2. Receipt Parsing Failures**
|
||||
- **Cause**: OpenAI API key missing or invalid
|
||||
- **Solution**: Set `OPENAI_API_KEY` environment variable or `OpenAI:ApiKey` in appsettings.json
|
||||
- **Cause**: PDF conversion error
|
||||
- **Solution**: Ensure Magick.NET is properly installed
|
||||
|
||||
**3. Card Not Found**
|
||||
- **Cause**: Auto mode can't extract last 4 digits from memo or filename
|
||||
- **Solution**: Use Manual mode and select card from dropdown, or rename CSV file to include card digits (e.g., `transactions-1234.csv`)
|
||||
|
||||
**4. Slow Dashboard Load**
|
||||
- **Cause**: Large transaction dataset without indexes
|
||||
- **Solution**: Ensure migrations have run (indexes on Date, Amount, Category)
|
||||
|
||||
**5. File Upload Fails**
|
||||
- **Cause**: File exceeds 10MB or unsupported format
|
||||
- **Solution**: Resize/compress image, or use supported format
|
||||
|
||||
## Deployment
|
||||
|
||||
### Database Migration
|
||||
```bash
|
||||
# Create migration
|
||||
dotnet ef migrations add MigrationName
|
||||
|
||||
# Update database
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
1. Update connection string in appsettings.json (Azure SQL, AWS RDS, etc.)
|
||||
2. Set `OPENAI_API_KEY` environment variable
|
||||
3. Configure `Receipts:StoragePath` (consider Azure Blob Storage)
|
||||
4. Enable HTTPS and HSTS
|
||||
5. Set `ASPNETCORE_ENVIRONMENT=Production`
|
||||
|
||||
### File Storage Considerations
|
||||
- **Local**: wwwroot/receipts (current implementation)
|
||||
- **Production**: Azure Blob Storage, AWS S3, or network share
|
||||
- Modify ReceiptManager to use cloud storage SDK
|
||||
- Update StoragePath format (blob URL instead of relative path)
|
||||
|
||||
## Conclusion
|
||||
|
||||
MoneyMap demonstrates a well-architected ASP.NET Core application with clear separation of concerns, testable services, and modern AI integration. The service layer pattern allows for easy maintenance and extension, while the Result pattern provides explicit error handling. The OpenAI receipt parsing feature showcases practical LLM integration for automating tedious data entry tasks.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-10-12
|
||||
**Version:** 1.1
|
||||
**Framework:** ASP.NET Core 8.0 / EF Core 9.0
|
||||
121
CLAUDE.md
Normal file
121
CLAUDE.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# MoneyMap - Claude Code Context
|
||||
|
||||
## Quick Overview
|
||||
|
||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application for personal finance tracking. Users can import bank transaction CSVs, categorize expenses, attach receipt images/PDFs, and parse receipts using OpenAI's Vision API.
|
||||
|
||||
## Architecture Documentation
|
||||
|
||||
**For detailed technical documentation, see [ARCHITECTURE.md](./ARCHITECTURE.md)**
|
||||
|
||||
The architecture document contains:
|
||||
- Complete technology stack
|
||||
- Core domain models (Transaction, Receipt, Card, Account, Merchant, etc.)
|
||||
- Service layer details (TransactionImporter, CardResolver, TransactionCategorizer, etc.)
|
||||
- Database schema and relationships
|
||||
- Key workflows and design patterns
|
||||
- Security and performance considerations
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
MoneyMap/
|
||||
├── Data/
|
||||
│ └── MoneyMapContext.cs # EF Core DbContext
|
||||
├── Models/
|
||||
│ ├── Account.cs # Bank accounts
|
||||
│ ├── Card.cs # Payment cards
|
||||
│ ├── Merchant.cs # Merchants/vendors
|
||||
│ ├── Transaction.cs # Core transaction entity
|
||||
│ ├── Receipt.cs # Receipt files
|
||||
│ ├── ReceiptLineItem.cs # Parsed line items
|
||||
│ └── ReceiptParseLog.cs # Parse attempt logs
|
||||
├── Services/
|
||||
│ ├── TransactionCategorizer.cs # Auto-categorization logic
|
||||
│ ├── ReceiptManager.cs # Receipt upload/storage
|
||||
│ └── OpenAIReceiptParser.cs # AI-powered receipt parsing
|
||||
├── Pages/
|
||||
│ ├── Index.cshtml[.cs] # Dashboard
|
||||
│ ├── Upload.cshtml[.cs] # CSV import
|
||||
│ ├── Transactions.cshtml[.cs] # Transaction list
|
||||
│ ├── EditTransaction.cshtml[.cs] # Edit transaction
|
||||
│ ├── ViewReceipt.cshtml[.cs] # Receipt details
|
||||
│ ├── CategoryMappings.cshtml[.cs]# Category rules
|
||||
│ ├── Merchants.cshtml[.cs] # Merchant management
|
||||
│ └── Recategorize.cshtml[.cs] # Bulk recategorization
|
||||
└── Program.cs # DI configuration
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Page
|
||||
1. Create `.cshtml` and `.cshtml.cs` files in `Pages/`
|
||||
2. Inherit from `PageModel`
|
||||
3. Add route via `@page` directive
|
||||
4. Register dependencies in constructor via DI
|
||||
|
||||
### Adding a New Service
|
||||
1. Create interface in appropriate namespace
|
||||
2. Create implementation class
|
||||
3. Register in `Program.cs`:
|
||||
```csharp
|
||||
builder.Services.AddScoped<IMyService, MyService>();
|
||||
```
|
||||
|
||||
### Adding a Database Migration
|
||||
```bash
|
||||
dotnet ef migrations add MigrationName
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
### Modifying Domain Models
|
||||
1. Update model class in `Models/`
|
||||
2. Update `MoneyMapContext.OnModelCreating()` if needed (relationships, indexes)
|
||||
3. Create and apply migration
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Service Layer Pattern**: Business logic lives in services, not pages
|
||||
2. **Result Pattern**: Services return result objects (not exceptions)
|
||||
3. **Dependency Injection**: All services injected via interfaces
|
||||
4. **Single Responsibility**: Each service has one clear purpose
|
||||
5. **Clean Architecture**: UI → Services → Data Access
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Duplicate Prevention**: Transactions have a unique constraint on (Date, Amount, Name, Memo, AccountId, CardId)
|
||||
- **Cascade Deletes**: Deleting a transaction cascades to receipts, parse logs, and line items
|
||||
- **Merchant Assignment**: Category mappings can auto-assign merchants to transactions
|
||||
- **Transfer Detection**: Transactions with `TransferToAccountId` are identified as transfers
|
||||
- **Receipt Parsing**: OpenAI API key required for receipt parsing (env var `OPENAI_API_KEY`)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Read [ARCHITECTURE.md](./ARCHITECTURE.md) for technical details
|
||||
2. Make changes to models, services, or pages
|
||||
3. Test locally
|
||||
4. Create database migration if schema changed
|
||||
5. Commit with descriptive message
|
||||
|
||||
## Configuration
|
||||
|
||||
See `appsettings.json`:
|
||||
- `ConnectionStrings:MoneyMapDb` - SQL Server connection
|
||||
- `OpenAI:ApiKey` - OpenAI API key (optional, use env var instead)
|
||||
- `Receipts:StoragePath` - Receipt storage location (relative to wwwroot)
|
||||
|
||||
## Testing
|
||||
|
||||
- All services use interfaces for mockability
|
||||
- Use in-memory database for integration tests
|
||||
- Mock `IReceiptParser` to avoid OpenAI API calls in tests
|
||||
|
||||
## Questions?
|
||||
|
||||
Refer to [ARCHITECTURE.md](./ARCHITECTURE.md) for comprehensive technical documentation including:
|
||||
- Detailed service descriptions
|
||||
- Database schema
|
||||
- Key workflows
|
||||
- Security considerations
|
||||
- Performance optimizations
|
||||
- Troubleshooting guide
|
||||
Reference in New Issue
Block a user