const Fastify = require("fastify"); const { AppError } = require("../../../src/utils/AppError"); const travelAuthorizationsRoutes = require("../../../src/routes/travelAuthorizations"); function buildAuthorizationRecord(overrides = {}) { const now = new Date("2026-03-10T00:00:00.000Z"); return { id: "auth-1", source: "corp-card", external_auth_no: "AUTH-001", employee_id: "employee-1", department_id: "dept-1", travel_request_id: "11111111-1111-4111-8111-111111111111", budget_pool_id: "pool-1", legal_entity_id: "legal-1", cost_center_id: "cc-1", authorized_amount: "1000.00", settled_amount: "0.00", reversed_amount: "0.00", currency: "CNY", occurred_at: now, expires_at: null, status: "authorized", created_at: now, updated_at: now, ...overrides, }; } function buildPrisma({ employees = [], travelRequests = [], existing = null, existingLedger = [] } = {}) { const prisma = { $queryRaw: vi.fn().mockResolvedValue([]), employee: { findMany: vi.fn().mockResolvedValue(employees), }, travelRequest: { findMany: vi.fn().mockResolvedValue(travelRequests), }, cardAuthorization: { findUnique: vi.fn().mockResolvedValue(existing), findMany: vi.fn().mockResolvedValue(existing ? [existing] : []), count: vi.fn().mockResolvedValue(existing ? 1 : 0), upsert: vi.fn().mockImplementation(async ({ create }) => buildAuthorizationRecord({ source: create.source, external_auth_no: create.external_auth_no, employee_id: create.employee_id, department_id: create.department_id, travel_request_id: create.travel_request_id, budget_pool_id: create.budget_pool_id, legal_entity_id: create.legal_entity_id, cost_center_id: create.cost_center_id, authorized_amount: create.authorized_amount, settled_amount: create.settled_amount, reversed_amount: create.reversed_amount, currency: create.currency, occurred_at: create.occurred_at, expires_at: create.expires_at, status: create.status, })), }, budgetLedgerEntry: { findMany: vi.fn().mockResolvedValue(existingLedger), create: vi.fn().mockResolvedValue({ id: "ledger-1" }), }, budgetPool: { findUnique: vi.fn().mockResolvedValue({ id: "pool-1", used_amount: "0.00" }), update: vi.fn().mockResolvedValue({ id: "pool-1" }), }, auditLog: { create: vi.fn().mockResolvedValue({ id: "audit-1" }), }, $transaction: vi.fn().mockImplementation(async (callback) => callback(prisma)), }; return prisma; } async function buildApp({ prisma, permissionCodes = ["integration.manage", "travel.payment.manage"] }) { const app = Fastify(); app.decorate("prisma", prisma); app.decorate("config", { travelSyncEnabled: true }); app.decorate("authenticate", async (request) => { if (!request.headers.authorization) { throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 }); } request.employee = { id: "actor-1", permissionCodes }; }); app.decorate("hasPermission", (employee, permissionCode) => (employee?.permissionCodes ?? []).includes(permissionCode)); app.decorate("requirePermission", (permissionCode) => async (request) => { await app.authenticate(request); if (!app.hasPermission(request.employee, permissionCode)) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } }); app.setErrorHandler((error, request, reply) => { if (error instanceof AppError) { return reply.status(error.statusCode).send({ requestId: request.id, error: { code: error.code, message: error.message, details: error.details ?? {}, }, }); } return reply.status(500).send({ requestId: request.id, error: { code: "SYS_001", message: "服务器内部错误", details: {} }, }); }); await app.register(travelAuthorizationsRoutes, { prefix: "/api" }); return app; } describe("travelAuthorizations routes", () => { it("imports authorizations on happy path", async () => { const prisma = buildPrisma({ employees: [{ id: "employee-1", department_id: "dept-1" }], travelRequests: [{ id: "11111111-1111-4111-8111-111111111111", applicant_employee_id: "employee-1", department_id: "dept-1", budget_pool_id: "pool-1", legal_entity_id: "legal-1", cost_center_id: "cc-1", }], }); const app = await buildApp({ prisma, permissionCodes: ["integration.manage"] }); const response = await app.inject({ method: "POST", url: "/api/travel/authorizations/import", headers: { authorization: "Bearer test" }, payload: { authorizations: [{ source: "corp-card", externalAuthNo: "AUTH-001", employeeId: "employee-1", travelRequestId: "11111111-1111-4111-8111-111111111111", authorizedAmount: "1000.00", }], }, }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual(expect.objectContaining({ imported: 1, authorizedCount: 1 })); await app.close(); }); it("treats duplicate import as idempotent", async () => { const existing = buildAuthorizationRecord(); const prisma = buildPrisma({ employees: [{ id: "employee-1", department_id: "dept-1" }], travelRequests: [{ id: "11111111-1111-4111-8111-111111111111", applicant_employee_id: "employee-1", department_id: "dept-1", budget_pool_id: "pool-1", legal_entity_id: "legal-1", cost_center_id: "cc-1", }], existing, existingLedger: [{ id: "ledger-1", budget_pool_id: "pool-1", entity_type: "card_authorization", entity_id: existing.id, kind: "authorize", amount: "1000.00", created_at: new Date("2026-03-10T00:00:00.000Z"), }], }); const app = await buildApp({ prisma, permissionCodes: ["integration.manage"] }); const response = await app.inject({ method: "POST", url: "/api/travel/authorizations/import", headers: { authorization: "Bearer test" }, payload: { authorizations: [{ source: "corp-card", externalAuthNo: "AUTH-001", employeeId: "employee-1", travelRequestId: "11111111-1111-4111-8111-111111111111", authorizedAmount: "1000.00", }], }, }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual(expect.objectContaining({ imported: 1, authorizedCount: 0 })); await app.close(); }); it("skips rows when request missing", async () => { const prisma = buildPrisma({ employees: [{ id: "employee-1", department_id: "dept-1" }], travelRequests: [], }); const app = await buildApp({ prisma, permissionCodes: ["integration.manage"] }); const response = await app.inject({ method: "POST", url: "/api/travel/authorizations/import", headers: { authorization: "Bearer test" }, payload: { authorizations: [{ source: "corp-card", externalAuthNo: "AUTH-404", employeeId: "employee-1", travelRequestId: "11111111-1111-4111-8111-111111111111", authorizedAmount: "1000.00", }], }, }); expect(response.statusCode).toBe(200); expect(response.json().data.skipped).toEqual([ expect.objectContaining({ reason: "travel_request_not_found" }), ]); await app.close(); }); it("skips rows when request employee mismatches", async () => { const prisma = buildPrisma({ employees: [{ id: "employee-1", department_id: "dept-1" }], travelRequests: [{ id: "11111111-1111-4111-8111-111111111111", applicant_employee_id: "employee-9", department_id: "dept-1", budget_pool_id: "pool-1", legal_entity_id: "legal-1", cost_center_id: "cc-1", }], }); const app = await buildApp({ prisma, permissionCodes: ["integration.manage"] }); const response = await app.inject({ method: "POST", url: "/api/travel/authorizations/import", headers: { authorization: "Bearer test" }, payload: { authorizations: [{ source: "corp-card", externalAuthNo: "AUTH-002", employeeId: "employee-1", travelRequestId: "11111111-1111-4111-8111-111111111111", authorizedAmount: "1000.00", }], }, }); expect(response.statusCode).toBe(200); expect(response.json().data.skipped).toEqual([ expect.objectContaining({ reason: "travel_request_employee_mismatch" }), ]); await app.close(); }); });