feat(F-179): completed feature
This commit is contained in:
@@ -2,7 +2,18 @@ import type pg from 'pg';
|
||||
import type { PosSaleInput, PosSaleResult, PosPaymentResult } from '../domain/pos-sale.js';
|
||||
import { AppError } from '../../../shared/errors.js';
|
||||
|
||||
interface PaymentRow { id: string; amount_cents: number; kind: string; }
|
||||
interface PaymentRow { id: string; amount_cents: number; provider: string; }
|
||||
interface OrderItemRow {
|
||||
variant_id: string;
|
||||
product_id: string;
|
||||
sku: string;
|
||||
ean: string | null;
|
||||
name: string;
|
||||
unit_price_cents: number;
|
||||
discount_cents: number;
|
||||
tax_cents: number;
|
||||
quantity: number;
|
||||
}
|
||||
interface OrderRow { id: string; idempotency_key: string; total_cents: number; created_at: Date; }
|
||||
|
||||
export class CreatePosSaleUseCase {
|
||||
@@ -25,32 +36,54 @@ export class CreatePosSaleUseCase {
|
||||
'SELECT id, idempotency_key, total_cents, created_at FROM orders_orders WHERE idempotency_key = $1',
|
||||
[input.idempotencyKey],
|
||||
);
|
||||
const items = await this.pool.query('SELECT * FROM orders_items WHERE order_id = $1', [existId]);
|
||||
const items = await this.pool.query<OrderItemRow>(
|
||||
`SELECT variant_id, product_id, sku, ean, name, unit_price_cents,
|
||||
discount_cents, tax_cents, quantity
|
||||
FROM orders_items WHERE order_id = $1`,
|
||||
[existId],
|
||||
);
|
||||
const payments = await this.pool.query<PaymentRow>(
|
||||
'SELECT id, amount_cents, kind FROM payments_transactions WHERE order_id = $1',
|
||||
'SELECT id, amount_cents, provider FROM payments_transactions WHERE order_id = $1',
|
||||
[existId],
|
||||
);
|
||||
const totalCents = (order.rows[0]?.total_cents ?? 0) as number;
|
||||
const mappedItems: PosSaleResult['items'] = items.rows as PosSaleResult['items'];
|
||||
const mappedItems: PosSaleResult['items'] = items.rows.map((row) => ({
|
||||
variantId: row.variant_id,
|
||||
productId: row.product_id,
|
||||
sku: row.sku,
|
||||
ean: row.ean,
|
||||
name: row.name,
|
||||
unitPriceCents: row.unit_price_cents,
|
||||
discountCents: row.discount_cents,
|
||||
taxCents: row.tax_cents,
|
||||
quantity: row.quantity,
|
||||
}));
|
||||
return {
|
||||
orderId: existId,
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
totalCents,
|
||||
items: mappedItems,
|
||||
payments: payments.rows.map(r => ({
|
||||
id: r.id, kind: r.kind as PosPaymentResult['kind'], amountCents: r.amount_cents,
|
||||
payments: payments.rows.map((row) => ({
|
||||
id: row.id,
|
||||
kind: providerToPaymentKind(row.provider),
|
||||
amountCents: row.amount_cents,
|
||||
})),
|
||||
createdAt: order.rows[0]?.created_at ?? new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Verify cash session is open (FOR UPDATE lock)
|
||||
const session = await client.query<{ id: string; status: string }>(
|
||||
`SELECT id, status FROM pos_cash_sessions WHERE id = $1 FOR UPDATE`,
|
||||
const session = await client.query<{ id: string; terminal_id: string; store_id: string; status: string }>(
|
||||
`SELECT id, terminal_id, store_id, status FROM pos_cash_sessions WHERE id = $1 FOR UPDATE`,
|
||||
[input.cashSessionId],
|
||||
);
|
||||
if (!session.rows[0]) { await client.query('ROLLBACK'); throw new AppError(404, 'SESSION_NOT_FOUND', 'Cash session not found'); }
|
||||
if (session.rows[0].status !== 'OPEN') { await client.query('ROLLBACK'); throw new AppError(409, 'SESSION_CLOSED', 'Cash session is not open'); }
|
||||
const sessionRow = session.rows[0];
|
||||
if (!sessionRow) { await client.query('ROLLBACK'); throw new AppError(404, 'SESSION_NOT_FOUND', 'Cash session not found'); }
|
||||
if (sessionRow.status !== 'OPEN') { await client.query('ROLLBACK'); throw new AppError(409, 'SESSION_CLOSED', 'Cash session is not open'); }
|
||||
if (sessionRow.terminal_id !== input.terminalId) {
|
||||
await client.query('ROLLBACK');
|
||||
throw new AppError(409, 'TERMINAL_SESSION_MISMATCH', 'Cash session belongs to another terminal');
|
||||
}
|
||||
|
||||
// 3. Compute totals
|
||||
const subtotalCents = input.items.reduce((s, i) => s + (i.unitPriceCents - i.discountCents) * i.quantity, 0);
|
||||
@@ -62,9 +95,22 @@ export class CreatePosSaleUseCase {
|
||||
|
||||
// 4. Create order
|
||||
const orderResult = await client.query<OrderRow>(
|
||||
`INSERT INTO orders_orders (user_id, idempotency_key, subtotal_cents, discount_cents, tax_cents, total_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, idempotency_key, total_cents, created_at`,
|
||||
[input.userId, input.idempotencyKey, subtotalCents, discountCents, taxCents, totalCents],
|
||||
`INSERT INTO orders_orders (
|
||||
user_id, idempotency_key, subtotal_cents, discount_cents, tax_cents, total_cents,
|
||||
source, terminal_id, cash_session_id, store_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 'pos', $7, $8, $9)
|
||||
RETURNING id, idempotency_key, total_cents, created_at`,
|
||||
[
|
||||
input.customerId ?? null,
|
||||
input.idempotencyKey,
|
||||
subtotalCents,
|
||||
discountCents,
|
||||
taxCents,
|
||||
totalCents,
|
||||
input.terminalId,
|
||||
input.cashSessionId,
|
||||
sessionRow.store_id,
|
||||
],
|
||||
);
|
||||
const orderRow = orderResult.rows[0];
|
||||
if (!orderRow) { await client.query('ROLLBACK'); throw new AppError(500, 'ORDER_CREATE_FAILED', 'Order insert returned no row'); }
|
||||
@@ -87,7 +133,7 @@ export class CreatePosSaleUseCase {
|
||||
const payResult = await client.query<{ id: string }>(
|
||||
`INSERT INTO payments_transactions (provider, provider_event_id, provider_payment_id, order_id, amount_cents, currency, status, raw)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
||||
[kind, `pos-${orderId}-${p.kind}`, `pos-${Date.now()}`, orderId, p.amountCents, 'EUR', 'COMPLETED', JSON.stringify(p)],
|
||||
[kind, `pos-${orderId}-${p.kind}`, `pos-${Date.now()}`, orderId, p.amountCents, 'EUR', 'succeeded', JSON.stringify(p)],
|
||||
);
|
||||
const payRow = payResult.rows[0];
|
||||
if (!payRow) { await client.query('ROLLBACK'); throw new AppError(500, 'PAYMENT_INSERT_FAILED', 'Payment insert returned no row'); }
|
||||
@@ -119,3 +165,9 @@ export class CreatePosSaleUseCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function providerToPaymentKind(provider: string): PosPaymentResult['kind'] {
|
||||
if (provider === 'pos_cash') return 'cash';
|
||||
if (provider === 'pos_card') return 'card';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user