Files
ladill-care/app/Services/Care/PharmacyService.php
T
isaaccladandCursor 015a4cc7fe
Deploy Ladill Care / deploy (push) Successful in 1m45s
Wire Care lists into Ladill Queue workflows instead of reception panels.
When Queue integration is on, role pages issue tickets into their own
department queues and drive Call next → Serve → Complete on the native
lists. Integration off leaves existing UI unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:27:48 +00:00

213 lines
7.3 KiB
PHP

<?php
namespace App\Services\Care;
use App\Models\DispensingRecord;
use App\Models\Drug;
use App\Models\DrugBatch;
use App\Models\Organization;
use App\Models\Prescription;
use App\Models\PrescriptionItem;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use InvalidArgumentException;
class PharmacyService
{
public function __construct(
protected CareQueueBridge $queueBridge,
) {}
public function queryDrugs(string $ownerRef, int $organizationId): Builder
{
return Drug::owned($ownerRef)->where('organization_id', $organizationId);
}
/**
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $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<int, Drug>
*/
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<int, DrugBatch>
*/
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<int, array<string, mixed>> $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);
$prescription = $prescription->fresh(['items', 'patient']);
$organization = Organization::query()->find($prescription->organization_id);
if ($organization) {
$this->queueBridge->complete($organization, $prescription);
}
return $prescription;
}
}