import { CatalogEntityKind } from '@prisma/client'; import { PROSTHESIS_TYPES, buildProsthesisStepCodes, } from '../../../prisma/catalog-seed-data'; import { generateLabCaseTasks } from './lab-case-task.generator'; function buildMockTx(options: { existingCount?: number; toothProsthesisRows: Array<{ treatmentDetailId?: string | null; lineId?: string | null; sourceKey?: string; tooth: string; prosthesisTypeCode: string; selectionGroupId?: string; treatmentType?: string; }>; prosthesisTypes: Array<{ code: string; steps: Array<{ stepOrder: number; workflowStepCode: string }>; }>; stepLabels?: Record; }) { const created: unknown[] = []; const tx = { labCaseTask: { count: jest.fn().mockResolvedValue(options.existingCount ?? 0), createMany: jest.fn().mockImplementation(({ data }) => { created.push(...data); return { count: data.length }; }), }, labCaseToothProsthesis: { findMany: jest.fn().mockResolvedValue( options.toothProsthesisRows.map((row) => ({ treatmentDetailId: row.treatmentDetailId ?? null, lineId: row.lineId ?? null, sourceKey: row.sourceKey ?? row.treatmentDetailId ?? row.lineId ?? '', treatmentType: row.treatmentType ?? 'prosthesis', tooth: row.tooth, prosthesisTypeCode: row.prosthesisTypeCode, selectionGroupId: row.selectionGroupId ?? '', detail: row.treatmentDetailId ? { id: row.treatmentDetailId, treatmentType: row.treatmentType ?? 'prosthesis', toothSelectionGroups: null, } : null, line: row.lineId ? { id: row.lineId, treatmentType: row.treatmentType ?? 'prosthesis', toothSelectionGroups: null, } : null, })), ), }, prosthesisType: { findMany: jest.fn().mockResolvedValue( options.prosthesisTypes.map((type) => ({ code: type.code, isActive: true, steps: type.steps.map((step) => ({ stepOrder: step.stepOrder, labWorkflowStep: { code: step.workflowStepCode }, })), })), ), }, catalogTranslation: { findMany: jest.fn().mockImplementation(({ where }) => { const codes = where.entityCode?.in ?? []; return codes.map((code: string) => ({ entityCode: code, locale: 'en', label: options.stepLabels?.[code] ?? code, entityKind: CatalogEntityKind.LAB_WORKFLOW_STEP, })); }), }, }; return { tx, created }; } function stepsFromSeed(code: string) { const seed = PROSTHESIS_TYPES.find((type) => type.code === code); if (!seed) { throw new Error(`Unknown prosthesis code: ${code}`); } return buildProsthesisStepCodes(seed).map((workflowStepCode, index) => ({ stepOrder: index + 1, workflowStepCode, })); } describe('generateLabCaseTasks', () => { it('creates tasks for pfm_crown with universal and type-specific steps', async () => { const pfmSteps = stepsFromSeed('pfm_crown'); const { tx, created } = buildMockTx({ toothProsthesisRows: [ { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'pfm_crown', }, ], prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }], stepLabels: { intraoral_scan: 'Intraoral Scan', packing: 'Packing' }, }); const count = await generateLabCaseTasks(tx as never, 'lab-case-1', 'en'); expect(count).toBe(pfmSteps.length); expect(created).toHaveLength(pfmSteps.length); expect(created[0]).toMatchObject({ teeth: ['14'], prosthesisTypeCode: 'pfm_crown', workflowStepCode: 'intraoral_scan', stepLabel: 'Intraoral Scan', status: 'IN_PROGRESS', }); const stepCodes = (created as Array<{ workflowStepCode: string }>).map( (row) => row.workflowStepCode, ); expect(stepCodes).toEqual(pfmSteps.map((step) => step.workflowStepCode)); expect(stepCodes).toContain('packing'); expect(stepCodes).toContain('shipping'); expect(stepCodes).toContain('milling_wet'); }); it('groups teeth sharing a prosthesis in one detail, and keeps other prosthesis separate', async () => { const pfmSteps = stepsFromSeed('pfm_crown'); const zirconiaSteps = stepsFromSeed('monolithic_zirconia'); const { tx, created } = buildMockTx({ toothProsthesisRows: [ { treatmentDetailId: 'detail-1', tooth: '15', prosthesisTypeCode: 'pfm_crown' }, { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'pfm_crown' }, { treatmentDetailId: 'detail-1', tooth: '16', prosthesisTypeCode: 'monolithic_zirconia' }, ], prosthesisTypes: [ { code: 'pfm_crown', steps: pfmSteps }, { code: 'monolithic_zirconia', steps: zirconiaSteps }, ], }); const count = await generateLabCaseTasks(tx as never, 'lab-case-group', 'en'); expect(count).toBe(pfmSteps.length + zirconiaSteps.length); const rows = created as Array<{ teeth: string[]; prosthesisTypeCode: string }>; const pfmRows = rows.filter((r) => r.prosthesisTypeCode === 'pfm_crown'); const zirconiaRows = rows.filter((r) => r.prosthesisTypeCode === 'monolithic_zirconia'); expect(pfmRows).toHaveLength(pfmSteps.length); expect(zirconiaRows).toHaveLength(zirconiaSteps.length); // Teeth sharing the prosthesis in the same detail are merged and sorted. expect(pfmRows.every((r) => JSON.stringify(r.teeth) === JSON.stringify(['14', '15']))).toBe(true); expect(zirconiaRows.every((r) => JSON.stringify(r.teeth) === JSON.stringify(['16']))).toBe(true); }); it('omits packing and shipping for smile_design', async () => { const smileSteps = stepsFromSeed('smile_design'); const { tx, created } = buildMockTx({ toothProsthesisRows: [ { treatmentDetailId: 'detail-1', tooth: '11', prosthesisTypeCode: 'smile_design', }, ], prosthesisTypes: [{ code: 'smile_design', steps: smileSteps }], }); const count = await generateLabCaseTasks(tx as never, 'lab-case-2', 'en'); expect(count).toBe(smileSteps.length); const stepCodes = (created as Array<{ workflowStepCode: string }>).map( (row) => row.workflowStepCode, ); expect(stepCodes).not.toContain('packing'); expect(stepCodes).not.toContain('shipping'); expect(stepCodes).toContain('printer_resin'); }); it('merges teeth with the same prosthesis type across selection groups', async () => { const pfmSteps = stepsFromSeed('pfm_crown'); const { tx, created } = buildMockTx({ toothProsthesisRows: [ { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'pfm_crown', selectionGroupId: 'connected-1', }, { treatmentDetailId: 'detail-1', tooth: '15', prosthesisTypeCode: 'pfm_crown', selectionGroupId: 'connected-1', }, { treatmentDetailId: 'detail-1', tooth: '21', prosthesisTypeCode: 'pfm_crown', selectionGroupId: 'single-21', }, ], prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }], }); const count = await generateLabCaseTasks(tx as never, 'lab-case-merge', 'en'); expect(count).toBe(pfmSteps.length); const rows = created as Array<{ teeth: string[]; prosthesisTypeCode: string }>; expect(rows).toHaveLength(pfmSteps.length); expect(rows.every((r) => JSON.stringify(r.teeth) === JSON.stringify(['14', '15', '21']))).toBe( true, ); }); it('skips generation when tasks already exist', async () => { const { tx } = buildMockTx({ existingCount: 3, toothProsthesisRows: [], prosthesisTypes: [], }); const count = await generateLabCaseTasks(tx as never, 'lab-case-3', 'en'); expect(count).toBe(0); expect(tx.labCaseTask.createMany).not.toHaveBeenCalled(); }); it('creates tasks from lab-origin lines using sourceKey and lineId', async () => { const pfmSteps = stepsFromSeed('pfm_crown'); const { tx, created } = buildMockTx({ toothProsthesisRows: [ { lineId: 'line-1', sourceKey: 'line-1', tooth: '11', prosthesisTypeCode: 'pfm_crown', }, { lineId: 'line-1', sourceKey: 'line-1', tooth: '21', prosthesisTypeCode: 'pfm_crown', }, ], prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }], }); const count = await generateLabCaseTasks(tx as never, 'lab-internal-1', 'en'); expect(count).toBe(pfmSteps.length); expect(created[0]).toMatchObject({ lineId: 'line-1', sourceKey: 'line-1', treatmentDetailId: null, teeth: ['11', '21'], }); }); });