Work / Field service
Case study · job scheduling & dispatch software for a service plumber
Field service for a plumbing company
Live demo belowPackaged field-service and job scheduling apps are built for the average plumbing company. This one is not average: it dispatches in arrival windows, buys material its own way, and takes deposits its own way. We built it one self-hosted application shaped around exactly that — online booking, quotes and invoices that share a single totals routine, a job workflow the office cannot break, purchasing raised straight off the quote, and a customer portal that stopped the phone ringing. When the business changes how it works, the software changes with it.
- Client
- A plumbing company — not named, at the client’s request
- Our role
- Discovery, product design, full build, hosting, ongoing maintenance
- Scope
- Marketing site & SEO, arrival-window booking, quotes, jobs, invoices, purchasing & bills, payments, customer portal, Android app
- Stack
- PHP · MariaDB · nginx on one VPS · Android TWA · Telegram & FCM alerts
This is a live sandbox, not a slideshow. The screens below are a faithful slice of the production back office, running in your browser against invented data — the real company’s customers, jobs and figures are not here. Click anything: nothing dispatches a plumber, charges a card, or emails a customer. Reload the page to reset it.
How it is built
One PHP application over MariaDB, served by nginx on a single small VPS. That is a deliberately unfashionable stack for a deliberately practical reason: a two-person office cannot carry a Kubernetes bill, and a plumbing company's peak load is the office manager and a customer paying an invoice at the same moment. The whole system — public site, booking, back office, portal, pay pages, cheque printing — is one deployable thing that one person can explain on a whiteboard.
It started life in Next.js and TypeScript. The hosting the client already paid for could not keep a Node process alive under its thread limits, so we ported the domain code to PHP rather than move them somewhere dearer. The state machines, the totals arithmetic and the booking maths came across line for line; the ports are commented with what they were ported from.
Money is integer cents, everywhere
No float ever touches a monetary value. Quantities are thousandths, so two and a half hours is exact. Quotes, jobs and invoices have the same shape — labour lines, material lines, a tax rate in basis points, a discount, and a per-document decision on whether to pass on the card processing fee — and one function computes their totals. The invariant subtotal − discount + tax + processing = total is asserted over every document in the database by a test, not just over new arithmetic.
The processing fee is a choice per document rather than a policy. A customer paying by cheque costs nothing to take money from, and in New York passing a card surcharge on to them anyway is hard to defend. A quote the customer has already seen never silently re-rates itself when the constant changes.
Cost and price on every material line
Two prices ride on each line and the difference matters: cost, what the company pays, and price, cost plus a twenty per cent margin. Both are snapshotted at the moment the line is written, so a quote the customer has already seen never silently re-prices. Purchase orders to the supplier carry cost; the customer’s document carries price; and because the PO is raised off the quote’s own material lines, the two lists cannot drift apart.
Booking windows, not appointment times
Plumbing is dispatched in arrival windows, so the public form offers a morning and an afternoon rather than a grid of exact times, with a capacity per window and a two o'clock cut-off for same-day. All slot maths runs in the business's timezone and is stored in UTC, because the server clock on shared hosting is whatever it is. Availability is re-checked at submit time so two customers cannot take the last window in the same day.
Public input never lands in the work-order table. A submission creates a booking request; the office reviews it, is shown whether the address or phone is already on file, and converts it into a job. Address lookups come first from properties the business has already worked at — the only source that can say already on file — and only then from a geocoder.
The job state machine
Unscheduled, scheduled, dispatched, in progress, completed, cancelled, void. A transition table is the only thing that decides what a job may become, so an invalid move is impossible to make from a form regardless of what the browser posts. Cancelled work was real and can be revived; void means the record should not exist, is final, requires a reason, and refuses if an invoice hangs off the job.
What could not be demonstrated in a browser
- Signature on file. An ACH authorisation is an append-only record: the signature image, signer, timestamp, IP, and a snapshot of the exact terms text shown at signing. Rewording the template later cannot alter what a customer is shown to have agreed to. Revocation sets a date; nothing is edited or deleted.
- E-checks. Remotely-created cheques with a valid MICR line and the amount in words, for customers who will not use a card.
- The Android app. A Trusted Web Activity around the live site — no second codebase — that also delivers push alerts for bookings, approvals, payments and chat messages to every staff phone.
- Support chat. A chat box on every public page whose messages land in the office's Telegram, because a plumber under a sink answers a phone notification and not a webmail tab.
- Payments. Card entry happens in the processor's hosted fields; no card or bank number is ever stored. The merchant account is still clearing, so production runs in a visibly labelled test mode.
What we would build next
- Route-aware scheduling — ordering a technician's day by drive time rather than by when the calls came in.
- Recurring maintenance agreements that raise their own booking requests.
- Parts on the van, so a job can go to whoever is already carrying the fitting.
Does your operation fit inside somebody else's app?
Ready-made software is built for the average business. If yours does a few things its own way, those are usually the things that make it good — and they are exactly what we build around.