feat(POS-011): completed feature

This commit is contained in:
chattie
2026-08-22 13:46:21 +02:00
parent f5f6e8ed72
commit 3f26b2438d
10 changed files with 193 additions and 10 deletions

View File

@@ -684,4 +684,124 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
});
});
// ── POS-011: List sales, void, receipts, session history ──────────────────
app.get('/pos/sales', {
schema: {
tags: ['POS Terminal'],
summary: 'List recent POS sales',
querystring: {
type: 'object',
properties: {
sessionId: { type: 'string', format: 'uuid' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
},
},
response: { 401: errorSchema },
} as FastifySchema,
}, async (request, reply) => {
const user = await authenticate(request);
requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray<Role>);
const { sessionId, limit = 20 } = request.query as { sessionId?: string; limit?: number };
let query = `SELECT o.id, o.total_cents AS "totalCents", o.subtotal_cents AS "subtotalCents",
o.discount_cents AS "discountCents", o.created_at AS "createdAt",
u.email AS "userEmail"
FROM orders_orders o
LEFT JOIN identity_users u ON u.id = o.user_id
WHERE o.idempotency_key IS NOT NULL`;
const params: unknown[] = [];
if (sessionId) { params.push(sessionId); query += ` AND o.id IN (SELECT order_id FROM pos_cash_sessions WHERE id = $${params.length})`; }
params.push(limit);
query += ` ORDER BY o.created_at DESC LIMIT $${params.length}`;
const result = await pool.query(query, params);
return reply.send({ items: result.rows });
});
app.post<{ Params: { id: string } }>('/pos/sales/:id/void', {
schema: {
tags: ['POS Admin'],
summary: 'Void a POS sale',
params: { type: 'object', properties: { id: { type: 'string', format: 'uuid' } } },
body: {
type: 'object',
required: ['reason'],
properties: { reason: { type: 'string', minLength: 1 } },
},
response: { 401: errorSchema, 403: errorSchema, 404: errorSchema, 409: errorSchema },
} as FastifySchema,
}, async (request, reply) => {
const user = await authenticate(request);
requireRole(user, 'admin'); // Only admins can void
const { id } = request.params;
const { reason } = (request.body ?? {}) as { reason?: string };
const order = await pool.query<{ id: string }>('SELECT id FROM orders_orders WHERE id = $1', [id]);
if (!order.rows[0]) throw new AppError(404, 'NOT_FOUND', 'Order not found');
await pool.query(
`INSERT INTO orders_order_events (order_id, event, actor_id, metadata)
VALUES ($1, 'VOIDED', $2, $3)`,
[id, user.id, JSON.stringify({ reason })],
);
return reply.send({ ok: true, voidedAt: new Date().toISOString() });
});
app.get<{ Params: { id: string } }>('/pos/sales/:id/receipt', {
schema: {
tags: ['POS Terminal'],
summary: 'Get receipt for a sale',
params: { type: 'object', properties: { id: { type: 'string', format: 'uuid' } } },
response: { 401: errorSchema, 404: errorSchema },
} as FastifySchema,
}, async (request, reply) => {
const user = await authenticate(request);
requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray<Role>);
const { id } = request.params;
const order = await pool.query(
`SELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id WHERE o.id = $1`,
[id],
);
if (!order.rows[0]) throw new AppError(404, 'NOT_FOUND', 'Order not found');
const items = await pool.query('SELECT * FROM orders_items WHERE order_id = $1', [id]);
const payments = await pool.query<{ amount_cents: number; provider: string }>(
'SELECT amount_cents, provider FROM payments_transactions WHERE order_id = $1',
[id],
);
return reply.send({
order: order.rows[0],
items: items.rows,
payments: payments.rows,
});
});
app.get('/pos/sessions', {
schema: {
tags: ['POS Admin'],
summary: 'List cash sessions',
querystring: {
type: 'object',
properties: { storeId: { type: 'string', format: 'uuid' }, status: { type: 'string', enum: ['OPEN', 'CLOSED'] }, limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 } },
},
response: { 401: errorSchema, 403: errorSchema },
} as FastifySchema,
}, async (request, reply) => {
const user = await authenticate(request);
requireRole(user, 'admin');
const { storeId, status, limit = 20 } = request.query as { storeId?: string; status?: string; limit?: number };
const conditions: string[] = [];
const params: unknown[] = [];
if (storeId) { params.push(storeId); conditions.push(`store_id = $${params.length}`); }
if (status) { params.push(status); conditions.push(`status = $${params.length}`); }
params.push(limit);
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')} AND ` : 'WHERE ';
const result = await pool.query(
`SELECT s.*, t.name AS "terminalName", u.email AS "userEmail"
FROM pos_cash_sessions s
LEFT JOIN pos_terminals t ON t.id = s.terminal_id
LEFT JOIN identity_users u ON u.id = s.user_id
${where} 1=1 ORDER BY s.created_at DESC LIMIT $${params.length}`,
params,
);
return reply.send({ items: result.rows });
});
}