Files
ladill-care/app/Services/Care/ConsultationService.php
isaaccladandCursor 3ee59a0956
Deploy Ladill Care / deploy (push) Failing after 1m9s
Activate Care workflows across check-in and financial clearance.
Enforce service-queue gates, cashier settlements, and clinical stage progression so patient journeys cannot bypass configured payment or authorization rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 21:13:06 +00:00

184 lines
6.8 KiB
PHP

<?php
namespace App\Services\Care;
use App\Models\Appointment;
use App\Models\Consultation;
use App\Models\ConsultationDocument;
use App\Models\Diagnosis;
use App\Models\VitalSign;
use Illuminate\Http\UploadedFile;
class ConsultationService
{
public function __construct(
protected AppointmentService $appointments,
) {}
public function startFromAppointment(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Consultation
{
if ($appointment->status !== Appointment::STATUS_IN_CONSULTATION) {
$this->appointments->startConsultation(
$appointment,
$ownerRef,
$appointment->practitioner_id,
$actorRef,
);
$appointment->refresh();
}
$existing = Consultation::where('appointment_id', $appointment->id)
->where('status', '!=', Consultation::STATUS_COMPLETED)
->latest('id')
->first();
if ($existing) {
return $existing->load(['vitalSigns', 'diagnoses', 'documents', 'patient', 'practitioner']);
}
$consultation = Consultation::create([
'owner_ref' => $ownerRef,
'visit_id' => $appointment->visit_id,
'appointment_id' => $appointment->id,
'practitioner_id' => $appointment->practitioner_id,
'patient_id' => $appointment->patient_id,
'status' => Consultation::STATUS_DRAFT,
'started_at' => now(),
]);
AuditLogger::record($ownerRef, 'consultation.started', $appointment->organization_id, $actorRef, Consultation::class, $consultation->id);
return $consultation->load(['patient', 'practitioner', 'visit']);
}
/**
* @param array<string, mixed> $data
*/
public function save(Consultation $consultation, string $ownerRef, array $data, ?string $actorRef = null): Consultation
{
$consultation->update([
'symptoms' => $data['symptoms'] ?? $consultation->symptoms,
'clinical_notes' => $data['clinical_notes'] ?? $consultation->clinical_notes,
'practitioner_id' => $data['practitioner_id'] ?? $consultation->practitioner_id,
]);
if (array_key_exists('vitals', $data) && is_array($data['vitals'])) {
$this->saveVitals($consultation, $ownerRef, $data['vitals'], $actorRef);
}
if (array_key_exists('diagnoses', $data)) {
$this->syncDiagnoses($consultation, $ownerRef, $data['diagnoses']);
}
if (! empty($data['documents'])) {
$this->storeDocuments($consultation, $ownerRef, $data['documents'], $actorRef);
}
AuditLogger::record($ownerRef, 'consultation.updated', $consultation->visit->organization_id, $actorRef, Consultation::class, $consultation->id);
return $consultation->fresh(['vitalSigns', 'diagnoses', 'documents', 'patient', 'practitioner', 'visit', 'appointment']);
}
public function complete(Consultation $consultation, string $ownerRef, ?string $actorRef = null): Consultation
{
$consultation->update([
'status' => Consultation::STATUS_COMPLETED,
'completed_at' => now(),
'completed_by' => $actorRef,
]);
if ($consultation->appointment) {
$this->appointments->complete($consultation->appointment, $ownerRef, $actorRef);
} elseif ($consultation->visit) {
app(VisitService::class)->complete($consultation->visit, $ownerRef, $actorRef);
}
AuditLogger::record($ownerRef, 'consultation.completed', $consultation->visit->organization_id, $actorRef, Consultation::class, $consultation->id);
return $consultation->fresh(['vitalSigns', 'diagnoses', 'documents', 'patient', 'practitioner']);
}
/**
* @param array<string, mixed> $vitals
* @param array{source?: string, device_id?: int|null, raw_payload?: array<string, mixed>|null} $provenance
*/
public function saveVitals(
Consultation $consultation,
string $ownerRef,
array $vitals,
?string $actorRef = null,
array $provenance = [],
): ?VitalSign {
$numeric = collect($vitals)->only([
'bp_systolic', 'bp_diastolic', 'pulse', 'temperature',
'weight_kg', 'height_cm', 'spo2', 'respiratory_rate',
])->filter(fn ($v) => $v !== null && $v !== '')->all();
if ($numeric === []) {
return null;
}
return VitalSign::create([
'owner_ref' => $ownerRef,
'consultation_id' => $consultation->id,
'bp_systolic' => $vitals['bp_systolic'] ?? null,
'bp_diastolic' => $vitals['bp_diastolic'] ?? null,
'pulse' => $vitals['pulse'] ?? null,
'temperature' => $vitals['temperature'] ?? null,
'weight_kg' => $vitals['weight_kg'] ?? null,
'height_cm' => $vitals['height_cm'] ?? null,
'spo2' => $vitals['spo2'] ?? null,
'respiratory_rate' => $vitals['respiratory_rate'] ?? null,
'recorded_by' => $actorRef,
'recorded_at' => now(),
'source' => $provenance['source'] ?? VitalSign::SOURCE_MANUAL,
'device_id' => $provenance['device_id'] ?? null,
'raw_payload' => $provenance['raw_payload'] ?? null,
]);
}
/**
* @param array<int, array<string, mixed>> $rows
*/
protected function syncDiagnoses(Consultation $consultation, string $ownerRef, array $rows): void
{
$consultation->diagnoses()->delete();
foreach ($rows as $row) {
if (empty($row['description'])) {
continue;
}
Diagnosis::create([
'owner_ref' => $ownerRef,
'consultation_id' => $consultation->id,
'code' => $row['code'] ?? null,
'description' => $row['description'],
'is_primary' => (bool) ($row['is_primary'] ?? false),
'notes' => $row['notes'] ?? null,
]);
}
}
/**
* @param array<int, UploadedFile> $files
*/
protected function storeDocuments(Consultation $consultation, string $ownerRef, array $files, ?string $actorRef): void
{
foreach ($files as $file) {
if (! $file instanceof UploadedFile) {
continue;
}
$path = $file->store("care/consultations/{$consultation->id}/documents", 'public');
ConsultationDocument::create([
'owner_ref' => $ownerRef,
'consultation_id' => $consultation->id,
'file_path' => $path,
'original_name' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'uploaded_by' => $actorRef,
]);
}
}
}