|
| 1 | +import { Hono } from 'hono'; |
| 2 | +import type { Env } from '../index'; |
| 3 | +import { getDb } from '../lib/db'; |
| 4 | + |
| 5 | +export const transactionRoutes = new Hono<{ Bindings: Env }>(); |
| 6 | + |
| 7 | +// List transactions with filtering and pagination |
| 8 | +transactionRoutes.get('/', async (c) => { |
| 9 | + const sql = getDb(c.env); |
| 10 | + const accountId = c.req.query('account_id') || null; |
| 11 | + const source = c.req.query('source') || null; |
| 12 | + const direction = c.req.query('direction') || null; |
| 13 | + const category = c.req.query('category') || null; |
| 14 | + const from = c.req.query('from') || null; |
| 15 | + const to = c.req.query('to') || null; |
| 16 | + const search = c.req.query('search') || null; |
| 17 | + const limit = Math.min(Number(c.req.query('limit')) || 50, 200); |
| 18 | + const offset = Number(c.req.query('offset')) || 0; |
| 19 | + const searchPattern = search ? `%${search}%` : null; |
| 20 | + |
| 21 | + const [countRow] = await sql` |
| 22 | + SELECT COUNT(*) AS total FROM cc_transactions |
| 23 | + WHERE (${accountId}::uuid IS NULL OR account_id = ${accountId}::uuid) |
| 24 | + AND (${source}::text IS NULL OR source = ${source}) |
| 25 | + AND (${direction}::text IS NULL OR direction = ${direction}) |
| 26 | + AND (${category}::text IS NULL OR category = ${category}) |
| 27 | + AND (${from}::date IS NULL OR tx_date >= ${from}::date) |
| 28 | + AND (${to}::date IS NULL OR tx_date <= ${to}::date) |
| 29 | + AND (${searchPattern}::text IS NULL OR counterparty ILIKE ${searchPattern} OR description ILIKE ${searchPattern}) |
| 30 | + `; |
| 31 | + |
| 32 | + const rows = await sql` |
| 33 | + SELECT * FROM cc_transactions |
| 34 | + WHERE (${accountId}::uuid IS NULL OR account_id = ${accountId}::uuid) |
| 35 | + AND (${source}::text IS NULL OR source = ${source}) |
| 36 | + AND (${direction}::text IS NULL OR direction = ${direction}) |
| 37 | + AND (${category}::text IS NULL OR category = ${category}) |
| 38 | + AND (${from}::date IS NULL OR tx_date >= ${from}::date) |
| 39 | + AND (${to}::date IS NULL OR tx_date <= ${to}::date) |
| 40 | + AND (${searchPattern}::text IS NULL OR counterparty ILIKE ${searchPattern} OR description ILIKE ${searchPattern}) |
| 41 | + ORDER BY tx_date DESC, created_at DESC |
| 42 | + LIMIT ${limit} OFFSET ${offset} |
| 43 | + `; |
| 44 | + |
| 45 | + return c.json({ |
| 46 | + transactions: rows, |
| 47 | + total: Number(countRow.total), |
| 48 | + limit, |
| 49 | + offset, |
| 50 | + }); |
| 51 | +}); |
| 52 | + |
| 53 | +// Summary: totals by direction and category for a date range |
| 54 | +// Must be registered before /:id to avoid matching "summary" as a UUID |
| 55 | +transactionRoutes.get('/summary/totals', async (c) => { |
| 56 | + const sql = getDb(c.env); |
| 57 | + const from = c.req.query('from') || null; |
| 58 | + const to = c.req.query('to') || null; |
| 59 | + const accountId = c.req.query('account_id') || null; |
| 60 | + |
| 61 | + const rows = await sql` |
| 62 | + SELECT direction, category, COUNT(*) AS count, SUM(amount::numeric) AS total |
| 63 | + FROM cc_transactions |
| 64 | + WHERE (${from}::date IS NULL OR tx_date >= ${from}::date) |
| 65 | + AND (${to}::date IS NULL OR tx_date <= ${to}::date) |
| 66 | + AND (${accountId}::uuid IS NULL OR account_id = ${accountId}::uuid) |
| 67 | + GROUP BY direction, category |
| 68 | + ORDER BY direction, total DESC |
| 69 | + `; |
| 70 | + |
| 71 | + return c.json({ summary: rows }); |
| 72 | +}); |
| 73 | + |
| 74 | +// Get single transaction |
| 75 | +transactionRoutes.get('/:id', async (c) => { |
| 76 | + const sql = getDb(c.env); |
| 77 | + const id = c.req.param('id'); |
| 78 | + const [tx] = await sql`SELECT * FROM cc_transactions WHERE id = ${id}`; |
| 79 | + if (!tx) return c.json({ error: 'Transaction not found' }, 404); |
| 80 | + return c.json(tx); |
| 81 | +}); |
0 commit comments