feat(F-111): completed feature

This commit is contained in:
chattie
2026-08-21 10:10:35 +02:00
parent 18f605591b
commit b87f4f0c85
13 changed files with 331 additions and 26 deletions

View File

@@ -12,7 +12,7 @@ import { OrderNotFoundError, OrderStateTransitionError } from '../domain/errors.
import type { OrderState } from '../domain/order.js';
import { PgOrderRepository } from '../infrastructure/pg-order-repository.js';
import { NoOpOrderEventPublisher } from '../infrastructure/no-op-event-publisher.js';
import { sendOrderStatusEmail } from '../infrastructure/order-status-mailer.js';
import { sendOrderStatusEmail, ORDER_STATE_LABELS } from '../infrastructure/order-status-mailer.js';
export interface OrdersRoutesDeps {
pool: pg.Pool;
@@ -275,10 +275,92 @@ export async function registerOrdersRoutes(
notificationError = error instanceof Error ? error.message : 'No se pudo enviar el email';
request.log.warn({ err: error, orderId: order.id }, 'order_status_email_failed');
}
await recordOrderHistory(
deps.pool,
order.id,
'STATE_CHANGE',
`Estado cambiado a ${ORDER_STATE_LABELS[order.state] ?? order.state}${order.trackingNumber ? ` · Tracking: ${order.trackingNumber}` : ''}`,
order.trackingNumber ?? null,
user.email,
);
return reply.send({ ...serializeOrder(order), notified, notificationError: notificationError || undefined });
},
);
// ── Gestión de envío (admin): tracking + historial ────────────────────────
const shippingSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Update order shipping (admin)',
description: 'Actualiza el número de seguimiento del pedido y registra el cambio en el historial.',
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', format: 'uuid' } },
},
body: { type: 'object' },
response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
};
app.post('/orders/:id/shipping', { schema: shippingSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { id } = parseJson(orderIdParamSchema, request.params);
const { trackingNumber, note } = parseJson(
z.object({
trackingNumber: z.string().trim().min(1).max(120),
note: z.string().trim().max(500).optional(),
}),
request.body,
);
const existing = await service.getOrderAdmin(id);
if (!existing) throw new AppError(404, 'ORDER_NOT_FOUND', 'Order not found');
await deps.pool.query(
`UPDATE orders_orders SET tracking_number = $2, updated_at = now() WHERE id = $1`,
[id, trackingNumber],
);
const message = `Seguimiento actualizado: ${trackingNumber}${note ? ` · Nota: ${note}` : ''}`;
await recordOrderHistory(deps.pool, id, 'SHIPPING_UPDATE', message, trackingNumber, user.email);
const updated = await service.getOrderAdmin(id);
return reply.send(serializeOrder(updated ?? { ...existing, trackingNumber }));
});
const historySchema: FastifySchema = {
tags: ['Orders'],
summary: 'Order history (admin)',
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', format: 'uuid' } },
},
response: { 401: errorSchema, 403: errorSchema },
};
app.get('/orders/:id/history', { schema: historySchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { id } = parseJson(orderIdParamSchema, request.params);
const result = await deps.pool.query<{
id: string;
event_type: string;
message: string;
tracking_number: string | null;
actor_email: string | null;
created_at: Date;
}>(
`SELECT id, event_type, message, tracking_number, actor_email, created_at
FROM orders_order_history WHERE order_id = $1 ORDER BY created_at ASC, id ASC`,
[id],
);
return reply.send({
items: result.rows.map((row) => ({
id: row.id,
eventType: row.event_type,
message: row.message,
trackingNumber: row.tracking_number,
actorEmail: row.actor_email,
createdAt: row.created_at,
})),
});
});
const editItemsSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Edit order items (admin)',
@@ -454,3 +536,26 @@ function serializeOrder(order: {
updatedAt: order.updatedAt.toISOString(),
};
}
/**
* Registra un evento en el historial del pedido. Los fallos se loguean pero
* nunca bloquean la operación principal (transición o actualización de envío).
*/
async function recordOrderHistory(
pool: pg.Pool,
orderId: string,
eventType: string,
message: string,
trackingNumber: string | null,
actorEmail: string,
): Promise<void> {
try {
await pool.query(
`INSERT INTO orders_order_history (order_id, event_type, message, tracking_number, actor_email)
VALUES ($1, $2, $3, $4, $5)`,
[orderId, eventType, message, trackingNumber, actorEmail],
);
} catch {
// Historial es best-effort: no debe romper la operación principal.
}
}