文件
hyb-oa/server/src/routes/travelOrders.js
T

153 行
4.5 KiB
JavaScript

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;