import type { FastifyInstance } from 'fastify'; import type { DB } from '../db.js'; import type { Bar, BarConfig, CreateTransactionRequest, CreateTransactionResponse, Drink, } from '@wutzcalc/shared'; export function registerPublicRoutes(app: FastifyInstance, db: DB) { app.get('/api/bars', async () => { const rows = db.prepare('SELECT id, name, pfand_cents FROM bars ORDER BY id').all() as Bar[]; return rows; }); app.get<{ Querystring: { bar: string } }>('/api/config', async (req, reply) => { const barId = Number(req.query.bar); if (!Number.isInteger(barId)) return reply.code(400).send({ error: 'bar required' }); const bar = db .prepare('SELECT id, name, pfand_cents FROM bars WHERE id = ?') .get(barId) as Bar | undefined; if (!bar) return reply.code(404).send({ error: 'bar not found' }); const drinks = db .prepare( `SELECT d.id, d.name, d.price_cents, d.archived FROM drinks d JOIN bar_drinks bd ON bd.drink_id = d.id WHERE bd.bar_id = ? AND d.archived = 0 ORDER BY bd.sort_order, d.id` ) .all(barId) as Drink[]; const config: BarConfig = { bar, drinks }; return config; }); app.post<{ Body: CreateTransactionRequest }>('/api/transactions', async (req, reply) => { const body = req.body ?? ({} as CreateTransactionRequest); const { client_uuid, bar_id, crew, items } = body; const pfand_returns = Math.max(0, Math.floor(body.pfand_returns ?? 0)); if (!client_uuid || typeof client_uuid !== 'string') { return reply.code(400).send({ error: 'client_uuid required' }); } if (!Number.isInteger(bar_id)) { return reply.code(400).send({ error: 'bar_id required' }); } if (!Array.isArray(items)) { return reply.code(400).send({ error: 'items required' }); } if (items.length === 0 && pfand_returns === 0) { return reply.code(400).send({ error: 'empty transaction' }); } const existing = db .prepare('SELECT id, total_cents FROM transactions WHERE client_uuid = ?') .get(client_uuid) as { id: number; total_cents: number } | undefined; if (existing) { const res: CreateTransactionResponse = { id: existing.id, total_cents: existing.total_cents }; return res; } const bar = db .prepare('SELECT id, pfand_cents FROM bars WHERE id = ?') .get(bar_id) as { id: number; pfand_cents: number } | undefined; if (!bar) return reply.code(404).send({ error: 'bar not found' }); const drinkStmt = db.prepare( `SELECT d.id, d.price_cents FROM drinks d JOIN bar_drinks bd ON bd.drink_id = d.id WHERE bd.bar_id = ? AND d.id = ? AND d.archived = 0` ); let total = 0; const priced: Array<{ drink_id: number; qty: number; unit_price_cents: number; pfand_cents_per_unit: number; }> = []; for (const item of items) { if (!Number.isInteger(item.drink_id) || !Number.isInteger(item.qty) || item.qty <= 0) { return reply.code(400).send({ error: 'invalid item' }); } const drink = drinkStmt.get(bar_id, item.drink_id) as | { id: number; price_cents: number } | undefined; if (!drink) return reply.code(400).send({ error: `drink ${item.drink_id} not at bar` }); const lineUnit = drink.price_cents + bar.pfand_cents; total += lineUnit * item.qty; priced.push({ drink_id: drink.id, qty: item.qty, unit_price_cents: drink.price_cents, pfand_cents_per_unit: bar.pfand_cents, }); } total -= bar.pfand_cents * pfand_returns; const paidTotal = crew ? 0 : total; const clientIp = req.ip ?? null; const insertTx = db.prepare( `INSERT INTO transactions (bar_id, total_cents, crew, pfand_returns, client_ip, client_uuid) VALUES (?, ?, ?, ?, ?, ?)` ); const insertItem = db.prepare( `INSERT INTO transaction_items (transaction_id, drink_id, qty, unit_price_cents, pfand_cents_per_unit, is_return) VALUES (?, ?, ?, ?, ?, 0)` ); const txId = db.transaction(() => { const info = insertTx.run(bar_id, paidTotal, crew ? 1 : 0, pfand_returns, clientIp, client_uuid); const id = Number(info.lastInsertRowid); for (const p of priced) { insertItem.run(id, p.drink_id, p.qty, p.unit_price_cents, p.pfand_cents_per_unit); } return id; })(); const res: CreateTransactionResponse = { id: txId, total_cents: paidTotal }; return res; }); }