const { normalizeRequestedResources, syncErpMasterData, } = require('../../../src/services/integrations/erpMasterData'); function createPrismaMock() { const syncStates = new Map(); const prisma = { $transaction: async (fn) => fn(prisma), integrationSyncState: { upsert: vi.fn().mockImplementation(async ({ where, create, update }) => { const key = `${where.provider_resource_type.provider}:${where.provider_resource_type.resource_type}`; const current = syncStates.get(key) ?? null; const next = current ? { ...current, ...update } : { ...create }; syncStates.set(key, next); return next; }), findMany: vi.fn().mockImplementation(async () => Array.from(syncStates.values())), }, integrationBinding: { findFirst: vi.fn().mockResolvedValue(null), upsert: vi.fn().mockResolvedValue({ id: 'binding-1' }), }, legalEntity: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue({ id: 'le-1' }), update: vi.fn().mockResolvedValue({ id: 'le-1' }), }, costCenter: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue({ id: 'cc-1' }), update: vi.fn().mockResolvedValue({ id: 'cc-1' }), }, supplier: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue({ id: 'sup-1' }), update: vi.fn().mockResolvedValue({ id: 'sup-1' }), }, }; return prisma; } describe('ERP Master Data Service', () => { const originalFetch = global.fetch; afterEach(() => { global.fetch = originalFetch; vi.restoreAllMocks(); }); it('normalizes resource order and dedupes input', () => { expect(normalizeRequestedResources(['supplier', 'legal_entity', 'supplier'])).toEqual([ 'legal_entity', 'supplier', ]); expect(normalizeRequestedResources([])).toEqual(['legal_entity', 'cost_center', 'supplier']); }); it('skips supplier rows without stable code', async () => { const prisma = createPrismaMock(); global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, json: vi.fn().mockResolvedValue({ suppliers: [{ code: '', name: 'bad supplier' }], }), }); const result = await syncErpMasterData(prisma, { erpSyncEnabled: true, erpSyncProvider: 'http', erpMasterDataEndpoint: 'https://example.com/master-data', erpMasterDataTimeoutMs: 500, }, { resources: ['supplier'], actorId: 'actor-1', logger: { error: vi.fn() }, }); const supplierState = result.items.find((item) => item.resourceType === 'supplier'); expect(result.ok).toBe(true); expect(supplierState.lastResult.skipped).toBe(1); expect(prisma.supplier.create).not.toHaveBeenCalled(); }); it('skips cost centers when legal entity is missing', async () => { const prisma = createPrismaMock(); global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, json: vi.fn().mockResolvedValue({ costCenters: [{ code: 'IT001', legalEntityCode: 'MISSING', name: 'IT中心' }], }), }); const result = await syncErpMasterData(prisma, { erpSyncEnabled: true, erpSyncProvider: 'http', erpMasterDataEndpoint: 'https://example.com/master-data', erpMasterDataTimeoutMs: 500, }, { resources: ['cost_center'], actorId: 'actor-1', logger: { error: vi.fn() }, }); const costCenterState = result.items.find((item) => item.resourceType === 'cost_center'); expect(costCenterState.lastResult.skipped).toBe(1); expect(costCenterState.lastResult.sampleSkipped[0].reason).toBe('legal_entity_missing'); }); it('skips rows when integration binding conflicts', async () => { const prisma = createPrismaMock(); prisma.supplier.findMany.mockResolvedValue([{ id: 'sup-local', code: 'SUP001', enabled: true, archived_at: null }]); prisma.integrationBinding.findFirst.mockResolvedValue({ local_id: 'another-local' }); global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, json: vi.fn().mockResolvedValue({ suppliers: [{ code: 'SUP001', externalId: 'ERP-SUP-001', name: '供应商' }], }), }); const result = await syncErpMasterData(prisma, { erpSyncEnabled: true, erpSyncProvider: 'http', erpMasterDataEndpoint: 'https://example.com/master-data', erpMasterDataTimeoutMs: 500, }, { resources: ['supplier'], actorId: 'actor-1', logger: { error: vi.fn() }, }); const supplierState = result.items.find((item) => item.resourceType === 'supplier'); expect(supplierState.lastResult.skipped).toBe(1); expect(supplierState.lastResult.sampleSkipped[0].reason).toBe('binding_conflict'); expect(prisma.supplier.update).not.toHaveBeenCalled(); }); });