const { TRAVEL_RECONCILIATION_MATCH_STATUSES, TRAVEL_RECONCILIATION_RESOLUTION_STATUSES, buildReconciliationItemsSnapshot, } = require('../../../src/services/travelReconciliation'); function buildOrder(overrides = {}) { return { id: 'order-1', external_order_no: 'EXT-001', linked_reimbursement_id: null, travel_request_id: 'travel-1', amount: '100.00', currency: 'CNY', status: 'paid', travel_request: { id: 'travel-1', request_no: 'CL-20260311-0001', }, ...overrides, }; } function buildTransaction(overrides = {}) { return { id: 'txn-1', external_txn_id: 'TXN-001', travel_request_id: 'travel-1', travel_order_id: 'order-1', amount: '100.00', currency: 'CNY', txn_type: 'charge', status: 'settled', travel_request: { id: 'travel-1', request_no: 'CL-20260311-0001', }, travel_order: { id: 'order-1', external_order_no: 'EXT-001', linked_reimbursement_id: null, status: 'paid', }, ...overrides, }; } function buildInvoice(overrides = {}) { return { id: 'inv-1', travel_order_id: 'order-1', total_amount: '100.00', ...overrides, }; } describe('travelReconciliation service', () => { it('auto-confirms exact matched order items', () => { const now = new Date('2026-03-11T00:00:00.000Z'); const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [buildTransaction()], invoices: [buildInvoice()], actorId: 'actor-1', now, }); expect(snapshot.summary).toEqual( expect.objectContaining({ totalOrders: 1, totalTransactions: 1, matchedItems: 1, pendingItems: 0, anomalyItems: 0, totalOrderAmount: '100.00', totalSettledAmount: '100.00', totalReversedAmount: '0.00', matchRate: '100.00', }), ); expect(snapshot.items[0]).toEqual( expect.objectContaining({ match_status: TRAVEL_RECONCILIATION_MATCH_STATUSES.matched, resolution_status: TRAVEL_RECONCILIATION_RESOLUTION_STATUSES.confirmed, confirmed_by: 'actor-1', confirmed_at: now, snapshot_json: expect.objectContaining({ boundInvoiceCount: 1, boundInvoiceAmount: '100.00', matchedTransactionIds: ['txn-1'], }), }), ); }); it('marks amount mismatch when settled charge amount differs', () => { const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [buildTransaction({ amount: '88.00' })], invoices: [], actorId: 'actor-1', now: new Date('2026-03-11T00:00:00.000Z'), }); expect(snapshot.items[0]).toEqual( expect.objectContaining({ match_status: TRAVEL_RECONCILIATION_MATCH_STATUSES.amountMismatch, resolution_status: TRAVEL_RECONCILIATION_RESOLUTION_STATUSES.pending, anomaly_code: TRAVEL_RECONCILIATION_MATCH_STATUSES.amountMismatch, transaction_amount: '88.00', }), ); }); it('marks currency mismatch when settled charge currency differs', () => { const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [buildTransaction({ currency: 'USD' })], invoices: [], actorId: 'actor-1', now: new Date('2026-03-11T00:00:00.000Z'), }); expect(snapshot.items[0].match_status).toBe(TRAVEL_RECONCILIATION_MATCH_STATUSES.currencyMismatch); expect(snapshot.items[0].resolution_status).toBe(TRAVEL_RECONCILIATION_RESOLUTION_STATUSES.pending); }); it('marks missing transaction when no settled charge exists', () => { const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [], invoices: [], actorId: 'actor-1', now: new Date('2026-03-11T00:00:00.000Z'), }); expect(snapshot.items[0].match_status).toBe(TRAVEL_RECONCILIATION_MATCH_STATUSES.missingTransaction); }); it('marks duplicate charge when multiple settled charges exist', () => { const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [ buildTransaction({ id: 'txn-1', external_txn_id: 'TXN-001' }), buildTransaction({ id: 'txn-2', external_txn_id: 'TXN-002' }), ], invoices: [], actorId: 'actor-1', now: new Date('2026-03-11T00:00:00.000Z'), }); expect(snapshot.items[0]).toEqual( expect.objectContaining({ match_status: TRAVEL_RECONCILIATION_MATCH_STATUSES.duplicateCharge, resolution_status: TRAVEL_RECONCILIATION_RESOLUTION_STATUSES.pending, }), ); }); it('marks reversed after payment when refund or reversal appears', () => { const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [buildTransaction({ txn_type: 'refund', status: 'settled' })], invoices: [], actorId: 'actor-1', now: new Date('2026-03-11T00:00:00.000Z'), }); expect(snapshot.items[0].match_status).toBe(TRAVEL_RECONCILIATION_MATCH_STATUSES.reversedAfterPayment); }); it('creates orphan transaction items for unlinked settled or reversed transactions', () => { const snapshot = buildReconciliationItemsSnapshot({ orders: [buildOrder()], transactions: [ buildTransaction(), buildTransaction({ id: 'txn-orphan', external_txn_id: 'TXN-ORPHAN', travel_order_id: null, txn_type: 'charge', status: 'settled', travel_order: null, }), ], invoices: [], actorId: 'actor-1', now: new Date('2026-03-11T00:00:00.000Z'), }); const orphan = snapshot.items.find((item) => item.card_transaction_id === 'txn-orphan'); expect(orphan).toEqual( expect.objectContaining({ item_kind: 'orphan_transaction', match_status: TRAVEL_RECONCILIATION_MATCH_STATUSES.orphanTransaction, resolution_status: TRAVEL_RECONCILIATION_RESOLUTION_STATUSES.pending, }), ); expect(snapshot.summary).toEqual( expect.objectContaining({ totalOrders: 1, totalTransactions: 2, matchedItems: 1, pendingItems: 1, anomalyItems: 1, matchRate: '50.00', }), ); }); });