import { Router } from 'express'; import { AppError } from '../lib/errors.js'; import { requireAuth, requirePolicy } from '../middleware/auth.js'; import { requireIdempotencyKey } from '../middleware/idempotency.js'; import { buildIdempotencyKey, readIdempotentResult, writeIdempotentResult } from '../services/idempotency-store.js'; import { addFavoriteStaff, clockIn, clockOut, createOrder, createStaffReview, updateOrder, cancelOrder, changeShiftStatus, assignStaffToShift, removeFavoriteStaff, acceptShift, } from '../services/command-service.js'; import { attendanceCommandSchema } from '../contracts/commands/attendance.js'; import { favoriteStaffSchema } from '../contracts/commands/favorite-staff.js'; import { orderCancelSchema } from '../contracts/commands/order-cancel.js'; import { orderCreateSchema } from '../contracts/commands/order-create.js'; import { orderUpdateSchema } from '../contracts/commands/order-update.js'; import { shiftAssignStaffSchema } from '../contracts/commands/shift-assign-staff.js'; import { shiftAcceptSchema } from '../contracts/commands/shift-accept.js'; import { shiftStatusChangeSchema } from '../contracts/commands/shift-status-change.js'; import { staffReviewSchema } from '../contracts/commands/staff-review.js'; const defaultHandlers = { addFavoriteStaff, assignStaffToShift, cancelOrder, changeShiftStatus, clockIn, clockOut, createOrder, createStaffReview, removeFavoriteStaff, acceptShift, updateOrder, }; function parseBody(schema, body) { const parsed = schema.safeParse(body || {}); if (!parsed.success) { throw new AppError('VALIDATION_ERROR', 'Invalid command payload', 400, { issues: parsed.error.issues, }); } return parsed.data; } async function runIdempotentCommand(req, res, work) { const route = `${req.baseUrl}${req.route.path}`; const compositeKey = buildIdempotencyKey({ userId: req.actor.uid, route, idempotencyKey: req.idempotencyKey, }); const existing = await readIdempotentResult(compositeKey); if (existing) { return res.status(existing.statusCode).json(existing.payload); } const payload = await work(); const responsePayload = { ...payload, idempotencyKey: req.idempotencyKey, requestId: req.requestId, }; const persisted = await writeIdempotentResult({ compositeKey, userId: req.actor.uid, route, idempotencyKey: req.idempotencyKey, payload: responsePayload, statusCode: 200, }); return res.status(persisted.statusCode).json(persisted.payload); } export function createCommandsRouter(handlers = defaultHandlers) { const router = Router(); router.post( '/orders/create', requireAuth, requireIdempotencyKey, requirePolicy('orders.create', 'order'), async (req, res, next) => { try { const payload = parseBody(orderCreateSchema, req.body); return await runIdempotentCommand(req, res, () => handlers.createOrder(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/orders/:orderId/update', requireAuth, requireIdempotencyKey, requirePolicy('orders.update', 'order'), async (req, res, next) => { try { const payload = parseBody(orderUpdateSchema, { ...req.body, orderId: req.params.orderId, }); return await runIdempotentCommand(req, res, () => handlers.updateOrder(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/orders/:orderId/cancel', requireAuth, requireIdempotencyKey, requirePolicy('orders.cancel', 'order'), async (req, res, next) => { try { const payload = parseBody(orderCancelSchema, { ...req.body, orderId: req.params.orderId, }); return await runIdempotentCommand(req, res, () => handlers.cancelOrder(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/shifts/:shiftId/change-status', requireAuth, requireIdempotencyKey, requirePolicy('shifts.change-status', 'shift'), async (req, res, next) => { try { const payload = parseBody(shiftStatusChangeSchema, { ...req.body, shiftId: req.params.shiftId, }); return await runIdempotentCommand(req, res, () => handlers.changeShiftStatus(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/shifts/:shiftId/assign-staff', requireAuth, requireIdempotencyKey, requirePolicy('shifts.assign-staff', 'shift'), async (req, res, next) => { try { const payload = parseBody(shiftAssignStaffSchema, { ...req.body, shiftId: req.params.shiftId, }); return await runIdempotentCommand(req, res, () => handlers.assignStaffToShift(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/shifts/:shiftId/accept', requireAuth, requireIdempotencyKey, requirePolicy('shifts.accept', 'shift'), async (req, res, next) => { try { const payload = parseBody(shiftAcceptSchema, { ...req.body, shiftId: req.params.shiftId, }); return await runIdempotentCommand(req, res, () => handlers.acceptShift(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/attendance/clock-in', requireAuth, requireIdempotencyKey, requirePolicy('attendance.clock-in', 'attendance'), async (req, res, next) => { try { const payload = parseBody(attendanceCommandSchema, req.body); return await runIdempotentCommand(req, res, () => handlers.clockIn(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/attendance/clock-out', requireAuth, requireIdempotencyKey, requirePolicy('attendance.clock-out', 'attendance'), async (req, res, next) => { try { const payload = parseBody(attendanceCommandSchema, req.body); return await runIdempotentCommand(req, res, () => handlers.clockOut(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/businesses/:businessId/favorite-staff', requireAuth, requireIdempotencyKey, requirePolicy('business.favorite-staff', 'staff'), async (req, res, next) => { try { const payload = parseBody(favoriteStaffSchema, { ...req.body, businessId: req.params.businessId, }); return await runIdempotentCommand(req, res, () => handlers.addFavoriteStaff(req.actor, payload)); } catch (error) { return next(error); } } ); router.delete( '/businesses/:businessId/favorite-staff/:staffId', requireAuth, requireIdempotencyKey, requirePolicy('business.unfavorite-staff', 'staff'), async (req, res, next) => { try { const payload = parseBody(favoriteStaffSchema, { ...req.body, businessId: req.params.businessId, staffId: req.params.staffId, }); return await runIdempotentCommand(req, res, () => handlers.removeFavoriteStaff(req.actor, payload)); } catch (error) { return next(error); } } ); router.post( '/assignments/:assignmentId/reviews', requireAuth, requireIdempotencyKey, requirePolicy('assignments.review-staff', 'assignment'), async (req, res, next) => { try { const payload = parseBody(staffReviewSchema, { ...req.body, assignmentId: req.params.assignmentId, }); return await runIdempotentCommand(req, res, () => handlers.createStaffReview(req.actor, payload)); } catch (error) { return next(error); } } ); return router; }