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>
134 lines
4.5 KiB
PHP
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();
|
|
}
|
|
}
|