const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { sendData } = require("../utils/reply"); const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags"); const ID_SCHEMA = z.string().uuid(); const SCOPE_TYPE_SCHEMA = z.enum(["department", "legal_entity", "domain"]); const WRITE_SCHEMA = z.object({ scopeType: SCOPE_TYPE_SCHEMA, scopeKey: z.string().trim().min(1).max(64), enabled: z.boolean().optional(), startsAt: z.string().datetime({ offset: true }).nullable().optional(), notes: z.string().trim().max(500).nullable().optional(), }).strict(); const PATCH_SCHEMA = WRITE_SCHEMA.partial().refine((value) => Object.keys(value).length > 0, { message: "empty_patch" }); function buildValidationError(details, statusCode = 400) { return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details }); } function parseId(value) { const parsed = ID_SCHEMA.safeParse(value); if (!parsed.success) throw buildValidationError({ id: "invalid_uuid" }); return parsed.data; } function parseBody(schema, body) { const parsed = schema.safeParse(body ?? {}); if (!parsed.success) throw buildValidationError(parsed.error.flatten()); return parsed.data; } async function ensureFeatureEnabled(fastify) { const gate = await evaluateFeatureFlag({ prisma: fastify.prisma, config: fastify.config, flagKey: FEATURE_FLAG_KEYS.cutoverConsole, strictDepartment: false }); if (!gate.enabled) throw buildValidationError({ featureFlag: "cutover_console_v1_disabled", reason: gate.reason }, 409); } function mapScope(item) { return { id: item.id, scopeType: item.scope_type, scopeKey: item.scope_key, enabled: item.enabled, startsAt: item.starts_at?.toISOString?.() ?? null, notes: item.notes ?? null, createdAt: item.created_at?.toISOString?.() ?? null, updatedAt: item.updated_at?.toISOString?.() ?? null, updatedBy: item.updated_by ?? null, }; } async function cutoverRoutes(fastify) { fastify.get("/cutover/scopes", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => { await ensureFeatureEnabled(fastify); const items = await fastify.prisma.cutoverScope.findMany({ orderBy: [{ scope_type: "asc" }, { scope_key: "asc" }] }); return sendData(reply, request, { items: items.map(mapScope) }); }); fastify.post("/cutover/scopes", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => { await ensureFeatureEnabled(fastify); const body = parseBody(WRITE_SCHEMA, request.body); const created = await fastify.prisma.cutoverScope.create({ data: { scope_type: body.scopeType, scope_key: body.scopeKey, enabled: body.enabled ?? false, starts_at: body.startsAt ? new Date(body.startsAt) : null, notes: body.notes ?? null, created_by: request.employee.id, updated_by: request.employee.id, }, }); return reply.code(201).send({ requestId: request.id, data: { item: mapScope(created) } }); }); fastify.patch("/cutover/scopes/:id", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => { await ensureFeatureEnabled(fastify); const scopeId = parseId(request.params?.id); const body = parseBody(PATCH_SCHEMA, request.body); const updated = await fastify.prisma.cutoverScope.update({ where: { id: scopeId }, data: { ...(body.scopeType !== undefined ? { scope_type: body.scopeType } : {}), ...(body.scopeKey !== undefined ? { scope_key: body.scopeKey } : {}), ...(body.enabled !== undefined ? { enabled: body.enabled } : {}), ...(body.startsAt !== undefined ? { starts_at: body.startsAt ? new Date(body.startsAt) : null } : {}), ...(body.notes !== undefined ? { notes: body.notes ?? null } : {}), updated_by: request.employee.id, }, }); return sendData(reply, request, { item: mapScope(updated) }); }); } module.exports = cutoverRoutes;