Skip to content

Commit de4e39f

Browse files
chitcommitclaude
andauthored
feat: add GET /api/transactions endpoint with filtering and summary (#78)
- List transactions with filters: account_id, source, direction, category, date range, search - Pagination via limit/offset (max 200) - GET /api/transactions/summary/totals — aggregated by direction + category - GET /api/transactions/:id — single transaction lookup Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c231f5 commit de4e39f

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { connectRoutes } from './routes/connect';
3131
import { ledgerRoutes } from './routes/ledger';
3232
import { tokenManagementRoutes } from './routes/token-management';
3333
import { jobRoutes } from './routes/jobs';
34+
import { transactionRoutes } from './routes/transactions';
3435
import { timelineRoutes } from './routes/timeline';
3536

3637
// Re-export ActionAgent DO class so the runtime can find it
@@ -121,6 +122,7 @@ app.use('/api/*', authMiddleware);
121122
// API routes
122123
app.route('/api/dashboard', dashboardRoutes);
123124
app.route('/api/accounts', accountRoutes);
125+
app.route('/api/transactions', transactionRoutes);
124126
app.route('/api/obligations', obligationRoutes);
125127
app.route('/api/disputes', disputeRoutes);
126128
app.route('/api/legal', legalRoutes);

src/routes/transactions.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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

Comments
 (0)