const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { sendData } = require("../utils/reply"); const { buildAuditLogData } = require("../utils/audit"); const { computeDelegationStatus } = require("../utils/delegation"); const { resolvePermissionsForRoles, hasPermission } = require("../constants/permissions"); const UUID_SCHEMA = z.string().uuid(); const CREATE_DELEGATION_SCHEMA = z.object({ delegateEmployeeId: z.string().uuid(), scope: z.enum(["reimbursement_submit", "all"]).optional(), validFrom: z.string().datetime().optional(), validTo: z.string().datetime().optional(), }); const CREATE_DELEGATION_GRANT_SCHEMA = CREATE_DELEGATION_SCHEMA.extend({ principalEmployeeId: z.string().uuid().optional(), }); const QUERY_STATUS_SCHEMA = z.object({ status: z.enum(["active", "scheduled", "expired", "revoked", "all"]).optional(), }); const LIST_DELEGATION_GRANTS_QUERY_SCHEMA = z.object({ includeInactive: z.enum(["true", "false", "1", "0"]).optional(), principalEmployeeId: z.string().uuid().optional(), delegateEmployeeId: z.string().uuid().optional(), }); function parseDelegationId(id) { const parsed = UUID_SCHEMA.safeParse(id); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { id: "invalid_uuid" }, }); } return parsed.data; } function parseCreateBody(body) { const parsed = CREATE_DELEGATION_SCHEMA.safeParse(body ?? {}); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten(), }); } const validFrom = parsed.data.validFrom ? new Date(parsed.data.validFrom) : new Date(); const validTo = parsed.data.validTo ? new Date(parsed.data.validTo) : null; if (Number.isNaN(validFrom.getTime())) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { validFrom: "invalid_datetime" }, }); } if (validTo && Number.isNaN(validTo.getTime())) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { validTo: "invalid_datetime" }, }); } if (validTo && validTo <= validFrom) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { validTo: "must_be_after_validFrom" }, }); } return { delegateEmployeeId: parsed.data.delegateEmployeeId, scope: parsed.data.scope ?? "all", validFrom, validTo, }; } function parseCreateGrantBody(body) { const parsed = CREATE_DELEGATION_GRANT_SCHEMA.safeParse(body ?? {}); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten(), }); } const validFrom = parsed.data.validFrom ? new Date(parsed.data.validFrom) : new Date(); const validTo = parsed.data.validTo ? new Date(parsed.data.validTo) : null; if (Number.isNaN(validFrom.getTime())) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { validFrom: "invalid_datetime" }, }); } if (validTo && Number.isNaN(validTo.getTime())) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { validTo: "invalid_datetime" }, }); } if (validTo && validTo <= validFrom) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { validTo: "must_be_after_validFrom" }, }); } return { principalEmployeeId: parsed.data.principalEmployeeId, delegateEmployeeId: parsed.data.delegateEmployeeId, scope: parsed.data.scope ?? "all", validFrom, validTo, }; } function parseStatusQuery(query) { const parsed = QUERY_STATUS_SCHEMA.safeParse(query ?? {}); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten(), }); } return parsed.data.status ?? "active"; } function parseListDelegationGrantsQuery(query) { const parsed = LIST_DELEGATION_GRANTS_QUERY_SCHEMA.safeParse(query ?? {}); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten(), }); } const includeInactive = ["true", "1"].includes(parsed.data.includeInactive ?? "false"); return { includeInactive, principalEmployeeId: parsed.data.principalEmployeeId, delegateEmployeeId: parsed.data.delegateEmployeeId, }; } function matchesStatusFilter(record, status, now) { if (status === "all") { return true; } return computeDelegationStatus(record, now) === status; } function mapEmployeeEmbedded(employee) { if (!employee) { return null; } return { id: employee.id, employeeNo: employee.employee_no, name: employee.name, departmentId: employee.department_id, status: employee.status, roleCodes: Array.isArray(employee.roles) ? employee.roles.map((item) => item.role_code) : [], }; } function toResponseItem(record, now) { return { id: record.id, principalEmployeeId: record.principal_employee_id, delegateEmployeeId: record.delegate_employee_id, scope: record.scope, validFrom: record.valid_from.toISOString(), validTo: record.valid_to ? record.valid_to.toISOString() : null, revokedAt: record.revoked_at ? record.revoked_at.toISOString() : null, createdAt: record.created_at.toISOString(), status: computeDelegationStatus(record, now), principal: record.principal_employee ? mapEmployeeEmbedded(record.principal_employee) : null, delegate: record.delegate_employee ? mapEmployeeEmbedded(record.delegate_employee) : null, }; } async function delegationRoutes(fastify) { async function ensureDelegateEligible(tx, delegateEmployeeId, scope) { const delegateEmployee = await tx.employee.findUnique({ where: { id: delegateEmployeeId }, select: { id: true, status: true, roles: { select: { role_code: true } }, }, }); if (!delegateEmployee || delegateEmployee.status !== "active") { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { delegateEmployeeId: "invalid_delegate" }, }); } const roleCodes = delegateEmployee.roles.map((item) => item.role_code); const permissionCodes = resolvePermissionsForRoles(roleCodes); if (scope === "reimbursement_submit") { if (!hasPermission(permissionCodes, "expense.proxy_submit")) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { delegateEmployeeId: "delegate_must_be_administration" }, }); } return delegateEmployee; } if (!hasPermission(permissionCodes, "expense.approve")) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { delegateEmployeeId: "delegate_must_be_approver" }, }); } return delegateEmployee; } async function loadGrantById(tx, delegationId) { return tx.delegationGrant.findUnique({ where: { id: delegationId }, include: { principal_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, delegate_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, }, }); } fastify.get( "/delegation-grants", { preHandler: fastify.authenticate, }, async (request, reply) => { const query = parseListDelegationGrantsQuery(request.query); const now = new Date(); const canManageAll = fastify.hasPermission(request.employee, "system.users"); const baseWhere = canManageAll ? { ...(query.principalEmployeeId ? { principal_employee_id: query.principalEmployeeId } : {}), ...(query.delegateEmployeeId ? { delegate_employee_id: query.delegateEmployeeId } : {}), } : { OR: [ { principal_employee_id: request.employee.id }, { delegate_employee_id: request.employee.id }, ], }; const where = { ...baseWhere, ...(query.includeInactive ? {} : { revoked_at: null, valid_from: { lte: now }, OR: [{ valid_to: null }, { valid_to: { gt: now } }], }), }; const items = await fastify.prisma.delegationGrant.findMany({ where, orderBy: { created_at: "desc" }, include: { principal_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, delegate_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, }, }); return sendData(reply, request, { includeInactive: query.includeInactive, items: items.map((item) => toResponseItem(item, now)), }); }, ); fastify.post( "/delegation-grants", { preHandler: fastify.authenticate, }, async (request, reply) => { const input = parseCreateGrantBody(request.body); const canManageAll = fastify.hasPermission(request.employee, "system.users"); const principalEmployeeId = input.principalEmployeeId ?? request.employee.id; if (principalEmployeeId !== request.employee.id && !canManageAll) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, }); } if (input.delegateEmployeeId === principalEmployeeId) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { delegateEmployeeId: "self_not_allowed" }, }); } const delegation = await fastify.prisma.$transaction(async (tx) => { const principal = await tx.employee.findUnique({ where: { id: principalEmployeeId }, select: { id: true, status: true }, }); if (!principal || principal.status !== "active") { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { principalEmployeeId: "invalid_principal" }, }); } await ensureDelegateEligible(tx, input.delegateEmployeeId, input.scope); const created = await tx.delegationGrant.create({ data: { principal_employee_id: principalEmployeeId, delegate_employee_id: input.delegateEmployeeId, scope: input.scope, valid_from: input.validFrom, valid_to: input.validTo, created_by: request.employee.id, }, }); await tx.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: "DELEGATION_CREATE", targetType: "delegation_grant", targetId: created.id, extra: { principalEmployeeId, delegateEmployeeId: input.delegateEmployeeId, scope: input.scope, }, }), }); return loadGrantById(tx, created.id); }); return sendData(reply, request, { delegation: toResponseItem(delegation, new Date()), }); }, ); fastify.post( "/delegation-grants/:id/revoke", { preHandler: fastify.authenticate, }, async (request, reply) => { const delegationId = parseDelegationId(request.params?.id); const now = new Date(); const canManageAll = fastify.hasPermission(request.employee, "system.users"); const delegation = await loadGrantById(fastify.prisma, delegationId); if (!delegation) { throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404, }); } const canRevoke = delegation.principal_employee_id === request.employee.id || canManageAll; if (!canRevoke) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, }); } if (delegation.revoked_at) { return sendData(reply, request, { delegation: toResponseItem(delegation, now), outcome: "already_revoked", }); } const revoked = await fastify.prisma.$transaction(async (tx) => { const updated = await tx.delegationGrant.update({ where: { id: delegationId }, data: { revoked_at: now, }, }); await tx.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: "DELEGATION_REVOKE", targetType: "delegation_grant", targetId: delegationId, extra: { principalEmployeeId: delegation.principal_employee_id, delegateEmployeeId: delegation.delegate_employee_id, }, }), }); return loadGrantById(tx, updated.id); }); return sendData(reply, request, { delegation: toResponseItem(revoked, now), outcome: "revoked", }); }, ); fastify.post( "/delegations", { preHandler: fastify.requirePermission("expense.proxy_manage"), }, async (request, reply) => { const input = parseCreateBody(request.body); if (input.delegateEmployeeId === request.employee.id) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { delegateEmployeeId: "self_not_allowed" }, }); } const delegation = await fastify.prisma.$transaction(async (tx) => { await ensureDelegateEligible(tx, input.delegateEmployeeId, input.scope); const created = await tx.delegationGrant.create({ data: { principal_employee_id: request.employee.id, delegate_employee_id: input.delegateEmployeeId, scope: input.scope, valid_from: input.validFrom, valid_to: input.validTo, created_by: request.employee.id, }, }); await tx.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: "DELEGATION_CREATE", targetType: "delegation_grant", targetId: created.id, extra: { principalEmployeeId: request.employee.id, delegateEmployeeId: input.delegateEmployeeId, scope: input.scope, }, }), }); return loadGrantById(tx, created.id); }); return sendData(reply, request, { delegation: toResponseItem(delegation, new Date()), }); }, ); fastify.get( "/delegations/mine", { preHandler: fastify.requirePermission("expense.proxy_manage"), }, async (request, reply) => { const status = parseStatusQuery(request.query); const now = new Date(); const [initiatedRaw, receivedRaw] = await Promise.all([ fastify.prisma.delegationGrant.findMany({ where: { principal_employee_id: request.employee.id, }, orderBy: { created_at: "desc" }, include: { principal_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, delegate_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, }, }), fastify.prisma.delegationGrant.findMany({ where: { delegate_employee_id: request.employee.id, }, orderBy: { created_at: "desc" }, include: { principal_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, delegate_employee: { select: { id: true, employee_no: true, name: true, department_id: true, status: true, roles: { select: { role_code: true } }, }, }, }, }), ]); const initiated = initiatedRaw .filter((item) => matchesStatusFilter(item, status, now)) .map((item) => toResponseItem(item, now)); const received = receivedRaw .filter((item) => matchesStatusFilter(item, status, now)) .map((item) => toResponseItem(item, now)); return sendData(reply, request, { status, initiated, received, }); }, ); fastify.post( "/delegations/:id/revoke", { preHandler: fastify.requirePermission("expense.proxy_manage"), }, async (request, reply) => { const delegationId = parseDelegationId(request.params?.id); const now = new Date(); const delegation = await fastify.prisma.delegationGrant.findUnique({ where: { id: delegationId }, }); if (!delegation) { throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404, }); } const canRevoke = delegation.principal_employee_id === request.employee.id || fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "system.settings"); if (!canRevoke) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, }); } if (delegation.revoked_at) { const embedded = await loadGrantById(fastify.prisma, delegationId); return sendData(reply, request, { delegation: toResponseItem(embedded ?? delegation, now), outcome: "already_revoked", }); } const revoked = await fastify.prisma.$transaction(async (tx) => { const updated = await tx.delegationGrant.update({ where: { id: delegationId }, data: { revoked_at: now, }, }); await tx.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: "DELEGATION_REVOKE", targetType: "delegation_grant", targetId: delegationId, extra: { principalEmployeeId: delegation.principal_employee_id, delegateEmployeeId: delegation.delegate_employee_id, }, }), }); return loadGrantById(tx, updated.id); }); return sendData(reply, request, { delegation: toResponseItem(revoked, now), outcome: "revoked", }); }, ); } module.exports = delegationRoutes;