import { assessDingWorkflowDelivery, assessDingWorkflowDeliveryBatch } from '@/lib/ding-workflow-delivery' import type { DingWorkflowDetailFormFields, OngoingDingWorkflowActiveTask } from '@/lib/ding-workflow-sync' const ART_PROCESS_CODE = 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838' const CONTENT_PROCESS_CODE = 'PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673' function formFields(overrides: Partial = {}): DingWorkflowDetailFormFields { return { platform: null, brand: null, category: null, spu: null, demandType: null, standardOutputDeadline: null, sourceType: null, taskType: null, installVideo: null, quantity: null, cameraQuantity: null, planningTaskStats: null, graphicTaskStats: null, imageTaskStats: null, ...overrides, } } function task( nodeName: string, createdAt: string, finishedAt: string | null, taskId = nodeName, ): OngoingDingWorkflowActiveTask { return { taskId, activityId: null, nodeName, assigneeUserId: null, assigneeName: '测试人员', assigneeDepartment: '品牌中心', isBrandCenter: true, isLeadership: false, isActive: finishedAt === null, reviewPassed: finishedAt !== null, result: finishedAt === null ? null : 'AGREE', createdAt, finishedAt, } } describe('Ding workflow delivery SLA', () => { it('uses post-July art task statistics and wall-clock days', () => { const assessment = assessDingWorkflowDelivery({ processCode: ART_PROCESS_CODE, sourceCreatedAt: '2026-07-10T00:00:00.000Z', sourceFinishedAt: '2026-07-11T00:00:00.000Z', formFields: formFields({ planningTaskStats: JSON.stringify([{ rowValue: [ { label: '策划类型', value: '新品策划全案/新品全套图文' }, { label: '策划细分', value: '全套新品的策划方案(主图视频脚本/策划方案/买家秀文案)' }, { label: '策划数量', value: '1' }, ], }]), }), tasks: [ task('品牌策划主管分配任务', '2026-07-10T00:00:00.000Z', '2026-07-10T00:05:00.000Z'), task('品牌策划主管审核', '2026-07-11T12:00:00.000Z', '2026-07-11T12:00:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'on_time', standardDuration: 2, durationUnit: 'day', actualDuration: 1.5, startNodeLabel: '品牌策划主管分配任务', endNodeLabel: '品牌策划主管审核', }) }) it('uses the legacy art demand/source matrix before July', () => { const assessment = assessDingWorkflowDelivery({ processCode: ART_PROCESS_CODE, sourceCreatedAt: '2026-04-20T00:00:00.000Z', sourceFinishedAt: '2026-04-24T00:00:00.000Z', formFields: formFields({ demandType: '新品', sourceType: '仅出图' }), tasks: [ task('品牌策划主管分配任务', '2026-04-20T00:00:00.000Z', '2026-04-20T00:05:00.000Z'), task('品牌策划主管审核', '2026-04-24T00:00:00.000Z', '2026-04-24T00:00:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'overtime', standardDuration: 3, actualDuration: 4 }) }) it('ends director-review content rules at 编导审核', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-03T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布景拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T00:00:00.000Z', '2026-08-01T00:05:00.000Z'), task('编导审核', '2026-08-03T00:00:00.000Z', '2026-08-03T00:00:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'on_time', standardDuration: 72, durationUnit: 'hour', actualDuration: 48, endNodeLabel: '编导审核', }) }) it('uses the confirmed content assignment and review timestamps after report-clock normalization', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-14T06:52:00.000Z', sourceFinishedAt: '2026-08-17T01:10:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '二次剪辑' }), tasks: [ task('内容主管分配任务', '2026-08-14T06:59:00.000Z', '2026-08-14T08:04:00.000Z'), task('编导', '2026-08-15T07:25:00.000Z', '2026-08-15T07:25:00.000Z'), task('编导审核', '2026-08-15T07:35:00.000Z', '2026-08-15T09:11:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'on_time', standardDuration: 48, durationUnit: 'hour', actualDuration: 26.2, startedAt: '2026-08-14T06:59:00.000Z', finishedAt: '2026-08-15T09:11:00.000Z', startNodeLabel: '内容主管分配任务', endNodeLabel: '编导审核', }) }) it('ends content-supervisor rules at 内容主管审核', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-02T06:00:00.000Z', formFields: formFields({ demandType: '白底图-策划专用', taskType: '布局拍摄' }), tasks: [ task('内容负责人分配任务', '2026-08-01T00:00:00.000Z', '2026-08-01T00:05:00.000Z'), task('内容主管审核', '2026-08-02T06:00:00.000Z', '2026-08-02T06:00:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'overtime', standardDuration: 24, durationUnit: 'hour', actualDuration: 30, startNodeLabel: '内容主管分配任务', endNodeLabel: '内容主管审核', }) }) it('does not use a leadership or content-owner task as the supervisor review', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-02T00:00:00.000Z', formFields: formFields({ demandType: '白底图-策划专用', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T00:00:00.000Z', '2026-08-01T00:05:00.000Z'), task('内容部负责人', '2026-08-01T06:00:00.000Z', '2026-08-01T06:05:00.000Z'), task('品牌中心负责人审核', '2026-08-01T06:05:00.000Z', '2026-08-01T06:10:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'pending', actualDuration: null, finishedAt: null, endNodeLabel: '内容主管审核', }) }) it('does not fall back to workflow completion when the required review task is missing', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, // 即使流程实例在本月完结,也必须有规则指定的审核节点完成时间。 sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-03T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T00:00:00.000Z', '2026-08-01T00:05:00.000Z'), ], }) expect(assessment).toMatchObject({ status: 'pending', standardDuration: 72, actualDuration: null, finishedAt: null, endNodeLabel: '编导审核', }) }) it('ignores a finish timestamp on an active review task', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: null, formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T00:00:00.000Z', '2026-08-01T00:05:00.000Z'), { ...task('编导审核', '2026-08-02T00:00:00.000Z', '2026-08-02T01:00:00.000Z'), isActive: true, reviewPassed: false, result: null, }, ], }) expect(assessment).toMatchObject({ status: 'pending', actualDuration: null, finishedAt: null, }) }) it('ignores an unconfirmed review with a mirrored finish timestamp', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: null, formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [{ ...task('内容主管分配任务', '2026-08-01T00:00:00.000Z', '2026-08-01T00:05:00.000Z'), taskId: 'running-review-with-finish-time', nodeName: '内容主管审核', isActive: false, reviewPassed: false, result: null, }], }) expect(assessment).toMatchObject({ status: 'pending', actualDuration: null, finishedAt: null, }) }) it('keeps missing configuration or milestone data pending', () => { const assessment = assessDingWorkflowDelivery({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: null, formFields: formFields({ demandType: '未知类型', taskType: '布局拍摄' }), tasks: [], }) expect(assessment).toMatchObject({ status: 'pending', standardDuration: null, actualDuration: null, }) }) it('defers content assessment start when the daily queue is full', () => { const inputs = [ { processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-02T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T01:00:00.000Z', '2026-08-01T01:00:00.000Z'), task('编导审核', '2026-08-02T00:00:00.000Z', '2026-08-02T00:00:00.000Z'), ], workflowId: 'a', workflowStatus: 'COMPLETED', }, { processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-03T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T02:00:00.000Z', '2026-08-01T02:00:00.000Z'), task('编导审核', '2026-08-03T00:00:00.000Z', '2026-08-03T00:00:00.000Z'), ], workflowId: 'b', workflowStatus: 'COMPLETED', }, { processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-04T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T03:00:00.000Z', '2026-08-01T03:00:00.000Z'), task('编导审核', '2026-08-04T00:00:00.000Z', '2026-08-04T00:00:00.000Z'), ], workflowId: 'c', workflowStatus: 'COMPLETED', }, { processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-05T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T04:00:00.000Z', '2026-08-01T04:00:00.000Z'), task('编导审核', '2026-08-05T00:00:00.000Z', '2026-08-05T00:00:00.000Z'), ], workflowId: 'd', workflowStatus: 'COMPLETED', }, ] const assessments = assessDingWorkflowDeliveryBatch(inputs) expect(assessments.slice(0, 3).map((assessment) => assessment.queueShiftDays)).toEqual([0, 0, 0]) expect(assessments[3]).toMatchObject({ queueDeferred: true, queueLimit: 3, queueSequence: 1, queueShiftDays: 1, startedAt: '2026-08-02T01:00:00.000Z', actualDuration: 71, status: 'on_time', }) }) it('uses a two-day interval for installation-video queue deferrals', () => { const makeInput = (id: string, day: string) => ({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: `2026-08-${day === '01' ? '03' : '05'}T00:00:00.000Z`, formFields: formFields({ demandType: '安装视频', taskType: '布局拍摄', installVideo: 'A档' }), tasks: [ task('内容主管分配任务', `2026-08-${day}T01:00:00.000Z`, `2026-08-${day}T01:00:00.000Z`), task('内容主管审核', `2026-08-${day === '01' ? '03' : '05'}T00:00:00.000Z`, `2026-08-${day === '01' ? '03' : '05'}T00:00:00.000Z`), ], workflowId: id, workflowStatus: 'COMPLETED', }) const [first, second] = assessDingWorkflowDeliveryBatch([makeInput('a', '01'), makeInput('b', '01')]) expect(first.queueShiftDays).toBe(0) expect(second.queueShiftDays).toBe(2) expect(second.startedAt).toBe('2026-08-03T01:00:00.000Z') }) it('uses businessId ordering and ignores the non-queue content-owner node', () => { const makeInput = (businessId: string, workflowId: string, extraOwnerTask = false) => ({ processCode: CONTENT_PROCESS_CODE, sourceCreatedAt: '2026-08-01T00:00:00.000Z', sourceFinishedAt: '2026-08-02T00:00:00.000Z', formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), tasks: [ task('内容主管分配任务', '2026-08-01T01:00:00.000Z', '2026-08-01T01:00:00.000Z'), ...(extraOwnerTask ? [task('内容部负责人', '2026-08-02T01:00:00.000Z', '2026-08-02T01:00:00.000Z')] : []), task('编导审核', '2026-08-02T00:00:00.000Z', '2026-08-02T00:00:00.000Z'), ], businessId, workflowId, workflowStatus: 'COMPLETED', }) const assessments = assessDingWorkflowDeliveryBatch([ makeInput('B-002', 'a', true), makeInput('B-001', 'z'), makeInput('B-003', 'b'), makeInput('B-004', 'c'), ]) // The external queue uses businessId for equal assignment timestamps, // and the unrelated 内容部负责人 task must not move the base date. expect(assessments.slice(0, 3).map((assessment) => assessment.queueSequence)).toEqual([2, 1, 3]) expect(assessments[0].queueShiftDays).toBe(0) expect(assessments[3]).toMatchObject({ queueDeferred: true, queueSequence: 1, queueShiftDays: 1, startedAt: '2026-08-02T01:00:00.000Z', }) }) })