Add AI categorization service that suggests categories, merchants, and
rules for uncategorized transactions. Users can review and approve
suggestions before applying them.
Features:
- TransactionAICategorizer service using OpenAI GPT-4o-mini
- Batch processing (5 transactions at a time) to avoid rate limits
- Confidence scoring (0-100%) for each suggestion
- AI suggests category, canonical merchant name, and pattern rule
- ReviewAISuggestions page to list uncategorized transactions
- ReviewAISuggestionsWithProposals page for manual review
- Apply individual suggestions or bulk apply high confidence (≥80%)
- Optional rule creation for future auto-categorization
- Cost: ~$0.00015 per transaction (~$0.015 per 100)
CategoryMapping enhancements:
- Confidence field to track AI confidence score
- CreatedBy field ("AI" or "User") to track rule origin
- CreatedAt timestamp for audit trail
Updated ARCHITECTURE.md with complete documentation of:
- TransactionAICategorizer service details
- ReviewAISuggestions page descriptions
- AI categorization workflow (Phase 1)
- Updated CategoryMappings schema
Next steps (Phase 2):
- Auto-apply high confidence suggestions
- Background job processing
- Batch API requests for better efficiency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
34 KiB
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 keyIssuer(string, 100) - Institution nameLast4(string, 4) - Last 4 digits of account numberOwner(string, 100) - Account owner name (optional)DisplayLabel(string, computed) - Formatted label for displayCards- Collection of cards linked to this accountTransactions- Collection of transactions for this account
Card (Models/Card.cs)
Represents a payment card (credit/debit) linked to an account.
Properties:
Id(int) - Primary keyAccountId(int) - Foreign key to AccountIssuer(string, 100) - Card issuer (VISA, MC, etc.)Last4(string, 4) - Last 4 digits of card numberOwner(string, 100) - Cardholder name (optional)DisplayLabel(string, computed) - Formatted label for displayTransactions- Navigation property to transactions
Merchant (Models/Merchant.cs)
Represents a merchant/vendor where transactions occur.
Properties:
Id(int) - Primary keyName(string, 100) - Merchant nameTransactions- Collection of transactions with this merchantCategoryMappings- Collection of category mapping rules
Transaction (Models/Transaction.cs)
Core entity representing a bank transaction.
Properties:
Id(long) - Primary keyDate(DateTime) - Transaction dateTransactionType(string, 20) - "DEBIT"/"CREDIT"Name(string, 200) - Transaction name from CSVMemo(string, 500) - Transaction descriptionAmount(decimal) - Amount (negative = debit, positive = credit)Category(string, 100) - Expense categoryMerchantId(int?) - Foreign key to MerchantNotes(string) - User notesAccountId(int) - Foreign key to Account (required)CardId(int?) - Foreign key to Card (optional)TransferToAccountId(int?) - Foreign key for transfersLast4(string, 4) - Cached last 4 digits from memoReceipts- Collection of attached receipts
Unique Index: Date + Amount + Name + Memo + AccountId + CardId (prevents duplicates)
Computed Properties:
IsCredit- Returns true if Amount > 0IsDebit- Returns true if Amount < 0IsTransfer- Returns true if TransferToAccountId is setPaymentMethodLabel- Formatted payment method string
Receipt (Models/Receipt.cs)
Stores uploaded receipt files (images/PDFs) linked to transactions.
Properties:
Id(long) - Primary keyTransactionId(long) - Foreign key to TransactionFileName(string, 260) - Original file name (sanitized)ContentType(string, 100) - MIME typeStoragePath(string, 1024) - Relative path in wwwrootFileSizeBytes(long) - File sizeFileHashSha256(string, 64) - SHA256 hash for deduplicationUploadedAtUtc(DateTime) - Upload timestamp
Parsed Fields (populated by AI parser):
Merchant(string, 200) - Merchant name extracted from receiptReceiptDate(DateTime?) - Date on receiptSubtotal,Tax,Total(decimal?) - Monetary amountsCurrency(string, 8) - Currency code
Navigation Properties:
ParseLogs- Collection of parse attemptsLineItems- 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 keyReceiptId(long) - Foreign key to ReceiptLineNumber(int) - Sequential line numberDescription(string, 300) - Item descriptionQuantity(decimal?) - Quantity purchased (null for services/fees)UnitPrice(decimal?) - Price per unitLineTotal(decimal) - Total for this line
ReceiptParseLog (Models/ReceiptParseLog.cs)
Logs each receipt parsing attempt for auditing.
Properties:
Id(long) - Primary keyReceiptId(long) - Foreign key to ReceiptProvider(string, 50) - "OpenAI"Model(string, 100) - Model used (e.g., "gpt-4o-mini")StartedAtUtc,CompletedAtUtc(DateTime)Success(bool) - Parse success statusConfidence(decimal?) - AI confidence scoreError(string?) - Error message if failedRawProviderPayloadJson(string?) - Full API response
CategoryMapping (Services/TransactionCategorizer.cs)
Pattern-based rules for auto-categorization with merchant linking.
Properties:
Id(int) - Primary keyCategory(string) - Category namePattern(string) - Merchant name pattern to matchMerchantId(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
ImportOperationResultwith stats
Workflow:
- Read CSV with flexible header mapping
- 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
- Bulk save to database
- 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})\bextracts 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 UISeedDefaultMappingsAsync()- 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
DashboardDataDTO
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_KEYor configOpenAI: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
TransactionAICategorizer (Services/TransactionAICategorizer.cs)
Interface: ITransactionAICategorizer
Responsibility: AI-powered categorization for uncategorized transactions.
Key Methods:
-
ProposeCategorizationAsync(Transaction transaction)- Analyzes transaction details (name, memo, amount, date)
- Calls OpenAI GPT-4o-mini with categorization prompt
- Returns
AICategoryProposalwith category, merchant, pattern, and confidence - Auto-suggests rule creation for high confidence (≥70%)
-
ProposeBatchCategorizationAsync(List<Transaction> transactions)- Processes transactions in batches of 5 to avoid rate limits
- Returns list of proposals for review
-
ApplyProposalAsync(long transactionId, AICategoryProposal proposal, bool createRule)- Updates transaction category and merchant
- Optionally creates CategoryMapping rule for future auto-categorization
- Returns
ApplyProposalResultwith success status
API Configuration:
- Model:
gpt-4o-mini - Temperature: 0.1 (deterministic)
- Max tokens: 300
- API key: Environment variable
OPENAI_API_KEYor configOpenAI:ApiKey - Cost:
$0.00015 per transaction ($0.015 per 100 transactions)
Prompt Strategy:
- Provides transaction details (name, memo, amount, date)
- Requests JSON response with category, canonical_merchant, pattern, confidence, reasoning
- Includes common category examples for context
- High confidence threshold (≥70%) suggests automatic rule creation
CategoryMapping Enhancements:
Confidence(decimal?) - AI confidence score (0.0-1.0)CreatedBy(string?) - "AI" or "User"CreatedAt(DateTime?) - Rule creation timestamp
Location: Services/TransactionAICategorizer.cs
Data Access Layer
MoneyMapContext (Data/MoneyMapContext.cs)
EF Core DbContext managing all database entities.
DbSets:
Accounts- Bank accountsCards- Payment cardsMerchants- Merchants/vendorsTransactions- Bank transactionsReceipts- Receipt filesReceiptParseLogs- Parse attempt logsReceiptLineItems- Receipt line itemsCategoryMappings- 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
ReviewAISuggestions.cshtml / ReviewAISuggestionsModel
Route: /ReviewAISuggestions
Purpose: AI-powered categorization suggestions for uncategorized transactions.
Features:
- Lists up to 50 most recent uncategorized transactions
- Generate AI suggestions button (processes up to 20 at a time)
- Cost transparency (~$0.00015 per transaction)
- Link to view uncategorized transactions
Dependencies: ITransactionAICategorizer
Location: Pages/ReviewAISuggestions.cshtml.cs
ReviewAISuggestionsWithProposals.cshtml / ReviewAISuggestionsWithProposalsModel
Route: /ReviewAISuggestionsWithProposals
Purpose: Review and apply AI categorization proposals.
Features:
- Display AI proposals with confidence scores
- Color-coded confidence indicators (green ≥80%, yellow 60-79%, red <60%)
- Individual actions: Accept (with/without rule), Reject, Edit Manually
- Bulk action: Apply all high-confidence suggestions (≥80%)
- Shows AI reasoning for each suggestion
- Stores proposals in session for review workflow
Dependencies: ITransactionAICategorizer
Location: Pages/ReviewAISuggestionsWithProposals.cshtml.cs
Configuration
appsettings.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
// 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
Id (int, PK)
Issuer (nvarchar(100), NOT NULL)
Last4 (nvarchar(4), NOT NULL)
Owner (nvarchar(100))
INDEX: (Issuer, Last4, Owner)
Cards Table
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
Id (int, PK)
Name (nvarchar(100), NOT NULL)
Transactions Table
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
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
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
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
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)
Confidence (decimal(18,2), NULL) -- AI confidence score
CreatedBy (nvarchar(max), NULL) -- "AI" or "User"
CreatedAt (datetime2, NULL) -- Rule creation timestamp
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
5. AI-Powered Categorization (Phase 1 - Manual Review)
User visits /ReviewAISuggestions
↓
ReviewAISuggestionsModel.OnGetAsync()
- Loads up to 50 recent uncategorized transactions
↓
User clicks "Generate AI Suggestions"
↓
ReviewAISuggestionsModel.OnPostGenerateSuggestionsAsync()
- Fetches up to 20 uncategorized transactions
- Calls TransactionAICategorizer.ProposeBatchCategorizationAsync()
↓
For each transaction (batches of 5):
TransactionAICategorizer.ProposeCategorizationAsync()
- Builds prompt with transaction details
- Calls OpenAI GPT-4o-mini API
- Parses JSON response
- Returns AICategoryProposal
↓
Store proposals in session
Redirect to /ReviewAISuggestionsWithProposals
↓
User reviews proposals with confidence scores
↓
User actions:
Option A: Apply + Create Rule
- Updates transaction category and merchant
- Creates CategoryMapping rule (CreatedBy="AI")
- Future similar transactions auto-categorized
Option B: Apply (No Rule)
- Updates transaction only
- No rule created
Option C: Reject
- Removes proposal from session
Option D: Edit Manually
- Redirects to EditTransaction page
↓
Proposal applied
Remove from session
Display success message
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)CardResolutionResultReceiptUploadResultReceiptParseResult
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:
// 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
- Multi-user support: Add authentication/authorization
- Budget tracking: Monthly budget limits per category
- Recurring transactions: Auto-detect and predict recurring expenses
- Data export: Export transactions to Excel/CSV
- Charts/graphs: Spending trends over time
- Mobile app: React Native or .NET MAUI
- Bank API integration: Direct import from bank APIs (Plaid, Yodlee)
- Receipt search: Full-text search on parsed line items
- Split transactions: Divide single transaction across categories
- Tags: Tag transactions with multiple labels
Technical Debt
- Move inline service implementations to separate files (Upload.cshtml.cs is 333 lines)
- Add logging (ILogger) to all services
- Add retry logic for OpenAI API calls (Polly)
- Implement background job processing for receipt parsing (Hangfire)
- Add integration tests for critical workflows
- 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_KEYenvironment variable orOpenAI:ApiKeyin 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
# Create migration
dotnet ef migrations add MigrationName
# Update database
dotnet ef database update
Production Configuration
- Update connection string in appsettings.json (Azure SQL, AWS RDS, etc.)
- Set
OPENAI_API_KEYenvironment variable - Configure
Receipts:StoragePath(consider Azure Blob Storage) - Enable HTTPS and HSTS
- 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