assertCompatible($visit, $unit, $bed); return DB::transaction(function () use ($visit, $unit, $bed, $ownerRef, $actorRef, $notes) { $hadPlacement = $visit->care_unit_id !== null; $this->endCurrentStay($visit, $ownerRef, $actorRef, $hadPlacement ? 'transfer' : 'admit', $notes); if ($bed) { $this->occupyBed($bed); } $stay = BedStay::create([ 'owner_ref' => $ownerRef, 'organization_id' => $visit->organization_id, 'visit_id' => $visit->id, 'care_unit_id' => $unit->id, 'bed_id' => $bed?->id, 'status' => BedStay::STATUS_ACTIVE, 'admitted_at' => now(), 'admitted_by' => $actorRef, 'reason' => $hadPlacement ? 'transfer' : 'admit', 'notes' => $notes, ]); $visit->update([ 'care_unit_id' => $unit->id, 'bed_id' => $bed?->id, 'placed_at' => now(), 'placed_by' => $actorRef, ]); AuditLogger::record( $ownerRef, $hadPlacement ? 'visit.transferred' : 'visit.placed', $visit->organization_id, $actorRef, Visit::class, $visit->id, [ 'care_unit_id' => $unit->id, 'bed_id' => $bed?->id, 'bed_stay_id' => $stay->id, ], ); return $stay->fresh(['careUnit', 'bed', 'visit']); }); } public function transfer( Visit $visit, CareUnit $unit, ?Bed $bed, string $ownerRef, ?string $actorRef = null, ?string $notes = null, ): BedStay { if ($visit->care_unit_id === null) { throw ValidationException::withMessages([ 'care_unit_id' => 'Visit is not currently placed. Use admit/place instead.', ]); } return $this->place($visit, $unit, $bed, $ownerRef, $actorRef, $notes); } /** * Clear unit/bed placement without completing the visit. */ public function dischargePlacement( Visit $visit, string $ownerRef, ?string $actorRef = null, ?string $notes = null, ): void { $visit->refresh(); if ($visit->care_unit_id === null && $visit->bed_id === null) { return; } DB::transaction(function () use ($visit, $ownerRef, $actorRef, $notes) { $this->endCurrentStay($visit, $ownerRef, $actorRef, 'discharge', $notes); $visit->update([ 'care_unit_id' => null, 'bed_id' => null, 'placed_at' => null, 'placed_by' => null, ]); AuditLogger::record( $ownerRef, 'visit.placement_discharged', $visit->organization_id, $actorRef, Visit::class, $visit->id, ); }); } protected function endCurrentStay( Visit $visit, string $ownerRef, ?string $actorRef, string $reason, ?string $notes, ): void { $active = BedStay::query() ->where('visit_id', $visit->id) ->active() ->lockForUpdate() ->get(); foreach ($active as $stay) { $stay->update([ 'status' => BedStay::STATUS_ENDED, 'discharged_at' => now(), 'discharged_by' => $actorRef, 'reason' => $reason, 'notes' => $notes ?? $stay->notes, ]); if ($stay->bed_id) { $bed = Bed::query()->lockForUpdate()->find($stay->bed_id); if ($bed && $bed->status === 'occupied') { $stillOccupied = BedStay::query() ->where('bed_id', $bed->id) ->active() ->where('id', '!=', $stay->id) ->exists(); if (! $stillOccupied) { $bed->update(['status' => 'available']); } } } } // Also free visit's current bed if stay rows were missing. if ($visit->bed_id) { $bed = Bed::query()->lockForUpdate()->find($visit->bed_id); if ($bed && $bed->status === 'occupied') { $stillOccupied = BedStay::query() ->where('bed_id', $bed->id) ->active() ->exists(); if (! $stillOccupied) { $bed->update(['status' => 'available']); } } } } protected function occupyBed(Bed $bed): void { $bed = Bed::query()->lockForUpdate()->findOrFail($bed->id); $conflict = BedStay::query() ->where('bed_id', $bed->id) ->active() ->exists(); if ($conflict || in_array($bed->status, ['occupied', 'maintenance', 'closed'], true)) { throw ValidationException::withMessages([ 'bed_id' => 'This bed is not available.', ]); } $bed->update(['status' => 'occupied']); } protected function assertCompatible(Visit $visit, CareUnit $unit, ?Bed $bed): void { if ((int) $visit->organization_id !== (int) $unit->organization_id) { throw ValidationException::withMessages([ 'care_unit_id' => 'Care unit belongs to a different organization.', ]); } if (! $unit->is_active) { throw ValidationException::withMessages([ 'care_unit_id' => 'Care unit is inactive.', ]); } if ($unit->kind === 'float_pool') { throw ValidationException::withMessages([ 'care_unit_id' => 'Patients cannot be placed on a float pool. Float pools are for staff only.', ]); } if (in_array($visit->status, [Visit::STATUS_COMPLETED], true)) { throw ValidationException::withMessages([ 'visit_id' => 'Cannot place a completed visit.', ]); } if ($bed) { if ((int) $bed->care_unit_id !== (int) $unit->id) { throw ValidationException::withMessages([ 'bed_id' => 'Bed does not belong to the selected care unit.', ]); } if (! $unit->supportsBeds()) { throw ValidationException::withMessages([ 'bed_id' => 'This care unit kind does not use beds.', ]); } if (! $bed->is_active) { throw ValidationException::withMessages([ 'bed_id' => 'Bed is inactive.', ]); } } elseif ($unit->supportsBeds() === false) { // outpatient / service — bed optional, OK } } }