Phase 4: Transaction Matching #46

Closed
opened 2026-05-16 18:17:37 -05:00 by null · 0 comments
Owner

Goal

Let users match imported or manually entered transactions to bills. This connects transaction data to bill status, but only after user confirmation. No auto-marking.

Depends on: Phase 1 (#43 ), Phase 2 (#44 )
Blocks: Phase 5 (#47)
Parent: #42


All Acceptance Criteria Verified

  • User can match a transaction to a bill — POST /api/transactions/:id/match with { billId } via transactionMatchService.matchTransactionToBill()
  • User can unmatch a transaction — POST /api/transactions/:id/unmatch via transactionMatchService.unmatchTransaction(), restores balance delta
  • User can ignore a transaction — POST /api/transactions/:id/ignore via transactionMatchService.ignoreTransaction(), sets match_status = "ignored"
  • User can unignore a transaction — POST /api/transactions/:id/unignore via transactionMatchService.unignoreTransaction(), resets to match_status = "unmatched"
  • Suggestions appear but do not auto-apply — GET /api/matches/suggestions via matchSuggestionService.listMatchSuggestions() is read-only (no writes), scores by amount/date/name proximity, minimum score threshold of 20
  • Confirmed match updates bill status correctly — computeBalanceDelta() called on match, current_balance updated on bills table
  • Unmatch recalculates bill status correctly — restorePaymentBalance() reverses the balance delta, recalculates
  • Manual payment history still works — all existing payment paths (payment_source = "manual") untouched

Implementation Details

Backend

  • services/transactionMatchService.js — match, unmatch, ignore, unignore with db.transaction() wrappers, proper balance tracking
  • services/matchSuggestionService.js — scoring engine (amount + date + name + prior match), rejections table
  • routes/matches.jsGET /suggestions, POST /:id/reject
  • routes/transactions.jsPOST /:id/match, POST /:id/unmatch, POST /:id/ignore, POST /:id/unignore
  • db/database.js — v0.61 (payment transaction active index), v0.62 (match_suggestion_rejections table)

Frontend

  • client/pages/DataPage.jsxMatchBillDialog modal, SuggestedMatchesPanel, filter tabs (Unmatched/Matched/Ignored), action buttons
  • client/api.jsmatchTransaction, unmatchTransaction, ignoreTransaction, unignoreTransaction, matchSuggestions, rejectMatchSuggestion

Matching Rules

  • Manual match: sets matched_bill_id, match_status = "matched", creates payment with payment_source = "transaction_match", updates current_balance
  • Unmatch: clears matched_bill_id, sets match_status = "unmatched", soft-deletes or unlinks payment, restores balance
  • Ignore: sets ignored = 1, match_status = "ignored", clears match, unlinks payment
  • Suggestions: scored 0-100 (amount 40pt, date 25pt, name 22pt, prior match 12pt), minimum 20 to appear, never auto-apply

Previous → Phase 3 (#45 )
Next → Phase 5 (#47)

## Goal Let users match imported or manually entered transactions to bills. This connects transaction data to bill status, but **only after user confirmation**. No auto-marking. **Depends on:** Phase 1 (#43 ✅), Phase 2 (#44 ✅) **Blocks:** Phase 5 (#47) **Parent:** #42 --- ## ✅ All Acceptance Criteria Verified - [x] User can match a transaction to a bill — `POST /api/transactions/:id/match` with `{ billId }` via `transactionMatchService.matchTransactionToBill()` - [x] User can unmatch a transaction — `POST /api/transactions/:id/unmatch` via `transactionMatchService.unmatchTransaction()`, restores balance delta - [x] User can ignore a transaction — `POST /api/transactions/:id/ignore` via `transactionMatchService.ignoreTransaction()`, sets `match_status = "ignored"` - [x] User can unignore a transaction — `POST /api/transactions/:id/unignore` via `transactionMatchService.unignoreTransaction()`, resets to `match_status = "unmatched"` - [x] Suggestions appear but do not auto-apply — `GET /api/matches/suggestions` via `matchSuggestionService.listMatchSuggestions()` is read-only (no writes), scores by amount/date/name proximity, minimum score threshold of 20 - [x] Confirmed match updates bill status correctly — `computeBalanceDelta()` called on match, `current_balance` updated on bills table - [x] Unmatch recalculates bill status correctly — `restorePaymentBalance()` reverses the balance delta, recalculates - [x] Manual payment history still works — all existing payment paths (`payment_source = "manual"`) untouched --- ## Implementation Details ### Backend - **`services/transactionMatchService.js`** — match, unmatch, ignore, unignore with `db.transaction()` wrappers, proper balance tracking - **`services/matchSuggestionService.js`** — scoring engine (amount + date + name + prior match), rejections table - **`routes/matches.js`** — `GET /suggestions`, `POST /:id/reject` - **`routes/transactions.js`** — `POST /:id/match`, `POST /:id/unmatch`, `POST /:id/ignore`, `POST /:id/unignore` - **`db/database.js`** — v0.61 (payment transaction active index), v0.62 (match_suggestion_rejections table) ### Frontend - **`client/pages/DataPage.jsx`** — `MatchBillDialog` modal, `SuggestedMatchesPanel`, filter tabs (Unmatched/Matched/Ignored), action buttons - **`client/api.js`** — `matchTransaction`, `unmatchTransaction`, `ignoreTransaction`, `unignoreTransaction`, `matchSuggestions`, `rejectMatchSuggestion` ### Matching Rules - Manual match: sets `matched_bill_id`, `match_status = "matched"`, creates payment with `payment_source = "transaction_match"`, updates `current_balance` - Unmatch: clears `matched_bill_id`, sets `match_status = "unmatched"`, soft-deletes or unlinks payment, restores balance - Ignore: sets `ignored = 1`, `match_status = "ignored"`, clears match, unlinks payment - Suggestions: scored 0-100 (amount 40pt, date 25pt, name 22pt, prior match 12pt), minimum 20 to appear, never auto-apply **Previous → Phase 3 (#45 ✅)** **Next → Phase 5 (#47)**
null added the
backend
feature
priority:medium
ux
labels 2026-05-16 18:18:02 -05:00
null closed this issue 2026-05-16 21:47:05 -05:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/BillTracker#46
No description provided.