feat(F-177): completed feature
This commit is contained in:
@@ -180,6 +180,59 @@ export async function registerOrdersRoutes(
|
||||
return reply.send({ items: items.map(serializeOrder), total });
|
||||
});
|
||||
|
||||
const staleNotificationsSchema: FastifySchema = {
|
||||
tags: ['Orders'],
|
||||
summary: 'List stale order notifications (admin)',
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hours: { type: 'integer', minimum: 1, maximum: 168, default: 24 },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 },
|
||||
},
|
||||
},
|
||||
response: { 401: errorSchema, 403: errorSchema },
|
||||
};
|
||||
app.get('/orders/notifications/stale', { schema: staleNotificationsSchema }, async (request, reply) => {
|
||||
const user = await deps.authenticate(request);
|
||||
requireRole(user, 'admin');
|
||||
const query = request.query as { hours?: string; limit?: string };
|
||||
const hours = Math.min(Math.max(Number(query.hours ?? 24) || 24, 1), 168);
|
||||
const limit = Math.min(Math.max(Number(query.limit ?? 20) || 20, 1), 50);
|
||||
const result = await deps.pool.query<{
|
||||
id: string;
|
||||
state: 'PENDING' | 'SHIPPED';
|
||||
total_cents: number;
|
||||
customer_email: string | null;
|
||||
state_changed_at: Date;
|
||||
age_hours: number;
|
||||
total_count: number;
|
||||
}>(
|
||||
`SELECT o.id, o.state, o.total_cents, u.email AS customer_email,
|
||||
o.state_changed_at,
|
||||
FLOOR(EXTRACT(EPOCH FROM (now() - o.state_changed_at)) / 3600)::int AS age_hours,
|
||||
COUNT(*) OVER()::int AS total_count
|
||||
FROM orders_orders o
|
||||
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||
WHERE o.state IN ('PENDING', 'SHIPPED')
|
||||
AND o.state_changed_at <= now() - ($1::int * interval '1 hour')
|
||||
ORDER BY o.state_changed_at ASC
|
||||
LIMIT $2`,
|
||||
[hours, limit],
|
||||
);
|
||||
return reply.send({
|
||||
total: result.rows[0]?.total_count ?? 0,
|
||||
thresholdHours: hours,
|
||||
items: result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
state: row.state,
|
||||
totalCents: row.total_cents,
|
||||
customerEmail: row.customer_email,
|
||||
stateChangedAt: row.state_changed_at.toISOString(),
|
||||
ageHours: row.age_hours,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
const getOrderAdminSchema: FastifySchema = {
|
||||
tags: ['Orders'],
|
||||
summary: 'Get order (admin)',
|
||||
|
||||
@@ -232,7 +232,12 @@ export class PgOrderRepository implements OrderRepository {
|
||||
|
||||
async updateState(id: string, state: OrderState, trackingNumber?: string, courier?: string): Promise<OrderView | undefined> {
|
||||
const result = await this.pool.query<OrderRow>(
|
||||
`UPDATE orders_orders SET state = $2, tracking_number = COALESCE($3, tracking_number), courier = COALESCE($4, courier), updated_at = now()
|
||||
`UPDATE orders_orders
|
||||
SET state = $2,
|
||||
tracking_number = COALESCE($3, tracking_number),
|
||||
courier = COALESCE($4, courier),
|
||||
state_changed_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $1 RETURNING *`,
|
||||
[id, state, trackingNumber ?? null, courier ?? null],
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function registerPaymentsRoutes(
|
||||
if (!row) throw new OrderNotFoundError();
|
||||
if (!ALLOWED_TRANSITIONS[row.state]?.includes(state)) throw new OrderNotFoundError();
|
||||
await deps.pool.query(
|
||||
'UPDATE orders_orders SET state = $2, updated_at = now() WHERE id = $1',
|
||||
'UPDATE orders_orders SET state = $2, state_changed_at = now(), updated_at = now() WHERE id = $1',
|
||||
[id, state],
|
||||
);
|
||||
const refreshed = await deps.pool.query<{
|
||||
|
||||
Reference in New Issue
Block a user