Skip to main content

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_movements tables. The mockDb adapter 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_typeExampleTypical inventory
clinicPaws & Claws VeterinaryIn-house dispensing medicines, vaccines, supplements + clinic consumables.
pharmacyVetMeds PharmacyPrescription medicines, OTC, vaccines, supplements and grooming basics.
retailHappyPet Retail StorePet 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).

kindUI labelTypical tenantsExpiry-trackedPrescription-capable
medicineMedicinesclinic, pharmacy
vaccineVaccinesclinic, pharmacy
pet_foodPet Foodclinic, pharmacy, retail
supplementSupplementsclinic, pharmacy, retail
accessoryAccessoriesretail
groomingGroomingpharmacy, retail
  • The canonical list lives in App\Models\Medicine::KINDS.
  • The API exposes it through the kind query filter on GET /api/v1/tenant/{tenant}/medicines?kind=pet_food.
  • The dashboard summary endpoint (/medicines/summary) returns a by_kind breakdown used by the kind-tab badges in the UI.
  • The frontend constant list is mirrored at ui/src/lib/inventoryKinds.js with 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 tabsAll · Medicines · Vaccines · Pet Food · Supplements · Accessories · Grooming. Each tab shows a live count taken from the summary's by_kind breakdown and filters the table via the kind query 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, and History.
  • 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.jsPOST /tenant/{tenant}/medicines.

FieldBackend columnNotes
NamenameRequired
SKU / codeskuRequired, unique per tenant
KindkindOne of medicine, vaccine, pet_food, supplement, accessory, grooming. Defaults to medicine.
CategorycategoryFree-text sub-classifier within a kind (e.g. Antibiotic, Dry Food, Toy).
FormformTablet, Capsule, Liquid, Paste — null for non-medicine kinds.
Strengthstrength250mg, 15g — null for non-medicine kinds.
Unitunitpcs, ml, kg, box, dose (vaccines), bag (pet food).
Opening quantitystock_quantityNon-negative
Minimum stockminimum_stockSafety floor
Reorder levelreorder_levelTriggers low-stock badge
Unit priceunit_priceCost price
Selling priceselling_priceRetail price
Requires prescriptionrequires_prescriptionTypically true for medicines and vaccines, false otherwise.
Batch numberbatch_numberOptional
Expiry dateexpiry_dateOptional; generally omitted for accessories.
Indications / side effectsindications[], side_effects[]JSON arrays
NotesnotesOptional

Stock Adjustment

StockAdjustmentForm.js / StockAdjustmentModal.jsPOST /tenant/{tenant}/stock-movements.

FieldBackend columnNotes
Medicinemedicine_idRequired, tenant-scoped
TypetypeOne of purchase, dispense, return, wastage, adjustment, transfer
QuantityquantityInteger > 0
Unit costunit_costOptional — total_value auto-computed
ReasonreasonFree text or predefined
ReferencereferencePO number, prescription id, adjustment id
VendorvendorFor purchase
Prescription numberprescription_numberFor dispense
Petpet_idFor dispense (tenant-scoped)
Batch / expirybatch_number, expiry_dateOptional overrides
NotesnotesOptional

The endpoint is wrapped in a DB transaction:

  1. Reloads the parent Medicine.
  2. Refuses the request (HTTP 422) if an out-movement would drive stock below zero.
  3. Updates Medicine.stock_quantity.
  4. Writes the StockMovement row with previous_stock and new_stock.

Stock History

StockMovementHistory.js / StockHistoryModal.jsGET /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, wastage or transfer movements 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_movements row — 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.