import assert from 'node:assert/strict' import test from 'node:test' import { mapExpenseClaimToRequest } from '../src/composables/useRequests.js' import { buildApplicationDetailFactItems, buildRelatedApplicationFactItems } from '../src/utils/expenseApplicationDetail.js' const CREATE_APPLICATION = '\u521b\u5efa\u7533\u8bf7' const DIRECT_MANAGER_APPROVAL = '\u76f4\u5c5e\u9886\u5bfc\u5ba1\u6279' const BUDGET_MANAGER_APPROVAL = '\u9884\u7b97\u7ba1\u7406\u8005\u5ba1\u6279' const APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210' const RETURNED = '\u9000\u56de' const WAIT_SUBMIT = '\u5f85\u63d0\u4ea4' const LINKED_APPLICATION = '\u5173\u8054\u5355\u636e' const PAID = '\u5df2\u4ed8\u6b3e' const ARCHIVED = '\u5df2\u5f52\u6863' const WAIT_LEADER_LI_APPROVAL = '\u7b49\u5f85 Leader Li \u6279\u590d' const WAIT_BUDGET_ZHAO_APPROVAL = '\u7b49\u5f85 \u8d75\u9884\u7b97 \u6279\u590d' const WAIT_BUDGET_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \u6279\u590d' const LEADER_RETURNED_STATUS = '\u9886\u5bfc\u5df2\u9000\u56de\uff0c\u5f85\u91cd\u65b0\u63d0\u4ea4' test('claim mapper exposes employee identifier for reviewer risk profile lookup', () => { const request = mapExpenseClaimToRequest({ id: 'claim-profile-1', claim_no: 'EXP-PROFILE-1', employee_id: 'emp-profile-1', employee_name: 'Alice', department_name: 'Finance', expense_type: 'travel', reason: 'Trip', location: 'Shanghai', amount: 1200, invoice_count: 1, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T02:00:00.000Z', status: 'submitted', approval_stage: DIRECT_MANAGER_APPROVAL, risk_flags_json: [], items: [] }) assert.equal(request.employeeId, 'emp-profile-1') assert.equal(request.employee_id, 'emp-profile-1') assert.equal(request.profileEmployeeId, 'emp-profile-1') }) test('claim mapper falls back to employee name for legacy profile lookup', () => { const request = mapExpenseClaimToRequest({ id: 'claim-profile-legacy', claim_no: 'EXP-PROFILE-LEGACY', employee_name: 'Legacy Alice', department_name: 'Finance', expense_type: 'travel', reason: 'Trip', location: 'Shanghai', amount: 1200, invoice_count: 1, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T02:00:00.000Z', status: 'submitted', approval_stage: DIRECT_MANAGER_APPROVAL, risk_flags_json: [], items: [] }) assert.equal(request.employeeId, '') assert.equal(request.employee_id, '') assert.equal(request.profileEmployeeId, 'Legacy Alice') }) test('application claims are mapped as application documents', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-1', claim_no: 'AP-20260525103045-ABCDEFGH', employee_name: '张三', department_name: '交付部', manager_name: 'Leader Li', expense_type: 'travel_application', reason: '支撑国网服务器上线部署', location: '上海', amount: 12000, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T02:00:00.000Z', status: 'submitted', approval_stage: '直属领导审批', risk_flags_json: [], items: [] }) assert.equal(request.documentTypeCode, 'application') assert.equal(request.documentTypeLabel, '申请单') assert.equal(request.typeLabel, '差旅费用申请') assert.equal(request.secondaryStatusLabel, '申请材料') assert.equal(request.secondaryStatusValue, '已进入审批流程') assert.equal(request.expenseTableSummary, '预计金额已随申请提交') assert.deepEqual( request.progressSteps.map((step) => step.label), [CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPROVAL_COMPLETED] ) assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false) assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false) assert.equal(request.progressSteps.some((step) => step.label === DIRECT_MANAGER_APPROVAL), false) assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false) assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.rawLabel, DIRECT_MANAGER_APPROVAL) assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.current, true) }) test('travel application detail splits trip time into departure and return rows', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-trip-time', claim_no: 'AP-20260602103045-TRIPTIME', employee_name: '张三', department_name: '交付部', manager_name: 'Leader Li', expense_type: 'travel_application', reason: '支撑国网仿生产环境部署', location: '上海', amount: 3000, invoice_count: 0, occurred_at: '2026-02-20T00:00:00.000Z', submitted_at: '2026-02-20T02:00:00.000Z', created_at: '2026-02-20T01:30:00.000Z', updated_at: '2026-02-20T02:00:00.000Z', status: 'submitted', approval_stage: '直属领导审批', risk_flags_json: [ { source: 'application_detail', application_detail: { application_type: '差旅费用申请', time: '2026-02-20 至 2026-02-23', location: '上海', reason: '支撑国网仿生产环境部署', days: '4 天', transport_mode: '火车', amount: '3000' } } ], items: [] }) const factItems = buildApplicationDetailFactItems(request) assert.deepEqual( factItems .filter((item) => ['trip_start_time', 'trip_return_time'].includes(item.key)) .map((item) => [item.label, item.value]), [ ['出发时间', '2026-02-20'], ['返回时间', '2026-02-23'] ] ) assert.equal(factItems.some((item) => item.label === '发生时间'), false) assert.equal(factItems.some((item) => item.label === '行程时间'), false) }) test('application claims wait for department P8 budget monitor after leader approval', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-budget', claim_no: 'AP-20260525103145-BUDGET', employee_name: '张三', department_name: '交付部', manager_name: 'Leader Li', expense_type: 'travel_application', reason: '支撑国网服务器上线部署', location: '上海', amount: 12000, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T03:00:00.000Z', status: 'submitted', approval_stage: BUDGET_MANAGER_APPROVAL, risk_flags_json: [ { source: 'manual_approval', event_type: 'expense_application_approval', operator: 'Leader Li', previous_approval_stage: DIRECT_MANAGER_APPROVAL, next_approval_stage: BUDGET_MANAGER_APPROVAL, next_approver_name: '赵预算', next_approver_grade: 'P8', created_at: '2026-05-25T03:00:00.000Z' } ], items: [] }) assert.deepEqual( request.progressSteps.map((step) => step.label), [CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPROVAL_COMPLETED] ) assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_ZHAO_APPROVAL)?.current, true) assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过') }) test('application budget wait label uses claim-level budget approver snapshot', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-budget-snapshot', claim_no: 'AP-20260525103145-BUDGET-SNAPSHOT', employee_name: 'Applicant Zhang', department_name: 'Engineering', manager_name: 'Leader Li', budget_approver_name: 'P8 Executive', budget_approver_grade: 'P8', budget_approver_role_code: 'executive', expense_type: 'travel_application', reason: 'Production deployment support', location: 'Beijing', amount: 12000, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T03:00:00.000Z', status: 'submitted', approval_stage: BUDGET_MANAGER_APPROVAL, risk_flags_json: [ { source: 'manual_approval', event_type: 'expense_application_approval', operator: 'Leader Li', previous_approval_stage: DIRECT_MANAGER_APPROVAL, next_approval_stage: BUDGET_MANAGER_APPROVAL, created_at: '2026-05-25T03:00:00.000Z' } ], items: [] }) assert.equal(request.budgetApproverName, 'P8 Executive') assert.deepEqual( request.progressSteps.map((step) => step.label), [CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPROVAL_COMPLETED] ) assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_P8_EXECUTIVE_APPROVAL)?.current, true) }) test('returned application claims include leader return node and supplement status', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-returned', claim_no: 'APP-20260525-RETURNED', employee_name: 'Applicant Zhang', department_name: 'Delivery', manager_name: 'Leader Li', expense_type: 'travel_application', reason: 'Project onsite support', location: 'Shanghai', amount: 12000, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: null, created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T04:00:00.000Z', status: 'returned', approval_stage: WAIT_SUBMIT, risk_flags_json: [ { source: 'manual_return', event_type: 'expense_application_return', operator: 'Leader Li', opinion: 'Need clearer budget explanation.', return_stage_key: 'direct_manager', next_status: 'returned', next_approval_stage: WAIT_SUBMIT, return_count: 2, created_at: '2026-05-25T04:00:00.000Z' } ], items: [] }) assert.deepEqual( request.progressSteps.map((step) => step.label), [CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, RETURNED, WAIT_SUBMIT] ) assert.equal(request.progressSteps.find((step) => step.label === RETURNED)?.time, 'Leader Li\u9000\u56de') assert.match(request.progressSteps.find((step) => step.label === RETURNED)?.detail, /2026-05-25/) assert.equal(request.progressSteps.find((step) => step.label === WAIT_SUBMIT)?.current, true) assert.equal(request.secondaryStatusValue, LEADER_RETURNED_STATUS) assert.equal(request.secondaryStatusTone, 'warning') assert.equal(request.progressSteps.some((step) => step.label === APPROVAL_COMPLETED), false) }) test('approved application claims complete after budget approval', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-approved', claim_no: 'AP-20260525113045-HGFEDCBA', employee_name: '张三', department_name: '交付部', manager_name: '李经理', expense_type: 'travel_application', reason: '支撑国网服务器上线部署', location: '上海', amount: 12000, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T03:00:00.000Z', status: 'approved', approval_stage: '审批完成', risk_flags_json: [ { source: 'manual_approval', event_type: 'expense_application_approval', operator: '李经理', previous_approval_stage: '直属领导审批', next_approval_stage: '预算管理者审批', next_approver_name: '赵预算', next_approver_grade: 'P8', created_at: '2026-05-25T03:00:00.000Z' }, { source: 'budget_approval', event_type: 'expense_application_budget_approval', operator: '赵预算', previous_approval_stage: '预算管理者审批', next_approval_stage: '审批完成', created_at: '2026-05-25T03:00:00.000Z' } ], items: [] }) assert.equal(request.documentTypeCode, 'application') assert.equal(request.workflowNode, '审批完成') assert.deepEqual( request.progressSteps.map((step) => step.label), ['创建申请', '直属领导审批', '预算管理者审批', '审批完成'] ) assert.equal(request.progressSteps.every((step) => step.done), true) assert.equal(request.progressSteps.find((step) => step.label === '直属领导审批')?.time, '李经理通过') assert.equal(request.progressSteps.find((step) => step.label === '预算管理者审批')?.time, '赵预算通过') }) test('application claims hide budget step when leader approval also covers budget approval', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-merged-budget', claim_no: 'APP-20260525-MERGED', employee_name: '张三', department_name: '交付部', manager_name: '李预算经理', expense_type: 'travel_application', reason: '支撑国网服务器上线部署', location: '上海', amount: 12000, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T03:00:00.000Z', status: 'approved', approval_stage: APPROVAL_COMPLETED, risk_flags_json: [ { source: 'manual_approval', event_type: 'expense_application_approval', operator: '李预算经理', previous_approval_stage: DIRECT_MANAGER_APPROVAL, next_approval_stage: APPROVAL_COMPLETED, budget_approval_merged: true, created_at: '2026-05-25T03:00:00.000Z' } ], items: [] }) assert.deepEqual( request.progressSteps.map((step) => step.label), [CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED] ) assert.equal(request.progressSteps.every((step) => step.done), true) assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false) assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, '李预算经理通过') }) test('approved application claims hide budget step when dynamic route skipped budget review', () => { const request = mapExpenseClaimToRequest({ id: 'claim-application-skipped-budget', claim_no: 'APP-20260525-SKIPPED', employee_name: '张三', department_name: '交付部', manager_name: 'Leader Li', expense_type: 'travel_application', reason: 'Project onsite support', location: 'Shanghai', amount: 500, invoice_count: 0, occurred_at: '2026-05-25T00:00:00.000Z', submitted_at: '2026-05-25T02:00:00.000Z', created_at: '2026-05-25T01:30:00.000Z', updated_at: '2026-05-25T03:00:00.000Z', status: 'approved', approval_stage: APPROVAL_COMPLETED, risk_flags_json: [ { source: 'approval_routing', event_type: 'expense_application_route_decision', requires_budget_review: false, route: 'approval_done', created_at: '2026-05-25T03:00:00.000Z' }, { source: 'manual_approval', event_type: 'expense_application_approval', operator: 'Leader Li', previous_approval_stage: DIRECT_MANAGER_APPROVAL, next_approval_stage: APPROVAL_COMPLETED, route_decision: { requires_budget_review: false, route: 'approval_done' }, created_at: '2026-05-25T03:00:00.000Z' } ], items: [] }) assert.deepEqual( request.progressSteps.map((step) => step.label), [CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED] ) assert.equal(request.progressSteps.every((step) => step.done), true) assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false) assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过') }) test('progress steps show approval operator time and current stay duration', () => { const originalNow = Date.now Date.now = () => new Date('2026-05-20T05:00:00.000Z').getTime() try { const request = mapExpenseClaimToRequest({ id: 'claim-1', claim_no: 'EXP-202605-001', employee_name: '张三', department_name: '市场部', expense_type: 'transport', reason: '交通报销', location: '上海', amount: 88, invoice_count: 1, occurred_at: '2026-05-20T01:00:00.000Z', submitted_at: '2026-05-20T02:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T03:30:00.000Z', status: 'submitted', approval_stage: '财务审批', risk_flags_json: [ { source: 'manual_approval', operator: '李经理', previous_approval_stage: '直属领导审批', next_approval_stage: '财务审批', created_at: '2026-05-20T03:30:00.000Z' } ], items: [] }) const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批') const financeStep = request.progressSteps.find((step) => step.label === '财务审批') const firstStep = request.progressSteps[0] assert.equal(request.riskSummary, '无') assert.equal(firstStep.label, LINKED_APPLICATION) assert.equal(leaderStep.time, '李经理通过') assert.match(leaderStep.detail, /2026-05-20/) assert.match(leaderStep.title, /李经理审批通过/) assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false) assert.equal(financeStep.current, true) assert.equal(financeStep.time, '停留 1小时30分钟') } finally { Date.now = originalNow } }) test('progress steps do not expose approver email when manager name is available', () => { const originalNow = Date.now Date.now = () => new Date('2026-05-20T05:00:00.000Z').getTime() try { const request = mapExpenseClaimToRequest({ id: 'claim-email-operator', claim_no: 'EXP-202605-003', employee_name: '张三', department_name: '市场部', manager_name: '李经理', expense_type: 'transport', reason: '交通报销', location: '上海', amount: 88, invoice_count: 1, occurred_at: '2026-05-20T01:00:00.000Z', submitted_at: '2026-05-20T02:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T03:30:00.000Z', status: 'submitted', approval_stage: '财务审批', risk_flags_json: [ { source: 'manual_approval', operator: 'manager@example.com', operator_username: 'manager@example.com', previous_approval_stage: '直属领导审批', next_approval_stage: '财务审批', created_at: '2026-05-20T03:30:00.000Z' } ], items: [] }) const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批') assert.equal(leaderStep.time, '李经理通过') assert.ok(!leaderStep.title.includes('manager@example.com')) } finally { Date.now = originalNow } }) test('travel expense items describe departure return and lodging time below the date', () => { const request = mapExpenseClaimToRequest({ id: 'claim-travel-time-labels', claim_no: 'EXP-202605-TRAVEL', employee_name: '张三', department_name: '市场部', expense_type: 'travel', reason: '北京客户现场出差', location: '北京', amount: 1108, invoice_count: 3, occurred_at: '2026-05-13T01:00:00.000Z', created_at: '2026-05-13T01:30:00.000Z', updated_at: '2026-05-13T03:30:00.000Z', status: 'draft', approval_stage: '待提交', risk_flags_json: [], items: [ { id: 'outbound-train', item_type: 'train_ticket', item_reason: '广州南-北京南', item_location: '北京', item_date: '2026-05-13', item_amount: 354, invoice_id: 'outbound.png' }, { id: 'hotel', item_type: 'hotel_ticket', item_reason: '北京全季酒店', item_location: '北京', item_date: '2026-05-14', item_amount: 100, invoice_id: 'hotel.png' }, { id: 'return-train', item_type: 'train_ticket', item_reason: '北京南-广州南', item_location: '广州', item_date: '2026-05-15', item_amount: 354, invoice_id: 'return.png' }, { id: 'allowance', item_type: 'travel_allowance', item_reason: '系统自动计算出差补贴', item_location: '北京', item_date: '2026-05-15', item_amount: 300, invoice_id: '', is_system_generated: true } ] }) assert.equal(request.expenseItems.find((item) => item.id === 'outbound-train')?.dayLabel, '出发时间') assert.equal(request.expenseItems.find((item) => item.id === 'return-train')?.dayLabel, '返回时间') assert.equal(request.expenseItems.find((item) => item.id === 'hotel')?.dayLabel, '住宿时间') assert.equal(request.expenseItems.find((item) => item.id === 'outbound-train')?.detail, '起始地-目的地') assert.equal(request.expenseItems.find((item) => item.id === 'return-train')?.detail, '起始地-目的地') assert.equal(request.expenseItems.find((item) => item.id === 'hotel')?.detail, '目的地酒店') assert.equal(request.expenseItems.at(-1)?.id, 'allowance') assert.equal(request.expenseItems.at(-1)?.dayLabel, '系统自动计算') }) test('ticket description helper does not show the destination city as detail text', () => { const request = mapExpenseClaimToRequest({ id: 'claim-ticket-detail-helper', claim_no: 'EXP-202605-ROUTE', employee_name: '张三', department_name: '市场部', expense_type: 'travel', reason: '上海项目出差', location: '上海', amount: 520, invoice_count: 2, occurred_at: '2026-05-13T01:00:00.000Z', created_at: '2026-05-13T01:30:00.000Z', updated_at: '2026-05-13T03:30:00.000Z', status: 'draft', approval_stage: '待提交', risk_flags_json: [], items: [ { id: 'flight', item_type: 'flight_ticket', item_reason: '广州白云-上海虹桥', item_location: '上海', item_date: '2026-05-13', item_amount: 320, invoice_id: 'flight.png' }, { id: 'ship', item_type: 'ship_ticket', item_reason: '上海港-舟山港', item_location: '舟山', item_date: '2026-05-14', item_amount: 200, invoice_id: 'ship.png' } ] }) assert.equal(request.expenseItems.find((item) => item.id === 'flight')?.detail, '起始地-目的地') assert.equal(request.expenseItems.find((item) => item.id === 'ship')?.detail, '起始地-目的地') assert.equal(request.expenseItems.find((item) => item.id === 'ship')?.name, '轮船票') }) test('finance approval moves reimbursement to pending payment step', () => { const request = mapExpenseClaimToRequest({ id: 'claim-finance-completed', claim_no: 'EXP-202605-004', employee_name: '张三', department_name: '市场部', expense_type: 'transport', reason: '交通报销', location: '上海', amount: 88, invoice_count: 1, occurred_at: '2026-05-20T01:00:00.000Z', submitted_at: '2026-05-20T02:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T04:00:00.000Z', status: 'pending_payment', approval_stage: '待付款', risk_flags_json: [ { source: 'manual_approval', operator: '李经理', previous_approval_stage: '直属领导审批', next_approval_stage: '财务审批', created_at: '2026-05-20T03:00:00.000Z' }, { source: 'finance_approval', operator: '财务复核', previous_approval_stage: '财务审批', next_approval_stage: '待付款', created_at: '2026-05-20T04:00:00.000Z' } ], items: [] }) const financeStep = request.progressSteps.find((step) => step.label === '财务审批') const paymentStep = request.progressSteps.find((step) => step.label === '待付款') assert.equal(request.riskSummary, '无') assert.equal(request.workflowNode, '待付款') assert.equal(request.approvalKey, 'pending_payment') assert.equal(financeStep.time, '财务复核通过') assert.match(financeStep.detail, /2026-05-20/) assert.equal(paymentStep.current, true) assert.equal(paymentStep.done, false) }) test('paid reimbursement marks payment progress step as complete', () => { const request = mapExpenseClaimToRequest({ id: 'claim-finance-paid', claim_no: 'EXP-202605-005', employee_name: '张三', department_name: '市场部', expense_type: 'transport', reason: '交通报销', location: '上海', amount: 88, invoice_count: 1, occurred_at: '2026-05-20T01:00:00.000Z', submitted_at: '2026-05-20T02:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T05:00:00.000Z', status: 'paid', approval_stage: '已付款', risk_flags_json: [ { source: 'finance_approval', operator: '财务复核', previous_approval_stage: '财务审批', next_approval_stage: '待付款', created_at: '2026-05-20T04:00:00.000Z' }, { source: 'payment', event_type: 'expense_claim_payment_completed', operator: '财务付款', previous_approval_stage: '待付款', next_approval_stage: '已付款', created_at: '2026-05-20T05:00:00.000Z' }, { source: 'application_handoff', event_type: 'expense_application_to_reimbursement_draft', application_claim_id: 'application-1', application_claim_no: 'APP-20260520-001', application_detail: { application_type: '差旅费用申请', application_content: '差旅费用申请 / 北京', application_reason: '支撑国网仿生产环境部署', application_days: '4 天', application_location: '北京', application_amount: '3000', application_time: '2026-05-20 至 2026-05-23', application_transport_mode: '高铁', application_lodging_daily_cap: '600元/天', application_subsidy_daily_cap: '120元/天', application_transport_policy: '按真实票据复核', application_policy_estimate: '交通按真实票据 + 住宿 2,400元 + 补贴 480元', application_rule_name: '差旅标准规则', application_rule_version: '2026.05' } } ], items: [] }) const paymentStep = request.progressSteps.find((step) => step.label === '待付款') const paidStep = request.progressSteps.find((step) => step.label === PAID) const archivedStep = request.progressSteps.find((step) => step.label === ARCHIVED) const linkedStep = request.progressSteps.find((step) => step.label === LINKED_APPLICATION) assert.equal(request.workflowNode, '已付款') assert.equal(request.approvalStatus, '已付款') assert.deepEqual( request.progressSteps.map((step) => step.label), [LINKED_APPLICATION, '待提交', '直属领导审批', '财务审批', '待付款', PAID, ARCHIVED] ) assert.equal(paymentStep.time, '待付款') assert.equal(paidStep.time, '已付款') assert.equal(paidStep.done, true) assert.equal(archivedStep.time, ARCHIVED) assert.equal(archivedStep.done, true) assert.equal(linkedStep.time, '已关联 APP-20260520-001') assert.equal(request.relatedApplication.claimNo, 'APP-20260520-001') assert.equal(request.relatedApplication.reason, '支撑国网仿生产环境部署') assert.equal(request.relatedApplication.days, '4 天') assert.equal(request.relatedApplication.time, '2026-05-20 至 2026-05-23') assert.equal(request.relatedApplication.tripStartDate, '2026-05-20') assert.equal(request.relatedApplication.tripEndDate, '2026-05-23') assert.equal(request.relatedApplication.transportMode, '高铁') assert.equal(request.relatedApplication.lodgingDailyCap, '600元/天') assert.equal(request.relatedApplication.subsidyDailyCap, '120元/天') assert.equal(request.relatedApplication.transportPolicy, '按真实票据复核') assert.equal(request.relatedApplication.policyEstimate, '交通按真实票据 + 住宿 2,400元 + 补贴 480元') assert.equal(request.relatedApplication.ruleLabel, '差旅标准规则 / 2026.05') assert.equal(request.relatedApplication.amountLabel, '¥3,000') assert.deepEqual( buildRelatedApplicationFactItems(request).map((item) => [item.label, item.value]), [ ['关联单据单号', 'APP-20260520-001'], ['申请内容', '差旅费用申请 / 北京'], ['出发时间', '2026-05-20'], ['返回时间', '2026-05-23'], ['申请天数', '4 天'], ['申请事由', '支撑国网仿生产环境部署'], ['申请地点', '北京'], ['出行方式', '高铁'], ['住宿上限/天', '600元/天'], ['补贴标准/天', '120元/天'], ['交通费用口径', '按真实票据复核'], ['规则测算参考', '交通按真实票据 + 住宿 2,400元 + 补贴 480元'], ['规则依据', '差旅标准规则 / 2026.05'], ['预计金额', '¥3,000'] ] ) }) test('reimbursement detail resolves linked application from guided entry context', () => { const request = mapExpenseClaimToRequest({ id: 'claim-linked-context', claim_no: 'EXP-20260520-009', employee_name: '张三', department_name: '交付部', expense_type: 'travel', reason: '支撑国网仿生产环境部署', location: '北京', amount: 654, invoice_count: 1, occurred_at: '2026-05-20T01:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T02:00:00.000Z', status: 'draft', approval_stage: '待提交', risk_flags_json: [ { source: 'application_link', event_type: 'expense_reimbursement_application_linked', review_form_values: { application_claim_id: 'application-guided-1', application_claim_no: 'AP-202605-001', application_reason: '支撑国网仿生产环境部署', application_location: '北京', application_amount: '3000', application_amount_label: '¥3,000', application_business_time: '2026-05-20 至 2026-05-23', application_days: '4 天', application_transport_mode: '高铁', application_lodging_daily_cap: '600元/天', application_subsidy_daily_cap: '120元/天', application_transport_policy: '按真实票据复核', application_policy_estimate: '交通按真实票据 + 住宿 2,400元 + 补贴 480元' }, expense_scene_selection: { application_claim_no: 'AP-202605-001' } } ], items: [] }) assert.equal(request.relatedApplication.claimNo, 'AP-202605-001') assert.equal(request.relatedApplication.reason, '支撑国网仿生产环境部署') assert.equal(request.relatedApplication.location, '北京') assert.equal(request.relatedApplication.time, '2026-05-20 至 2026-05-23') assert.equal(request.relatedApplication.tripStartDate, '2026-05-20') assert.equal(request.relatedApplication.tripEndDate, '2026-05-23') assert.equal(request.relatedApplication.days, '4 天') assert.equal(request.relatedApplication.transportMode, '高铁') assert.equal(request.relatedApplication.lodgingDailyCap, '600元/天') assert.equal(request.relatedApplication.subsidyDailyCap, '120元/天') assert.equal(request.relatedApplication.policyEstimate, '交通按真实票据 + 住宿 2,400元 + 补贴 480元') assert.equal(request.relatedApplication.amountLabel, '¥3,000') assert.deepEqual(request.expenseItems, []) }) test('reimbursement detail hides stale application placeholder and allowance rows before receipts', () => { const request = mapExpenseClaimToRequest({ id: 'claim-linked-stale-placeholder', claim_no: 'EXP-20260520-010', employee_name: '张三', department_name: '交付部', expense_type: 'travel', reason: '支撑国网仿生产环境部署', location: '上海', amount: 3480, invoice_count: 0, occurred_at: '2026-02-20T00:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T02:00:00.000Z', status: 'draft', approval_stage: '待提交', risk_flags_json: [ { source: 'application_link', event_type: 'expense_reimbursement_application_linked', review_form_values: { application_claim_id: 'application-guided-stale', application_claim_no: 'AP-202605-010', application_reason: '支撑国网仿生产环境部署', application_location: '上海', application_amount: '3000', application_amount_label: '¥3,000', application_business_time: '2026-02-20 至 2026-02-23', application_days: '4 天', application_transport_mode: '火车' } } ], items: [ { id: 'placeholder-travel', item_type: 'travel', item_reason: '支撑国网仿生产环境部署', item_location: '上海', item_amount: 3000, item_date: '2026-02-20', invoice_id: '' }, { id: 'stale-allowance', item_type: 'travel_allowance', item_reason: '系统自动计算出差补贴:上海市,1天,120.00元/天', item_location: '直辖市/特区', item_amount: 120, item_date: '2026-02-20', invoice_id: '' } ] }) assert.equal(request.relatedApplication.claimNo, 'AP-202605-010') assert.equal(request.relatedApplication.days, '4 天') assert.equal(request.amount, 0) assert.deepEqual(request.expenseItems, []) assert.equal(request.expenseTableSummary, '暂无费用明细') }) test('reimbursement detail hides stale allowance when linked application days differ', () => { const request = mapExpenseClaimToRequest({ id: 'claim-linked-stale-allowance', claim_no: 'EXP-20260520-011', employee_name: '张三', department_name: '交付部', expense_type: 'travel', reason: '支撑国网仿生产环境部署', location: '上海', amount: 474, invoice_count: 1, occurred_at: '2026-02-20T00:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T02:00:00.000Z', status: 'draft', approval_stage: '待提交', risk_flags_json: [ { source: 'application_link', event_type: 'expense_reimbursement_application_linked', application_claim_no: 'AP-202605-011', application_detail: { application_reason: '支撑国网仿生产环境部署', application_location: '上海', application_time: '2026-02-20 至 2026-02-23', application_days: '4 天', application_transport_mode: '火车' } } ], items: [ { id: 'outbound-train', item_type: 'train_ticket', item_reason: '武汉-上海', item_location: '上海', item_amount: 354, item_date: '2026-02-20', invoice_id: 'ticket-1.png' }, { id: 'stale-allowance', item_type: 'travel_allowance', item_reason: '系统自动计算出差补贴:上海市,1天,120.00元/天', item_location: '直辖市/特区', item_amount: 120, item_date: '2026-02-20', invoice_id: '' } ] }) assert.equal(request.relatedApplication.days, '4 天') assert.deepEqual(request.expenseItems.map((item) => item.id), ['outbound-train']) assert.equal(request.amount, 354) assert.equal(request.expenseTableSummary, '共 1 条费用明细,已关联 1 张票据') }) test('current direct manager step shows how long the claim has stayed there', () => { const originalNow = Date.now Date.now = () => new Date('2026-05-20T05:15:00.000Z').getTime() try { const request = mapExpenseClaimToRequest({ id: 'claim-2', claim_no: 'EXP-202605-002', employee_name: '王五', department_name: '市场部', expense_type: 'office', reason: '办公用品', location: '上海', amount: 128, invoice_count: 1, occurred_at: '2026-05-20T01:00:00.000Z', submitted_at: '2026-05-20T02:00:00.000Z', created_at: '2026-05-20T01:30:00.000Z', updated_at: '2026-05-20T02:00:00.000Z', status: 'submitted', approval_stage: '直属领导审批', risk_flags_json: [], items: [] }) const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批') const submitStep = request.progressSteps.find((step) => step.label === '待提交') assert.equal(submitStep.time, '王五提交') assert.match(submitStep.detail, /2026-05-20/) assert.equal(leaderStep.current, true) assert.equal(leaderStep.time, '停留 3小时15分钟') } finally { Date.now = originalNow } })