No Walk-In on Service
A service ticket cannot, under any circumstances, be attached to a walk-in customer. This is the canonical example of how the Hub enforces a domain invariant when one defense isn't enough.
James's spec: "a Service order, under no circumstances, can have a Walk-In attached. I don't even want the option to do it." Five independent layers, any one of which would be sufficient on its own.
Why the invariant matters
A walk-in is an anonymous transaction — the customer pays now, leaves now, the shop has no record of who they were. That's fine for a $30 chain lube. It's catastrophic for a service ticket:
- Service tickets are promises. "I'll have your bike ready Thursday." The shop has to be able to phone, text, or hold the bike until pickup. There is no one to phone if the customer is "walk-in."
- Tickets carry custody of property. The bike is physically in the shop, sometimes for days. Without a named owner, recovering it after a mistake (wrong customer picked up, bike stolen, owner died) is impossible.
- Service work has warranty obligations. A $200 tune-up comes with a 30-day re-true. A walk-in has no record to look up.
- Service revenue is taxable but not pre-paid. A service ticket's money posts on invoice, not on drop-off. The shop needs a customer-of-record for AR.
Every reason a walk-in works for retail is the opposite of every reason a service ticket exists. There is no edge case where the invariant should bend.
The five-layer defense
operator clicks "Keep walk-in" in Service customer picker
↓ ↑
(1) handler bails (5) MutationObserver removes
(2) DOM never injected (4) CSS forces display: none
(3) data-mode='service' ↑ defends against insertion
↑ defends against display
↑ exposes mode to CSS+JS
↑ defends against state mutation
↑ defends against existence
Each layer is independently sufficient. The operator has to defeat all five to attach a walk-in to a service ticket.
Layer 1 — Architectural: handler bails
The walkInClick handler reads its modal context first. If the modal is in service mode, the handler returns before touching any state. The walk-in callback path cannot fire from inside a Service customer picker — the function exits before it would.
function walkInClick(e) {
const modal = e.target.closest('.customer-picker-modal');
if (modal?.dataset.mode === 'service') return; // bail
// … walk-in attach logic
}
Layer 2 — DOM injection: the button isn't there
The Walk-In button is createElement-ed only when the modal opens in sales mode. In service mode, any leftover instance (from a previous open in sales mode) gets removeChild-ed before the modal is shown.
function openCustomerPicker(mode /* 'sales' | 'service' */) {
modal.dataset.mode = mode;
const existing = modal.querySelector('#sales-cust-walk-in');
if (mode === 'service' && existing) existing.remove();
if (mode === 'sales' && !existing) modal.append(createWalkInButton());
modal.showModal();
}
Layer 3 — data-mode attribute: mode is in the DOM
The modal element carries data-mode="sales" or data-mode="service" on every open. This exposes the mode to CSS selectors, JS observers, and any future code that needs to branch on it without re-querying app state. It's the anchor every other layer reads off.
Layer 4 — CSS: structural hide
A stylesheet (injected once at module init) makes the Walk-In button invisible whenever its modal ancestor is in service mode. !important so no inline style override can defeat it.
.customer-picker-modal[data-mode="service"] #sales-cust-walk-in {
display: none !important;
}
Even if all the JS defenses were stripped, the operator could never see — let alone click — the button.
Layer 5 — MutationObserver: defensive sweep
A MutationObserver watches the modal's footer-right region. If anything inserts a #sales-cust-walk-in element while the modal is in service mode — a race, a browser extension, a stale renderer, a debugger experiment — the observer removes it on the next tick.
new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.id === 'sales-cust-walk-in' &&
node.closest('.customer-picker-modal')?.dataset.mode === 'service') {
node.remove();
}
}
}
}).observe(footerRight, { childList: true, subtree: true });
This is the paranoid layer. It's not solving a known problem; it's solving every unknown problem that ends in "and somehow the button appeared anyway."
Why five (not one, not two)
The instinct says: "Pick the best layer and skip the rest." That instinct is wrong for invariants you can't afford to break.
| Threat | What kills it |
|---|---|
| Operator typo, misclick, race | Layer 1 (handler bail) |
| Code regression that removes the bail | Layer 2 (DOM never created) |
| Race that inserts the button before the open finishes | Layer 4 (CSS hides it) |
| Browser extension injection | Layer 5 (MutationObserver) |
| Future refactor that misses one of the above | The other four still hold |
The cost of five layers is ~30 lines of code in one file. The cost of one mistake — service ticket attached to walk-in, bike lost, customer wronged — is unrecoverable. The asymmetry justifies the redundancy.
When to apply this pattern
Not everywhere. Most invariants only need one defense and a test. Five-layer defense earns its complexity only when:
- The invariant is non-negotiable. Not "should rarely happen" — can never happen.
- The cost of violation is unrecoverable. Data loss, money loss, custody loss.
- The threat surface is plural. Operator action, race conditions, third-party code, future refactors.
- The defense surface is plural. The thing you're forbidding exists in multiple representations (state, DOM, CSS, runtime mutations) and each representation needs its own veto.
When those four conditions all hold, layer up. When they don't, don't — defensive code without a real threat is just noise.
See also
- Slice 4 — Service tickets — where the customer-of-record requirement lives
- Header-icon doctrine — the cross-tab UI discipline that the Service tab inherits
- Audit everything — the chain that would catch the invariant being violated if these layers failed (defense in depth all the way down)
- Fail quietly, recover loudly — the inverse for non-critical paths