HVAC contractor, southern Ontario (3-4 technicians, one dispatch office)

Scheduling, Field Work Orders, and QuickBooks Desktop for an HVAC Contractor

Paper work orders priced by hand, no double-booking check on the schedule, on-call hours added manually every week, and a bookkeeper re-typing every invoice into QuickBooks Desktop from a paper trail, plus a previous GPS-tracking rollout the crew had already killed once.

What Was Built

A dispatch board, a technician mobile flow with consent-first location, an office pricing-review queue, and a QuickBooks Desktop integration over the Web Connector with cent-match and no-double-invoice guards, built to sit alongside the client's existing ledger, not replace it.

What's Provable Today

Pre-launch at last recorded build activity: 200 concurrent work-order creations produced 200 distinct numbers with zero collisions, a 20-case overtime test suite passed against Ontario's ESA rules, and the full intake-to-QuickBooks storyline passed end-to-end in an automated test. No production usage data exists yet, see the honest status note below.

By Serg Litt

The client’s own words, unedited

The best source for this build wasn’t a discovery workshop. It was one email. The client, an HVAC contractor running three to four technicians out of a single dispatch office in southern Ontario, described their own process in their own words, and that description became the requirements brief:

“Right now, I get all of the supplier invoices, add a markup and add to our physical WO’s and then [the bookkeeper] types up in QuickBooks.”

Every invoice went through two manual re-entry steps: a supplier invoice copied onto a paper work order, then that paper work order re-typed into QuickBooks by hand. Thirteen concrete pain points came out of that email and a short follow-up thread. In order:

  1. No mandatory fields on new customers. Records were entered ad hoc. The client asked, in so many words, for hard-required fields, email, bill-to, site address, phone, “so it is filled out properly.”
  2. No schedule-conflict warning. Technicians were assigned by memory. Nothing in the system checked for double-booking.
  3. No pricing structure on the work order. Labour, materials, service charge, and parking weren’t itemised. The bookkeeper reconstructed the total from paper, after the fact.
  4. On-call hours added by hand. A technician on call for the week needed 3 extra hours added to their timesheet manually, every single week, by the office.
  5. No aggregate view of technician hours, vacation, or overtime over any period.
  6. Quarterly maintenance tracked with no automatic prompting. Recurring jobs didn’t surface as due. Someone had to remember.
  7. No centralized customer or site record, and no tap-to-navigate to a job site from the field.
  8. No live technician status. Once a job was assigned, the office had no way to see whether the technician had accepted it, was on the way, was delayed, or had turned it down.
  9. No structured overtime calculation or disclosure. The 1.5x multiplier wasn’t calculated automatically or shown clearly to the customer signing the work order. The client asked for a visible marker, something like “3*”, so the multiplier is legible on the signed document. Weekend hours were also overtime but weren’t treated as their own automatic rule.
  10. A previous GPS-tracking attempt was a deal-breaker. In the client’s own words: “we would need to discuss the GPS tracking as this was the deal breaker last time.” That sentence shaped an entire part of the build. More on that below.
  11. Parking reimbursement was informal, paid by technicians out of pocket and reimbursed at the office’s discretion, unverified.
  12. No customer-facing sign-off record with photos, and no consistent emailed copy of a completed work order.
  13. No warranty-call notification loop. For manufacturer warranty service, the manufacturer needed manual updates on scheduling, parts, and completion. This one arrived as a follow-up ask two weeks into the build, not in the original brief.

Thirteen items, each traceable to a specific, nameable moment in a specific person’s workday. That’s a better spec than most feature-request lists, because nothing on it is invented. It’s what a dispatch office wrote down when asked what was broken.

What got built

The dispatch board

The office runs a technician-by-day board: one job card per work order, native drag-and-drop to reschedule, with a confirmation step before a date or technician assignment actually changes. If a dispatcher tries to put a technician into an overlapping window, the board shows a non-blocking warning rather than refusing the move outright, a deliberate choice, because the client wanted the system to flag the conflict, not override the dispatcher’s judgment call.

Customer intake now enforces the hard-required fields the client asked for: email, bill-to, site address, phone. A “due this week” filter surfaces recurring and quarterly maintenance jobs automatically, closing the gap where those jobs used to depend on someone remembering. The board polls for status updates every five seconds rather than running a full push or websocket layer, chosen deliberately as the smallest mechanism that answers “is this live enough” for a four-person crew watching one screen.

The technician’s phone

Each work order gets a unique number from a concurrency-safe counter. That detail matters more than it sounds: the old paper numbering was unreliable enough to be a real risk, and the new counter was load-tested at 200 simultaneous work-order creations with zero collisions and zero gaps.

On the technician’s own phone, a job page carries four status buttons, Accepted, On My Way, Delayed, Rejected, visible to the office close to real time, directly answering pain point 8. Clock in and clock out are tracked per visit, with a manual override in 0.25-hour increments for the cases where the system’s read needs a human correction. Photos come from the camera or the photo library, and iPhone photos in their native HEIC format are converted to JPEG and downscaled server-side automatically, because HEIC files don’t display everywhere and nobody should have to know that to use the app. Tapping the site address opens it directly in Google Maps.

Parking got built as a headline feature, not an edge case, because the client was explicit that it should never sit behind a feature flag. A technician photographs the receipt and enters an amount, which routes to an office approval queue before it can land on an invoice. For jobs in the downtown core, a “nearest parking lot” lookup runs off a public municipal parking-occupancy open-data set, bundled at build time and refreshed out of band, plus a one-time office-side geocoding pass on each job site. There’s no live GPS behind this lookup, no payment integration, and no third-party API call at runtime, it’s a static dataset doing a static job.

Overtime is calculated automatically under two independent, non-stacking rules: a flat 1.5x on Saturday and Sunday, and Ontario’s Employment Standards Act 44-hour weekly threshold, whichever applies, never both at once. The multiplier and the reason for it (“weekend” or “after reaching 44 hours in the week”) render as plain, readable text on the signed document, not a bare asterisk with a footnote nobody reads. A 20-case table-driven test suite, built directly from the ESA rules, covers edge cases including a shift that straddles the 44-hour threshold mid-visit.

Customer sign-off happens on the technician’s own device with a signature pad. Once signed, the hours and overtime wording on that document are frozen. A correction made elsewhere in the same week’s timesheet cannot silently rewrite a document a customer already signed.

Location, done the way the client actually asked for it

Pain point 10 deserves its own section, because it’s the clearest example in this build of listening to a stated objection instead of re-litigating it. The client had tried GPS tracking before, with a different vendor, and it broke crew trust badly enough that they named it as “the deal breaker last time” before any scoping conversation even started.

The system built here captures location only after the technician has consented, and only in two narrow ways: a one-shot GPS stamp on a deliberate tap (On My Way, clock in, clock out, sign-off), and coarse, foreground-only pings roughly every 10-15 minutes while a job page is actually open on screen. There is no background tracker. It does not run while the phone is locked. It does not run continuously. That last constraint is also an honest technical limit of a browser-based app rather than a hidden feature, stated plainly rather than glossed over.

Office review before anything becomes an invoice

Every completed job lands in a pricing-review queue before it can become an invoice, matching the client’s own description of the process: “Admin will review, esp. for typos and to add pricing.” Four pricing fields are customer-level and per-customer overridable: labour rate, minimum call hours (default 2), materials markup percentage, and service charge rate, matching the brief’s ask for programmable, customer-specific pricing rather than one flat rate for everyone. A “No Cost” toggle exists for jobs done free of charge, and it still produces a printable work order.

Office staff see pricing. Technicians never do. That’s enforced with a strict two-role permission model, and it’s checked by automated tests that scan the literal rendered HTML of every technician-facing page and PDF for the absence of dollar signs, rate figures, markup figures, or sell-price strings, not just “the template doesn’t show it,” but a test that greps what actually got rendered.

One PDF and HTML rendering template serves the on-screen document, the printed PDF, and the emailed customer copy. One code path means the screen, the printout, and the email can’t drift apart from each other over time.

A later addendum added three automatic emails to a manufacturer contact for warranty service calls, scheduled, parts needed, complete, matching the client’s follow-up ask, backed by a shared, office-maintained manufacturer directory so contacts are reusable across jobs.

The QuickBooks Desktop integration, as the centrepiece

Most vendors treat “the client is still on QuickBooks Desktop” as a migration problem to be solved by moving them to something cloud-based. That was never on the table here. The client runs QuickBooks Desktop on their own Windows PC, and they were never leaving it. So the integration was built to sit alongside that ledger, not replace it.

QuickBooks Desktop is the single ledger of record. The web application never keeps its own invoice or payment table. It builds a reviewed, priced work order and hands it to QuickBooks, which assigns the real invoice number. That’s a deliberate architectural decision, not a limitation: an earlier plan that kept a local invoice record alongside QuickBooks’ own record was deleted specifically because two ledgers is one too many. Duplicate sources of truth are a liability in any integration project, and this one closed that door early.

The connection runs through QuickBooks’ own Web Connector, a SOAP/qbXML polling mechanism Intuit ships for exactly this purpose, because the client’s software is the desktop edition, not QuickBooks Online, and a cloud API wasn’t an option.

Three safety rails sit on the money path:

  • No double-invoicing. An invoice-creation request is refused outright if that job already has a QuickBooks transaction ID attached to it.
  • Cent-match reconciliation. The computed total is checked against what QuickBooks reports back before anything is stored. A mismatch aborts the operation rather than silently recording a number that’s wrong by even a cent.
  • Non-destructive customer sync. Customer records are imported from the real company file and can be created or updated, but never deleted.

Ontario HST (13%) and the ESA’s 44-hour overtime threshold are hard-coded constants rather than configuration fields, a deliberate call for a single-province, single-jurisdiction client, and one the build documents explicitly as something that would need to become real configuration if the client ever operated outside Ontario.

Hard details

A server-rendered web application with lightweight, no-build-step interactivity and no separate JS framework to maintain. An embedded, single-file database, chosen deliberately over a client-server database once QuickBooks became the single ledger of record, nothing left in the data model needs a second writer process. PDF generation renders the same template used on screen, so the screen, the PDF, and the emailed copy share one rendering path.

It runs as a single self-hosted container on an already-owned server, sharing that box’s spare capacity under an enforced resource cap so it can never starve other work sharing the same host. Ingress runs through a private outbound tunnel with no open inbound port, TLS terminated at the edge. Backups combine continuous database replication with an independent hourly snapshot (database, photos, and a checksum manifest) shipped to separate storage, one-week retention. The restore drill was actually run, not just documented: the team wiped the data volume and proved a database row and a media file written before the wipe both came back.

Around 10,100 lines of application code across the field-service domain and the QuickBooks connector, covered by roughly 70 automated test files spanning pricing math, permissions, the QuickBooks connector against recorded fixtures, PDF rendering, and GPS consent boundaries. There is no native mobile app, technicians use an “add to home screen” installable web page, avoiding an app-store distribution requirement entirely. Deliberately not built: offline mode, websockets, a general pricing-rules engine, a customer login portal, multi-tenancy, SMS notifications, and automated parking payment.

The economics, honestly

This system was built and hosted specifically to avoid an ongoing per-seat subscription, running on infrastructure the operator already owned rather than standing up anything new and billed. As of 2026-08-28, published and third-party-reported pricing for a comparable off-the-shelf product at roughly five seats (three to four technicians plus one office user) looks like this:

Product Approximate monthly cost, ~5 seats
Jobber (Connect tier, annual billing) ~$99-149/month
Housecall Pro (Essentials tier, up to 5 users) ~$149-189/month
FieldPulse ~$300-600/month (quote-based, third-party estimate)
Zuper ~$300-600/month (quote-based, third-party estimate)
ServiceTitan ~$1,000-2,000+/month before add-ons, plus an additional implementation fee and a multi-year contract reviewers commonly describe as substantial (specific figures not independently verified)

Jobber and Housecall Pro figures come from their own public pricing pages. FieldPulse, Zuper, and ServiceTitan don’t publish pricing, so those figures are third-party-reported estimates, not vendor-confirmed, treat them accordingly if you’re comparing quotes yourself.

Against that, this system’s marginal hosting cost is effectively zero. It shares an existing always-free-tier cloud server the operator already runs for other client work, under an enforced CPU and memory cap. There’s no per-seat licence of any kind, a fifth or sixth technician costs nothing beyond that person’s own phone. An earlier, since-superseded hosting plan (a small dedicated cloud server) was costed at roughly $6-10/month before the zero-cost shared-server approach replaced it, worth noting only as a second data point, since even that fallback was an order of magnitude below any SaaS competitor’s price. That figure is historical and describes an abandoned plan, not what the live system runs on.

None of that makes a custom build the right call for every shop, and the project’s own research says so directly: Jobber is honestly the better product for a shop this size on pure feature-for-dollar terms. The case for building custom here isn’t “cheaper and better across the board.” It’s narrower and more specific than that, a handful of business rules a generic product doesn’t do out of the box: automatic, non-stacking overtime with a customer-visible multiplier; consent-first (not covert) GPS; true per-customer pricing without paying for a rules-engine tier; and integration with the specific accounting software already in use, instead of requiring a switch to a cloud accounting product.

The other trade-offs are real too. A custom build has no vendor support line and no team of engineers behind it besides the one who built it, every bug is this team’s bug, where a SaaS product spreads that risk across thousands of customers. Zero per-seat cost doesn’t mean zero cost: someone still had to design, build, test, and maintain this system; that cost is just a one-time build rather than a recurring bill. And a single self-hosted server, even with replication, is a smaller blast radius than a large SaaS vendor’s infrastructure, exactly why the backup and restore discipline above isn’t optional polish, but the mitigation for the trade-off a “just use a SaaS” argument would otherwise cite as its own advantage.

Where this stands today

This project was still in demo and pre-launch build as of its last recorded activity. Everything in the sections above describes what was built and verified in that build, not measured outcomes from live use, because none exist yet. To be specific about what is and isn’t backed by evidence:

Verified, traceable to a specific test or decision: the full intake-to-QuickBooks storyline (intake, assign, technician accepts, clock in/out, photo, sign-off, office review and pricing, queue to QuickBooks) passed end-to-end in an automated test. The system was verified on a production-equivalent container image to render a real PDF, survive a full data-volume wipe and restore with both a database row and a media file intact, reject a request without correct CSRF/TLS-proxy headers, and log a real error traceback on an unhandled failure. Work-order numbering was load-tested at 200 concurrent creations with zero collisions. The overtime engine passed its 20-case test suite built from Ontario’s own ESA rules.

Not yet evidenced, and not claimed here: actual time saved per invoice cycle, any reduction in billing errors, adoption by the office or crew, or any dollar figure tied to real usage. None of that data exists, because the system was running on seeded demonstration data against a QuickBooks Desktop connector that had itself not yet been confirmed against the real company file’s tax codes and edition. Whether the client’s actual overtime policy matches what was built (flat weekend 1.5x plus the ESA 44-hour floor) was never confirmed with the office before this build reached its last recorded state, the project’s own risk log flags this as the single highest-risk unconfirmed assumption in the whole build, and it should be treated as unconfirmed unless a reader has since verified current status directly.

This case study describes engineering work that was actually done and actually tested, against a client’s own documented pain points. It does not claim a result it cannot back up. When this system goes live and gathers real usage, that’s a different case study, not a rewrite of this one.

Want the same kind of build for your shop?

Book the assessment. We will walk your actual paper trail and tell you plainly what is worth automating.

Book Your Free Assessment
Call Now: 647.479.8770Text Serg Direct
Book Your Free Assessment