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; tooth: string; prosthesisTypeCode: 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, tooth: row.tooth, prosthesisTypeCode: row.prosthesisTypeCode, detail: { id: row.treatmentDetailId, treatmentType: row.treatmentType ?? 'prosthesis', }, })), ), }, 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('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(); }); });