feat(F-188): completed feature
This commit is contained in:
@@ -12,6 +12,7 @@ import { GetPosConfigUseCase } from '../application/get-pos-config.js';
|
||||
import { OpenCashSessionUseCase } from '../application/open-cash-session.js';
|
||||
import { CloseCashSessionUseCase } from '../application/close-cash-session.js';
|
||||
import { CreatePosSaleUseCase } from '../application/create-pos-sale.js';
|
||||
import { ReceiveRestPaymentUseCase } from '../application/receive-rest-payment.js';
|
||||
import { buildPosReceipt } from '../application/build-pos-receipt.js';
|
||||
import { sendTransactionalEmail } from '../../notifications/index.js';
|
||||
import { Argon2PasswordHasher } from '../../identity/index.js';
|
||||
@@ -62,6 +63,7 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
|
||||
const paymentMethodRepo = new PgPaymentMethodRepository(pool);
|
||||
const sessionRepo = new PgCashSessionRepository(pool);
|
||||
const createPosSale = new CreatePosSaleUseCase(pool);
|
||||
const receiveRestPayment = new ReceiveRestPaymentUseCase(pool);
|
||||
|
||||
const listStores = new ListStoresUseCase(storeRepo);
|
||||
const listTerminals = new ListTerminalsUseCase(terminalRepo);
|
||||
@@ -1321,6 +1323,8 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
|
||||
type: 'object',
|
||||
properties: {
|
||||
sessionId: { type: 'string', format: 'uuid' },
|
||||
state: { type: 'string', enum: ['PENDING', 'COMPLETED'] },
|
||||
storeId: { type: 'string', format: 'uuid' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
||||
},
|
||||
},
|
||||
@@ -1330,25 +1334,129 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
|
||||
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[] = [];
|
||||
const params = request.query as {
|
||||
sessionId?: string;
|
||||
state?: string;
|
||||
storeId?: string;
|
||||
limit?: number;
|
||||
};
|
||||
const sessionId = params.sessionId;
|
||||
const state = params.state;
|
||||
const storeId = params.storeId;
|
||||
const limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
|
||||
const conditions: string[] = [`o.idempotency_key IS NOT NULL`];
|
||||
const values: unknown[] = [];
|
||||
if (sessionId) {
|
||||
params.push(sessionId);
|
||||
query += ` AND o.id IN (SELECT order_id FROM pos_cash_sessions WHERE id = $${params.length})`;
|
||||
values.push(sessionId);
|
||||
conditions.push(`o.cash_session_id = $${values.length}`);
|
||||
}
|
||||
params.push(limit);
|
||||
query += ` ORDER BY o.created_at DESC LIMIT $${params.length}`;
|
||||
const result = await pool.query(query, params);
|
||||
if (state) {
|
||||
values.push(state);
|
||||
conditions.push(`o.state = $${values.length}`);
|
||||
}
|
||||
if (storeId) {
|
||||
values.push(storeId);
|
||||
conditions.push(`o.store_id = $${values.length}`);
|
||||
}
|
||||
values.push(limit);
|
||||
const query = `
|
||||
SELECT o.id, o.state,
|
||||
o.total_cents AS "totalCents", o.subtotal_cents AS "subtotalCents",
|
||||
o.discount_cents AS "discountCents", o.created_at AS "createdAt",
|
||||
o.store_id AS "storeId", o.terminal_id AS "terminalId",
|
||||
o.cash_session_id AS "cashSessionId",
|
||||
COALESCE(payments.sum_paid, 0)::int AS "paidCents",
|
||||
(o.total_cents - COALESCE(payments.sum_paid, 0))::int AS "outstandingCents",
|
||||
u.email AS "userEmail"
|
||||
FROM orders_orders o
|
||||
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||
LEFT JOIN (
|
||||
SELECT order_id, SUM(amount_cents) AS sum_paid
|
||||
FROM payments_transactions
|
||||
WHERE status = 'succeeded'
|
||||
GROUP BY order_id
|
||||
) payments ON payments.order_id = o.id
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT $${values.length}
|
||||
`;
|
||||
const result = await pool.query(query, values);
|
||||
return reply.send({ items: result.rows });
|
||||
},
|
||||
);
|
||||
|
||||
app.post<{ Params: { id: string } }>(
|
||||
'/pos/sales/:id/payments',
|
||||
{
|
||||
schema: {
|
||||
tags: ['POS Terminal'],
|
||||
summary: 'Apply additional payments to a pending POS sale',
|
||||
params: { type: 'object', properties: { id: { type: 'string', format: 'uuid' } } },
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['idempotencyKey', 'cashSessionId', 'terminalId', 'payments'],
|
||||
properties: {
|
||||
idempotencyKey: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
cashSessionId: { type: 'string', format: 'uuid' },
|
||||
terminalId: { type: 'string', format: 'uuid' },
|
||||
payments: { type: 'array', minItems: 1, items: { type: 'object' } },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
400: errorSchema,
|
||||
401: errorSchema,
|
||||
403: errorSchema,
|
||||
404: errorSchema,
|
||||
409: errorSchema,
|
||||
},
|
||||
} as FastifySchema,
|
||||
},
|
||||
async (request, reply) => {
|
||||
const user = await authenticate(request);
|
||||
requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray<Role>);
|
||||
const payment = z
|
||||
.object({
|
||||
methodCode: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9_-]{1,32}$/)
|
||||
.optional(),
|
||||
kind: z.enum(['cash', 'card', 'other']).optional(),
|
||||
amountCents: z.number().int().min(1),
|
||||
tenderedCents: z.number().int().min(0).optional(),
|
||||
last4: z
|
||||
.string()
|
||||
.regex(/^\d{1,4}$/)
|
||||
.optional(),
|
||||
})
|
||||
.refine((value) => Boolean(value.methodCode || value.kind), {
|
||||
message: 'methodCode is required',
|
||||
});
|
||||
const body = parseJson(
|
||||
z.object({
|
||||
idempotencyKey: z.string().min(1).max(128),
|
||||
cashSessionId: z.string().uuid(),
|
||||
terminalId: z.string().uuid(),
|
||||
payments: z.array(payment).min(1),
|
||||
}),
|
||||
request.body ?? {},
|
||||
);
|
||||
const boundTerminalId = request.headers['x-terminal-id'];
|
||||
if (typeof boundTerminalId !== 'string' || boundTerminalId !== body.terminalId) {
|
||||
throw new AppError(
|
||||
403,
|
||||
'POS_TERMINAL_MISMATCH',
|
||||
'La venta no pertenece al terminal vinculado',
|
||||
);
|
||||
}
|
||||
const result = await receiveRestPayment.execute({
|
||||
orderId: (request.params as { id: string }).id,
|
||||
userId: user.id,
|
||||
...body,
|
||||
});
|
||||
return reply.code(201).send(result);
|
||||
},
|
||||
);
|
||||
|
||||
app.post<{ Params: { id: string } }>(
|
||||
'/pos/sales/:id/void',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user