# Story 3.3: Manual Transaction Entry

Status: review

## Story

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

1. `GET /transactions/create` renders `TransactionForm` with fields: date, merchant, amount, category (dropdown), account (dropdown), notes (optional).
2. Valid submit saves: `date`, `merchant_normalized` = merchant input, `merchant_raw` = merchant input, `amount` as Decimal, `category_id`, `account_id`, `is_manual=True`, optional notes, `dedup_hash` computed and stored.
3. `dedup_hash` = SHA256(`merchant_normalized` + `str(amount)` + `date_str`) where amount is normalized to 2dp.
4. Before saving, `duplicate_detector.flag_duplicates([new_hash], recent_hashes)` is called with existing `dedup_hash` values from transactions within ±3 days of the entered date.
5. If flagged: flash `'warning'` "Possible duplicate transaction detected.", re-render form with data intact and a "Save Anyway" option (hidden `force_save=1` field). User may submit again to force-save or navigate away to cancel.
6. If `force_save=1` on POST: skip duplicate check, save immediately.
7. On successful save → PRG redirect to `/transactions/`, flash `'success'` "Transaction added."
8. Invalid data re-renders form with inline errors via `render_field` — no redirect.
9. `GET /transactions/` returns HTTP 200 (minimal list stub — fully built in Story 3.5).
10. `pytest tests/` passes — all 145 prior tests still green.

## Tasks / Subtasks

- [ ] **Task 1: Write `tests/test_blueprints/test_transaction_create.py`** (TDD RED)
- [ ] **Task 2: `app/blueprints/transactions/forms.py`** — TransactionForm
- [ ] **Task 3: `app/blueprints/transactions/routes.py`** — create route + minimal list stub
- [ ] **Task 4: Templates** — `transactions/create.html`, `transactions/index.html`
- [ ] **Task 5: Add `template_folder` to transactions blueprint**
- [ ] **Task 6: Run full suite GREEN**

## Dev Notes

### dedup_hash helper

```python
import hashlib
from decimal import Decimal

def compute_dedup_hash(merchant: str, amount: Decimal, date_str: str) -> str:
    normalized_amt = str(Decimal(str(amount)).quantize(Decimal('0.01')))
    raw = merchant + normalized_amt + date_str
    return hashlib.sha256(raw.encode()).hexdigest()
```

Place in `app/blueprints/transactions/routes.py` (private to this module).

### Duplicate check window

Query `dedup_hash` for existing transactions where `date` is within ±3 days of `form.date.data`. Use `date_utils.add_months` is not needed — simple `timedelta(days=3)`.

```python
from datetime import timedelta
window_start = (form.date.data - timedelta(days=3)).isoformat()
window_end   = (form.date.data + timedelta(days=3)).isoformat()
recent_hashes = [t.dedup_hash for t in
    Transaction.query
        .filter(Transaction.date >= window_start,
                Transaction.date <= window_end,
                Transaction.dedup_hash.isnot(None))
        .all()]
```

### TransactionForm

```python
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, TextAreaField, HiddenField
from wtforms.fields import DateField
from wtforms.validators import DataRequired, Optional

class TransactionForm(FlaskForm):
    date            = DateField('Date', validators=[DataRequired()])
    merchant        = StringField('Merchant / Payee', validators=[DataRequired()])
    amount          = StringField('Amount', validators=[DataRequired()])
    category_id     = SelectField('Category', coerce=int, validators=[Optional()])
    account_id      = SelectField('Account', coerce=int, validators=[DataRequired()])
    notes           = TextAreaField('Notes', validators=[Optional()])
```

Set `form.category_id.choices` and `form.account_id.choices` in the route (dynamic from DB).
`category_id` is optional (may be left as 0 / blank → stored as NULL).
Add a leading `(0, '— Select Category —')` option for category.

### Routes

Blueprint is mounted at `/transactions` (url_prefix in app factory).

- `GET  /` → minimal list stub (returns 200, expanded in Story 3.5)
- `GET  /create` → render form
- `POST /create` → validate, dedup check, save or warn

### File List

| File | Status |
|------|--------|
| `tests/test_blueprints/test_transaction_create.py` | NEW |
| `app/blueprints/transactions/forms.py` | IMPLEMENT |
| `app/blueprints/transactions/routes.py` | IMPLEMENT |
| `app/blueprints/transactions/__init__.py` | MODIFY (add template_folder) |
| `app/blueprints/transactions/templates/transactions/create.html` | NEW |
| `app/blueprints/transactions/templates/transactions/index.html` | NEW |

---

## Dev Agent Record

### Agent Model Used
claude-sonnet-4-6

### Debug Log References
None

### Completion Notes List
- 20 new tests, all green first run — zero regressions against 145 prior tests
- Duplicate detection: `force_save=1` hidden field injected by template on re-render; route reads `request.form.get('force_save')` directly (outside WTForms) to bypass check
- `validate_amount()` from utils used for amount validation; errors appended to `form.amount.errors` for inline display via `render_field`
- Category is optional: `category_id=0` mapped to `NULL` in the DB
- transactions blueprint `__init__.py` updated to include `template_folder='templates'`
- 165 passed, 1 skipped total

### File List
- `tests/test_blueprints/test_transaction_create.py` — NEW (20 tests)
- `app/blueprints/transactions/forms.py` — IMPLEMENTED
- `app/blueprints/transactions/routes.py` — IMPLEMENTED
- `app/blueprints/transactions/__init__.py` — MODIFIED (added template_folder)
- `app/blueprints/transactions/templates/transactions/create.html` — NEW
- `app/blueprints/transactions/templates/transactions/index.html` — NEW (stub, expanded Story 3.5)

### Change Log
- 2026-05-28: Story implemented and moved to review status
