Case study 04 · Pharmaceutical inventory

Priomed

Inventory management for a multi-branch medical supply distributor — stock, procurement, controlled adjustments, and dispensing, with the audit trail pharmaceutical operations require. Deliberately compact.

Sole architect & engineer · 150 of 154 commits Nov 2024 – Jan 2026
Laravel 11 PHP 8.2+ Filament v4 Tailwind 4 MySQL Spatie Permission · Shield DomPDF Larastan L5 + baseline Rector Pint
~14k lines of PHP
33 Eloquent models
90 migrations
24 Filament resources
7 report pages
13 test files

The point

The interesting thing about Priomed is not its size — it’s its discipline. At ~14,000 lines it is the smallest of the systems here, and it was built as a clean layered design applied consistently from the first commit: written architectural rules, enforced invariants, and a plan for its own technical debt.

Where AJA Savory proves breadth, Priomed proves the opposite — and more transferable — skill: building something small and correct.

The architecture

Five rules, written into the project’s own documentation and held.

01

Thin models, fat services

Models carry relationships, query scopes, and simple state checks — nothing else. Every piece of business logic lives in app/Services, organised by bounded context: Inventory, Procurement, Sales, Support.

This was a written rule in the project’s own documentation, not an aspiration, and the codebase holds to it.

02

Branch isolation as a global scope

BranchIsolationScope plus a BranchIsolationService enforce multi-branch data separation at the query layer rather than in each controller — a forgotten where() cannot leak one branch’s inventory into another’s.

For pharmaceutical stock, that invariant belongs in architecture, not in code review.

03

Approvals behind a contract

ApprovalServiceInterface and an ApprovalResult value object give price adjustments, inventory adjustments, and sales one approval shape — one implementation rather than three copies with three sets of bugs.

04

A shared CRUD base

BaseSimpleCrudResource collapses the ~15 reference-data resources — brands, categories, generics, units, dosage strengths, classifications — into one pattern, leaving hand-written resources for the parts that actually differ.

05

Static analysis as a ratchet

Larastan at level 5 with a checked-in baseline; the intent recorded in config is to climb toward level 8 without ever letting new errors in. Rector handles automated upgrades.

Debt in the open

The domain model carries a naming mistake made early: SupplyPullout actually represents a Sale, not a pullout. Rather than paper over it, the mismatch is documented prominently for anyone reading the code, and the rename is scheduled as explicit work.

Debt you can name, contain, and schedule is a maintenance cost. Debt that silently misleads the next engineer is a defect. The difference is the documentation.

What it does

Multi-branch stock levels and branch-to-branch transfers · purchase orders with deliveries that post into inventory · inventory and price adjustments under approval workflow · patient records and dispensing · party-list sales · Filament exporters · six purpose-built report pages — daily sales, monthly generic-vs-branded, monthly GL sales, party-list sales, stock on hand, branch transfers.

Outcome

A pharmaceutical distributor runs its branch inventory, procurement, and dispensing on ~14k lines of PHP that one engineer can hold in their head — with the invariants that matter (branch isolation, approval workflow, audit trail) enforced by architecture rather than vigilance.