The software would not raise an invoice without a booking
Every invoice had to sit on a booking. A partner's share, a supplier's mounting bill, an advance, a credit: real invoices with no booking could not be recorded.
You go to raise an invoice and the form will not let you, because it wants a booking and there is no booking. Your partner is invoicing you for their share of a site’s profit. A supplier is billing you for mounting work. A client has sent an advance. Somebody is owed a credit. None of those is a booking, and the software’s answer to all of them was that they could not be recorded.
That is not a rare edge case in an outdoor media business. Most invoices do come from a booking, which is exactly why the rule was written; but the ones that do not are the ones that involve partners, suppliers and money already received. Every one of them ends up either recorded against a booking it does not belong to, or kept outside the system altogether, in a spreadsheet nobody reconciles. Either way the books in the software are not the books.
What was actually going on
Whoever built the invoice screen had reasoned correctly from the common case. Every example they had been shown had a booking behind it, so the record was built to require one, and that requirement was placed in the one location nothing can get round: the database itself. Screens can be changed. An administrator can override a permission. An import can skip a check. A rule written into the database stops everything, including your own support staff at three in the afternoon when a client is waiting.
A rule like that is a claim about the world — this thing cannot exist without that thing — and here it was false. It took one conversation about how the business actually raises invoices to find out.
What we changed
The requirement was relaxed: an invoice, and each line on it, may now exist without a booking. A small detail alongside it matters more than it looks. Previously, a booking that had anything billed against it could never be deleted, even one created by mistake; the support answer became “mark it cancelled and ignore it for ever”. Now removing a booking leaves its old invoice lines intact and simply unlinks them.
And the screens that genuinely should insist on a booking still do. The rule moved from the place it could not be changed to the place it can.
The same decision, made well, elsewhere
Purchase orders are central to how this client bills; their sample documents were full of them, and the obvious design is to hang every invoice off a purchase order. The rule written into the specification instead was that the purchase order is optional everywhere. A convenience, never a gate. An invoice can be raised against one if it exists, or directly if it does not.
That came from the business, not from a technical preference: a client may release a purchase order, or agree by email, or agree on the phone, and the work starts either way. Making the purchase order mandatory would have been false about the business and would have produced exactly the same dead end as the booking rule, on the document their billing actually runs on. The principle in the business flow document is one sentence: build for the simple project, and make everything else optional. Multi-partner deals, purchase orders, agents, expenses and settlements are layered on top, never mandatory friction on the common case.
What it did not fix, and where we would still get it wrong
The mistake is not carelessness. It is that the rule gets written at the moment the business is least understood, on the day the record is designed, from a description of the common case. Everything known at that point says the relationship is mandatory, because every example has both halves.
There is no way to be smarter at that moment. What we do now instead is treat the first request to relax a required field as information rather than as a change request. The useful question is not “can we” but “what else did we assume from the same description of the common case?” In this system that question found the same shape twice more: an approval step on expenses that no expense could ever reach, because every expense was created already approved, and a site record that could be deleted outright despite carrying photographs, bookings and money.
The mechanism
The invoice’s booking reference and the line’s were not-null columns with foreign keys behind them. The change was three lines of migration: drop the not-null on both, and change the line’s foreign key from restrict-on-delete to set-null, so that deleting a booking leaves its historical lines rather than refusing.
Relaxing a constraint after data exists is cheap — every existing row already satisfies a stricter rule than the new one, so the migration is additive and needs no backfill. Tightening one later is the expensive direction, because every row that violates it has to be found and repaired. That asymmetry is a reason to lean permissive in the database and strict in the application. The application can refuse an invoice without a booking on the screens where that should not happen, and be persuaded otherwise next month. The database cannot be persuaded of anything.
Put the rule where you can change it. A database constraint is the right home for something that must be true of every record for the life of the system — identity, tenancy, references that must resolve. It is the wrong home for a claim about how a business usually works.
Where this ends up
Invoices in Sazinga AdBoard can therefore exist with no booking behind them — a partner’s share, a mounting charge, an advance, a credit — while the screens that should insist on a booking still do.