Ship commercial Dentistry suite with odontogram and treatment plans.
Deploy Ladill Care / deploy (push) Failing after 36s
Deploy Ladill Care / deploy (push) Failing after 36s
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>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user