---
stepsCompleted: [1, 2, 3, 4]
inputDocuments:
  - docs/prds/prd-financials-2026-05-25/prd.md
  - docs/architecture.md
---

# Personal Finance App - Epic Breakdown

## Overview

This document provides the complete epic and story breakdown for the Personal Finance App, decomposing the requirements from the PRD and Architecture into implementable stories organized by delivery phase and user value.

## Requirements Inventory

### Functional Requirements

**Phase 1 — Core Foundation**

FR-1.1: The dashboard is the default landing page and renders without page reload after initial load.
FR-1.2: Dashboard surfaces five widgets: Budget Burn (per-category progress bars, color-coded green/yellow/red), Upcoming Bills (next 30 days with amounts and days-until-due), Debt Paydown Progress (if active plan exists: per-card progress + projected payoff timeline), Recent Transactions (last 10: date/merchant/amount/category), Monthly Spending Summary (current month vs. prior month total, delta highlighted).
FR-1.3: Each dashboard widget links to its full management view.
FR-1.4: User can manually add a transaction with: date, merchant/payee, amount, category, account, and optional notes.
FR-1.5: User can edit or delete any existing transaction.
FR-1.6: Transactions are categorized. Default system category set: Housing, Groceries, Dining, Transportation, Utilities, Healthcare, Entertainment, Shopping, Subscriptions, Personal Care, Debt Payments, Income, Transfers, Uncategorized.
FR-1.7: User can create, rename, and delete custom categories.
FR-1.8: Transaction list view supports filtering by date range, category, account, and amount range, plus free-text search on merchant/notes.
FR-1.9: Transaction list is paginated (50 per page default) and sortable by date, amount, merchant.
FR-1.10: User can set a monthly budget amount per category.
FR-1.11: On first use (or when fewer than 2 months of data exist), app offers a 50/30/20 budget template (50% needs / 30% wants / 20% savings-debt). User can accept as-is or adjust allocations before saving.
FR-1.12: Once ≥2 months of transaction history exists, budget recommendation engine activates and suggests per-category budgets based on the user's own 3-month rolling average spend, rounded to the nearest $5. Recommendations surfaced as a "Refresh recommendations" prompt — not applied automatically.
FR-1.13: Budget view shows per-category: budgeted amount, amount spent to date this month, remaining amount, and percentage used.
FR-1.14: Budgets roll monthly; unused amounts do not carry forward.
FR-1.15: User can view historical budget vs. actuals for any prior month.
FR-1.15a: Settings page allows the user to enter a monthly net income figure. Used by analytics (FR-3.4) for income-relative spending flags. Single stored value updated manually as needed.
FR-1.16: User can add a recurring bill with: name, amount, due date (day of month or specific date), account/payee, category, and active/inactive status.
FR-1.17: Bills view lists all bills sorted by next due date, showing amount, status (upcoming / due today / overdue), and days until due.
FR-1.18: User can mark a bill as paid for the current cycle, which logs a transaction and advances the next due date.
FR-1.19: User can add a debt (non-credit-card) with: name, current balance, interest rate, minimum payment, and due date. Debts appear in the bills list and feed the dashboard upcoming bills widget.
FR-1.20: User inputs one or more credit card accounts, each with: card name/issuer, current balance, APR, minimum payment, credit limit.
FR-1.21: User inputs a total monthly extra payment amount available for paydown beyond minimums.
FR-1.22: Planner calculates and presents plans under all five strategies simultaneously for comparison: Avalanche (highest-APR first), Snowball (lowest-balance first), Highest Balance First, Proportional (distributed by balance), Custom (user-specified per-card amounts).
FR-1.23: Each strategy plan displays: per-card payoff month/year, total interest paid, total months to debt-free, month-by-month amortization table (expandable), and a payoff timeline chart.
FR-1.24: User selects one strategy to "activate." Only one plan is active at a time.
FR-1.25: Active plan is monitored on the dashboard (FR-1.2) and in a dedicated Paydown Monitor view showing: actual vs. planned balance per card per month, deviation alerts (in-app flag if a card's actual balance exceeds the plan's projected balance by >5%), and projected vs. actual payoff date.
FR-1.26: User can update card balances monthly to keep monitoring accurate. Updates are logged with a timestamp.
FR-1.27: User can switch the active strategy at any time; the prior plan is archived with a timestamp and the new plan recalculates from current balances.

**Phase 2 — PDF Statement Import**

FR-2.1: User can upload one or more PDF files via the import UI. Supported issuers: Chase, Bank of America, Apple Card, Kohls, Discover, DCU Credit Union, Target.
FR-2.2: System detects the issuer automatically from PDF content (header text, layout patterns) before parsing. If detection confidence is low, user is prompted to confirm the issuer.
FR-2.3: Parser extracts for each transaction: date, merchant/description (raw), amount, debit/credit flag, and statement period. Parsed transactions are staged for review before any are committed to the database.
FR-2.4: Staging review screen shows all extracted transactions in a table. User can edit merchant name, category, or amount on any row before confirming.
FR-2.5: Auto-categorization applies merchant name matching against a built-in merchant→category map. Unrecognized merchants flagged as Uncategorized for user review. User corrections to merchant→category mappings saved and reapplied on future imports.
FR-2.6: Merchant name normalization cleans raw statement strings (e.g., "AMZN*MARKETPLACE 04/22" → "Amazon") using a configurable normalization table.
FR-2.7: Duplicate detection: a transaction is flagged as a probable duplicate if another transaction with the same date, amount, and normalized merchant exists within a 3-day window. Flagged duplicates shown to user for confirm-keep or dismiss-skip before commit.
FR-2.8: After user review and confirmation, transactions are committed to the main transaction table tagged with their source account and import batch ID.
FR-2.9: Import history log records: upload date, issuer, statement period, transaction count imported, duplicates skipped, and any parse errors.
FR-2.10: Parse errors (pages or rows that could not be extracted) surfaced per-file with page number and raw text sample for manual entry of missed transactions.

**Phase 3 — Analytics & Insights**

FR-3.1: Spending pattern view shows monthly spend by category as a stacked bar chart covering all available history, with a trend line per category.
FR-3.2: Year-over-year and month-over-month comparisons available for any category or total spend.
FR-3.3: Subscription detector scans transactions for recurring charges: same merchant, similar amounts (within ±$1), on a recurring interval (weekly, monthly, annual). Detected subscriptions listed with: merchant, estimated amount, recurrence, and total paid in the last 12 months.
FR-3.4: Bad spending pattern flags surface automatically when: a category exceeds its budget by >20% for 2 consecutive months; a subscription has not had a matching transaction in 60+ days; or total dining/entertainment exceeds 15% of the monthly income figure entered in Settings (FR-1.15a).
FR-3.5: Budget recommendation engine (per FR-1.12) powered by ≥2 months of imported history. Recommendations include suggested amount, 3-month average that drove it, and comparison to current budget.
FR-3.6: Merchant spend summary view: top 20 merchants by total spend, sortable by total amount or transaction count, filterable by date range.

### NonFunctional Requirements

NFR-1: Performance — Dashboard renders in under 2 seconds on the host machine. PDF import parse+stage completes in under 30 seconds per statement file.
NFR-2: Data integrity — SQLite WAL mode enabled. All writes transactional. Import uses a staged commit (nothing written to main tables until user confirms review).
NFR-3: Recoverability — A manual database backup/export function downloads the SQLite file. Export of all transactions to CSV available from the transaction view (both full export and filtered-view export).
NFR-4: Usability — All primary flows (add transaction, run paydown plan, import PDF, check dashboard) completable in ≤3 clicks/steps from the main nav. Primer CSS provides consistent, accessible UI patterns throughout.
NFR-5: Browser support — Modern desktop browsers (Chrome, Firefox, Edge — current stable). No mobile optimization required.
NFR-6: No external calls — The application makes no outbound network requests. All computation is local. No analytics, telemetry, or CDN dependencies.

### Additional Requirements

*From Architecture — Technical implementation requirements that shape story structure:*

- AR-1: Project scaffold created using manual Flask Application Factory pattern (no cookiecutter). `tests/conftest.py` is the **first file written** — before any model or service code, using in-memory SQLite for all DB-touching tests.
- AR-2: `test_duplicate_detector.py` spec written **before** the first Alembic migration (TDD order enforced).
- AR-3: All 9 architectural boundaries must be respected throughout implementation: (1) Flask/Service, (2) Service/DB, (3) Parser/DB, (4) Staging/MainDB, (5) Analytics+Insights/Flask, (6) Amortization/Everything, (7) DuplicateDetector/DB Session, (8) Pandas Isolation, (9) Single-User Scope.
- AR-4: Phase 1 `transaction` model pre-populates nullable Phase 2 columns: `issuer VARCHAR`, `merchant_raw TEXT`, `import_batch_id INTEGER FK nullable`, `confidence_score FLOAT nullable` — eliminates live migration at Phase 2 onboarding.
- AR-5: Database indexes created in the Phase 1 first migration: `transactions(date)`, `transactions(category_id)`, `transactions(account_id)`, `bills(due_date)`, `paydown_balance_updates(account_id, updated_at)`.
- AR-6: `merchant_mapping` model created in Phase 1 schema even though it is primarily populated in Phase 2. Required for `category_service.py` cascades.
- AR-7: `duplicate_detector` called from **both** the import pipeline (staging_pipeline.py) **and** the manual transaction entry route (transactions blueprint). Shared infrastructure, not Phase 2-only.
- AR-8: All 8 template components (alert, empty_state, page_header, stat_card, pagination, confirm_modal, form_row, progress_bar) established in `app/templates/components/` **before** any blueprint view is written.
- AR-9: `tokens.css` (domain semantic states → Primer utility class mappings) and `app.css` created before any blueprint stylesheet work. All custom CSS classes prefixed `fin-`.
- AR-10: Primer CSS and Chart.js served from `static/vendor/` (local copies). No CDN references anywhere.
- AR-11: `separate import_staging.db` (`instance/import_staging.db`) used as the staging boundary. `staging_pipeline.py` manages both staging and main DB connections; cleans up staging rows after commit via soft-delete (`committed_at` column).
- AR-12: Canonical `StagedTransaction` @dataclass fields: `date`, `merchant_raw`, `merchant_normalized`, `amount`, `is_credit`, `issuer`, `dedup_hash`, `confidence_score`, `raw_text`. Canonical `ParseError` @dataclass fields: `page_number`, `raw_text`, `reason`, `parser_version`. Both defined in `services/pdf_parsers/base.py`.
- AR-13: `AmortizationResult` is a plain @dataclass with primitive fields only (no ORM references) to prevent `DetachedInstanceError` in templates.
- AR-14: PRG (Post/Redirect/Get) pattern mandatory for all state-changing HTML routes. `flash()` categories: `'success'`, `'error'`, `'warning'`, `'info'` only.
- AR-15: `wsgi.py` exposes `application = create_app()` for Apache mod_wsgi deployment.
- AR-16: WAL mode configured via SQLAlchemy event listener in `extensions.py` — fires on every new connection, never assumed.
- AR-17: `api/` blueprint uses sub-modules by domain: `transactions.py`, `budgets.py`, `paydown.py`, `analytics.py`. All API GET responses are direct JSON (no envelope wrapper); POST success: `{"success": true, "id": N}`; errors: `{"error": "...", "code": HTTP_STATUS}`.

### UX Design Requirements

*No UX Design document exists at this time. UX Design workflow is planned as the next step after Epics & Stories. UX-DRs will be added in a future revision when the UX spec is complete.*

### FR Coverage Map

| FR | Epic | Summary |
|----|------|---------|
| AR-1 → AR-10 | Epic 1 | Scaffold, models, migrations, indexes, duplicate_detector TDD (incl. staged stubs) |
| AR-11 → AR-17 | Epic 2 | Template components, vendor assets, CSS tokens, JS utilities, errors helper |
| Account setup | Epic 3 | Story 3.1 — checking/savings/credit account CRUD; default Cash account seeded |
| FR-1.4 | Epic 3 | Manual transaction add with duplicate detection |
| FR-1.5 | Epic 3 | Edit/delete transactions |
| FR-1.6 | Epic 3 | System category defaults (14 seeded) |
| FR-1.7 | Epic 3 | Custom category CRUD |
| FR-1.8 | Epic 3 | Transaction filtering and search |
| FR-1.9 | Epic 3 | Pagination and sorting |
| NFR-3 | Epic 3 | CSV export (all + filtered view) |
| FR-1.10 | Epic 4 | Monthly budget per category |
| FR-1.11 | Epic 4 | 50/30/20 first-use template |
| FR-1.12 | Epics 4+10 | 50/30/20 stub path (Epic 4); history-based path completion (Epic 10) |
| FR-1.13 | Epic 4 | Budget actuals view + basic spending chart (Chart.js) |
| FR-1.14 | Epic 4 | Monthly rollover (no carry-forward) |
| FR-1.15 | Epic 4 | Historical budget vs. actuals |
| FR-1.15a | Epic 4 | Settings — monthly income |
| FR-1.16 | Epic 5 | Recurring bill add/edit |
| FR-1.17 | Epic 5 | Bills list view with status |
| FR-1.18 | Epic 5 | Mark bill as paid (logs transaction, advances due date) |
| FR-1.19 | Epic 5 | Non-credit debt tracking |
| FR-1.20 | Epic 6 | Credit card account input |
| FR-1.21 | Epic 6 | Extra monthly payment input |
| FR-1.22 | Epic 6 | 5-strategy comparison |
| FR-1.23 | Epic 6 | Strategy detail (amortization table + payoff chart) |
| FR-1.24 | Epic 6 | Plan activation |
| FR-1.25 | Epic 7 | Paydown Monitor + deviation alerts (>5%) |
| FR-1.26 | Epic 7 | Monthly balance updates with timestamps |
| FR-1.27 | Epic 7 | Strategy switch + plan archival |
| FR-1.1 | Epic 8 | Dashboard landing page |
| FR-1.2 | Epic 8 | Five dashboard widgets (all showing real data from Epics 3–7) |
| FR-1.3 | Epic 8 | Widget-to-full-view links |
| FR-2.1 | Epic 9 | PDF upload UI + 7 supported issuers |
| FR-2.2 | Epic 9 | Issuer auto-detection |
| FR-2.3 | Epic 9 | Transaction extraction + staging |
| FR-2.4 | Epic 9 | Staged review with inline edits |
| FR-2.5 | Epic 9 | Auto-categorization + user-correction persistence |
| FR-2.6 | Epic 9 | Merchant name normalization |
| FR-2.7 | Epic 9 | Duplicate detection (staged + main DB) |
| FR-2.8 | Epic 9 | Commit staged transactions to main DB |
| FR-2.9 | Epic 9 | Import history log |
| FR-2.10 | Epic 9 | Parse error surfacing |
| FR-3.1 | Epic 10 | Spending patterns stacked bar chart + trend lines |
| FR-3.2 | Epic 10 | YoY / MoM comparisons |
| FR-3.3 | Epic 10 | Subscription detector |
| FR-3.4 | Epic 10 | Bad spending pattern flags (3 types) |
| FR-3.5 | Epic 10 | History-based budget recommendations |
| FR-3.6 | Epic 10 | Merchant spend summary |
| NFR-1 | Epics 8+9 | Dashboard perf (Epic 8), PDF parse perf (Epic 9) |
| NFR-2 | Epics 1+9 | WAL mode (Epic 1), staged commit (Epic 9) |
| NFR-4 | All epics | ≤3-click flows via shared components |
| NFR-5 | Epic 1 | Vanilla JS, no framework overhead |
| NFR-6 | Epic 2 | Vendored Primer CSS + Chart.js, no CDN |

## Epic List

### Epic 1: Technical Foundation — Scaffold, DB & Models
Running Flask app on localhost with full database schema, all migrations, test suite wired, and every architectural constraint enforced from day one. Dev agents for all subsequent epics work against a verified, stable foundation.
**FRs covered:** AR-1 → AR-10 (scaffold, all core models incl. MerchantMapping with full Phase 2 fields, DB migration + 5 indexes, duplicate_detector TDD with staged-dedup stubs, date_utils, validators, config.py, wsgi.py)
**Key constraint:** tests/conftest.py is the first file written. test_duplicate_detector.py is written before the first Alembic migration. MerchantMapping model specced with full Phase 2 fields (raw_pattern, normalized, category_id, user_confirmed) to prevent Epic 9 mid-stream migration.

### Epic 2: Technical Foundation — Shared Infrastructure
Complete shared component library, CSS architecture, vendored assets, and JS utilities. Every dev agent for Epics 3–10 has a consistent, tested toolkit to build against.
**FRs covered:** AR-11 → AR-17 (8 template components with smoke tests, tokens.css, app.css with fin- prefix + data-loading pattern, Primer CSS + Chart.js vendored, base.html with active_page convention, modal.js / loading.js / chart-defaults.js, api_error helper with contract test, 404/500 error pages)

### Epic 3: Transaction & Category Management
User can manually track all spending — add transactions with duplicate detection, organize by category (including custom categories), search and filter history, export to CSV, and see a welcoming first-run experience on an empty database.
**FRs covered:** FR-1.4, FR-1.5, FR-1.6, FR-1.7, FR-1.8, FR-1.9, NFR-3
**Additions:** Story 3.1 Account Setup (gap fix — needed before transaction entry); first-run/empty-state story

### Epic 4: Budget Management & Settings
User can plan monthly spending — set category budgets, use the 50/30/20 template for first-time setup, track actuals vs. budget with a simple spending chart, view historical months, and record their monthly income.
**FRs covered:** FR-1.10, FR-1.11, FR-1.12 (50/30/20 stub path via budget_recommender.fifty_thirty_twenty()), FR-1.13, FR-1.14, FR-1.15, FR-1.15a
**Additions:** Basic Chart.js spending-by-category chart in budget actuals view (early feedback loop before full analytics in Epic 10). budget_recommender interface contract locked here (abstract protocol) so Epic 10 implementation is verified against the same contract.

### Epic 5: Bills & Debts Tracking
User can track all recurring financial obligations — add recurring bills, mark them as paid (which logs a transaction and advances the due date), see what's coming due, and record non-credit debts.
**FRs covered:** FR-1.16, FR-1.17, FR-1.18, FR-1.19

### Epic 6: Paydown Planner
User can calculate and compare all 5 debt paydown strategies side-by-side, review detailed amortization tables and payoff charts, and activate a plan.
**FRs covered:** FR-1.20, FR-1.21, FR-1.22, FR-1.23, FR-1.24

### Epic 7: Paydown Monitor
User can actively track their activated paydown plan against reality — update card balances monthly, see in-app deviation alerts when actual balances exceed projected by >5%, and switch strategies (archiving the prior plan).
**FRs covered:** FR-1.25, FR-1.26, FR-1.27
**QA note:** Test fixtures must be generated by calling amortization.py functions directly (not hardcoded magic numbers). Required edge-case coverage: 5% boundary case, mid-month balance update, strategy-switch baseline reset.

### Epic 8: Dashboard (Phase 1 Capstone)
User has a complete financial home base — a single dashboard showing all 5 widgets with real data from all features built in Epics 3–7. This is the Phase 1 delivery milestone: the app is fully functional.
**FRs covered:** FR-1.1, FR-1.2, FR-1.3, NFR-1 (dashboard <2s performance)
**Note:** Built last so all widgets show real data: budget burn (Epic 4), upcoming bills (Epic 5), debt paydown progress (Epic 7), recent transactions (Epic 3), monthly summary (Epics 3–4). No empty-state compromises.

### Epic 9: PDF Statement Import
User can bulk-import up to 6+ months of historical statements from 7 supported issuers via a 3-step wizard, with staged review, auto-categorization, merchant normalization, and duplicate detection.
**FRs covered:** FR-2.1, FR-2.2, FR-2.3, FR-2.4, FR-2.5, FR-2.6, FR-2.7, FR-2.8, FR-2.9, FR-2.10, NFR-1 (PDF parse <30s), NFR-2 (staged commit integrity)
**Spike stories required before implementation (OQ-3/4/5):**
- Spike 9.A: Apple Card PDF structure analysis → fixture acquired + OCR strategy documented (OQ-3)
- Spike 9.B: Target RedCard dual-card format analysis → both statement types fixture + parsing strategy (OQ-4)
- Spike 9.C: DCU Credit Union PDF template analysis → fixture acquired + extraction template documented (OQ-5)
Each spike AC: "PDF fixture acquired, parsing strategy documented, test skeleton written." Implementation story cannot begin until spike is closed.

### Epic 10: Analytics & Insights
User gets data-driven insights from their transaction history — spending pattern charts, year-over-year comparisons, subscription detection, bad-spending flags, and history-based budget recommendations (completing FR-1.12).
**FRs covered:** FR-3.1, FR-3.2, FR-3.3, FR-3.4, FR-3.5, FR-3.6, FR-1.12 (history-based path completion)
**Note:** budget_recommender.from_history() implements the abstract protocol locked in Epic 4. Contract test from Epic 4 runs against this implementation to verify interface compatibility.

---

## Epic 1: Technical Foundation — Scaffold, DB & Models

Running Flask app on localhost with full database schema, all migrations, test suite wired, and every architectural constraint enforced from day one. Dev agents for all subsequent epics work against a verified, stable foundation.

### Story 1.1: Project Scaffold & Environment Setup

As a developer,
I want the project directory structure created with all Python dependencies installed,
So that I have a complete, reproducible development environment ready for immediate feature implementation.

**Acceptance Criteria:**

**Given** an empty project directory at `/var/www/html/financials/`
**When** the scaffold setup is complete
**Then** the full directory tree exists per architecture spec: `app/` with `models/`, `blueprints/{dashboard,transactions,budgets,settings,bills,paydown,import_pdf,analytics,api}/` (each with `__init__.py`, `routes.py`, `forms.py`, `templates/{blueprint}/`), `services/{pdf_parsers/,analytics/,insights/}/`, `utils/`, `templates/components/`, `static/{vendor/{primer/,chartjs/},css/,js/}/`; `migrations/`, `tests/{fixtures/pdfs/,test_models/,test_services/,test_parsers/,test_analytics/,test_blueprints/}/`, `instance/`
**And** `requirements.txt` is generated from `pip freeze` containing: flask, flask-sqlalchemy, flask-migrate, pdfplumber, pandas, python-dateutil, flask-wtf, pytest
**And** `.gitignore` contains: `instance/`, `.venv/`, `__pycache__/`, `*.pyc`, `*.db`
**And** `python -m pytest --collect-only` runs without import errors

---

### Story 1.2: Test Infrastructure — conftest.py

As a developer,
I want `tests/conftest.py` with an in-memory SQLite test fixture,
So that all database-touching tests run in complete isolation from the real database, enabling fast and repeatable tests from the first story onwards.

**Note:** This is the FIRST file written — before any model code exists.

**Acceptance Criteria:**

**Given** the scaffold from Story 1.1 exists
**When** `tests/conftest.py` is created
**Then** it defines an `app` pytest fixture that creates the Flask app with `TESTING=True` and `SQLALCHEMY_DATABASE_URI='sqlite:///:memory:'`
**And** it defines a `db` pytest fixture that calls `db.create_all()` before yielding and `db.drop_all()` after, using `app.app_context()`
**And** it defines a `client` pytest fixture wrapping the test app with Flask's test client
**And** `pytest tests/` runs with 0 failures and 0 errors

---

### Story 1.3: Staged Transaction Contracts & Duplicate Detector (TDD)

As a developer,
I want `StagedTransaction` and `ParseError` defined as plain dataclasses, and `duplicate_detector` built test-first before any migration runs,
So that the core deduplication contract is locked in code with tests before the database schema is created.

**Note:** `tests/test_services/test_duplicate_detector.py` is written and passing BEFORE `flask db init` is run.

**Acceptance Criteria:**

**Given** `conftest.py` from Story 1.2 exists
**When** this story is complete
**Then** `app/services/pdf_parsers/base.py` defines two plain `@dataclass` classes: `StagedTransaction` (date, merchant_raw, merchant_normalized, amount: Decimal, is_credit: bool, issuer, dedup_hash, confidence_score: float, raw_text) and `ParseError` (page_number: int, raw_text, reason, parser_version)
**And** `app/services/duplicate_detector.py` implements `flag_duplicates(new_fingerprints: list[str], existing_fingerprints: list[str]) -> list[bool]` accepting only plain Python types (no ORM objects, no `db.session`)
**And** `dedup_hash` is computed as `SHA256(normalized_merchant + str(amount) + date.isoformat())`
**And** `tests/test_services/test_duplicate_detector.py` passes with tests covering: exact match detected, no match, empty new_fingerprints → empty list, empty existing_fingerprints → all False
**And** a `@pytest.mark.skip("wired in Epic 9")` stub test for the staged-vs-main dedup path is present, documenting the future seam
**And** `pytest tests/test_services/test_duplicate_detector.py` passes before any migration is run

---

### Story 1.4: Core Domain Models & First Database Migration

As a developer,
I want all Phase 1 domain models created and the first database migration applied with all required indexes,
So that the schema is stable, forward-compatible with Phase 2, and won't require live-data migrations later.

**Acceptance Criteria:**

**Given** Stories 1.1–1.3 are complete
**When** this story is complete
**Then** model files exist in `app/models/` for: Account, Transaction (with Phase 2 nullable columns: issuer, confidence_score FLOAT, dedup_hash VARCHAR — all nullable, commented "Phase 2 — do not remove"), Category (is_system BOOL, is_active BOOL soft-delete), Budget, Bill, Debt, PaydownPlan (with archived_at and last_recalculated_at nullable), PaydownPlanCard, PaydownBalanceUpdate, ImportBatch, MerchantMapping (with **user_confirmed BOOL default False** — full Phase 2 schema), Settings, StagedTransactionModel (for staging DB, with committed_at TIMESTAMP nullable)
**And** `flask db init && flask db migrate && flask db upgrade` completes without errors
**And** the migration includes 5 required indexes: `idx_transactions_date`, `idx_transactions_category`, `idx_transactions_account`, `idx_bills_due_date`, `idx_balance_updates_account`
**And** `tests/test_models/test_schema.py` queries `sqlite_master` and asserts all 5 indexes exist
**And** the migration seeds 14 system categories (`is_system=True`, `is_active=True`): Housing, Groceries, Dining, Transportation, Utilities, Healthcare, Entertainment, Shopping, Subscriptions, Personal Care, Debt Payments, Income, Transfers, Uncategorized

---

### Story 1.5: App Factory, WAL Mode & Deployment Configuration

As a user,
I want the Flask application running at localhost with WAL mode enabled and a proper deployment configuration,
So that the app can be started locally for development and deployed to Apache mod_wsgi for stable serving.

**Acceptance Criteria:**

**Given** all models and the migration from Story 1.4 exist
**When** this story is complete
**Then** `app/__init__.py` defines `create_app(config=None)` that loads config, initialises `db` and `migrate`, registers all blueprint stubs, and returns the app
**And** `app/extensions.py` registers a `@event.listens_for(engine, "connect")` listener executing `PRAGMA journal_mode=WAL` on every new connection
**And** `app/utils/date_utils.py` wraps `relativedelta` for month-end-safe arithmetic with documented conventions; achieves 100% branch coverage in tests
**And** `app/utils/validators.py` contains `validate_amount(value) -> Decimal` raising `ValueError` on invalid input; 100% branch coverage in tests
**And** `config.py` defines `DevelopmentConfig` and `ProductionConfig`; DB path driven by `DATABASE_URL` environment variable
**And** `wsgi.py` exposes `application = create_app()`
**And** `flask run` starts successfully and `GET /` returns HTTP 200
**And** `pytest tests/` passes with all Stories 1.2–1.4 tests green

---

## Epic 2: Technical Foundation — Shared Infrastructure

Complete shared component library, CSS architecture, vendored assets, and JS utilities. Every dev agent for Epics 3–10 has a consistent, tested toolkit to build against.

### Story 2.1: Base Template & Navigation Structure

As a user,
I want a consistent navigation shell with active-page highlighting across the app,
So that I can move between all sections from any page without confusion.

**Acceptance Criteria:**

**Given** the app factory from Story 1.5 is running
**When** this story is complete
**Then** `app/templates/base.html` exists with: full page shell, `<nav>` linking all sections (Dashboard, Transactions, Budgets, Bills, Paydown, Import, Analytics, Settings), `{% block content %}`, flash message rendering for all 4 categories (success/error/warning/info)
**And** the nav marks the active link using the `active_page` context variable — convention documented in a comment at the top of `base.html`
**And** every `render_template()` call passes `active_page='<blueprint_name>'` (enforced by smoke test)
**And** `app/templates/errors/404.html` and `errors/500.html` extend `base.html`
**And** `@app.errorhandler(404)` and `@app.errorhandler(500)` are registered in `create_app()`
**And** `GET /` returns HTTP 200 with the base template rendered

---

### Story 2.2: Shared Template Components (8 Components)

As a developer,
I want all 8 shared Jinja2 template components created before any blueprint view is built,
So that every feature in Epics 3–10 uses consistent, tested UI building blocks with no pattern duplication.

**Acceptance Criteria:**

**Given** `base.html` from Story 2.1 exists
**When** this story is complete
**Then** all 8 files exist in `app/templates/components/`: `alert.html` (inline contextual: success/error/warning/info), `empty_state.html` (title + description + optional CTA), `page_header.html` (title + subtitle + optional primary action), `stat_card.html` (label + value + optional delta), `pagination.html` (prev/next links; params: page, total, per_page, url_for_page), `confirm_modal.html` (focus trap, Esc closes, focus returns to trigger), `form_row.html` (Flask-WTF label + input + inline error), `progress_bar.html` (% bar + color-state token safe/warning/danger + label)
**And** `tests/test_blueprints/test_components.py` renders each component with minimal valid context and asserts HTTP 200 with no Jinja2 `UndefinedError`
**And** each component uses `{% include %}` or `{% macro %}` pattern (not `{% extends %}`)

---

### Story 2.3: CSS Architecture & Vendor Assets

As a developer,
I want `tokens.css`, `app.css`, and all vendor assets in place before any feature stylesheet work begins,
So that every blueprint uses consistent domain semantic tokens instead of hardcoded Primer class names, and no CDN calls are ever made.

**Acceptance Criteria:**

**Given** `base.html` references the CSS and vendor paths
**When** this story is complete
**Then** `app/static/css/tokens.css` defines: `--color-safe`, `--color-warning`, `--color-danger` mapped to Primer utility classes; a comment block documenting the mapping convention
**And** `app/static/css/app.css` defines: `fin-` prefixed utility classes, the `[data-loading="true"]` pattern (`.fin-content` opacity 0.4 + pointer-events none; `.fin-spinner` display block), and Primer overrides
**And** Primer CSS is present at `app/static/vendor/primer/` (local copy — no CDN)
**And** Chart.js bundle is present at `app/static/vendor/chartjs/chart.min.js`
**And** a grep of the codebase finds zero references to `cdn.`, `unpkg.`, `jsdelivr.`, or `cdnjs.` (NFR-6 enforced)
**And** `GET /static/vendor/chartjs/chart.min.js` returns HTTP 200

---

### Story 2.4: JavaScript Utilities & API Error Helper

As a developer,
I want shared JavaScript utilities and a Python API error helper in place,
So that all fetch interactions, modals, loading states, and API error responses follow a single consistent pattern.

**Acceptance Criteria:**

**Given** vendor assets from Story 2.3 are in place
**When** this story is complete
**Then** `app/static/js/chart-defaults.js` sets `Chart.defaults` once at page load: consistent palette, tooltip styling, `responsive: true`
**And** `app/static/js/modal.js` handles the confirm modal: triggered by `data-confirm-target`, focus trap, Esc closes, focus returns to trigger on close
**And** `app/static/js/loading.js` implements `setLoading(element, bool)` toggling `element.dataset.loading`
**And** `app/utils/errors.py` defines `api_error(message: str, code: int) -> Response` returning `jsonify({"error": message, "code": code})` with the given HTTP status
**And** a contract test asserts: response `Content-Type` is `application/json`, body has `error` and `code` keys, HTTP status matches the `code` argument
**And** all 3 JS files are referenced in `base.html`

---

## Epic 3: Transaction & Category Management

User can manually track all spending — add transactions with duplicate detection, organize by category (including custom categories), search and filter history, export to CSV, and see a welcoming first-run experience on an empty database.

### Story 3.1: Account Setup

As a user,
I want to add and manage my bank and payment accounts (checking, savings, credit),
So that I can assign transactions to the correct account and the app knows which accounts I use.

**Acceptance Criteria:**

**Given** I navigate to `/settings/accounts` (or an accounts section accessible from the nav)
**When** I submit the "Add Account" form with valid data
**Then** an Account record is created with: name (required, e.g. "Chase Checking"), type (dropdown: checking / savings / credit / loan), optional institution name; `is_active=True`
**And** on valid submit → PRG redirect to accounts list, flash `'success'` "Account added."
**And** all existing accounts are listed with name, type, and institution
**And** I can edit any account at `/settings/accounts/<id>/edit`; PRG redirect on submit
**And** I can delete an account with no associated transactions via `confirm_modal.html`; on confirm → hard-delete, flash `'success'`
**And** attempting to delete an account that has transactions returns flash `'error'` "Account in use — cannot be deleted." (RESTRICT FK enforced at application layer)
**And** a default "Cash" account of type `checking` is seeded at DB init so the transaction form always has at least one option
**And** when only one account exists, the transaction form auto-selects it (no required user action for single-account users)

---

### Story 3.2: Category Foundation — System Defaults & Custom Category CRUD

As a user,
I want the 14 system categories available from day one and the ability to create, rename, and delete my own categories,
So that I can organize transactions meaningfully without being locked into a fixed taxonomy.

**Acceptance Criteria:**

**Given** the DB migration from Story 1.4 seeded the 14 system categories
**When** the category management page is loaded
**Then** all 14 system categories are listed with a "System" badge; system categories show no delete option
**And** a "New Category" form lets the user create a custom category (name required, max 50 chars); on submit → PRG redirect, flash `'success'`
**And** custom categories can be renamed; the rename persists; flash `'success'`
**And** deleting a custom category with no associated transactions soft-deletes it (`is_active=False`); flash `'success'`
**And** attempting to delete a category that has associated transactions returns flash `'error'` "Category in use — cannot be deleted." (RESTRICT FK enforced at application layer)
**And** system categories cannot be deleted; the delete button is absent from the UI
**And** `category_service.py` owns all mutation logic; blueprint routes only call service functions and commit

---

### Story 3.3: Manual Transaction Entry

As a user,
I want to add a transaction with date, merchant, amount, category, account, and optional notes — with automatic duplicate detection,
So that I can accurately record my spending and be warned before I accidentally enter a duplicate.

**Acceptance Criteria:**

**Given** at least one category and one account exist
**When** I submit the "Add Transaction" form with valid data
**Then** the transaction is saved with: date, merchant_raw = merchant_normalized, amount as Decimal, category_id, account_id, `is_manual=True`, optional notes; `dedup_hash` computed and stored
**And** before saving, `duplicate_detector.flag_duplicates([new_hash], recent_hashes)` is called with fingerprints from the last 3 days; if flagged, a warning flash is shown and the form re-renders with the duplicate highlighted — user may still save or cancel
**And** on successful save → PRG redirect to transaction list, flash `'success'` "Transaction added."
**And** the form uses `TransactionForm` (Flask-WTF) with `{{ form.hidden_tag() }}`
**And** invalid data re-renders the form with inline errors via `form_row.html` — no redirect

---

### Story 3.4: Transaction Edit & Delete

As a user,
I want to edit or delete any transaction I've entered,
So that I can correct mistakes without losing all my data.

**Acceptance Criteria:**

**Given** at least one transaction exists
**When** I navigate to `/transactions/<id>/edit` and submit valid changes
**Then** all fields update, `dedup_hash` recomputes, PRG redirect, flash `'success'` "Transaction updated."
**And** editing to create a duplicate shows the same warning as Story 3.2
**And** clicking "Delete" shows `confirm_modal.html`; on confirm → `POST /transactions/<id>/delete` → hard-delete → PRG redirect, flash `'success'`
**And** GET/POST to a non-existent transaction ID returns HTTP 404

---

### Story 3.5: Transaction List — Filtering, Search, Sorting & Pagination

As a user,
I want to browse my full transaction history with filtering, search, and pagination,
So that I can quickly find specific transactions without scrolling through hundreds of entries.

**Acceptance Criteria:**

**Given** transactions exist in the database
**When** I visit `/transactions/`
**Then** transactions display 50 per page (default), sorted by date descending
**And** filter controls: date range, category dropdown, account dropdown, amount range (min/max) — all optional and combinable
**And** free-text search matches against `merchant_normalized` and `notes` (case-insensitive LIKE)
**And** active filters persist in URL query parameters (bookmarkable)
**And** sortable columns: date, amount, merchant — toggles asc/desc with arrow indicator
**And** pagination uses `pagination.html` and preserves all active filters across pages
**And** when no transactions match filters, `empty_state.html` shows "No transactions match your filters" with a "Clear filters" link
**And** the total count of matching transactions is displayed above the list

---

### Story 3.6: CSV Transaction Export

As a user,
I want to export my transactions to CSV — all transactions or just the current filtered view,
So that I can analyse my data in a spreadsheet or keep a local backup. (NFR-3)

**Acceptance Criteria:**

**Given** I am on the transaction list page
**When** I click "Export All"
**Then** the browser downloads `transactions_<YYYY-MM-DD>.csv` with all transactions regardless of filters; columns: date, merchant, amount, category, account, notes
**And** when I click "Export View"
**Then** the browser downloads a CSV with only filtered-view transactions (all pages, not just current)
**And** amounts export as plain decimal strings (`29.99`); dates as ISO 8601 (`YYYY-MM-DD`)
**And** response has `Content-Type: text/csv` and `Content-Disposition: attachment` headers

---

### Story 3.7: First-Run & Empty State Experience

As a user,
I want a welcoming experience when I first open the app with no data,
So that I know exactly what to do next and the empty app doesn't feel broken.

**Acceptance Criteria:**

**Given** the database has no transactions
**When** I visit `/transactions/`
**Then** `empty_state.html` renders with: title "No transactions yet", description "Add your first transaction to start tracking your spending.", CTA "Add Transaction" → `/transactions/create`
**And** every other section with no data (budgets, bills) uses `empty_state.html` with section-appropriate messaging and a logical CTA
**And** all empty states use the same `empty_state.html` component (visual consistency)
**And** after adding the first transaction and returning to the list, the transaction is shown — not the empty state

---

## Epic 4: Budget Management & Settings

User can plan monthly spending — set category budgets, use the 50/30/20 template for first-time setup, track actuals vs. budget with a simple spending chart, view historical months, and record their monthly income.

### Story 4.1: Settings — Monthly Income

As a user,
I want to enter my monthly net income in Settings,
So that the app can flag when my spending ratios exceed healthy thresholds and the budget template can calculate appropriate allocations. (FR-1.15a)

**Acceptance Criteria:**

**Given** I navigate to `/settings/`
**When** the Settings page loads
**Then** a form shows the current `monthly_income` value (blank if not yet set)
**And** I can enter or update the figure (positive decimal); on submit → Settings record upserted, PRG redirect, flash `'success'` "Settings saved."
**And** the Settings model enforces a single row (upsert pattern, `id=1` convention)
**And** the settings value is never imported directly by service or analytics modules — extracted at route layer and passed as a parameter
**And** `render_template()` passes `active_page='settings'`

---

### Story 4.2: Monthly Budget Entry Per Category

As a user,
I want to set a monthly budget amount for each category,
So that I can define my spending plan and track how much I have left in each area. (FR-1.10, FR-1.14)

**Acceptance Criteria:**

**Given** at least one active category exists
**When** I visit `/budgets/` and submit budget amounts
**Then** Budget records are saved/updated (category_id, month, year, amount); PRG redirect, flash `'success'`
**And** budgets are month-specific — a May 2026 budget does not carry forward to June 2026
**And** categories with no budget set display $0.00 budgeted (not an error)
**And** invalid amounts (negative, non-numeric) show inline errors via `form_row.html`; form uses Flask-WTF with CSRF

---

### Story 4.3: First-Use Budget Template (50/30/20)

As a user,
I want the app to offer a 50/30/20 budget template when I have fewer than 2 months of transaction history,
So that I can get a sensible starting point without guessing at every category. (FR-1.11, FR-1.12 stub)

**Acceptance Criteria:**

**Given** fewer than 2 months of transaction history exist
**When** I visit `/budgets/recommend`
**Then** the 50/30/20 template is shown: 50% of monthly income to needs (Housing, Utilities, Groceries, Transportation, Healthcare, Debt Payments), 30% to wants (Dining, Entertainment, Shopping, Subscriptions, Personal Care), 20% to savings/debt
**And** `budget_recommender.fifty_thirty_twenty(monthly_income)` in `services/insights/budget_recommender.py` produces the allocations; this function defines the `BudgetRecommendation` interface contract that Epic 10 will also satisfy
**And** I can adjust any allocation before saving; total must not exceed `monthly_income` (validated)
**And** "Apply These Budgets" saves allocations as Budget records for the current month; PRG redirect, flash `'success'`
**And** if `monthly_income` is not set, the page prompts "Please set your monthly income in Settings first" with a link

---

### Story 4.4: Budget Actuals View with Spending Chart

As a user,
I want to see how much I've spent against each category budget this month, with a visual chart,
So that I can immediately see where I'm on track and where I'm overspending. (FR-1.13)

**Acceptance Criteria:**

**Given** budgets and transactions exist for the current month
**When** I visit `/budgets/`
**Then** each category row shows: budgeted amount, amount spent this month, remaining amount, and percentage used
**And** `progress_bar.html` renders each row with color-state token: safe (< 80%), warning (80–99%), danger (≥ 100%)
**And** a Chart.js bar chart shows budgeted vs. actual spend per category side-by-side; uses `chart-defaults.js` palette
**And** the chart loads via `GET /api/budgets` returning `[{category, budgeted, spent}]` using SQL aggregation (no Python loops over all rows)
**And** categories with no transactions show $0 spent; categories with no budget show "No budget set" with a link

---

### Story 4.5: Historical Budget vs. Actuals

As a user,
I want to view budget vs. actual spending for any past month,
So that I can review my history and see whether my budget accuracy has improved over time. (FR-1.15)

**Acceptance Criteria:**

**Given** budget and transaction records exist for at least one prior month
**When** I select a past month from the month-picker
**Then** the view shows budget vs. spend for that month (same layout as Story 4.4)
**And** the selected month is reflected in the URL (`?month=2026-04`) so the view is bookmarkable
**And** the chart and table both update for the selected month
**And** months with no data are not shown in the picker; current month is the default view

---

## Epic 5: Bills & Debts Tracking

User can track all recurring financial obligations — add recurring bills, mark them as paid (which logs a transaction and advances the due date), see what's coming due, and record non-credit debts.

### Story 5.1: Recurring Bill Setup & Management

As a user,
I want to add and manage recurring bills with a due date, amount, and payee,
So that I never lose track of what I owe each month and when it's due. (FR-1.16)

**Acceptance Criteria:**

**Given** I navigate to `/bills/`
**When** I submit the "Add Bill" form with valid data
**Then** a Bill record is created with: name (required), amount (positive decimal), due_day (1–31), payee, category (optional), `is_active=True`
**And** on valid submit → PRG redirect to bills list, flash `'success'` "Bill added."
**And** I can edit any bill field at `/bills/<id>/edit`; on submit → PRG redirect, flash `'success'`
**And** I can toggle a bill's active/inactive status; inactive bills are hidden by default but accessible via "Show inactive" toggle
**And** deleting a bill with no payment history shows `confirm_modal.html`; on confirm → hard-delete, flash `'success'`

---

### Story 5.2: Bills List View

As a user,
I want to see all my upcoming bills sorted by due date with clear status indicators,
So that I know at a glance what's coming up, what's due today, and what's overdue. (FR-1.17)

**Acceptance Criteria:**

**Given** at least one active bill exists
**When** I visit `/bills/`
**Then** bills are listed sorted by next due date ascending
**And** each row shows: name, amount, days until due, and status badge: "Upcoming" (> 1 day), "Due Today" (0 days), "Overdue" (past due)
**And** next due date is calculated from `due_day` using `date_utils.py` month-end-safe arithmetic (e.g., due_day=31 in February → Feb 28)
**And** default view shows next 30 days; a "View all" toggle shows all active bills
**And** when no active bills exist, `empty_state.html` renders with "No bills yet" and a CTA to add one

---

### Story 5.3: Mark Bill as Paid

As a user,
I want to mark a bill as paid for the current cycle,
So that it logs the payment as a transaction and automatically advances the next due date. (FR-1.18)

**Acceptance Criteria:**

**Given** an active bill exists
**When** I click "Mark as Paid" on a bill
**Then** a Transaction record is created: date = today, merchant_normalized = bill name, amount = bill amount, category_id = bill's category (or Uncategorized), `is_manual=True`
**And** `bill.last_paid_date` updates to today; next due date advances one month via `date_utils.py` month-end-safe arithmetic
**And** the bill moves from "Overdue"/"Due Today" to "Upcoming" at the new due date
**And** PRG redirect; flash `'success'` "Bill marked as paid. Transaction logged."

---

### Story 5.4: Non-Credit Debt Tracking

As a user,
I want to add and track non-credit debts (personal loans, medical bills) with balance, interest rate, and minimum payment,
So that all my financial obligations are visible in one place alongside my bills. (FR-1.19)

**Acceptance Criteria:**

**Given** I navigate to `/bills/`
**When** I submit the "Add Debt" form
**Then** a Debt record is created with: name (required), current_balance (positive decimal), interest_rate (non-negative float), min_payment (positive decimal), due_date (specific date)
**And** debts appear in the bills list interleaved with bills, sorted by next due date; debts show a "Debt" badge
**And** I can edit any debt at `/bills/debts/<id>/edit`; delete uses `confirm_modal.html`
**And** debts feed the Dashboard "Upcoming Bills" widget (Epic 8) — widget queries both bills and debts tables

---

## Epic 6: Paydown Planner

User can calculate and compare all 5 debt paydown strategies side-by-side, review detailed amortization tables and payoff charts, and activate a plan.

### Story 6.1: Credit Card Account Setup

As a user,
I want to add and manage my credit card accounts with balance, APR, minimum payment, and credit limit,
So that the paydown planner has accurate data to calculate my debt-free timeline. (FR-1.20)

**Acceptance Criteria:**

**Given** I navigate to `/paydown/`
**When** I submit the "Add Credit Card" form with valid data
**Then** an Account record is created with: card name, issuer (optional), `type='credit'`, current_balance, apr (%), min_payment, credit_limit
**And** on valid submit → PRG redirect, flash `'success'` "Card added."
**And** I can edit any card at `/paydown/cards/<id>/edit`; PRG redirect on submit
**And** all credit cards listed with balance, APR, and minimum payment
**And** when no cards exist, `empty_state.html` renders with a CTA to add the first card

---

### Story 6.2: Amortization Engine — All 5 Strategies

As a developer,
I want `services/amortization.py` to implement all 5 paydown strategies as pure functions,
So that the calculation logic is fully isolated, testable without Flask or a database, and correct before any UI is built. (FR-1.21, FR-1.22)

**Acceptance Criteria:**

**Given** a list of card data (balance, APR, min_payment) and an extra_monthly_payment amount
**When** any strategy function is called
**Then** `amortization.py` implements module-level functions (no class): `calculate_avalanche`, `calculate_snowball`, `calculate_highest_balance`, `calculate_proportional`, `calculate_custom`
**And** each returns an `AmortizationResult` plain `@dataclass` with primitive fields only: `strategy: str`, `months_to_payoff: int`, `total_interest_paid: Decimal`, `payoff_date: datetime.date`, `per_card_schedule: list[dict]` (all Decimal/date primitives — no ORM references)
**And** zero imports outside stdlib + `decimal` + `datetime` (Boundary 6 enforced)
**And** `tests/test_services/test_amortization.py` covers: correct payoff month for a known scenario, Avalanche directs extra to highest APR, Snowball to lowest balance, zero extra falls back to minimums, single card, $0-balance cards skipped
**And** all monetary arithmetic uses `Decimal` — no `float` (test asserts type)

---

### Story 6.3: Strategy Comparison View

As a user,
I want to see all 5 paydown strategies calculated simultaneously in a comparison table,
So that I can immediately understand the trade-offs and choose the best approach. (FR-1.22)

**Acceptance Criteria:**

**Given** at least one credit card with balance > 0 exists and an extra monthly payment amount has been entered
**When** I visit `/paydown/compare`
**Then** all 5 strategies display in a comparison table: Strategy Name, Months to Payoff, Total Interest Paid, Debt-Free Date
**And** the Custom row shows per-card allocation inputs; re-calculating updates only the Custom row without page reload (JS + `POST /api/paydown/calculate-custom`)
**And** calculations call `amortization.py` functions — no calculation logic in the blueprint
**And** if `extra_monthly_payment` is not set, a prompt asks for it before comparing
**And** if total card balance is $0, `empty_state.html` shows "You have no outstanding credit card debt."
**And** the currently active strategy (if any) is visually indicated

---

### Story 6.4: Strategy Detail — Amortization Table & Payoff Chart

As a user,
I want to expand any strategy to see a month-by-month amortization table and a payoff timeline chart,
So that I can understand exactly how my debt will be paid down. (FR-1.23)

**Acceptance Criteria:**

**Given** the comparison view from Story 6.3 is loaded
**When** I click "View Details" on any strategy
**Then** `/paydown/strategy/<strategy_name>` shows: per-card payoff month/year, total interest paid, total months to payoff
**And** a month-by-month amortization table (collapsed by default, expandable) shows: payment, principal, interest, remaining balance per card per month
**And** a Chart.js line chart shows remaining balance over time per card (one line per card); x-axis = month, y-axis = balance; uses `chart-defaults.js`
**And** chart data loads via `GET /api/paydown/schedule/<strategy_name>` returning `per_card_schedule` from `AmortizationResult`

---

### Story 6.5: Plan Activation

As a user,
I want to activate one paydown strategy as my official plan,
So that the app can monitor my progress against it. (FR-1.24)

**Acceptance Criteria:**

**Given** I am viewing a strategy detail page
**When** I click "Activate This Plan"
**Then** a PaydownPlan record is created: strategy, extra_monthly_payment, `is_active=True`, `created_at=now()`
**And** any previously active plan is archived atomically: `is_active=False`, `archived_at=now()` — both in a single transaction via `plan_lifecycle.activate_plan()`
**And** PaydownPlanCard records link the plan to each card with its monthly_allocation
**And** after activation → PRG redirect to `/paydown/monitor`, flash `'success'` "Plan activated."
**And** only one plan can be `is_active=True` at any time (enforced at service layer)

---

## Epic 7: Paydown Monitor

User can actively track their activated paydown plan against reality — update card balances monthly, see in-app deviation alerts when actual balances exceed projected by >5%, and switch or archive strategies.

### Story 7.1: Paydown Monitor View & Deviation Detection

As a user,
I want to see how my actual card balances compare to my plan's projections, with alerts when I'm falling behind,
So that I can catch drift early before it compounds. (FR-1.25)

**Acceptance Criteria:**

**Given** an active paydown plan exists
**When** I visit `/paydown/monitor`
**Then** the monitor shows each card: planned balance for current month, most recent actual balance, variance (actual − planned), and status indicator
**And** `services/plan_monitor.py` flags a card when `actual_balance > planned_balance * 1.05` (strictly > 5% over) with an `alert.html` "⚠ Behind Plan" warning
**And** deviation check triggers on every page load (synchronous)
**And** `plan_monitor.py` accepts `AmortizationResult` and list of `PaydownBalanceUpdate` records as inputs — no Flask context, no direct DB session (Boundary 5)
**And** `tests/test_services/test_plan_monitor.py` generates planned balances by calling `amortization.py` directly (not hardcoded); covers: exactly at 5% (not flagged), 5.01% over (flagged), below plan (not flagged), no updates yet
**And** stale-data indicator appears if most recent update > 30 days old
**And** if no active plan exists, `empty_state.html` renders with a link to `/paydown/compare`

---

### Story 7.2: Monthly Balance Updates

As a user,
I want to update my card balances each month to keep monitoring accurate,
So that deviation alerts reflect reality rather than starting balances. (FR-1.26)

**Acceptance Criteria:**

**Given** an active plan exists
**When** I submit a new balance for a card on the monitor page
**Then** a PaydownBalanceUpdate record is created: account_id, balance (positive decimal), `updated_at=now()`
**And** old updates are preserved (not overwritten); most recent per card is used for monitoring
**And** after update → deviation check runs immediately; PRG redirect back to `/paydown/monitor`
**And** each card has its own inline balance input and submit button on the monitor page
**And** submitting for a card not in the active plan returns flash `'error'` "Card not in active plan."

---

### Story 7.3: Strategy Switch & Plan Archival

As a user,
I want to switch to a different paydown strategy at any time, with my old plan preserved for reference,
So that I can adapt my approach without losing my history. (FR-1.27)

**Acceptance Criteria:**

**Given** an active paydown plan exists
**When** I click "Switch Strategy" and select a new strategy
**Then** `plan_lifecycle.activate_plan()` archives the old plan (`is_active=False`, `archived_at=now()`) and creates the new plan (`is_active=True`) atomically in one DB transaction
**And** if the commit fails, neither change persists (rollback)
**And** the new plan recalculates from current balances (most recent PaydownBalanceUpdate per card, not original starting balances)
**And** after switching → PRG redirect to `/paydown/monitor`, flash `'success'` "Strategy switched. New plan activated."
**And** `/paydown/history` shows archived plans: strategy name, activation date, archive date, final extra_monthly_payment (view-only)

---

## Epic 8: Dashboard (Phase 1 Capstone)

User has a complete financial home base — a single dashboard showing all 5 widgets with real data from Epics 3–7. This is the Phase 1 delivery milestone: the app is fully functional.

### Story 8.1: Dashboard Shell & Summary Service

As a user,
I want the dashboard to be the app's default landing page with a consistent shell,
So that I land on my financial overview every time I open the app. (FR-1.1)

**Acceptance Criteria:**

**Given** the app is running
**When** I navigate to `/` or `/dashboard/`
**Then** the dashboard renders using `base.html` with `active_page='dashboard'`
**And** `services/summary.py` defines `get_dashboard_data(month, active_plan_id, monthly_income)` — plain parameters, no Flask context (Boundaries 1 & 5)
**And** the route extracts month, active_plan_id, and monthly_income at the route layer and passes them as parameters
**And** `get_dashboard_data()` runs 5 sequential queries in a single `db.session`; uses SQL aggregation throughout
**And** `GET /` returns HTTP 200 in under 2 seconds (NFR-1)

---

### Story 8.2: Budget Burn & Monthly Summary Widgets

As a user,
I want to see my budget burn rate per category and my monthly spending total on the dashboard,
So that I can gauge my financial health at a glance. (FR-1.2 partial)

**Acceptance Criteria:**

**Given** budgets and transactions exist for the current month
**When** the dashboard loads
**Then** the Budget Burn widget shows a `progress_bar.html` per category: name, spent/budgeted, color-coded green (< 80%), yellow (80–99%), red (≥ 100%)
**And** the Monthly Spending Summary widget shows: current month total, prior month total, delta highlighted in semantic color
**And** both widgets link to `/budgets/` (FR-1.3)
**And** when no budgets exist, the widget shows `empty_state.html` "No budgets set" with a link to `/budgets/`
**And** all aggregations use SQL `func.sum` — no Python loops over transaction rows

---

### Story 8.3: Upcoming Bills & Recent Transactions Widgets

As a user,
I want to see upcoming bills and recent transactions on the dashboard,
So that I can stay on top of due dates and spot unexpected charges immediately. (FR-1.2 partial)

**Acceptance Criteria:**

**Given** bills and transactions exist
**When** the dashboard loads
**Then** the Upcoming Bills widget shows bills and debts due within 30 days, sorted by due date; each row shows name, amount, days-until-due, status badge
**And** the Recent Transactions widget shows the last 10 transactions: date, merchant_normalized, amount, category
**And** both widgets link to their full views via "View all" links (FR-1.3)
**And** each widget uses `empty_state.html` with appropriate messaging and CTA when no data exists

---

### Story 8.4: Debt Paydown Progress Widget & Full Dashboard Integration

As a user,
I want to see my active paydown plan progress on the dashboard, completing the full five-widget view,
So that all key financial metrics are visible in one place and Phase 1 is fully functional. (FR-1.2 complete, FR-1.3)

**Acceptance Criteria:**

**Given** an active paydown plan exists
**When** the dashboard loads
**Then** the Debt Paydown Progress widget shows: each card's current balance, planned balance for this month, progress bar toward $0, and projected payoff date
**And** cards flagged by `plan_monitor.py` (> 5% behind plan) show an `alert.html` warning in the widget
**And** the widget links to `/paydown/monitor` (FR-1.3)
**And** when no active plan exists, the widget shows `empty_state.html` "No active paydown plan" with a link to `/paydown/compare`
**And** `tests/test_blueprints/test_dashboard.py` covers: all 5 widgets render with data, all 5 widgets render empty-state when no data, HTTP 200 in all cases
**And** `GET /` responds in < 2 seconds (NFR-1 verified)

---

## Epic 9: PDF Statement Import

User can bulk-import up to 6+ months of historical statements from 7 supported issuers via a 3-step wizard, with staged review, auto-categorization, merchant normalization, and duplicate detection.

### Spike 9.A: Apple Card PDF Structure Analysis

As a developer,
I want to analyse Apple Card PDF files and document the parsing strategy before any implementation begins,
So that the Apple Card parser story has a verified fixture and approach. (OQ-3)

**Acceptance Criteria:**

**Given** at least one scrubbed Apple Card PDF statement is available
**When** this spike is complete
**Then** `tests/fixtures/pdfs/apple_card_sample.pdf` is committed
**And** `tests/fixtures/pdfs/apple_card_notes.md` documents: whether pdfplumber works directly or OCR fallback is required, transaction table coordinates/patterns, `parser_version` string
**And** `tests/test_parsers/test_apple_parser.py` exists with `@pytest.mark.skip("implement in Story 9.9")` test cases showing expected `StagedTransaction` output
**And** Story 9.9 is unblocked

---

### Spike 9.B: Target RedCard PDF Structure Analysis

As a developer,
I want to analyse Target RedCard PDFs (credit and debit variants) before implementation,
So that the Target parser handles both formats correctly. (OQ-4)

**Acceptance Criteria:**

**Given** scrubbed Target RedCard Credit and Debit PDF samples are available
**When** this spike is complete
**Then** `tests/fixtures/pdfs/target_credit_sample.pdf` and `target_debit_sample.pdf` are committed
**And** `tests/fixtures/pdfs/target_notes.md` documents: how to distinguish credit vs. debit in PDF content, structural differences, extraction approach, `parser_version`
**And** `tests/test_parsers/test_target_parser.py` exists with skip-annotated tests for both card types
**And** Story 9.10 is unblocked

---

### Spike 9.C: DCU Credit Union PDF Structure Analysis

As a developer,
I want to analyse DCU Credit Union PDFs and document a manual extraction template if needed,
So that the DCU parser story has a verified approach. (OQ-5)

**Acceptance Criteria:**

**Given** a scrubbed DCU Credit Union PDF statement is available
**When** this spike is complete
**Then** `tests/fixtures/pdfs/dcu_sample.pdf` is committed
**And** `tests/fixtures/pdfs/dcu_notes.md` documents: extraction approach (direct pdfplumber vs. coordinate-based template), layout patterns, `parser_version`
**And** `tests/test_parsers/test_dcu_parser.py` exists with skip-annotated tests
**And** Story 9.11 is unblocked

---

### Story 9.1: Import Wizard Shell & Staging Pipeline Foundation

As a user,
I want a clear 3-step import wizard (Upload → Review → Confirm) backed by a staging pipeline,
So that no transactions ever touch my main database until I explicitly approve them. (FR-2.1, FR-2.3, NFR-2)

**Acceptance Criteria:**

**Given** I navigate to `/import-pdf/`
**When** I land on the upload step
**Then** the wizard shows a 3-step progress indicator: Upload → Review → Confirm
**And** `staging_pipeline.py` defines: `begin_import(pdf_paths)`, `commit_import(batch_id)`, `abandon_import(batch_id)`
**And** `staging_pipeline.py` opens `instance/import_staging.db` via a dedicated `staging_engine` — never touched by any other module (Boundary 4)
**And** `commit_import()` wraps main DB promotion in a single transaction; sets `committed_at` on staged rows after commit (idempotent cleanup)
**And** `abandon_import()` deletes all staging rows for the batch
**And** `tests/test_services/test_staging_pipeline.py` covers commit and abandon flows using in-memory test DBs

---

### Story 9.2: PDF Issuer Detector

As a user,
I want the app to automatically detect which bank issued my statement,
So that I don't have to manually specify the issuer for every upload. (FR-2.2)

**Acceptance Criteria:**

**Given** a PDF file has been uploaded
**When** `detector.detect_issuer(raw_text)` is called
**Then** `services/pdf_parsers/detector.py` returns one of: `"chase"`, `"bofa"`, `"apple"`, `"kohls"`, `"discover"`, `"dcu"`, `"target"`
**And** `detector.py` NEVER imports from any issuer module (Boundary 3)
**And** low-confidence detection raises `DetectionError`; wizard prompts user to select issuer manually
**And** `tests/test_parsers/test_detector.py` covers correct detection for all 7 issuers and low-confidence raises `DetectionError`

---

### Story 9.3: Merchant Normalizer & Staging Pipeline Integration

As a developer,
I want merchant normalization integrated into the staging pipeline before duplicate detection runs,
So that raw statement strings are cleaned before dedup hashes are computed, preventing false negatives. (FR-2.6, AR-7)

**Acceptance Criteria:**

**Given** a list of StagedTransaction objects with raw merchant values
**When** `merchant_normalizer.normalize_all(staged_txns, merchant_lookup)` is called
**Then** known merchants map to normalized names; unknown merchants keep `merchant_raw` as `merchant_normalized`
**And** normalization runs BEFORE `duplicate_detector.flag_duplicates()` in `begin_import()` (order verified by test)
**And** `merchant_normalizer.py` accepts a lookup dict passed in by caller — no direct DB session access
**And** tests cover: known merchant maps, unknown preserves raw, case-insensitive matching

---

### Story 9.4: Staged Review UI & Commit Flow

As a user,
I want to review all extracted transactions before importing, with the ability to edit any row,
So that I can correct misread merchants, wrong categories, or amounts before they enter my data. (FR-2.4, FR-2.7, FR-2.8)

**Acceptance Criteria:**

**Given** `begin_import()` has staged transactions
**When** I visit `/import-pdf/review/<batch_id>`
**Then** all staged rows display: date, merchant (editable), category (editable dropdown), amount (editable), duplicate flag indicator
**And** duplicate-flagged rows are highlighted with `alert.html`; each has "Keep" and "Skip" buttons
**And** inline edits update `StagedTransactionModel` via AJAX `POST /api/import/update-staged/<id>`
**And** "Confirm Import" → `commit_import(batch_id)` → PRG redirect, flash `'success'` "Imported N transactions, skipped M duplicates."
**And** "Cancel" → `abandon_import(batch_id)` → redirect to `/import-pdf/`, flash `'info'` "Import cancelled."
**And** a test covers the mixed case: batch with clean and duplicate-flagged rows; only non-skipped rows committed
**And** the skip-annotated staged-dedup stub from Story 1.3 is now fully implemented and the `@pytest.mark.skip` removed

---

### Story 9.5: Auto-Categorization & User Correction Persistence

As a user,
I want transactions auto-categorized during import, with my corrections saved for future imports,
So that repeated merchants are categorized correctly without manual effort every time. (FR-2.5)

**Acceptance Criteria:**

**Given** staged transactions exist in the review UI
**When** auto-categorization runs during `begin_import()`
**Then** each staged transaction's category is set by looking up `merchant_normalized` in `MerchantMapping` records
**And** merchants with no mapping are flagged as "Uncategorized" for user review
**And** when I change a category in the review UI, a prompt asks "Save this mapping for future imports?" — if confirmed, a `MerchantMapping` record is created/updated with `user_confirmed=True`
**And** user-confirmed mappings take precedence over system mappings on future imports
**And** `tests/test_services/test_staging_pipeline.py` covers: known merchant auto-categorized, unknown flagged as Uncategorized, user correction persists

---

### Story 9.6: Import History Log & Parse Error Display

As a user,
I want to see a log of all past imports and any rows that couldn't be parsed,
So that I can track what I've already imported and manually enter any missed transactions. (FR-2.9, FR-2.10)

**Acceptance Criteria:**

**Given** at least one import has been committed
**When** I visit `/import-pdf/history`
**Then** a table shows all ImportBatch records: upload date, issuer, statement period, transaction count, duplicates skipped, parse error count
**And** each batch row expands to show ParseError records: page number, raw text sample, reason, parser_version
**And** parse errors include an "Add manually" link → `/transactions/create` pre-filled with raw text in notes
**And** batches with zero parse errors show a "✓ Clean import" indicator
**And** when no imports exist, `empty_state.html` renders "No imports yet"

---

### Story 9.7: Chase Statement Parser

As a developer,
I want a Chase-specific PDF parser implementing the base contract,
So that Chase transactions are extracted correctly. (FR-2.1, FR-2.3)

**Acceptance Criteria:**

**Given** `tests/fixtures/pdfs/chase_sample.pdf` exists
**When** `ChaseParser().parse(pdf_path)` is called
**Then** returns `tuple[list[StagedTransaction], list[ParseError]]`; `issuer='chase'`, `parser_version='chase-v1'`
**And** extends `BaseParser`; zero SQLAlchemy imports; partial failure returns results + errors, never raises
**And** `tests/test_parsers/test_chase_parser.py` passes: correct transaction count, 3 spot-checks, at least one credit correctly flagged `is_credit=True`

---

### Story 9.8: Bank of America, Discover & Kohls Parsers

As a developer,
I want parsers for BofA, Discover, and Kohls implementing the base contract,
So that statements from these three issuers are correctly extracted. (FR-2.1, FR-2.3)

**Acceptance Criteria:**

**Given** scrubbed fixtures exist for all three issuers
**When** each parser's `parse(pdf_path)` is called
**Then** each returns `tuple[list[StagedTransaction], list[ParseError]]` with correct issuer and parser_version fields
**And** all three extend `BaseParser`; zero SQLAlchemy imports; partial failure handled
**And** `test_bofa_parser.py`, `test_discover_parser.py`, `test_kohls_parser.py` each pass with transaction count check and 3 spot-checks

---

### Story 9.9: Apple Card Statement Parser *(requires Spike 9.A)*

As a developer,
I want an Apple Card parser handling the non-standard layout documented in Spike 9.A. (FR-2.1, FR-2.3, OQ-3)

**Acceptance Criteria:**

**Given** Spike 9.A is complete
**When** `AppleCardParser().parse(pdf_path)` is called
**Then** returns correct `tuple[list[StagedTransaction], list[ParseError]]`; uses strategy from `apple_card_notes.md`
**And** if pytesseract required: listed in `requirements.txt`, steps documented in module docstring
**And** skip-annotated tests from Spike 9.A are now implemented and passing

---

### Story 9.10: Target RedCard Statement Parser *(requires Spike 9.B)*

As a developer,
I want a Target RedCard parser handling both credit and debit formats documented in Spike 9.B. (FR-2.1, FR-2.3, OQ-4)

**Acceptance Criteria:**

**Given** Spike 9.B is complete
**When** `TargetParser().parse(pdf_path)` is called on either card type
**Then** correctly distinguishes credit vs. debit; `is_credit` set correctly for both formats
**And** skip-annotated tests from Spike 9.B are implemented and passing

---

### Story 9.11: DCU Credit Union Statement Parser *(requires Spike 9.C)*

As a developer,
I want a DCU Credit Union parser using the extraction approach documented in Spike 9.C. (FR-2.1, FR-2.3, OQ-5)

**Acceptance Criteria:**

**Given** Spike 9.C is complete
**When** `DCUParser().parse(pdf_path)` is called
**Then** returns correct tuple using approach from `dcu_notes.md`
**And** skip-annotated tests from Spike 9.C are implemented and passing

---

## Epic 10: Analytics & Insights

User gets data-driven insights from their transaction history — spending pattern charts, year-over-year comparisons, subscription detection, bad-spending flags, and history-based budget recommendations completing FR-1.12.

### Story 10.1: Analytics Infrastructure — Rolling Aggregator & Blueprint Shell

As a developer,
I want the analytics service infrastructure and blueprint shell in place before individual analytics features are built,
So that all Phase 3 services share the same rolling-window foundation with boundary rules enforced from the start.

**Acceptance Criteria:**

**Given** the main DB has transaction history
**When** this story is complete
**Then** `services/rolling_aggregator.py` implements `category_rolling_avg(months: int, df) -> dict[str, Decimal]` — no Flask context, accepts DataFrame not live session (Boundaries 5 & 8)
**And** pandas confined to `services/analytics/` and `services/rolling_aggregator.py` only; grep confirms no other file imports pandas (Boundary 8)
**And** the analytics blueprint shell exists with `/analytics/` returning HTTP 200
**And** `services/analytics/` and `services/insights/` module stubs exist for all planned modules
**And** `tests/test_analytics/conftest.py` seeds 3 months of transaction fixtures for all analytics tests

---

### Story 10.2: Spending Patterns Chart

As a user,
I want to see my monthly spending by category as a stacked bar chart covering all my history,
So that I can understand how my spending has evolved over time. (FR-3.1)

**Acceptance Criteria:**

**Given** at least 2 months of transaction history exist
**When** I visit `/analytics/spending`
**Then** a Chart.js stacked bar chart shows monthly spend per category; x-axis = month, y-axis = total spend; each category is a consistently colored stacked segment
**And** a trend line per category is overlaid on a secondary axis
**And** chart data loads via `GET /api/analytics/spending` → `trend_analytics.monthly_by_category(df)` — accepts DataFrame, no Flask context (Boundaries 5 & 8)
**And** when fewer than 2 months of data exist, `empty_state.html` renders "Not enough history yet — import at least 2 months of statements."

---

### Story 10.3: Month-over-Month & Year-over-Year Comparisons

As a user,
I want to compare spending for any category across months and years,
So that I can see whether my spending is improving or growing in specific areas. (FR-3.2)

**Acceptance Criteria:**

**Given** sufficient transaction history exists
**When** I use the comparison controls on `/analytics/spending`
**Then** a category selector and comparison type (MoM / YoY) filter the chart
**And** MoM: current month vs. prior month side-by-side; delta amount and percentage highlighted
**And** YoY: selected month vs. same month one year prior; "No data for this period" if unavailable
**And** filters persist in URL query parameters
**And** `trend_analytics.mom_comparison(df, category, month)` and `yoy_comparison(df, category, month)` implement the logic as pure functions accepting DataFrames

---

### Story 10.4: Subscription Detector

As a user,
I want the app to automatically detect my recurring subscriptions from transaction history,
So that I can see what I'm paying for regularly and spot anything unexpected. (FR-3.3)

**Acceptance Criteria:**

**Given** at least 2 months of transaction history exists
**When** I visit `/analytics/subscriptions`
**Then** `services/insights/subscription_detector.detect(df)` scans for: same merchant_normalized, similar amounts (±$1), recurring on weekly/monthly/annual interval
**And** results listed with: merchant, estimated amount, recurrence interval, total paid in last 12 months
**And** detector never writes to the DB; results presented to user only (generative insight)
**And** `subscription_detector.py` accepts a DataFrame — no Flask context, no DB session
**And** tests cover: monthly recurring detected, annual detected, irregular charges not flagged, ±$1 variance accepted

---

### Story 10.5: Bad Spending Pattern Flags

As a user,
I want automatic flags when my spending patterns indicate a potential problem,
So that I'm proactively alerted without having to check manually. (FR-3.4)

**Acceptance Criteria:**

**Given** transaction history, budget records, and monthly_income are available
**When** I visit `/analytics/insights`
**Then** `services/insights/pattern_flags.check(df, budgets, monthly_income)` generates flags for:
- Category exceeded budget by > 20% for 2 consecutive months → "⚠ Consistently over budget: {category}"
- Detected subscription with no matching transaction in 60+ days → "⚠ Possible cancelled subscription still charging: {merchant}"
- Total Dining + Entertainment > 15% of monthly_income this month → "⚠ Dining & Entertainment at {X}% of income this month"

**And** flags display as `alert.html` components with `type='warning'`
**And** `monthly_income` passed as plain parameter — never queried directly by `pattern_flags.py` (Boundaries 1 & 5)
**And** no flags triggered → `'info'` message "No spending concerns detected this month"
**And** tests cover all 3 flag types and the no-flag case

---

### Story 10.6: Merchant Spend Summary

As a user,
I want to see my top 20 merchants by total spend, sortable and filterable by date range,
So that I can identify where most of my money goes. (FR-3.6)

**Acceptance Criteria:**

**Given** transaction history exists
**When** I visit `/analytics/merchants`
**Then** a table shows top 20 merchants: merchant name, total amount, transaction count, average transaction amount
**And** sortable by total amount (default desc) or transaction count; date range filter persists in URL params
**And** `services/analytics/merchant_analytics.top_merchants(df, n=20)` implements aggregation — accepts DataFrame, no Flask context
**And** when fewer than 20 distinct merchants exist, all are shown

---

### Story 10.7: History-Based Budget Recommendations

As a user,
I want budget recommendations based on my actual 3-month spending history,
So that my budgets are grounded in reality rather than a generic formula. (FR-3.5, FR-1.12 completion)

**Acceptance Criteria:**

**Given** at least 2 months of transaction history exist
**When** I visit `/budgets/recommend`
**Then** `budget_recommender.from_history(rolling_avgs)` is called with results from `rolling_aggregator.category_rolling_avg(months=3)`
**And** each recommendation shows: category, suggested amount (3-month avg rounded to nearest $5), the avg that drove it, comparison to current budget
**And** `budget_recommender.from_history()` satisfies the same `BudgetRecommendation` interface contract defined in Story 4.3 — the Epic 4 contract test passes against this implementation
**And** route logic: `months_available >= 2` → `from_history()`; else → `fifty_thirty_twenty()` (Story 4.3 fallback retained)
**And** tests cover: history-based path, fallback path, interface contract test passes for both implementations
