, * anesthesia?: ?string, * notes?: ?string, * plan_item_id?: ?int, * bill?: bool * } $data */ public function complete( Organization $organization, Visit $visit, array $data, string $ownerRef, ?string $actorRef = null, ): DentalProcedure { $visit->loadMissing('patient'); $code = (string) ($data['procedure_code'] ?? ''); $service = collect($this->shell->provisionedServices($organization, 'dentistry')) ->first(fn ($s) => ($s['code'] ?? '') === $code) ?? DentalCatalog::defaultServices()[$code] ?? null; if (! $service) { throw new \InvalidArgumentException("Unknown dental procedure [{$code}]."); } $tooth = $data['tooth_code'] ?? null; if ($tooth && ! DentalCatalog::isValidToothCode($tooth)) { throw new \InvalidArgumentException("Invalid FDI tooth code [{$tooth}]."); } $surfaces = array_values(array_intersect( array_map('strtoupper', $data['surfaces'] ?? []), DentalCatalog::surfaces(), )); $planItem = null; if (! empty($data['plan_item_id'])) { $planItem = DentalPlanItem::query()->find((int) $data['plan_item_id']); if ($planItem && $planItem->plan?->patient_id !== $visit->patient_id) { throw new \InvalidArgumentException('Plan item does not belong to this patient.'); } } $label = $service['label']; if ($tooth) { $label .= ' ยท tooth '.$tooth; if ($surfaces !== []) { $label .= ' ('.implode('', $surfaces).')'; } } $amount = (int) ($service['amount_minor'] ?? 0); $procedure = DentalProcedure::create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'patient_id' => $visit->patient_id, 'visit_id' => $visit->id, 'plan_item_id' => $planItem?->id, 'tooth_code' => $tooth, 'surfaces' => $surfaces, 'procedure_code' => $code, 'label' => $label, 'anesthesia' => $data['anesthesia'] ?? null, 'status' => DentalProcedure::STATUS_COMPLETED, 'amount_minor' => $amount, 'bill_line_item_id' => null, 'provider_ref' => $actorRef ?? $ownerRef, 'notes' => $data['notes'] ?? null, 'performed_at' => now(), ]); if ($data['bill'] ?? true) { $bill = $this->bills->generateFromVisit($visit, $ownerRef, $actorRef); $bill = $this->bills->addManualLineItem($bill, $ownerRef, [ 'type' => $service['type'] ?? 'procedure', 'description' => $label, 'quantity' => 1, 'unit_price_minor' => $amount, 'source_type' => 'dental_procedure', 'source_id' => $procedure->id, ], $actorRef); $billLineId = $bill->lineItems()->where('source_type', 'dental_procedure')->where('source_id', $procedure->id)->value('id'); $procedure->update(['bill_line_item_id' => $billLineId]); if ($planItem && $billLineId) { $planItem->update(['bill_line_item_id' => $billLineId]); } } if ($planItem) { $this->plans->markItemDone($planItem); $plan = $planItem->plan; if ($plan && $plan->status === DentalTreatmentPlan::STATUS_ACCEPTED) { $plan->update(['status' => DentalTreatmentPlan::STATUS_IN_PROGRESS]); } } AuditLogger::record( $ownerRef, 'dental.procedure.completed', $organization->id, $actorRef, DentalProcedure::class, $procedure->id, ); return $procedure->fresh(); } }