All projects

Platform2026

PharmaSys

Management software for the pharmacy of a medical-social centre in Togo: sales, batch stock, insurance claims, medical acts, purchasing, staff and accounting in one tool.

Sole developer (product, architecture, development, deployment)

The PharmaSys dashboard: 'Bonjour, Admin', Thursday 10 September 2026. A dark card 'CA DU JOUR - PHARMACIE 43 070 F' with 'Semaine 43 070 F', 'Mois 156 725 F' and '47% du mois precedent'. Metric cards for '16 Mes ventes / +100% vs hier', '2 692 Panier moyen', '14 Alertes stock (1 expire, 12 bas)' and '36 085 F Creances (2), 1 en retard'. A 'Dernieres ventes' list with sale references, customer names and times, and an 'Etat du stock' panel showing 0 ruptures, 12 stock bas, 1 lot expire, 1 expirant bientot and 12 a transferer reserve vers officine.
The PharmaSys dashboard: 'Bonjour, Admin', Thursday 10 September 2026. A dark card 'CA DU JOUR - PHARMACIE 43 070 F' with 'Semaine 43 070 F', 'Mois 156 725 F' and '47% du mois precedent'. Metric cards for '16 Mes ventes / +100% vs hier', '2 692 Panier moyen', '14 Alertes stock (1 expire, 12 bas)' and '36 085 F Creances (2), 1 en retard'. A 'Dernieres ventes' list with sale references, customer names and times, and an 'Etat du stock' panel showing 0 ruptures, 12 stock bas, 1 lot expire, 1 expirant bientot and 12 a transferer reserve vers officine.

PharmaSys runs the daily operations of a single-site dispensary. It covers the point of sale, batch-tracked inventory, the split between what a patient pays and what an insurer reimburses, medical consultations and acts, supplier purchasing, cash register sessions, staff and payroll, and reporting. It is built for West African officine practice: amounts in CFA francs, mobile money payments (TMoney, Flooz), and a full third-party-payer workflow for public and private insurers. Staff work through screens scoped to their role, so a preparer assembles a sale, a cashier collects payment, and a manager handles claims and the back office. The system has been in daily production use at the centre since March 2026.

The problem

Running an officine means holding two things together at once. Every drug is a dated batch that has to be sold before it expires, kept in the right zone, and reordered before it runs out. And every sale for an insured patient has to be split at the counter between the patient's share and the insurer's, then that insurer debt has to be tracked and recovered claim by claim. Kept by hand across ledgers and spreadsheets, expiring stock slips through, the coverage rate is guessed at the counter, and what each insurer owes is hard to see.

Concrete usage figures for this project are not published.

The approach

I built one Laravel application for the whole officine workflow instead of connecting separate tools. Stock is held as dated batches with first-expired-first-out depletion and separate shelf and reserve zones, with transfers between them. A coverage engine computes the insurer and patient share on every line at the point of sale, following a fixed order of rules (product, category, patient plan, insurer default), and records how it got there. Insurer debts are grouped into submission slips that follow reimbursement and rejection per insurer. The whole app is server-rendered so it stays quick to load on the centre's connection and simple to deploy on shared hosting.

The result

  • In daily production use at the centre since March 2026, on a single officine, across preparer, cashier, manager and pharmacist roles.
  • One application now covers what was handled separately: sales, batch stock and expiry, insurance split and claims, purchasing, cash sessions, payroll and accounting.
  • Insured sales are split automatically at the counter, and every claim carries a stored breakdown of how its rate was computed, so a rejection can be checked line by line.
  • Insurer debts are grouped into submission slips that track reimbursement and rejection per insurer, instead of a running total.

Concrete usage figures for this project are not published.

Key features

  • Point of sale with a two-step flow: a preparer builds the basket and submits it, a cashier takes payment. Cash, TMoney, Flooz, cheque, split payment and sale on credit.
  • Batch-tracked stock: expiry date per batch, first-expired-first-out selling, shelf and reserve zones, transfers between zones, stocktakes and a quarantine flag.
  • Automatic insurance split on every sale, with a per-line override to bill an item fully to the patient, and a prescription or prior-authorization check.
  • Insurance claims grouped into submission slips, with reimbursement (spread proportionally across the claims) and rejection tracked per insurer.
  • Medical consultations and acts billed at the same counter, with their own pricing and a thermal ticket.
  • Purchasing: supplier orders, partial deliveries, invoices, payments and returns.
  • Cash register sessions with opening and closing counts and a session report.
  • Staff: schedules, attendance, leave requests with approval, and payroll with payslips.
  • Alerts centre for stock-outs, low stock, expiring and expired batches, and overdue credit.
  • CSV import and export per data type, plus a full backup archive.
  • Editable roles and permissions, an audit log of every change, and a built-in user guide covering every module.
Engineering — Architecture, decisions and trade-offs

Architecture

Monolithic Laravel 12 / PHP 8.3 application, server-rendered with Blade, Tailwind and Vite, on PostgreSQL. Around 56 Eloquent models with UUID primary keys and soft deletes, around 40 enums for statuses and types, and decimal casting on every monetary field. Business rules live in observers and services rather than controllers: the PENDING to COMPLETED sale transition triggers first-expired-first-out stock depletion at the batch-and-location pivot, accounting entries, and insurance-claim creation through a single SaleObserver, and cancelling a sale replays those movements in reverse from the stock-movement log. A custom role and permission layer (not a package) gates every route, with around 52 permissions across 5 system roles plus editable custom roles, and per-request permission caching. Clinical fields (diagnosis, notes) are encrypted at rest with Laravel's encrypter. Human-readable references (VNT-YYYYMMDD-0001 and similar) are generated with a locked sequence read so concurrent sales cannot collide. Three scheduled commands handle overdue credit, claim contest deadlines and notification generation. PDF documents (receipts, payslips, claim slips, cash reports, insurance statements) are produced with DomPDF. Assets are pre-built and committed; the app runs on shared hosting with the scheduler on cron and no queue worker. A 68-test PHPUnit suite (193 assertions) covers the POS workflow, sale reversal, purchase reception, the consultation queue and claim slips, on in-memory SQLite.

Engineering challenges

  • First-expired-first-out depletion under concurrency: batches are pre-allocated inside the sale transaction with row locks (fefo()->lockForUpdate()), and the real decrement happens on the status transition in the observer, against the batch-and-location pivot rather than a single quantity column, so shelf and reserve stay consistent and a cancellation can be reversed exactly.
  • Sellable stock is a computed thing, not a column: it is only the quantity in zones marked sellable, not expired and not quarantined. The POS, the alerts centre and the transfer suggestions all read from that one definition, expressed both in PHP and as a correlated SQL subquery for paginated lists.
  • The coverage rate for a line can come from a product rule, a category rule, the patient's plan or the insurer default, each optionally scoped to a plan. One service resolves that order for every line and stores the resulting breakdown on the claim, so a later rejection can be argued against the exact calculation.
  • Reversing a completed sale: cancellation reads back the StockMovement records for that sale reference and undoes stock, customer stats, accounting and the claim, rather than trusting a single "before" snapshot.

Technical decisions

  • Custom role and permission layer over a package (Spatie): checks are tied to specific business actions (approve, validate, submit, override), not just CRUD, and permissions are cached per request. A hand-rolled layer kept that explicit and dependency-free.
  • Server-rendered Blade over a single-page app: the centre runs on shared hosting with a variable connection. Full page rendering keeps the app fast to load and simple to deploy, with assets committed and no build step on the server.
  • Encrypt clinical fields at rest: after looking at how sensitive health data is handled elsewhere, diagnosis and clinical notes are stored encrypted, so a database dump does not expose patient history. This was a design choice, not a client requirement.
  • Two-step point of sale (preparer, then cashier): it mirrors how the counter actually works and separates who assembles an order from who holds the cash drawer. It also gives one clean point, the status transition, where all the side effects (stock, accounting, claim) run exactly once.
  • The whole third-party-payer model (insurers, plans, granular coverage rules, submission slips, proportional reimbursement) is my design, not a spec handed over by the client.

Stack

  • Laravel 12
  • PHP 8.3
  • PostgreSQL
  • Blade
  • Tailwind CSS
  • Vite
  • DomPDF
  • ApexCharts
  • PHPUnit

Sole developer (product, architecture, development, deployment) — Private medical-social centre in Kara, Togo (anonymized). Single attached pharmacy. (2026)

Next projectTchona