/** @jest-environment node */ import mssql from 'mssql' import { prisma } from '@/lib/prisma' import { getPerformanceConversionSyncMetadata, getPerformanceConversionTimeseries, readPerformanceConversionWindows, performanceConversionWorkflowItems } 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('merges repeated product reads across workflows but keeps their independent cross-month 15-day windows', async () => { const original = await workflow() const first = performanceConversionWorkflowItems({ ...original, processInstanceId: 'sep', sourceFinishedAtRaw: '2026-09-30T08:00:00Z' })[0] const second = performanceConversionWorkflowItems({ ...original, processInstanceId: 'oct', sourceFinishedAtRaw: '2026-10-02T08:00:00Z' })[0] const sameDay = (date: string) => metricRow({ metricDate: new Date(`${date}T00:00:00Z`) }) localRows.mockResolvedValue([sameDay('2026-09-30'), sameDay('2026-10-14'), sameDay('2026-10-16')]) const result = await readPerformanceConversionWindows([first, second, first], { intentWindowOnly: true }) expect(localRows).toHaveBeenCalledTimes(1) expect(localRows.mock.calls[0][0].where.metricDate).toEqual({ gte: new Date('2026-09-30T00:00:00Z'), lte: new Date('2026-10-16T00:00:00Z') }) expect(result.get(first.key)?.daily).toHaveLength(15) expect(result.get(first.key)?.daily[14].date).toBe('2026-10-14') expect(result.get(second.key)?.daily[0].date).toBe('2026-10-02') expect(result.get(second.key)?.daily[14].date).toBe('2026-10-16') }) 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('preserves each flow bootstrap fallback when a merged product read contains another synced window', async () => { const original = await workflow() const oldFlow = performanceConversionWorkflowItems({ ...original, processInstanceId: 'synced', sourceFinishedAtRaw: '2026-08-01T08:00:00Z' })[0] const newFlow = performanceConversionWorkflowItems({ ...original, processInstanceId: 'bootstrap', sourceFinishedAtRaw: '2026-10-01T08:00:00Z' })[0] localRows.mockResolvedValue([metricRow({ metricDate: new Date('2026-08-01T00:00:00Z'), favorites: '1' })]) mockSql.__request.query.mockResolvedValue({ recordset: [metricRow({ metricDate: new Date('2026-10-01T00:00:00Z'), favorites: '9' })] }) const result = await readPerformanceConversionWindows([oldFlow, newFlow], { intentWindowOnly: true }) expect(localRows).toHaveBeenCalledTimes(1) expect(mockSql.__request.query).toHaveBeenCalledTimes(1) expect(result.get(oldFlow.key)?.sourceDaily[0].favorites).toBe(1) expect(result.get(newFlow.key)?.sourceDaily[0].favorites).toBe(9) }) 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() }) })