/** @jest-environment node */ import { artPointPersonResolver, assessArtWorkflowPoints, calculateArtWorkflowPoints, parseArtPointRows } from '../art-workflow-points' import { normalizeDetailFormFields, normalizeWorkflowTasks } from '../ding-workflow-sync' import type { DingWorkflowDeliveryAssessment } from '../ding-workflow-delivery' const people = [ { id: 'p1', dingtalkUserId: 'ding1', realName: '甲', flowerName: '花甲', displayName: '花甲' }, { id: 'p2', dingtalkUserId: 'ding2', realName: '乙', flowerName: '花乙', displayName: '花乙' }, ] const assessment: DingWorkflowDeliveryAssessment = { status: 'overtime', standardDuration: 2, actualDuration: 3.2, durationUnit: 'day', startedAt: '', finishedAt: '', startNodeLabel: '', endNodeLabel: '' } const input = { workflowKey: '20260901-key', plannerId: 'p1', planningRows: [{ 策划数量: '2', 策划单分值: '10' }], graphicRows: [{ 全案策划: '花甲', 平面数量: '3', 平面单分值: '10' }, { 全案策划: '花乙', 平面数量: '1', 平面单分值: '4' }], imageRows: [], assessment } test('策划与平面/出图数量乘单分值,超时天数向上取整每天扣5,同人只扣一次,允许负积分', () => { const result = calculateArtWorkflowPoints(input, people) expect(result.get('p1')).toBe(40) // 20+30-ceil(1.2)*5 expect(result.get('p2')).toBe(-6) }) test('角色免扣仅在承担扣分的角色生效;人工增减只应用一次', () => { const result = calculateArtWorkflowPoints(input, people, { overtimeExemptions: [{ workflowKey: input.workflowKey, name: '花甲', role: 'fullCase' }], deductions: [{ workflowKey: input.workflowKey, name: '甲', points: 3, type: 'add' }, { workflowKey: input.workflowKey, name: '甲', points: 2, type: 'deduct' }, { workflowKey: input.workflowKey, name: '甲', points: 99, enabled: false }], }) expect(result.get('p1')).toBe(51) expect(result.get('p2')).toBe(-6) }) test('只有人工调整的人员不承担该流程超时扣分', () => { const result = calculateArtWorkflowPoints({ ...input, graphicRows: [], imageRows: [] }, people, { overtimeExemptions: [], deductions: [{ workflowKey: input.workflowKey, name: '花乙', points: 3, type: 'add' }], }) expect(result.get('p2')).toBe(3) }) test('稳定钉钉ID优先,同名冲突与多选不归人,JSON表单解析保留联系人', () => { const resolve = artPointPersonResolver([...people, { ...people[1], id: 'p3', flowerName: '花甲', displayName: '花甲', dingtalkUserId: 'ding3' }]) expect(resolve('花甲')).toBeNull() expect(resolve([{ emplId: 'ding1', name: '花甲' }])).toBe('p1') expect(resolve(JSON.stringify([{ emplId: 'ding1', name: '花甲' }]))).toBe('p1') expect(resolve([{ emplId: 'ding1' }, { emplId: 'ding2' }])).toBeNull() const rows = parseArtPointRows(JSON.stringify([{ rowValue: [{ label: '全案策划', value: [{ emplId: 'ding1' }] }] }])) expect(resolve(rows[0]['全案策划'])).toBe('p1') expect(parseArtPointRows('invalid')).toEqual([]) }) test('千位逗号按源码移除,按角色分别保留两位小数后汇总', () => { const result = calculateArtWorkflowPoints({ ...input, planningRows: [{ 策划数量: '1,000', 策划单分值: '0.001006' }], graphicRows: [{ 全案策划: '花甲', 平面数量: '1', 平面单分值: '1.006' }], assessment: { ...assessment, actualDuration: null, standardDuration: null }, }, people) expect(result.get('p1')).toBe(2.02) }) test('积分节点只使用指定活动ID;首次分配至末次审核连续计时,无时效仍保留审核时间', () => { const tasks = normalizeWorkflowTasks([ { taskId: 'assign', activityId: '38da_544a', createdAt: '2026-09-01T00:00:00Z' }, { taskId: 'assign-again', activityId: '38da_544a', createdAt: '2026-09-03T00:00:00Z' }, { taskId: 'old-review', activityId: 'c255_84b3', finishedAt: '2026-08-31T00:00:00Z' }, { taskId: 'review', activityId: 'c255_84b3', finishedAt: '2026-09-04T03:00:00Z', reviewPassed: false }, { taskId: 'other', activityId: 'other', nodeName: '品牌策划主管审核', finishedAt: '2026-10-01T00:00:00Z' }, ]) const result = assessArtWorkflowPoints(tasks, normalizeDetailFormFields({})) expect(result.startedAt).toBe('2026-09-01T00:00:00.000Z') expect(result.finishedAt).toBe('2026-09-04T03:00:00.000Z') expect(result.actualDuration).toBe(3.13) expect(result.standardDuration).toBeNull() })