Starting from nothing
There was no maintenance contract concept in the account. Technicians served plans that lived in a spreadsheet, renewals were remembered rather than tracked, and nothing connected a visit to the money that paid for it.
The first build introduced the records that were missing: a contract, the visits expected under it, and the equipment each visit covers. On top of those sit the rules a plan actually needs. A contract cannot go active until it is signed. A renewal clones forward to the next term and refuses to reactivate until it is signed again. A scheduled check flags a contract whose visit window has not been satisfied by a distinct completed visit, which is a different test from counting visits.
- Maintenance contractplan, term, stored card token
- Every morninga scheduled run reads every contract
- Due this cycle?yes charge the card · no not charged
- Not chargedfree first year, complimentary, or already paid
- Charge the cardone key per billing cycle, never twice
- Approved?yes invoice and payment · no try again
- Invoice and paymentposted, the term moves forward
- Try againsame day, after 3 days, after 7
- Payment holdafter the third decline
Card numbers never reach the ERP: only a token and its last four digits. Test runs charge a dollar or nothing at all.
The billing engine
The second build charges saved cards on a daily run. The interesting work was not the charge, it was everything around a charge that fails.
- A deterministic key per contract and period, held in a charge ledger, so a re-run on the same day cannot take a second payment.
- A failed charge holds its place in a retry ladder and does not advance the billing term, because advancing the term on a decline quietly forgives the money.
- Gateway webhooks arrive out of order, so payment events are deduplicated by event id rather than trusted by arrival.
- Legacy members were grandfathered on terms that never involved a card on file. Enrolling them in autopay silently was never an option, so they invoice per visit instead.
- Cancellation prorates, and records who approved the refund and why.
A bug worth naming
Invoices were landing as pending approval, which meant payments could not attach to them. The money was arriving and had nowhere to go. The fix set approval status at creation, with a fallback after save on every write path that creates an invoice. There were more of those paths than anyone expected, which is the actual lesson.
Plans that start free
Some plans are sold with the first year free. Those go on the invoice at zero, carry the value of year two on the contract, and start charging the saved card on their own after twelve months. I also wrote the guide the office uses to enter a plan from start to finish.
Validating before going live
Both builds were run end to end against a real gateway in a test account first: a live authorization, a real token vault, a decline, a refund and a duplicate webhook. The last step was a one dollar charge on the live gateway: approved, invoiced, paid, the term moved forward, then the same run again to prove it would not charge twice. Only then did a customer contract get touched.
Systems inside it
- Contracts, planned visits and covered equipment
- Renewals that wait for a signature
- Recurring card billing with retries
- Payment webhooks that never count twice
- Plans that start with a free first year