2026-07-10 18:17:13 -05:00
|
|
|
// Fully type-checked (tsconfig.server.json); leading import type keeps Node type-stripping active.
|
2026-07-06 11:26:50 -05:00
|
|
|
import type { Req, Res, Next } from '../types/http';
|
2026-05-16 21:36:04 -05:00
|
|
|
const router = require('express').Router();
|
refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
converted with @ts-nocheck (typing deferred). All requires updated to .cts.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:34:08 -05:00
|
|
|
const { getDb } = require('../db/database.cts');
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
const {
|
|
|
|
|
ApiError,
|
|
|
|
|
ValidationError,
|
|
|
|
|
NotFoundError,
|
|
|
|
|
ConflictError,
|
|
|
|
|
} = require('../utils/apiError.cts');
|
2026-07-06 14:23:53 -05:00
|
|
|
const {
|
|
|
|
|
applyBankPaymentAsSourceOfTruth,
|
|
|
|
|
reactivatePaymentsOverriddenBy,
|
|
|
|
|
} = require('../services/paymentAccountingService.cts');
|
2026-05-16 21:36:04 -05:00
|
|
|
const {
|
|
|
|
|
listMatchSuggestions,
|
|
|
|
|
rejectMatchSuggestion,
|
2026-07-06 10:54:10 -05:00
|
|
|
} = require('../services/matchSuggestionService.cts');
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const { learnMerchantRuleFromMatch } = require('../services/billMerchantRuleService.cts');
|
2026-07-06 10:47:16 -05:00
|
|
|
const { markMatched, markUnmatched } = require('../services/transactionMatchState.cts');
|
2026-07-05 13:51:37 -05:00
|
|
|
const { serializePayment } = require('../services/paymentValidation.cts');
|
2026-07-05 13:48:44 -05:00
|
|
|
const { todayLocal } = require('../utils/dates.mts');
|
2026-05-16 21:36:04 -05:00
|
|
|
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
// 4xx service errors keep their message/code on the wire; anything else is
|
|
|
|
|
// rethrown raw so the terminal handler masks + logs it as a 5xx.
|
2026-07-10 18:17:13 -05:00
|
|
|
function toMatchError(err: any) {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
if (err.status && err.status < 500) {
|
|
|
|
|
return new ApiError(err.code || 'MATCH_ERROR', err.message, err.status, { field: err.field });
|
2026-05-16 21:36:04 -05:00
|
|
|
}
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
return err;
|
2026-05-16 21:36:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/matches/suggestions
|
2026-07-06 11:26:50 -05:00
|
|
|
router.get('/suggestions', (req: Req, res: Res) => {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
res.json(listMatchSuggestions(req.user.id, req.query));
|
2026-05-16 21:36:04 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// POST /api/matches/:id/reject
|
2026-07-06 11:26:50 -05:00
|
|
|
router.post('/:id/reject', (req: Req, res: Res) => {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
res.json(rejectMatchSuggestion(req.user.id, req.params.id));
|
2026-05-16 21:36:04 -05:00
|
|
|
});
|
|
|
|
|
|
2026-05-29 03:02:36 -05:00
|
|
|
// POST /api/matches/confirm — link a transaction to a bill and record a payment
|
2026-07-06 11:26:50 -05:00
|
|
|
router.post('/confirm', (req: Req, res: Res) => {
|
2026-07-06 14:23:53 -05:00
|
|
|
const txId = parseInt(req.body?.transaction_id, 10);
|
2026-05-29 03:02:36 -05:00
|
|
|
const billId = parseInt(req.body?.bill_id, 10);
|
|
|
|
|
if (!Number.isInteger(txId) || !Number.isInteger(billId)) {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw ValidationError('transaction_id and bill_id are required integers');
|
2026-05-29 03:02:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const db = getDb();
|
2026-07-06 14:23:53 -05:00
|
|
|
const tx = db
|
|
|
|
|
.prepare('SELECT * FROM transactions WHERE id = ? AND user_id = ?')
|
|
|
|
|
.get(txId, req.user.id);
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
if (!tx) throw NotFoundError('Transaction not found', 'transaction_id');
|
2026-05-29 03:02:36 -05:00
|
|
|
if (tx.match_status === 'matched') {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw ConflictError(
|
|
|
|
|
'Transaction is already matched to a bill',
|
|
|
|
|
'transaction_id',
|
|
|
|
|
'ALREADY_MATCHED',
|
|
|
|
|
);
|
2026-05-29 03:02:36 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const bill = db
|
|
|
|
|
.prepare('SELECT * FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
|
|
|
|
|
.get(billId, req.user.id);
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
if (!bill) throw NotFoundError('Bill not found', 'bill_id');
|
2026-05-29 03:02:36 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const existing = db
|
|
|
|
|
.prepare('SELECT id FROM payments WHERE transaction_id = ? AND deleted_at IS NULL')
|
|
|
|
|
.get(txId);
|
|
|
|
|
if (existing)
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw ConflictError(
|
|
|
|
|
'A payment is already linked to this transaction',
|
|
|
|
|
undefined,
|
|
|
|
|
'DUPLICATE_MATCH',
|
|
|
|
|
);
|
2026-05-29 03:02:36 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const paidDate =
|
|
|
|
|
tx.posted_date || (tx.transacted_at ? tx.transacted_at.slice(0, 10) : todayLocal());
|
|
|
|
|
const amount = Math.round(Math.abs(tx.amount)); // tx.amount and payments.amount are both cents
|
2026-05-29 03:02:36 -05:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
db.exec('BEGIN');
|
2026-07-06 14:23:53 -05:00
|
|
|
const payResult = db
|
|
|
|
|
.prepare(
|
|
|
|
|
"INSERT INTO payments (bill_id, amount, paid_date, payment_source, transaction_id) VALUES (?, ?, ?, 'transaction_match', ?)",
|
|
|
|
|
)
|
|
|
|
|
.run(billId, amount, paidDate, txId);
|
|
|
|
|
|
|
|
|
|
const paymentForAccounting = db
|
|
|
|
|
.prepare('SELECT * FROM payments WHERE id = ?')
|
|
|
|
|
.get(payResult.lastInsertRowid);
|
2026-06-07 01:05:48 -05:00
|
|
|
applyBankPaymentAsSourceOfTruth(db, bill, paymentForAccounting);
|
2026-05-29 03:02:36 -05:00
|
|
|
|
refactor(match): one canonical writer for transaction match state (IMP-CODE-03)
match_status, matched_bill_id and ignored must move together, but they were
updated by copy-pasted inline UPDATEs across six routes/services — exactly how
they drift apart (QA-B5-04 left match_status='matched' with a NULL bill).
Add services/transactionMatchState.js (markMatched / markUnmatched / markIgnored,
each ownership-scoped, returning rows changed) and route the six single-
transaction transitions through it: matchTransactionToBill, unmatchTransaction,
ignoreTransaction, unignoreTransaction (transactionMatchService), the match/
unmatch handlers (routes/matches), and unmatch-on-payment-delete (routes/
transactions, routes/payments).
Guarded bulk auto-match sweeps (subscription tracking, merchant-rule matching,
historical import) and the retention purge intentionally keep their own queries
— their WHERE clauses carry idempotency guards (AND match_status='unmatched')
the simple helper must not silently drop.
Test: tests/transactionMatchState.test.js (transitions + ownership scoping).
transactionMatchService/subscriptionService regression suites still pass;
server 122 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:02:10 -05:00
|
|
|
markMatched(db, req.user.id, txId, billId);
|
2026-06-06 18:30:21 -05:00
|
|
|
|
|
|
|
|
// Learn a merchant→bill rule from this explicit confirmation so future
|
|
|
|
|
// synced transactions from the same merchant auto-match. Best-effort.
|
|
|
|
|
learnMerchantRuleFromMatch(db, req.user.id, billId, tx);
|
|
|
|
|
|
2026-05-29 03:02:36 -05:00
|
|
|
db.exec('COMMIT');
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const payment = db
|
|
|
|
|
.prepare('SELECT * FROM payments WHERE id = ?')
|
|
|
|
|
.get(payResult.lastInsertRowid);
|
|
|
|
|
const updated = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-29 03:02:36 -05:00
|
|
|
SELECT t.*, b.name AS matched_bill_name
|
|
|
|
|
FROM transactions t
|
|
|
|
|
LEFT JOIN bills b ON b.id = t.matched_bill_id AND b.deleted_at IS NULL
|
|
|
|
|
WHERE t.id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(txId);
|
2026-06-11 20:12:31 -05:00
|
|
|
res.json({ transaction: updated, payment: serializePayment(payment) });
|
2026-05-29 03:02:36 -05:00
|
|
|
} catch (err) {
|
2026-07-06 14:23:53 -05:00
|
|
|
try {
|
|
|
|
|
db.exec('ROLLBACK');
|
|
|
|
|
} catch {}
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw toMatchError(err);
|
2026-05-29 03:02:36 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// POST /api/matches/:transactionId/unmatch — remove a manual match
|
2026-07-06 11:26:50 -05:00
|
|
|
router.post('/:transactionId/unmatch', (req: Req, res: Res) => {
|
2026-05-29 03:02:36 -05:00
|
|
|
const txId = parseInt(req.params.transactionId, 10);
|
|
|
|
|
if (!Number.isInteger(txId)) {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw ValidationError('transactionId must be an integer');
|
2026-05-29 03:02:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const db = getDb();
|
2026-07-06 14:23:53 -05:00
|
|
|
const tx = db
|
|
|
|
|
.prepare('SELECT * FROM transactions WHERE id = ? AND user_id = ?')
|
|
|
|
|
.get(txId, req.user.id);
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
if (!tx) throw NotFoundError('Transaction not found');
|
2026-05-29 03:02:36 -05:00
|
|
|
if (tx.match_status !== 'matched') {
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw ConflictError('Transaction is not matched', undefined, 'NOT_MATCHED');
|
2026-05-29 03:02:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
db.exec('BEGIN');
|
2026-07-06 14:23:53 -05:00
|
|
|
const matchedPayments = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-07 01:05:48 -05:00
|
|
|
SELECT *
|
|
|
|
|
FROM payments
|
|
|
|
|
WHERE transaction_id = ? AND payment_source = 'transaction_match' AND deleted_at IS NULL
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(txId);
|
2026-06-07 01:05:48 -05:00
|
|
|
for (const payment of matchedPayments) {
|
|
|
|
|
reactivatePaymentsOverriddenBy(db, payment.id);
|
|
|
|
|
}
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
`
|
2026-05-29 03:02:36 -05:00
|
|
|
UPDATE payments SET deleted_at = datetime('now'), updated_at = datetime('now')
|
|
|
|
|
WHERE transaction_id = ? AND payment_source = 'transaction_match' AND deleted_at IS NULL
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
).run(txId);
|
refactor(match): one canonical writer for transaction match state (IMP-CODE-03)
match_status, matched_bill_id and ignored must move together, but they were
updated by copy-pasted inline UPDATEs across six routes/services — exactly how
they drift apart (QA-B5-04 left match_status='matched' with a NULL bill).
Add services/transactionMatchState.js (markMatched / markUnmatched / markIgnored,
each ownership-scoped, returning rows changed) and route the six single-
transaction transitions through it: matchTransactionToBill, unmatchTransaction,
ignoreTransaction, unignoreTransaction (transactionMatchService), the match/
unmatch handlers (routes/matches), and unmatch-on-payment-delete (routes/
transactions, routes/payments).
Guarded bulk auto-match sweeps (subscription tracking, merchant-rule matching,
historical import) and the retention purge intentionally keep their own queries
— their WHERE clauses carry idempotency guards (AND match_status='unmatched')
the simple helper must not silently drop.
Test: tests/transactionMatchState.test.js (transitions + ownership scoping).
transactionMatchService/subscriptionService regression suites still pass;
server 122 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:02:10 -05:00
|
|
|
markUnmatched(db, req.user.id, txId);
|
2026-05-29 03:02:36 -05:00
|
|
|
db.exec('COMMIT');
|
|
|
|
|
res.json({ ok: true });
|
|
|
|
|
} catch (err) {
|
2026-07-06 14:23:53 -05:00
|
|
|
try {
|
|
|
|
|
db.exec('ROLLBACK');
|
|
|
|
|
} catch {}
|
refactor(routes): throw-pattern for summary, matches, snowball, dataSources
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:19 -05:00
|
|
|
throw toMatchError(err);
|
2026-05-29 03:02:36 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-16 21:36:04 -05:00
|
|
|
module.exports = router;
|