feat(F-186): completed feature
This commit is contained in:
167
project/src/modules/pos/application/build-pos-receipt.ts
Normal file
167
project/src/modules/pos/application/build-pos-receipt.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type pg from 'pg';
|
||||
import type { PosPaymentKind, PosReceipt, PosReceiptPayment } from '../domain/pos-sale.js';
|
||||
import { AppError } from '../../../shared/errors.js';
|
||||
|
||||
type Queryable = Pick<pg.Pool, 'query'> | Pick<pg.PoolClient, 'query'>;
|
||||
|
||||
interface ReceiptOrderRow {
|
||||
id: string;
|
||||
receipt_number: string | null;
|
||||
subtotal_cents: number;
|
||||
discount_cents: number;
|
||||
tax_cents: number;
|
||||
total_cents: number;
|
||||
created_at: Date;
|
||||
cash_session_id: string;
|
||||
terminal_id: string;
|
||||
customer_email: string | null;
|
||||
store_name: string;
|
||||
address: string | null;
|
||||
tax_id: string | null;
|
||||
contact_email: string | null;
|
||||
contact_phone: string | null;
|
||||
receipt_header: string | null;
|
||||
receipt_footer: string | null;
|
||||
terminal_name: string;
|
||||
cashier_email: string;
|
||||
return_policy: string | null;
|
||||
}
|
||||
|
||||
interface ReceiptItemRow {
|
||||
name: string;
|
||||
sku: string;
|
||||
quantity: number;
|
||||
unit_price_cents: number;
|
||||
discount_cents: number;
|
||||
tax_cents: number;
|
||||
is_free_item: boolean;
|
||||
}
|
||||
|
||||
interface ReceiptPaymentRow {
|
||||
amount_cents: number;
|
||||
provider: string;
|
||||
raw: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export async function buildPosReceipt(queryable: Queryable, orderId: string): Promise<PosReceipt> {
|
||||
const orderResult = await queryable.query<ReceiptOrderRow>(
|
||||
`SELECT o.id, o.receipt_number, o.subtotal_cents, o.discount_cents, o.tax_cents,
|
||||
o.total_cents, o.created_at, o.cash_session_id, o.terminal_id,
|
||||
customer.email AS customer_email,
|
||||
store.name AS store_name, store.address, store.tax_id, store.contact_email,
|
||||
store.contact_phone, store.receipt_header, store.receipt_footer,
|
||||
terminal.name AS terminal_name, cashier.email AS cashier_email,
|
||||
receipt_settings.return_policy
|
||||
FROM orders_orders o
|
||||
JOIN pos_stores store ON store.id = o.store_id
|
||||
JOIN pos_terminals terminal ON terminal.id = o.terminal_id
|
||||
JOIN pos_cash_sessions session ON session.id = o.cash_session_id
|
||||
JOIN backoffice_users cashier ON cashier.id = session.user_id
|
||||
LEFT JOIN identity_users customer ON customer.id = o.user_id
|
||||
LEFT JOIN pos_receipt_settings receipt_settings ON receipt_settings.store_id = o.store_id
|
||||
WHERE o.id = $1 AND o.source = 'pos'`,
|
||||
[orderId],
|
||||
);
|
||||
const order = orderResult.rows[0];
|
||||
if (!order) throw new AppError(404, 'POS_RECEIPT_NOT_FOUND', 'No se encontró el ticket');
|
||||
|
||||
const [itemResult, paymentResult] = await Promise.all([
|
||||
queryable.query<ReceiptItemRow>(
|
||||
`SELECT name, sku, quantity, unit_price_cents, discount_cents, tax_cents, is_free_item
|
||||
FROM orders_items WHERE order_id = $1 ORDER BY created_at, id`,
|
||||
[orderId],
|
||||
),
|
||||
queryable.query<ReceiptPaymentRow>(
|
||||
`SELECT amount_cents, provider, raw
|
||||
FROM payments_transactions WHERE order_id = $1 AND status = 'succeeded'
|
||||
ORDER BY created_at, id`,
|
||||
[orderId],
|
||||
),
|
||||
]);
|
||||
|
||||
const payments: PosReceiptPayment[] = paymentResult.rows.map((row) => {
|
||||
const raw = row.raw ?? {};
|
||||
const kind = isPaymentKind(raw.kind) ? raw.kind : providerKind(row.provider);
|
||||
const tendered = integerOrNull(raw.tenderedCents);
|
||||
const change = integerOrZero(raw.changeCents);
|
||||
return {
|
||||
methodCode: stringOr(raw.methodCode, row.provider.replace(/^pos_/, '')),
|
||||
methodLabel: stringOr(raw.methodLabel, paymentKindLabel(kind)),
|
||||
kind,
|
||||
amountCents: Number(row.amount_cents),
|
||||
tenderedCents: tendered,
|
||||
changeCents: change,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
receiptNumber: order.receipt_number ?? `POS-${order.id.slice(0, 8).toUpperCase()}`,
|
||||
orderId: order.id,
|
||||
issuedAt: order.created_at,
|
||||
company: {
|
||||
name: order.store_name,
|
||||
address: order.address,
|
||||
taxId: order.tax_id,
|
||||
email: order.contact_email,
|
||||
phone: order.contact_phone,
|
||||
},
|
||||
terminal: { id: order.terminal_id, name: order.terminal_name },
|
||||
cashier: order.cashier_email,
|
||||
sessionId: order.cash_session_id,
|
||||
customerEmail: order.customer_email,
|
||||
items: itemResult.rows.map((item) => {
|
||||
const subtotalCents = Number(item.unit_price_cents) * Number(item.quantity);
|
||||
const discountCents = Number(item.discount_cents) * Number(item.quantity);
|
||||
const taxCents = Number(item.tax_cents) * Number(item.quantity);
|
||||
return {
|
||||
name: item.name,
|
||||
sku: item.sku,
|
||||
quantity: Number(item.quantity),
|
||||
unitPriceCents: Number(item.unit_price_cents),
|
||||
subtotalCents,
|
||||
discountCents,
|
||||
taxCents,
|
||||
totalCents: subtotalCents - discountCents + taxCents,
|
||||
freeItem: item.is_free_item,
|
||||
};
|
||||
}),
|
||||
subtotalCents: Number(order.subtotal_cents),
|
||||
discountCents: Number(order.discount_cents),
|
||||
taxCents: Number(order.tax_cents),
|
||||
totalCents: Number(order.total_cents),
|
||||
payments,
|
||||
changeCents: payments.reduce((sum, payment) => sum + payment.changeCents, 0),
|
||||
header: order.receipt_header,
|
||||
returnPolicy:
|
||||
order.return_policy ?? 'Se admiten devoluciones durante 15 días con el ticket de compra.',
|
||||
footer: order.receipt_footer,
|
||||
};
|
||||
}
|
||||
|
||||
function isPaymentKind(value: unknown): value is PosPaymentKind {
|
||||
return value === 'cash' || value === 'card' || value === 'other';
|
||||
}
|
||||
|
||||
function providerKind(provider: string): PosPaymentKind {
|
||||
if (provider === 'pos_cash') return 'cash';
|
||||
if (provider === 'pos_card') return 'card';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function paymentKindLabel(kind: PosPaymentKind): string {
|
||||
if (kind === 'cash') return 'Efectivo';
|
||||
if (kind === 'card') return 'Tarjeta';
|
||||
return 'Otro';
|
||||
}
|
||||
|
||||
function stringOr(value: unknown, fallback: string): string {
|
||||
return typeof value === 'string' && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function integerOrNull(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isInteger(value) ? value : null;
|
||||
}
|
||||
|
||||
function integerOrZero(value: unknown): number {
|
||||
return typeof value === 'number' && Number.isInteger(value) ? value : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user