Files
ladill-care/app/Services/Care/Dentistry/DentalProcedureService.php
T
isaaccladandCursor 0edd653187
Deploy Ladill Care / deploy (push) Failing after 36s
Ship commercial Dentistry suite with odontogram and treatment plans.
Replace placeholder clinical forms with patient-level FDI charting, multi-visit plans, procedure-to-bill linking, imaging, reports, and demo seed data.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 16:33:34 +00:00

134 lines
4.5 KiB
PHP

<?php
namespace App\Services\Care\Dentistry;
use App\Models\DentalPlanItem;
use App\Models\DentalProcedure;
use App\Models\DentalTreatmentPlan;
use App\Models\Organization;
use App\Models\Visit;
use App\Services\Care\AuditLogger;
use App\Services\Care\BillService;
use App\Services\Care\SpecialtyShellService;
class DentalProcedureService
{
public function __construct(
protected SpecialtyShellService $shell,
protected BillService $bills,
protected DentalTreatmentPlanService $plans,
) {}
/**
* @param array{
* procedure_code: string,
* tooth_code?: ?string,
* surfaces?: list<string>,
* 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();
}
}