153 行
4.5 KiB
JavaScript
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;
|