feat(F-111): completed feature
This commit is contained in:
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user