import { DING_WORKFLOW_ALLOWED_PROCESS_CODES, DING_WORKFLOW_BRANDS, DING_WORKFLOW_NEW_PRODUCT_DEMAND_TYPE, DING_WORKFLOW_STARTED_AT_CUTOFF_ISO, buildBrandCenterOngoingArtWorkflowWhere, buildBrandCenterOngoingWorkflowWhere, buildMonthlyLaunchOngoingWorkflowWhere, buildMonthlyLaunchWorkflowWhere, getShanghaiMonthRange, getDingWorkflowActivityNodeName, getDingWorkflowProjectType, isAllowedDingWorkflowProcessCode, isDingWorkflowStartedOnOrAfterCutoff, isDingWorkflowOngoing, isDingWorkflowCompletedInRange, isDingWorkflowDeliveryAssessmentInRange, isDingWorkflowApprovedCompletion, normalizeDingWorkflowBrand, normalizeDingWorkflowDepartment, normalizeDingWorkflowDetailValue, normalizeDingWorkflowProcessCode, parseDingWorkflowSourceCursor, resolveDingWorkflowInitiator, resolveDingWorkflowDetailMilestones, serializeDingWorkflowSourceCursor, sourceWorkflowTimestampToIso, storedWorkflowTimestampToIso, } from '@/lib/ding-workflow-sync' jest.mock('mssql', () => ({ __esModule: true, default: {}, })) jest.mock('@/lib/prisma', () => ({ prisma: {} })) jest.mock('@/lib/brand-center-access', () => ({ resolveBrandCenterAccessForUser: jest.fn() })) describe('ding workflow snapshot sync semantics', () => { test.each([ ['RUNNING', null, true], ['NEW', '', true], ['COMPLETED', null, false], ['TERMINATED', null, false], ['CANCELED', null, false], ['RUNNING', '2026-08-20 10:30:00', false], ['', null, false], ])('status %s and finish time %s resolves ongoing=%s', (status, finishTime, expected) => { expect(isDingWorkflowOngoing(status, finishTime)).toBe(expected) }) it('interprets upstream report timestamps as Asia/Shanghai wall-clock values', () => { expect(sourceWorkflowTimestampToIso('2026-08-20 10:30:00')).toBe('2026-08-20T02:30:00.000Z') expect(sourceWorkflowTimestampToIso('2026-08-20T10:30:00Z')).toBe('2026-08-20T02:30:00.000Z') expect(sourceWorkflowTimestampToIso('2026-08-20T10:30:00+08:00')).toBe('2026-08-20T02:30:00.000Z') expect(sourceWorkflowTimestampToIso('invalid')).toBeNull() }) it('does not reinterpret canonical snapshot timestamps a second time', () => { expect(storedWorkflowTimestampToIso('2026-08-20T02:30:00.000Z')).toBe('2026-08-20T02:30:00.000Z') expect(storedWorkflowTimestampToIso('2026-08-20 10:30:00')).toBe('2026-08-20T02:30:00.000Z') expect(storedWorkflowTimestampToIso('invalid')).toBeNull() }) it('only includes workflows started from 2026-04-01 Asia/Shanghai', () => { expect(DING_WORKFLOW_STARTED_AT_CUTOFF_ISO).toBe('2026-03-31T16:00:00.000Z') expect(isDingWorkflowStartedOnOrAfterCutoff('2026-03-31 23:59:59')).toBe(false) expect(isDingWorkflowStartedOnOrAfterCutoff('2026-04-01 00:00:00')).toBe(true) expect(isDingWorkflowStartedOnOrAfterCutoff('2026-03-31T23:59:59.999Z')).toBe(false) expect(isDingWorkflowStartedOnOrAfterCutoff('2026-04-01T00:00:00.000Z')).toBe(true) expect(isDingWorkflowStartedOnOrAfterCutoff('invalid')).toBe(false) }) it('uses Shanghai calendar boundaries for monthly completion', () => { const range = getShanghaiMonthRange(new Date('2026-08-26T03:00:00.000Z')) expect(range.start.toISOString()).toBe('2026-07-31T16:00:00.000Z') expect(range.end.toISOString()).toBe('2026-08-31T16:00:00.000Z') expect(isDingWorkflowCompletedInRange('COMPLETED', '2026-08-01 00:00:00', range)).toBe(true) expect(isDingWorkflowCompletedInRange('COMPLETED', '2026-09-01 00:00:00', range)).toBe(false) expect(isDingWorkflowCompletedInRange('TERMINATED', '2026-08-12 12:00:00', range)).toBe(false) expect(isDingWorkflowCompletedInRange('COMPLETED', null, range)).toBe(false) }) it('uses the delivery assessment finish node for monthly inclusion and excludes pending', () => { const range = getShanghaiMonthRange(new Date('2026-08-26T03:00:00.000Z')) expect(isDingWorkflowDeliveryAssessmentInRange({ status: 'on_time', finishedAt: '2026-08-31T15:59:59.000Z', }, range)).toBe(true) expect(isDingWorkflowDeliveryAssessmentInRange({ status: 'overtime', finishedAt: '2026-07-31T16:00:00.000Z', }, range)).toBe(true) expect(isDingWorkflowDeliveryAssessmentInRange({ status: 'on_time', finishedAt: '2026-07-31T15:59:59.000Z', }, range)).toBe(false) expect(isDingWorkflowDeliveryAssessmentInRange({ status: 'pending', finishedAt: '2026-08-10T00:00:00.000Z', }, range)).toBe(false) }) it('only treats completed workflows without a rejected decision as approved completions', () => { expect(isDingWorkflowApprovedCompletion('COMPLETED', 'AGREE', null)).toBe(true) expect(isDingWorkflowApprovedCompletion('completed', null, 'NONE')).toBe(true) expect(isDingWorkflowApprovedCompletion('COMPLETED', 'REFUSE', null)).toBe(false) expect(isDingWorkflowApprovedCompletion('COMPLETED', '已拒绝', null)).toBe(false) expect(isDingWorkflowApprovedCompletion('COMPLETED', null, 'reject')).toBe(false) expect(isDingWorkflowApprovedCompletion('TERMINATED', 'AGREE', null)).toBe(false) }) it('only accepts the two approved process types', () => { expect(DING_WORKFLOW_ALLOWED_PROCESS_CODES).toEqual([ 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838', 'PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673', ]) expect(isAllowedDingWorkflowProcessCode(' proc-3571c915-0506-4490-a91a-8aa7ea33f838 ')).toBe(true) expect(isAllowedDingWorkflowProcessCode('PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673')).toBe(true) expect(isAllowedDingWorkflowProcessCode('PROC-OTHER')).toBe(false) expect(isAllowedDingWorkflowProcessCode(null)).toBe(false) }) it('maps all confirmed content workflow milestone activity ids', () => { const contentCode = DING_WORKFLOW_ALLOWED_PROCESS_CODES[1] expect(getDingWorkflowActivityNodeName(contentCode, 'cc59_3e5a')).toBe('内容主管分配任务') expect(getDingWorkflowActivityNodeName(contentCode, '9fb8_32e7')).toBe('内容主管分配任务') expect(getDingWorkflowActivityNodeName(contentCode, 'a021_1e2e')).toBe('编导审核') expect(getDingWorkflowActivityNodeName(contentCode, '39ba_efb1')).toBe('编导审核') expect(getDingWorkflowActivityNodeName(contentCode, '6c43_575f')).toBe('内容主管审核') }) it('upgrades the legacy workflow-only cursor to a three-table cursor', () => { expect(parseDingWorkflowSourceCursor('2026-08-26 19:42:36')).toEqual({ version: 3, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: null, schemeId: null, }) }) it('round-trips the Workflow, Task and Scheme incremental cursors', () => { const cursor = { version: 3 as const, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: 2248339, schemeId: 10634556, } expect(parseDingWorkflowSourceCursor(serializeDingWorkflowSourceCursor(cursor))).toEqual(cursor) }) it('preserves empty Task and Scheme cursors so the next run performs alignment', () => { const cursor = { version: 3 as const, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: null, schemeId: null, } expect(parseDingWorkflowSourceCursor(serializeDingWorkflowSourceCursor(cursor))).toEqual(cursor) }) it('upgrades version 2 cursors and requests one full Task and Scheme alignment', () => { expect(parseDingWorkflowSourceCursor(JSON.stringify({ version: 2, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: 2248339, schemeId: 10634556, }))).toEqual({ version: 3, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: null, schemeId: null, }) }) it('rejects malformed version 3 cursor ids and safely requests re-alignment', () => { expect(parseDingWorkflowSourceCursor(JSON.stringify({ version: 3, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: -1, schemeId: 'invalid', }))).toEqual({ version: 3, workflowUpdatedAt: '2026-08-26 19:42:36', taskId: null, schemeId: null, }) }) it('only queries ongoing workflows whose current assignee belongs to Brand Center', () => { expect(buildBrandCenterOngoingWorkflowWhere()).toEqual({ isOngoing: true, processCode: { in: [...DING_WORKFLOW_ALLOWED_PROCESS_CODES] }, sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO }, activeTasks: { array_contains: [{ isBrandCenter: true, isActive: true }] }, }) }) it('reuses the ongoing-project art-design subset for monthly launches', () => { expect(buildBrandCenterOngoingArtWorkflowWhere()).toEqual({ isOngoing: true, processCode: DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO }, activeTasks: { array_contains: [{ isBrandCenter: true, isActive: true }] }, }) expect(buildMonthlyLaunchOngoingWorkflowWhere()).toEqual({ ...buildBrandCenterOngoingArtWorkflowWhere(), brand: { in: [...DING_WORKFLOW_BRANDS] }, demandType: DING_WORKFLOW_NEW_PRODUCT_DEMAND_TYPE, }) }) it('only queries post-April art-design workflows for the six-brand monthly launch metric', () => { expect(buildMonthlyLaunchWorkflowWhere()).toEqual({ processCode: 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838', sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO }, brand: { in: [...DING_WORKFLOW_BRANDS] }, demandType: '新品', }) }) it('normalizes process codes before persistence and comparison', () => { expect(normalizeDingWorkflowProcessCode(' proc-ae9ff3e1-a442-4e6a-85cf-0f2b86409673 ')) .toBe('PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673') expect(normalizeDingWorkflowProcessCode('')).toBeNull() }) it('maps the two process codes to the dashboard task types', () => { expect(getDingWorkflowProjectType('PROC-3571C915-0506-4490-A91A-8AA7EA33F838')).toBe('美术设计流程') expect(getDingWorkflowProjectType('PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673')).toBe('内容视频和图片拍摄') expect(getDingWorkflowProjectType('PROC-OTHER')).toBe('其他流程') }) test.each([ ['sid-startevent', '发起人'], ['1de3_de55', '部门负责人审核'], ['94c1_1f0d', '部门总监审核'], ['3cb8_8096', '品牌策划总监审批'], ['38da_544a', '品牌策划主管分配任务'], ['31cf_5b23', '出策划案'], ['a9e1_5be0', '出图'], ['de46_708d', '平面'], ['2f9e_36f0', 'AI视频'], ['b306_c849', '出图'], ['f073_738b', '平面'], ['6864_5429', '出图'], ['162b_23ea', '平面'], ['c9fe_a6b8', '出图/平面/AI视频'], ['c255_84b3', '品牌策划主管审核'], ['ebb9_213a', '运营接收图片并评分'], ['b426_1125', '运营接收图片并评分'], ['3e66_c105', '运营接收图片并评分'], ['64bb_b63a', '运营接收图片并评分'], ['2748_c5f9', '运营接收图片并评分'], ['45f4_e095', '运营接收图片并评分'], ['a8aa_ce41', '部门负责确认'], ['00d9_b57f', '抄送部门总监'], ])('maps art-design activity %s to %s from the supplied process definition', (activityId, expected) => { expect(getDingWorkflowActivityNodeName(DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], activityId)).toBe(expected) }) it('does not reuse art-design activity names for other processes or unknown nodes', () => { expect(getDingWorkflowActivityNodeName(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], '38da_544a')).toBeNull() expect(getDingWorkflowActivityNodeName(DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], 'unknown-node')).toBeNull() expect(getDingWorkflowActivityNodeName(null, '38da_544a')).toBeNull() }) it('normalizes approved brands and groups unmatched values as other brands', () => { expect(normalizeDingWorkflowBrand('自有品牌-亿家达')).toBe('亿家达') expect(normalizeDingWorkflowBrand('索菲亚')).toBe('索菲亚') expect(normalizeDingWorkflowBrand('新品牌')).toBe('其他品牌') expect(normalizeDingWorkflowBrand(null)).toBe('其他品牌') }) it('normalizes DingTalk department fields before grouping requester dimensions', () => { expect(normalizeDingWorkflowDepartment('["运营中心E"]')).toBe('运营中心E') expect(normalizeDingWorkflowDepartment('[{"name":"品牌中心"}]')).toBe('品牌中心') expect(normalizeDingWorkflowDepartment(['运营中心E', '运营中心E'])).toBe('运营中心E') expect(normalizeDingWorkflowDepartment('综合支持部')).toBe('综合支持部') expect(normalizeDingWorkflowDepartment(null)).toBeNull() }) it('normalizes scalar, array and contact-shaped detail form values', () => { expect(normalizeDingWorkflowDetailValue('天猫')).toBe('天猫') expect(normalizeDingWorkflowDetailValue('["书桌","书柜"]')).toBe('书桌 / 书柜') expect(normalizeDingWorkflowDetailValue([{ name: '亿家达' }, { name: '亿家达' }])).toBe('亿家达') expect(normalizeDingWorkflowDetailValue(null)).toBeNull() }) it('uses the confirmed art and content-video milestone definitions', () => { const tasks = [ { taskId: 'task-1', activityId: 'art-start', nodeName: '品牌策划主管分配任务', assigneeUserId: 'user-1', assigneeName: '甲', assigneeDepartment: '品牌中心', isBrandCenter: true, isLeadership: false, isActive: false, reviewPassed: true, result: 'AGREE', createdAt: '2026-08-01T01:00:00.000Z', finishedAt: '2026-08-01T02:00:00.000Z', }, { taskId: 'task-2', activityId: 'art-end', nodeName: '品牌策划主管审核', assigneeUserId: 'user-2', assigneeName: '乙', assigneeDepartment: '品牌中心', isBrandCenter: true, isLeadership: true, isActive: false, reviewPassed: true, result: 'AGREE', createdAt: '2026-08-02T01:00:00.000Z', finishedAt: '2026-08-02T02:00:00.000Z', }, ] expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], tasks)).toEqual({ assignmentNodeLabel: '品牌策划主管分配任务', assignmentStartedAt: '2026-08-01T01:00:00.000Z', reviewNodeLabel: '品牌策划主管审核', reviewFinishedAt: '2026-08-02T02:00:00.000Z', }) expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], [ { ...tasks[0], nodeName: '内容部负责人', createdAt: '2026-08-03T01:00:00.000Z' }, { ...tasks[0], taskId: 'task-3', nodeName: '编导', finishedAt: '2026-08-04T02:00:00.000Z' }, { ...tasks[0], taskId: 'task-4', nodeName: '编导', finishedAt: '2026-08-05T02:00:00.000Z' }, ])).toEqual({ assignmentNodeLabel: '内容主管分配任务', assignmentStartedAt: '2026-08-03T01:00:00.000Z', reviewNodeLabel: '内容主管审核', reviewFinishedAt: null, }) expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], [ { ...tasks[0], nodeName: '内容部负责人', createdAt: '2026-08-06T01:00:00.000Z' }, { ...tasks[0], taskId: 'task-5', nodeName: '编导', finishedAt: '2026-08-07T02:00:00.000Z' }, { ...tasks[0], taskId: 'task-6', nodeName: '品牌中心负责人审核', finishedAt: '2026-08-08T02:00:00.000Z' }, ])).toEqual({ assignmentNodeLabel: '内容主管分配任务', assignmentStartedAt: '2026-08-06T01:00:00.000Z', reviewNodeLabel: '内容主管审核', reviewFinishedAt: '2026-08-08T02:00:00.000Z', }) }) it('does not expose a content supervisor review time before the review is confirmed', () => { const pendingSupervisorReview = { taskId: 'pending-supervisor-review', activityId: 'content-review', nodeName: '内容主管审核', assigneeUserId: 'user-2', assigneeName: '乙', assigneeDepartment: '品牌中心', isBrandCenter: true, isLeadership: true, isActive: true, reviewPassed: false, result: null, createdAt: '2026-08-27T03:00:00.000Z', finishedAt: '2026-08-27T03:19:00.000Z', } expect(resolveDingWorkflowDetailMilestones( DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], [ pendingSupervisorReview, { ...pendingSupervisorReview, taskId: 'director-review', nodeName: '编导审核', isActive: false, reviewPassed: true, result: 'AGREE', finishedAt: '2026-08-27T02:30:00.000Z', }, ], )).toEqual({ assignmentNodeLabel: '内容主管分配任务', assignmentStartedAt: null, reviewNodeLabel: '内容主管审核', reviewFinishedAt: null, }) }) it('matches initiators by DingTalk roster profile and formats the HR organization path', () => { expect(resolveDingWorkflowInitiator({ displayName: '荀瑶', departmentName: '品类九部', departmentPath: '江苏简尚品牌管理有限公司/运营中心A/事业四部/品类九部/淘系四组', }, { name: '旧需求人', department: '旧需求部门', })).toEqual({ name: '荀瑶', department: '运营中心A-事业四部-品类九部-淘系四组', }) }) it('uses the workflow snapshot when the initiator is absent from the HR roster', () => { expect(resolveDingWorkflowInitiator(null, { name: '林发起', department: '[{"name":"品牌中心-品牌创意A部"}]', })).toEqual({ name: '林发起', department: '品牌中心-品牌创意A部', }) }) })