132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
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;
|
|
});
|
|
}
|