const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { sendData } = require("../utils/reply"); const { buildAuditLogData } = require("../utils/audit"); const { normalizeTravelImportRows } = require("../services/integrations/travel"); const IMPORT_SCHEMA = z.object({ orders: z.array(z.record(z.string(), z.unknown())).min(1).max(100), }); function mapTravelOrder(item) { return { id: item.id, source: item.source, externalOrderNo: item.external_order_no, employeeId: item.employee_id, departmentId: item.department_id ?? null, linkedReimbursementId: item.linked_reimbursement_id ?? null, occurredAt: item.occurred_at?.toISOString?.()?.slice(0, 10) ?? null, amount: item.amount?.toString?.() ?? null, currency: item.currency, status: item.status, createdAt: item.created_at?.toISOString?.() ?? null, updatedAt: item.updated_at?.toISOString?.() ?? null, }; } async function travelOrdersRoutes(fastify) { fastify.post( "/travel/orders/import", { preHandler: fastify.requirePermission("integration.manage"), }, async (request, reply) => { if (!fastify.config.travelSyncEnabled) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409, details: { integration: "travel_sync_disabled" }, }); } const parsed = IMPORT_SCHEMA.safeParse(request.body ?? {}); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten(), }); } const rows = normalizeTravelImportRows(parsed.data.orders); if (!rows.length) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { orders: "empty_after_normalization" }, }); } const employeeIds = Array.from(new Set(rows.map((item) => item.employeeId))); const employees = await fastify.prisma.employee.findMany({ where: { id: { in: employeeIds }, status: "active", }, select: { id: true, department_id: true, }, }); const employeeMap = new Map(employees.map((item) => [item.id, item])); const created = []; const skipped = []; for (const row of rows) { const employee = employeeMap.get(row.employeeId); if (!employee) { skipped.push({ source: row.source, externalOrderNo: row.externalOrderNo, reason: "employee_not_found", }); continue; } const upserted = await fastify.prisma.travelOrder.upsert({ where: { source_external_order_no: { source: row.source, external_order_no: row.externalOrderNo, }, }, update: { employee_id: row.employeeId, department_id: employee.department_id, linked_reimbursement_id: row.linkedReimbursementId, occurred_at: row.occurredAt, amount: row.amount, currency: row.currency, status: "imported", raw_payload: row.rawPayload, updated_by: request.employee.id, }, create: { source: row.source, external_order_no: row.externalOrderNo, employee_id: row.employeeId, department_id: employee.department_id, linked_reimbursement_id: row.linkedReimbursementId, occurred_at: row.occurredAt, amount: row.amount, currency: row.currency, status: "imported", raw_payload: row.rawPayload, created_by: request.employee.id, updated_by: request.employee.id, }, }); created.push(upserted); } await fastify.prisma.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: "TRAVEL_ORDERS_IMPORTED", targetType: "travel_order", extra: { requested: rows.length, createdOrUpdated: created.length, skipped, }, }), }); return sendData(reply, request, { requested: rows.length, imported: created.length, skipped, items: created.map(mapTravelOrder), }); }, ); } module.exports = travelOrdersRoutes;