const Fastify = require("fastify"); const { AppError } = require("../../../src/utils/AppError"); const reimbursementRoutes = require("../../../src/routes/reimbursements"); const REIMBURSEMENT_ID = "11111111-1111-4111-8111-111111111111"; const OWNER_ID = "22222222-2222-4222-8222-222222222222"; const SUBMITTER_ID = "33333333-3333-4333-8333-333333333333"; const DEPARTMENT_ID = "44444444-4444-4444-8444-444444444444"; const PENDING_INSTANCE_ID = "55555555-5555-4555-8555-555555555555"; function buildReimbursementRecord(overrides = {}) { return { id: REIMBURSEMENT_ID, form_number: "BX20260410001", submitter_id: SUBMITTER_ID, owner_id: OWNER_ID, department_id: DEPARTMENT_ID, created_at: new Date("2026-04-10T09:00:00.000Z"), reimbursement_date: new Date("2026-04-10T00:00:00.000Z"), total_amount: "188.80", status: "reviewing", description: "客户拜访报销", project_id: null, linked_travel_request_id: null, department: { id: DEPARTMENT_ID, name: "华东销售部" }, project: null, travel_request: null, owner: { id: OWNER_ID, employee_no: "E1001", name: "张三" }, submitter: { id: SUBMITTER_ID, employee_no: "E1002", name: "李四" }, items: [ { id: "item-1", expense_type_id: "expense-type-1", expense_date: new Date("2026-04-09T00:00:00.000Z"), amount: "188.80", description: "客户打车", expense_type: { id: "expense-type-1", code: "travel_taxi", name: "打车费", category: "travel", enabled: true, }, }, ], attachments: [], invoice_pool_items: [ { id: "invoice-1", status: "bound", invoice_code: "FP001", invoice_number: "0001", invoice_date: new Date("2026-04-09T00:00:00.000Z"), seller_name: "上海供应商", total_amount: "188.80", original_name: "receipt.pdf", linked_item_id: "item-1", }, ], workflow_instances: [ { id: PENDING_INSTANCE_ID, current_step_id: "finance_approve", current_status: "pending", context_data: { workflow_steps_snapshot: [ { id: "manager_approve", name: "主管审批" }, { id: "finance_approve", name: "财务审批" }, ], }, workflow_definition: { definition_json: { steps: [ { id: "manager_approve", name: "主管审批" }, { id: "finance_approve", name: "财务审批" }, ], }, }, workflow_tasks: [ { id: "task-1", step_name: "财务审批", assignee_id: "finance-1", assignee_role: "finance", }, ], }, ], ...overrides, }; } async function buildApp({ employeeId = OWNER_ID, permissionCodes = ["expense.view_own"], reimbursement = buildReimbursementRecord(), latestWorkflowInstance = { id: PENDING_INSTANCE_ID, current_step_id: "finance_approve", created_at: new Date("2026-04-10T10:00:00.000Z"), context_data: { workflow_steps_snapshot: [ { id: "manager_approve", name: "主管审批" }, { id: "finance_approve", name: "财务审批" }, ], }, workflow_definition: { definition_json: { steps: [ { id: "manager_approve", name: "主管审批" }, { id: "finance_approve", name: "财务审批" }, ], }, }, }, workflowHistoryRows = [ { id: "history-start", action: "start", comment: null, payload: { step_id: "manager_approve" }, created_at: new Date("2026-04-10T10:00:00.000Z"), actor: { name: "李四" }, workflow_task: { step_name: "主管审批" }, }, { id: "history-approve", action: "approve", comment: "同意", payload: { from_step_id: "manager_approve", to_step_id: "finance_approve" }, created_at: new Date("2026-04-10T12:00:00.000Z"), actor: { name: "王经理" }, workflow_task: { step_name: "主管审批" }, }, { id: "history-skip", action: "skip", comment: null, payload: { step_id: "ceo_approve" }, created_at: new Date("2026-04-10T12:01:00.000Z"), actor: { name: "系统" }, workflow_task: null, }, ], } = {}) { const app = Fastify(); const prisma = { reimbursementForm: { findUnique: vi.fn(async ({ where }) => { if (where.id !== REIMBURSEMENT_ID) { return null; } return reimbursement; }), }, workflowInstance: { findFirst: vi.fn(async () => latestWorkflowInstance), }, workflowHistory: { findMany: vi.fn(async () => workflowHistoryRows), }, entityRiskFlag: { findMany: vi.fn(async () => []), }, budgetPool: { findMany: vi.fn(async () => []), }, }; app.decorate("prisma", prisma); app.decorate("config", { budgetControlMode: "warn", appTimezone: "Asia/Shanghai", }); app.decorate("authenticate", async (request) => { if (!request.headers.authorization) { throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 }); } request.employee = { id: employeeId, permissionCodes, roleCodes: [], department_id: DEPARTMENT_ID, }; }); app.decorate( "hasPermission", (employee, permissionCode) => (employee?.permissionCodes ?? []).includes("*") || (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.decorate("assertEntityPermission", () => {}); 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(reimbursementRoutes, { prefix: "/api" }); return { app, prisma }; } describe("GET /api/reimbursements/:id detail enhancements", () => { it("returns department, submittedAt, expanded invoices and printable approvalHistory", async () => { const { app } = await buildApp(); const response = await app.inject({ method: "GET", url: `/api/reimbursements/${REIMBURSEMENT_ID}`, headers: { authorization: "Bearer test" }, }); expect(response.statusCode).toBe(200); expect(response.json().data).toMatchObject({ department: { id: DEPARTMENT_ID, name: "华东销售部", }, createdAt: "2026-04-10T09:00:00.000Z", submittedAt: "2026-04-10T10:00:00.000Z", invoices: [ { id: "invoice-1", invoiceCode: "FP001", invoiceNumber: "0001", invoiceDate: "2026-04-09", sellerName: "上海供应商", totalAmount: "188.80", originalName: "receipt.pdf", linkedItemId: "item-1", }, ], approvalHistory: [ { id: "history-start", stepName: "主管审批", action: "start", actorName: "李四", comment: null, createdAt: "2026-04-10T10:00:00.000Z", }, { id: "history-approve", stepName: "主管审批", action: "approve", actorName: "王经理", comment: "同意", createdAt: "2026-04-10T12:00:00.000Z", }, ], }); expect(response.json().data.workflowProgress).toMatchObject({ currentStepIndex: 1, totalSteps: 2, }); await app.close(); }); it("returns null submittedAt and empty approvalHistory when reimbursement has never been submitted", async () => { const reimbursement = buildReimbursementRecord({ status: "draft", workflow_instances: [], }); const { app } = await buildApp({ reimbursement, latestWorkflowInstance: null, workflowHistoryRows: [], }); const response = await app.inject({ method: "GET", url: `/api/reimbursements/${REIMBURSEMENT_ID}`, headers: { authorization: "Bearer test" }, }); expect(response.statusCode).toBe(200); expect(response.json().data.submittedAt).toBe(null); expect(response.json().data.approvalHistory).toEqual([]); await app.close(); }); });