const { buildAssetLedgerSummaryDedupeKey, buildAssetLedgerSummaryPayload, enqueueAssetLedgerSummaryExport, hashAssetPayload, resolveAssetLedgerSummaryChange, } = require('../../../src/services/integrations/erpAsset'); describe('ERP Asset Service', () => { function createAsset(overrides = {}) { return { id: 'asset-1', asset_no: 'ZC-001', name: 'ThinkPad X1', status: 'in_use', location: '上海办公室-A区', activated_at: '2026-03-09T00:00:00.000Z', retired_at: null, created_at: '2026-03-09T08:00:00.000Z', updated_at: '2026-03-09T08:00:00.000Z', legal_entity_id: 'le-1', cost_center_id: 'cc-1', purchase_request_id: 'pr-1', assignee_employee_id: 'emp-1', category: { id: 'cat-1', code: 'LAPTOP', name: '笔记本电脑', document_category: { code: 'ASSET_STD' }, }, legal_entity: { id: 'le-1', code: 'HY', name: '海音达' }, cost_center: { id: 'cc-1', code: 'IT001', name: 'IT中心' }, purchase_request: { id: 'pr-1', request_no: 'PR-001', title: '办公电脑采购' }, assignee: { id: 'emp-1', employee_no: 'E0001', name: '张三' }, ...overrides, }; } it('builds asset ledger summary payload for create event', () => { const payload = buildAssetLedgerSummaryPayload({ asset: createAsset(), sourceEventName: 'asset.card.created', changeType: 'created', changedFields: [], occurredAt: '2026-03-09T08:00:00.000Z', }); expect(payload.exportType).toBe('asset_ledger_summary'); expect(payload.sourceEventName).toBe('asset.card.created'); expect(payload.changeType).toBe('created'); expect(payload.assetNo).toBe('ZC-001'); expect(payload.category.code).toBe('LAPTOP'); expect(payload.legalEntity.code).toBe('HY'); expect(payload.costCenter.code).toBe('IT001'); expect(payload.purchaseRequest.requestNo).toBe('PR-001'); expect(payload.assignee.employeeNo).toBe('E0001'); }); it('detects financial-dimension changes only', () => { const previousAsset = createAsset(); expect( resolveAssetLedgerSummaryChange(previousAsset, { ...previousAsset, name: 'ThinkPad X1 Carbon Gen 12', location: '上海办公室-B区', }), ).toBeNull(); expect( resolveAssetLedgerSummaryChange(previousAsset, { ...previousAsset, cost_center_id: 'cc-2', }), ).toEqual({ sourceEventName: 'asset.ledger.summary.updated', changeType: 'financial_dimensions_changed', changedFields: ['costCenterId'], }); expect( resolveAssetLedgerSummaryChange(previousAsset, { ...previousAsset, status: 'retired', }), ).toEqual({ sourceEventName: 'asset.ledger.summary.updated', changeType: 'retired', changedFields: ['status'], }); }); it('builds deterministic dedupe key and payload hash', () => { const asset = createAsset(); const payload = buildAssetLedgerSummaryPayload({ asset, changedFields: [] }); const changedPayload = buildAssetLedgerSummaryPayload({ asset: createAsset({ cost_center: { id: 'cc-2', code: 'IT002', name: 'IT二部' }, cost_center_id: 'cc-2' }), sourceEventName: 'asset.ledger.summary.updated', changeType: 'financial_dimensions_changed', changedFields: ['costCenterId'], }); expect(buildAssetLedgerSummaryDedupeKey('asset-1')).toBe('asset_card:asset-1:ledger_summary'); expect(hashAssetPayload(payload)).not.toBe(hashAssetPayload(changedPayload)); }); it('resets same outbox when asset payload changes', async () => { const prisma = { integrationOutbox: { findUnique: vi.fn().mockResolvedValue({ id: 'outbox-1', status: 'succeeded', payload: { assetNo: 'ZC-001', costCenter: { id: 'cc-1', code: 'IT001' } }, }), update: vi.fn().mockResolvedValue({ id: 'outbox-1', status: 'pending' }), }, }; const result = await enqueueAssetLedgerSummaryExport( prisma, { integrationOutboxMaxAttempts: 5 }, { actorId: 'actor-1', asset: createAsset({ cost_center_id: 'cc-2', cost_center: { id: 'cc-2', code: 'IT002', name: 'IT二部' }, }), sourceEventName: 'asset.ledger.summary.updated', changeType: 'financial_dimensions_changed', changedFields: ['costCenterId'], }, ); expect(prisma.integrationOutbox.update).toHaveBeenCalled(); expect(result.eventType).toBe('erp.asset.export'); expect(result.requeued).toBe(true); }); });