Files
Krow-workspace/backend/command-api/src/routes/commands.js

278 lines
7.8 KiB
JavaScript

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;
}