Module: Inventory
Track medicine and consumable stock, with an audit trail of every adjustment.
✅ Backend status: live. The inventory module is now backed by real Laravel endpoints and the
medicines/stock_movementstables. ThemockDbadapter is retained only for local-only UI development. See Backend Routes and Database Schema.
Tenant business types
The same inventory module is used by three kinds of tenants, distinguished by
the tenants.business_type column:
business_type | Example | Typical inventory |
|---|---|---|
clinic | Paws & Claws Veterinary | In-house dispensing medicines, vaccines, supplements + clinic consumables. |
pharmacy | VetMeds Pharmacy | Prescription medicines, OTC, vaccines, supplements and grooming basics. |
retail | HappyPet Retail Store | Pet food, accessories, grooming products, supplements, toys (no prescriptions). |
For retail-shop items the pharmacy-specific fields (form, strength,
batch_number, requires_prescription) are simply left empty / false.
Item kinds (taxonomy)
Every inventory row now carries a first-class kind column that
identifies what kind of product the row represents. The free-text
category column remains available as a sub-classifier inside a kind
(e.g. kind = medicine, category = Antibiotic).
kind | UI label | Typical tenants | Expiry-tracked | Prescription-capable |
|---|---|---|---|---|
medicine | Medicines | clinic, pharmacy | ✅ | ✅ |
vaccine | Vaccines | clinic, pharmacy | ✅ | ✅ |
pet_food | Pet Food | clinic, pharmacy, retail | ✅ | ❌ |
supplement | Supplements | clinic, pharmacy, retail | ✅ | ❌ |
accessory | Accessories | retail | ❌ | ❌ |
grooming | Grooming | pharmacy, retail | ✅ | ❌ |
- The canonical list lives in
App\Models\Medicine::KINDS. - The API exposes it through the
kindquery filter onGET /api/v1/tenant/{tenant}/medicines?kind=pet_food. - The dashboard summary endpoint (
/medicines/summary) returns aby_kindbreakdown used by the kind-tab badges in the UI. - The frontend constant list is mirrored at
ui/src/lib/inventoryKinds.jswith label, icon and tone per kind.
Purpose
- See current stock levels.
- Add new medicines / inventory items.
- Adjust stock (restock, write-off, consumption, transfer).
- Audit: who changed what, when, why.
Screens
Inventory dashboard
Component: InventoryDashboard.js. Sections:
- Overview stat cards — total items, total units in stock, total inventory value, low-stock count, expiring-soon count. Driven by
GET /tenant/{tenant}/medicines/summary. - Kind tabs —
All · Medicines · Vaccines · Pet Food · Supplements · Accessories · Grooming. Each tab shows a live count taken from the summary'sby_kindbreakdown and filters the table via thekindquery parameter. - Items table — list with: Name, Kind, Category, Strength / Form, Stock, Unit price, Total value, Expiry, Status. Backed by
GET /tenant/{tenant}/medicines. - Stock alerts — low-stock and expiring-soon banners, with one-click Restock →
StockAdjustmentForm. - Row actions — icon buttons for
Add stock,Dispense/Sell, andHistory. - Add item — opens
AddMedicineForm.js. The button label tracks the active kind tab ("Add Medicine", "Add Vaccine", "Add Pet Food", etc.).
Add Medicine form
AddMedicineForm.js → POST /tenant/{tenant}/medicines.
| Field | Backend column | Notes |
|---|---|---|
| Name | name | Required |
| SKU / code | sku | Required, unique per tenant |
| Kind | kind | One of medicine, vaccine, pet_food, supplement, accessory, grooming. Defaults to medicine. |
| Category | category | Free-text sub-classifier within a kind (e.g. Antibiotic, Dry Food, Toy). |
| Form | form | Tablet, Capsule, Liquid, Paste — null for non-medicine kinds. |
| Strength | strength | 250mg, 15g — null for non-medicine kinds. |
| Unit | unit | pcs, ml, kg, box, dose (vaccines), bag (pet food). |
| Opening quantity | stock_quantity | Non-negative |
| Minimum stock | minimum_stock | Safety floor |
| Reorder level | reorder_level | Triggers low-stock badge |
| Unit price | unit_price | Cost price |
| Selling price | selling_price | Retail price |
| Requires prescription | requires_prescription | Typically true for medicines and vaccines, false otherwise. |
| Batch number | batch_number | Optional |
| Expiry date | expiry_date | Optional; generally omitted for accessories. |
| Indications / side effects | indications[], side_effects[] | JSON arrays |
| Notes | notes | Optional |
Stock Adjustment
StockAdjustmentForm.js / StockAdjustmentModal.js → POST /tenant/{tenant}/stock-movements.
| Field | Backend column | Notes |
|---|---|---|
| Medicine | medicine_id | Required, tenant-scoped |
| Type | type | One of purchase, dispense, return, wastage, adjustment, transfer |
| Quantity | quantity | Integer > 0 |
| Unit cost | unit_cost | Optional — total_value auto-computed |
| Reason | reason | Free text or predefined |
| Reference | reference | PO number, prescription id, adjustment id |
| Vendor | vendor | For purchase |
| Prescription number | prescription_number | For dispense |
| Pet | pet_id | For dispense (tenant-scoped) |
| Batch / expiry | batch_number, expiry_date | Optional overrides |
| Notes | notes | Optional |
The endpoint is wrapped in a DB transaction:
- Reloads the parent
Medicine. - Refuses the request (HTTP 422) if an out-movement would drive stock below zero.
- Updates
Medicine.stock_quantity. - Writes the
StockMovementrow withprevious_stockandnew_stock.
Stock History
StockMovementHistory.js / StockHistoryModal.js →
GET /tenant/{tenant}/medicines/{medicine}/stock-movements. Shows every movement
for a medicine, including who performed the adjustment and timestamps.
Business rules
- Quantity cannot go negative — the backend returns 422 on
dispense,wastageortransfermovements larger than current stock. The UI also blocks it. - Low-stock alerts surface when
stock_quantity <= GREATEST(reorder_level, minimum_stock)(badge on medicine row, counter on dashboard). - Expiry alerts surface for medicines expiring within 90 days (configurable via
?days=on the alerts endpoint). - Deleting a medicine is not available; inactive medicines can be flagged via
status = 'inactive'or'discontinued'. - Every stock change writes exactly one
stock_movementsrow — the history is append-only.
Backend surface
# Inventory items
GET /api/v1/tenant/{tenant}/medicines
POST /api/v1/tenant/{tenant}/medicines
GET /api/v1/tenant/{tenant}/medicines/{id}
PATCH /api/v1/tenant/{tenant}/medicines/{id}
# Dashboards / alerts
GET /api/v1/tenant/{tenant}/medicines/summary
GET /api/v1/tenant/{tenant}/medicines/alerts/low-stock
GET /api/v1/tenant/{tenant}/medicines/alerts/expiry
# Stock movements (audit trail)
GET /api/v1/tenant/{tenant}/stock-movements
POST /api/v1/tenant/{tenant}/stock-movements
GET /api/v1/tenant/{tenant}/medicines/{id}/stock-movements
Eloquent models: Medicine (tenant-scoped) and StockMovement (tenant-scoped,
FK to Medicine + adjuster User + optional dispensed-for Pet).
Frontend adapter
The SPA talks to the inventory endpoints through HttpPharmacyService
(ui/src/services/adapters/http/pharmacy.service.js). The matching
MockPharmacyService has identical method signatures so the two adapters
are interchangeable in createApiServices({ mode }).
Key methods (all take { tenantId, ... }):
listMedicines({ tenantId, filters })getMedicineById({ tenantId, medicineId })createMedicine({ tenantId, payload })updateMedicine({ tenantId, medicineId, payload })summary({ tenantId })lowStockAlerts({ tenantId })expiryAlerts({ tenantId, days })stockMovements({ tenantId, filters })movementsForMedicine({ tenantId, medicineId })addStockMovement({ tenantId, payload })updateStock({ tenantId, medicineId, quantity, type, reference, notes, unitCost })— convenience wrapper
Seed data
database/seeders/InventorySeeder.php provisions one tenant of each type
(Paws & Claws Veterinary, VetMeds Pharmacy, HappyPet Retail Store) and
loads a realistic multi-kind starter catalog into each, plus an initial
purchase stock movement per item:
- Clinic — medicines (Amoxicillin, Carprofen), vaccines (DHPP, Rabies), supplement (Joint Chews).
- Pharmacy — medicines (Fipronil, Meloxicam), vaccine (FVRCP), supplement (Omega-3), grooming (Medicated Shampoo).
- Retail — pet food (Dog / Cat), accessories (Collar, Toy), grooming (Shampoo, Brush), supplement (Multivitamin).
Run it with php artisan db:seed or re-seed with
php artisan migrate:fresh --seed.