where('organization_id', $organizationId); } /** * @param array $filters */ public function listDrugs(string $ownerRef, int $organizationId, array $filters = [], ?int $branchId = null): LengthAwarePaginator { $query = $this->queryDrugs($ownerRef, $organizationId) ->with(['batches']) ->orderBy('name'); if ($branchId !== null) { $query->where(function (Builder $q) use ($branchId) { $q->where('branch_id', $branchId)->orWhereNull('branch_id'); }); } if ($search = trim((string) ($filters['q'] ?? ''))) { $query->where(function (Builder $inner) use ($search) { $inner->where('name', 'like', "%{$search}%") ->orWhere('generic_name', 'like', "%{$search}%") ->orWhere('sku', 'like', "%{$search}%"); }); } return $query->paginate((int) ($filters['per_page'] ?? 30))->withQueryString(); } /** * @param array $data */ public function createDrug(Organization $organization, string $ownerRef, array $data): Drug { $drug = Drug::create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $data['branch_id'] ?? null, 'name' => $data['name'], 'generic_name' => $data['generic_name'] ?? null, 'sku' => $data['sku'] ?? null, 'unit' => $data['unit'] ?? 'unit', 'unit_price_minor' => (int) ($data['unit_price_minor'] ?? 0), 'reorder_level' => (int) ($data['reorder_level'] ?? 10), 'is_active' => (bool) ($data['is_active'] ?? true), ]); AuditLogger::record($ownerRef, 'drug.created', $organization->id, null, Drug::class, $drug->id); return $drug; } /** * @param array $data */ public function updateDrug(Drug $drug, string $ownerRef, array $data): Drug { $drug->update([ 'branch_id' => $data['branch_id'] ?? $drug->branch_id, 'name' => $data['name'], 'generic_name' => $data['generic_name'] ?? null, 'sku' => $data['sku'] ?? null, 'unit' => $data['unit'] ?? $drug->unit, 'unit_price_minor' => (int) ($data['unit_price_minor'] ?? 0), 'reorder_level' => (int) ($data['reorder_level'] ?? 10), 'is_active' => (bool) ($data['is_active'] ?? true), ]); AuditLogger::record($ownerRef, 'drug.updated', $drug->organization_id, null, Drug::class, $drug->id); return $drug->fresh(['batches']); } /** * @param array $data */ public function receiveBatch(Drug $drug, string $ownerRef, array $data, ?string $actorRef = null): DrugBatch { $batch = DrugBatch::create([ 'owner_ref' => $ownerRef, 'drug_id' => $drug->id, 'batch_number' => $data['batch_number'], 'expiry_date' => $data['expiry_date'] ?? null, 'quantity_on_hand' => (int) ($data['quantity_on_hand'] ?? 0), 'cost_minor' => (int) ($data['cost_minor'] ?? 0), 'received_at' => now(), ]); AuditLogger::record($ownerRef, 'drug.batch_received', $drug->organization_id, $actorRef, DrugBatch::class, $batch->id); return $batch->load('drug'); } /** * @return Collection */ public function lowStock(string $ownerRef, int $organizationId): Collection { return $this->queryDrugs($ownerRef, $organizationId) ->where('is_active', true) ->with('batches') ->get() ->filter(fn (Drug $drug) => $drug->stockOnHand() <= $drug->reorder_level); } /** * @return Collection */ public function expiredBatches(string $ownerRef, int $organizationId): Collection { return DrugBatch::owned($ownerRef) ->whereHas('drug', fn (Builder $q) => $q->where('organization_id', $organizationId)) ->whereNotNull('expiry_date') ->where('expiry_date', '<', now()->toDateString()) ->where('quantity_on_hand', '>', 0) ->with('drug') ->get(); } /** * @param array> $allocations */ public function dispensePrescription( Prescription $prescription, string $ownerRef, array $allocations, ?string $actorRef = null, ): Prescription { if ($prescription->status !== Prescription::STATUS_ACTIVE) { throw new InvalidArgumentException('Only active prescriptions can be dispensed.'); } $prescription->load('items'); foreach ($allocations as $row) { if (empty($row['prescription_item_id']) || empty($row['drug_batch_id'])) { continue; } $item = $prescription->items->firstWhere('id', (int) $row['prescription_item_id']); if (! $item || $item->is_procedure) { continue; } $qty = max(1, (int) ($row['quantity'] ?? 1)); $batch = DrugBatch::owned($ownerRef)->findOrFail((int) $row['drug_batch_id']); if ($batch->isExpired()) { throw new InvalidArgumentException("Batch {$batch->batch_number} is expired."); } if ($batch->quantity_on_hand < $qty) { throw new InvalidArgumentException("Insufficient stock for {$batch->drug->name}."); } $batch->decrement('quantity_on_hand', $qty); DispensingRecord::create([ 'owner_ref' => $ownerRef, 'prescription_id' => $prescription->id, 'prescription_item_id' => $item->id, 'drug_batch_id' => $batch->id, 'quantity' => $qty, 'dispensed_by' => $actorRef, 'dispensed_at' => now(), ]); if (! $item->drug_id) { $item->update(['drug_id' => $batch->drug_id]); } } $prescription->update([ 'status' => Prescription::STATUS_DISPENSED, 'dispensed_at' => now(), 'dispensed_by' => $actorRef, ]); AuditLogger::record($ownerRef, 'prescription.dispensed', $prescription->organization_id, $actorRef, Prescription::class, $prescription->id); return $prescription->fresh(['items', 'patient']); } }