/** @jest-environment node */ import mssql from 'mssql' import { prisma } from '@/lib/prisma' import { getPerformanceConversionSyncMetadata, getPerformanceConversionTimeseries } from '@/lib/performance-conversion' jest.mock('@/lib/brand-center-dashboard-access', () => ({ assertBrandCenterDashboardAccess: jest.fn().mockResolvedValue(undefined) })) jest.mock('@/lib/ding-workflow-sync', () => ({ DING_WORKFLOW_ALLOWED_PROCESS_CODES: ['art-process'], DING_WORKFLOW_STARTED_AT_CUTOFF_ISO: '2026-03-31T16:00:00.000Z', getDingWorkflowSyncMetadata: jest.fn(), isDingWorkflowApprovedCompletion: jest.fn(() => true), })) jest.mock('@/lib/prisma', () => ({ prisma: { dingWorkflowSnapshot: { findUnique: jest.fn() }, performanceConversionSyncState: { findUnique: jest.fn() }, performanceConversionMetricSnapshot: { findMany: jest.fn() }, } })) jest.mock('mssql', () => { const request = { input: jest.fn(), query: jest.fn() } request.input.mockReturnValue(request) const pool = { connect: jest.fn(), close: jest.fn(), request: () => request } return { __esModule: true, default: { ConnectionPool: jest.fn(() => pool), NVarChar: (length: number) => length, __request: request, __pool: pool, } } }) const workflow = prisma.dingWorkflowSnapshot.findUnique as jest.Mock const state = prisma.performanceConversionSyncState.findUnique as jest.Mock const localRows = prisma.performanceConversionMetricSnapshot.findMany as jest.Mock const mockSql = mssql as typeof mssql & { __request: { input: jest.Mock; query: jest.Mock }; __pool: { connect: jest.Mock; close: jest.Mock } } const productId = '1076069062942' function metricRow(overrides = {}) { return { sourceRowId: '10', metricDate: new Date('2026-10-20T00:00:00Z'), productId, sourceBrand: '朴美家具旗舰店', productName: '测试商品', visitors: '100', paymentUsers: '3', conversionRate: '2.5%', clickRate: '1.5%', clicks: '80', impressions: '1000', promotionSpend: '10', promotionRevenue: '50', roi: '2.75', favorites: '5', cartUsers: '6', paymentAmount: '150', ...overrides, } } describe('source-isolated performance conversion reads', () => { beforeAll(() => jest.useFakeTimers().setSystemTime(new Date('2026-10-22T04:00:00Z'))) afterAll(() => jest.useRealTimers()) beforeEach(() => { jest.clearAllMocks() workflow.mockResolvedValue({ processInstanceId: 'flow-pumei', businessId: 'approval', title: '测试', result: 'agree', bizAction: 'finish', status: 'completed', processCode: 'art-process', brand: '朴美', sourceFinishedAtRaw: '2026-10-01T08:00:00Z', formFields: { productId, platform: '天猫' }, }) state.mockResolvedValue({ status: 'ready', lastCompletedAt: new Date('2026-10-21T07:00:00Z'), sourceCursorRaw: JSON.stringify({ version: 2, lastSourceDate: '2026-10-21', tmallStoreDates: { 朴美: '2026-10-20', 顾家: '2026-10-19' } }), }) localRows.mockResolvedValue([metricRow()]) mockSql.__pool.connect.mockResolvedValue(undefined) mockSql.__pool.close.mockResolvedValue(undefined) mockSql.__request.query.mockResolvedValue({ recordset: [] }) process.env.REPORT_DB_SERVER = 'report.internal' process.env.REPORT_DB_DATABASE = 'report' process.env.REPORT_DB_USER = 'reader' process.env.REPORT_DB_PASSWORD = 'test-only' }) it('uses the reported Overall click/conversion rates and reported ROI, not unrelated derived ratios', async () => { const result = await getPerformanceConversionTimeseries(null, { workflowId: 'flow-pumei', productId }) expect(result.latest).toMatchObject({ clickRate: 0.015, paymentConversionRate: 0.025, roi: 2.75, impressions: 1000 }) expect(result.product.brandMatchesWorkflow).toBe(true) expect(localRows).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ productId, sourceKey: { startsWith: 'tmall-store-v1:朴美:' }, }) })) expect(mockSql.__pool.connect).not.toHaveBeenCalled() }) it('keeps null, zero and missing calendar days distinct', async () => { localRows.mockResolvedValue([metricRow({ impressions: null, roi: '0', favorites: null })]) const result = await getPerformanceConversionTimeseries(null, { workflowId: 'flow-pumei', productId }) expect(result.latest).toMatchObject({ impressions: null, roi: 0, favorites: null }) expect(result.daily[0]).toMatchObject({ date: '2026-10-01', impressions: null, roi: null }) }) it('does not label the old cursor as a completed sync for a newly introduced source', async () => { state.mockResolvedValue({ status: 'ready', lastCompletedAt: new Date(), sourceCursorRaw: JSON.stringify({ version: 1, lastSourceDate: '2026-10-21' }) }) await expect(getPerformanceConversionSyncMetadata('朴美')).resolves.toEqual({ status: 'pending', lastCompletedAt: null, lastSourceDate: null }) await expect(getPerformanceConversionSyncMetadata('亿家达')).resolves.toMatchObject({ status: 'ready', lastSourceDate: '2026-10-21' }) }) it('reports each store actual progress separately instead of the current calendar date', async () => { await expect(getPerformanceConversionSyncMetadata('朴美')).resolves.toMatchObject({ lastSourceDate: '2026-10-20' }) await expect(getPerformanceConversionSyncMetadata('顾家')).resolves.toMatchObject({ lastSourceDate: '2026-10-19' }) await expect(getPerformanceConversionSyncMetadata('索菲亚')).resolves.toMatchObject({ status: 'pending', lastSourceDate: null }) }) it('reads the same new source during the pre-snapshot bootstrap gap', async () => { const newProductId = '1076069062943' const row = await workflow() workflow.mockResolvedValue({ ...row, formFields: { productId: newProductId } }) state.mockResolvedValue(null) mockSql.__request.query.mockResolvedValue({ recordset: [metricRow({ productId: newProductId, clicks: null })] }) const result = await getPerformanceConversionTimeseries(null, { workflowId: 'flow-pumei', productId: newProductId }) expect(result.latest?.roi).toBe(2.75) expect(mockSql.__request.query.mock.calls[0][0]).toContain('FROM [rpt].[TmallStoreOverall] o') expect(mockSql.__request.input).toHaveBeenCalledWith('store', expect.anything(), '朴美家具旗舰店') expect(localRows).not.toHaveBeenCalled() }) it('does not attach Tmall metrics to a same-ID workflow explicitly on another platform', async () => { const row = await workflow() workflow.mockResolvedValue({ ...row, formFields: { productId, platform: '京东' } }) const result = await getPerformanceConversionTimeseries(null, { workflowId: 'flow-pumei', productId }) expect(result.latest).toBeNull() expect(localRows).not.toHaveBeenCalled() expect(mockSql.__pool.connect).not.toHaveBeenCalled() }) })