Skip to content
Let's Talk
Projects
ERP / POS

Laundry POS System

Full stack rebuild of a single-workstation MS Access laundry POS into a Laravel + React/Inertia web platform — relational MySQL schema reverse-engineered from the .mdb file, role-scoped admin, queued Twilio SMS + Mailgun notifications driven by an order state machine, and an audited artisan migration that imported every legacy record without loss.

CLIENT
Laundry Business (Confidential)
France
ROLE
Full Stack Engineer
DURATION
6 months
TEAM
2
  • 6

    Core modules built end-to-end — order intake, customer profiles, dynamic pricing, notifications, reporting, and RBAC — each as a discrete Laravel domain with its own controllers, form requests, and Inertia pages

  • 3

    Role tiers wired through policy classes and middleware — Owner, Manager, Staff — with field-level scoping on pricing, reporting, and historical data plus per-action authorization on every controller method

  • 4

    Order lifecycle states driving queued Twilio SMS and Mailgun email notifications — received, in-progress, ready for collection, collected — dispatched off Eloquent model events with idempotent job handlers

Tech Stack

  • Laravel
  • React.js
  • Inertia.js
  • Alpine.js
  • Tailwind CSS
  • MySql
  • AWS EC2
  • AWS S3
  • Cloudflare
  • Forge
  • Redis
  • Twilio
  • Mailgun

Delivery

  1. PHASE 01

    Schema Reverse-Engineering from the MS Access .mdb

    Connected to the legacy .mdb over ODBC and walked every form, query, report, and macro to extract the implicit relational model. Documented each table, foreign key, and embedded business rule into a structured English specification that became the source of truth for the new MySQL schema.

  2. PHASE 02

    Async Contract Verification

    Sent each translated module spec to the client as an annotated email — original Access screens alongside the proposed Laravel data model and screen behaviour. The client confirmed or corrected by return email in French. Every business rule was verified before a controller was written.

  3. PHASE 03

    Module-by-Module Build on Laravel + Inertia + React

    Built the system as discrete Laravel domains — order intake, customer profiles, pricing rules, notifications, reporting, RBAC — each shipped as a working Inertia/React screen and signed off before the next module began. Kept feedback loops short and the final delivery clean.

  4. PHASE 04

    Audited Data Migration via Artisan

    Wrote a custom artisan command that read the legacy .mdb over ODBC, validated every row against the new schema's constraints, and routed any inconsistent record into a review log instead of importing silently. Final import ran only after the flagged set was reconciled with the client — fully auditable, zero data loss.

Outcomes

  • 6 domain modules delivered end-to-end on Laravel + Inertia/React — each with its own controllers, form requests, policies, Inertia pages, and feature-scoped React components.
  • Eloquent-driven order state machine with 4 lifecycle states, dispatching queued Twilio SMS and Mailgun email jobs via Redis — replacing 100% of the manual collection calls.
  • Role-based access control enforced through Laravel policies and Inertia middleware — Owner, Manager, and Staff each see only the routes, actions, and fields permitted by their role.
  • Audited artisan migration moved every historical order and customer record from the .mdb file into MySQL with zero data loss, backed by a row-level validation log signed off by the client.
  • Cloud-deployed on AWS EC2 via Forge behind Cloudflare, with S3 for receipts and document storage and Redis for queues, sessions, and cache — eliminating the single-workstation failure point.
  • End-of-day reports rebuilt as Laravel report classes producing the exact totals and groupings the owner had in the legacy system, exported as PDF off a queued job.
  • Form-request validation layered on every write path, with React-side mirrors via Inertia errors so input errors surface inline without round-tripping the page.

Challenges & Learnings

  • Reverse-engineering a relational schema out of MS Access.

    Access tolerates loose data — nullable foreign keys, duplicate natural keys, free-text fields used as enums — and after years of live use the legacy database had silently accumulated all of it. A naive import would either fail on MySQL constraints or carry corruption forward. The fix was a two-stage migration. Stage one ran the .mdb through a read-only ODBC reader inside an artisan command and projected each row against the new schema's constraints (foreign-key existence, enum membership, non-null required fields, deduplication on customer phone + name). Any failure was written to a structured review log with the row, the offending field, and the rule that rejected it — not skipped, not coerced. Stage two ran only after the client signed off on the reconciled review log. The result was an importable dataset that was provably consistent with the new model, and an audit trail proving it.

  • Designing a state machine that survives retries and partial failures.

    Order status transitions trigger external side effects — a Twilio SMS, a Mailgun email, a printer receipt, sometimes all three. These have to be safe to retry: a queue worker that crashes mid-job cannot end up double-charging or double-notifying a customer. The design uses Eloquent model events to dispatch one queued job per status transition, each job keyed by `(order_id, target_status)` with an idempotency record in Redis. If a job is retried after a partial failure, it sees the idempotency key, completes only the side effects that didn't yet succeed, and short-circuits anything already done. Every transition is logged, so the operator can reconstruct exactly which notifications fired for any order — useful both for debugging and for resolving customer disputes.