A customer completes an intake questionnaire once. The price, the delivery estimate, the justification for each line, the service agreement, the payment record, and the project's onboarding plan are all derived from that one set of answers.
Language models take part at several points in that sequence. None of them can set a rate. This document describes how that constraint is implemented, where it holds structurally, what the design costs to maintain, and the one path where the guarantee is procedural rather than structural.
The quote is not the deliverable. It is the first write in a sequence that ends with a provisioned project, and every stage after it reads the record that first write produced rather than composing its own version of the same facts.
Five customer-visible artifacts descend from one set of QuoteIntakeStep rows. Each is
produced by a named rule reading those rows, not by a separate authoring step that happens to agree
with them.
baseline_snapshot.The reason to build it this way is narrow. A tool that writes its schedule in a second place will eventually quote one scope and schedule another, because nothing forces the two representations to stay in agreement. Deriving both from the same component list removes the second representation instead of adding a check that compares them. The same argument applies to the justification text, the contract, and the onboarding plan.
What follows is how each stage enforces that, and what the arrangement costs.
Three things can supply a value: what the customer explicitly selected, what the deterministic engine computed, and what the model drafted. They are ranked, and the ranking is applied as a single pass in one direction.
The ordering is visible in three call sites. The baseline is computed and persisted at
QuoteAiController:56, before any request leaves the process. The model is not called until
:170. Enforcement runs at QuoteDraftService:448, after the reply is parsed.
/** * BASELINE ENFORCEMENT (ONE PASS, NO DUPLICATES) * Precedence: * 1) Intake mappers win (marketing + hosting) * 2) Baseline wins vs AI for remaining core selections/terms */ $baselineKeys = [ 'package', 'hosting_plan', 'hosting_price', // plan + block volumes (hyperscaler mode) 'maintenance_plan', 'design_plan', // marketing_plan intentionally handled below 'payment_option', 'term_length', 'design_price', ];
The single pass matters. Applying a precedence rule twice is how a percentage discount lands on an already-discounted line, which is the defect that produced the capitals in that comment.
Every key that can carry money is checked against configuration before it resolves: 12 application packages, 29 hosting plans, 5 maintenance tiers, 5 design plans, 8 marketing plans, 3 payment options, and 37 modifiers. Ninety-nine keys in total. A key outside that set does not price at zero and does not silently drop.
public static function assertApplicationKey(string $key): void { if (!array_key_exists($key, self::applications())) { throw new InvalidArgumentException("Invalid application key: {$key}"); } }
Intake itself is messier than the vocabulary. Customers enter $6,000 where an integer is
expected, answer a differently worded question, or describe the business in prose. That input is
normalised by a parser whose docblock states its constraints as a contract:
/**
* Deterministic, local-only parser to convert free-text / messy inputs into
* structured "answers" your pricing layer already understands.
*
* NO AI CALLS. NO NETWORK. NO SIDE EFFECTS.
*/
The ranking is easiest to describe on the drafting path, but it is tested hardest on the negotiation
path, where the model is asked to act rather than to explain. When a customer pushes back on price, the
model returns a structured change set: modifiers to add, modifiers to remove, plans to switch.
QuoteScenarioService filters that set before any of it is applied.
// negotiation — only by changing the underlying requirement. The // prompt tells the model not to offer it; this makes it impossible. $blockedRemovals = []; if (!empty($changes['remove_modifiers'])) { $blockedRemovals = array_values(array_filter( $changes['remove_modifiers'], fn ($k) => QuoteNegotiationService::isDerivedRequirementKey((string) $k) ));
Derived-requirement keys — anything prefixed hipaa_, unit_,
tenant_, phone_line_, phone_routing_, or
large_dataset_, plus six named fees — cannot be removed by negotiation at all. They are
present because an intake answer requires them. Removing the charge without removing the requirement
would quote a platform the customer has already stated they need.
A second filter handles capability overlap. Configuration under
quote.modifier_supersedes names which services include which others. Adding a broader
service consolidates the narrower one it covers rather than stacking on top of it; adding a narrower
service that is already covered is dropped, and the covering service is named in the log. Only after
both filters does the change set reach calculatePricing, which resolves every rate from
configuration or from the persisted quote columns the baseline pass owns.
Baseline enforcement is guarded on a baseline that built successfully. If
BaselineQuoteBuilder throws — an invalid configuration key, a malformed answer — both
call sites log a warning and continue, and the enforcement block is skipped. The model's draft then
survives unreviewed. Making this structural means failing the request instead, so the customer sees
an error rather than an unchecked draft. That trade has not been made yet.
Intake asks people to quantify things they have never measured: catalogue size, monthly traffic, stored data volume. Trusting those answers produces an undersized platform. Ignoring them produces a quote the customer does not recognise.
Storage resolves against two independent floors. The declared floor is the band the customer selected. The estimated floor is computed from what the intake already proves: product count and images per product, migrated content volume, monthly traffic, subscriptions, customer accounts, the data profile follow-up, and unit count. The larger of the two wins, and a configured buffer is applied on top, because sizing capacity to exactly the volume of the data is not a capacity plan.
// Layered resolution: the computed evidence and the declared band are // both inputs; the larger wins. On top of the winner sits the // free-space buffer — performance needs slack, so the requirement is // never sized to 100% of the data. $resolvedGb = max($declaredGb, $estimatedGb); $bufferPct = max(0.0, (float) ($cfg['free_space_buffer_pct'] ?? 0)); $bufferedGb = (int) ceil($resolvedGb * (1 + $bufferPct));
Three outcomes follow. When the declared band covers the evidence, the declaration stands. When the answer is "Unsure" or missing, the estimate carries the requirement and is flagged as an estimate to confirm, so a skipped question never resolves to zero. When the evidence exceeds the declaration, the evidence wins and the discrepancy is recorded with both figures.
The third case is the commercially significant one. The system raises the requirement and shows the customer both numbers rather than billing the difference quietly, which makes the follow-up conversation about the catalogue they described rather than about a line they did not expect.
Hosting class resolves the same way, from usage drivers rather than from a small, medium, large picker. Strict per-unit isolation raises the class to dedicated; so does an organisation of sixteen or more units. Both are recorded as flags naming the answers that caused them.
The interesting case is an explicit collision: the customer has chosen VPS, and their other answers imply dedicated. The preference wins the class, the capacity floor rises within it, and the disagreement is written down with both verbatim answers.
// Explicit-vs-explicit collision: the hosting preference wins the // class, capacity floors rise within it, and the collision is // flagged with both verbatim answers — the justification record. if ($explicit === 'vps' && $strictIsolation) { $recommendedClass = 'dedicated'; $r?->flag( 'flag.hosting.collision', ['hosting_preference' => ..., 'unit_data_isolation' => ...], 'Explicit VPS preference AND strict per-unit isolation both selected', 'VPS preference kept; VPS capacity floor raised; dedicated recommended', 'explicit_preference_wins_class_with_recommendation' ); }
The customer reads one sentence generated from that flag: "You preferred VPS hosting — we kept your choice, raised its capacity floor for your scale, and noted dedicated as the recommended upgrade path." This is the precedence rule from the previous section doing something visible. The explicit selection outranks the engine, and the engine records its dissent rather than discarding it.
The pass that computes the price records why it computed it. Each rule that fires writes the verbatim answers it read, the requirement it derived, the priced outcome, and its own name. That record persists with the quote.
/**
* Record a FLAG: the exact relationship between explicit user inputs and
* a derived service requirement. Flags justify the quote line-by-line,
* persist with the baseline snapshot, and protect against misalignment
* claims — every priced (or deliberately suppressed) outcome names the
* verbatim answers and the deterministic rule that produced it.
*
* @param array $inputs question id => verbatim selected answer(s)
* @param string $requirement derived service requirement (human-readable)
* @param string $resolution priced outcome (modifier key/qty, package, or suppression)
* @param string $rule name of the deterministic rule that fired
*/
Twenty-seven flag codes are defined across the pricing services. Two of them are internal to delivery estimation and never shown. The remainder persist with the baseline snapshot and drive the customer-facing text.
Flags become second-person lines through a presenter that matches on the flag code and returns wording. It performs no arithmetic and makes no decisions, and it is the only place that wording exists, so the review page and the signed PDF cannot diverge. An unrecognised code falls back to the recorded requirement text, so a rule added later renders as a plain sentence rather than as silence.
/**
* Deterministic verbal layer: each persisted flag (inputs → requirement →
* resolution) becomes one second-person reassurance line — "you told us X,
* we built Y." Suppression flags become not-double-charged trust lines.
* No AI, no math, no judgment — wording only, sourced here once so the
* review page and the signed PDF speak identical sentences.
*/
Both HIPAA packages include database encryption at rest, and HIPAA Complete includes audit logging.
When a customer selects a package and also selects a capability the package covers, the resolver records
the non-charge as an outcome — db_encryption_fee NOT charged — included in HIPAA package —
with the same four fields as any priced flag. The presenter then renders it: "Database encryption is
already included in your HIPAA package — you were not charged separately for it."
Recording suppression as an outcome rather than as an absence is what makes it renderable. A rule that simply declines to append a modifier leaves nothing for a later pass to find.
Delivery time is derived from the identical emitted modifier list that priced the work, with suppression and quantities already applied, plus the package and design plan. A quote and its schedule therefore describe the same amount of work by construction.
/**
* Derives working-day components from the SAME emitted modifier list that
* priced the work (suppression and quantities already applied) plus the
* package and design plan — one derivation, two outputs (dollars and days).
*
* The workload term reads the count of projects currently in build state;
* with 0–1 active builds it is ×1.0, so during scarcity the estimate is pure
* complexity and the factor self-activates only when builds overlap.
*/
The workload term reads live state, and that has a consequence worth stating: the same intake can produce different dates in different weeks. The estimate is therefore explainable but not reproducible from the answers alone. The active build count is written into the flag for exactly that reason, so a date quoted three months ago can still be accounted for. Any failure in that lookup returns ×1.0, which makes the fallback a pure complexity estimate rather than an error.
Automation that cannot be overridden does not survive contact with selling. The operator surface is a tablet point of sale: it loads a resolved quote, allows discretion within a defined shape, and closes the sale in person.
Discounts apply per component — application, design, hosting, maintenance, and each modifier individually — as a percentage, a flat amount, or both, floored at zero. Every discount input is persisted next to the line it modified, together with the original amount, the discounted amount, and the difference. An operator can move a price. The total remains reconstructible from what they entered, which is what makes the discount reviewable later rather than merely visible now.
Acceptance of terms, confirmation of ownership, the signature image, device geolocation with its
reported accuracy, IP, and user agent are written as a SaleEvidence row inside the same
transaction that creates the payment intent. The row's id travels in the intent's own metadata, so the
two records reference each other from both sides.
return DB::transaction(function () use ($liveQuote, $sale) { $evidence = $this->writePendingEvidence($liveQuote, $sale); $intent = $stripe->paymentIntents->create([ 'amount' => $amountCents, 'metadata' => [ 'live_quote_id' => (string) $liveQuote->id, 'evidence_id' => (string) $evidence->id, 'channel' => 'admin_tablet_pos', ], ]); // Note: LiveQuote.status is NOT changed here. Stays 'generated' until // payment actually clears. Card decline = zero state corruption.
A declined card in a customer's kitchen leaves the evidence in place, which is correct: the customer did accept the terms, at that place and time. It creates no quote, no project, and no status change.
Each evidence row carries a SHA-256 hash over its own fields, recomputed and compared by
hashMatches(). This is a checksum rather than a signature. It detects a row altered after
capture; it does not defend against an actor who can rewrite the row and the hash together. The external
anchor for that is Stripe's own immutable record, reachable through the evidence id in the payment
metadata, which is why the linkage is written in both directions.
The webhook that confirms payment is the only thing that creates a project. Before it writes anything, it reconciles what Stripe reports against what the checkout recorded.
if ( $stripeAmountCents !== (int) $checkout->amount_total_cents || $stripeCurrency !== (string) $checkout->currency ) { Log::error('[BILLING_MISMATCH] Stripe != QuoteCheckout expected', [...]); throw new \RuntimeException('Billing mismatch: refusing to finalize.'); }
Throwing here is deliberate. Stripe treats the delivery as failed and retries, which means a mismatch produces a retry and a logged error rather than a project provisioned against a total nobody agreed to.
agreement_snapshot freezes what was sold: package, plans, the pricing totals, the
baseline snapshot, the operator's adjustments, and the quote's row_version.
current_billable carries what is billed now and is expected to change as the project does.
Keeping them apart means a later upgrade changes the bill without rewriting the record of what the
customer signed, which is the distinction that matters in a dispute.
// === ORGANIZATION & COMPLIANCE FOUNDATION (Day-1) ===
// Gated by the same explicit intake answers (or snapshot modifier keys
// for hand-built quotes) that priced the corresponding services.
Each gate fires on the exact intake answer that priced the corresponding service, or on the equivalent modifier key in the agreement snapshot when the quote was hand-built at the terminal and has no intake rows. A customer priced for per-unit data isolation is asked for a unit roster and receives isolation verification steps in the build phase. A customer who was not priced for it is not asked. The plan cannot request something nobody paid for, or omit something somebody did, because it reads the record that set the price rather than a checklist maintained alongside it.
Underneath sits ordinary multi-tenant scaffolding: organisations, businesses, teams, invitations, and six authorisation policies. A system that takes money and provisions work has to answer who is allowed to see a record before it does anything else with it.
The same rule resolves four unrelated situations across two platforms, which is what makes it a position rather than a coincidence.
generated; the evidence of acceptance persists on its own.In each case the convenient implementation writes the optimistic state and reconciles afterwards. Choosing otherwise costs a retry path and a reconciliation story in every subsystem that touches the boundary. What it buys is that an operator reading a record does not have to establish whether it is provisional before acting on it.
A description of what a design prevents is half a description. These are the places where this one is slow, fragile, or unfinished.
app/Services/Quote/Pricing over a 99-key configured vocabulary. Adding a billable service is a registry key, a resolver rule, a flag code, a presenter line, a duration weight, and an onboarding gate — in six files, in the right order. A pricing table would take one row.None of these are arguments against the approach. They are the reason it is worth stating what the approach actually bought: the artifacts a customer sees cannot disagree with one another, because there is no second place where a disagreeing version could have been written.
The headless-browser site analyser described in the companion case study — the acquisition pipeline that gives an autonomous receptionist its starting knowledge — was built here first, for a different reason.
It was a sales instrument. Read a prospect's website before quoting it, so the first conversation starts from evidence rather than from what someone remembered to mention. Its output still feeds this system's quote context.
Carried into the receptionist platform, the same component supplies a business's published site as its receptionist's knowledge on day one. The interface did not need to change, because it had been written to answer a question about a website rather than to serve a quoting tool.
Designed, built, and operated by one engineer.
Laravel · Inertia · React · MySQL · Reverb · Stripe · Playwright. A deterministic pricing layer of 3,937 lines over a 99-key vocabulary, 27 named justification rules, a tablet point of sale with hashed sale evidence, and project provisioning gated by the answers that set the price.
Every decision recorded here was reached the same way: build it, sell with it, find where it misrepresents something, and change the mechanism rather than restate the intention.
Code excerpts are from the running system. Comments are verbatim; parameter lists and log payloads are elided for width. Line references are to the current tree. Figures are drawn with D3 and printed as vector.