<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Pricing on Brian Faust</title><link>https://wimdy.works/tags/pricing/</link><description>Recent content in Pricing on Brian Faust</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Mon, 01 Jan 0001 00:00:00 +0000</lastBuildDate><atom:link href="https://wimdy.works/tags/pricing/rss.xml" rel="self" type="application/rss+xml"/><item><title>Testing the Go Port Against PHP</title><link>https://wimdy.works/thoughts/make-cross-language-checks-explain-their-differences/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/make-cross-language-checks-explain-their-differences/</guid><description>&lt;p&gt;A Go function can return the same failure code as the PHP function and still
behave differently in the application. Laravel might replace the exception
before the PHP resolver sees it. A database reader might fail while loading a
relationship that a direct unit test supplied as an already populated struct.
Neither difference appears in a test which compares only the final price.&lt;/p&gt;
&lt;p&gt;The rating engine&amp;rsquo;s migration checks therefore needed more than translated
assertions. They needed executable observations from PHP, a clear point at
which to compare them with Go, and enough detail to explain a disagreement.
Otherwise, “the implementations match” could mean little more than “both tests
contain the same assumption.”&lt;/p&gt;</description></item><item><title>Retire the Browser, Keep the Test Runner</title><link>https://wimdy.works/thoughts/retire-the-browser-keep-the-test-runner/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/retire-the-browser-keep-the-test-runner/</guid><description>&lt;p&gt;The Go port deliberately dropped the browser UI. The API tester was the awkward
exception: not its pages, but the program behind them. It could create a test
configuration, publish it, make real API calls, extract identifiers, wait for a
result and clean up afterward. Deleting its screen was straightforward.
Deleting all of that would have thrown away a useful part of the migration.&lt;/p&gt;
&lt;p&gt;Replacing it with a CLI also exposed distinctions the screen could make easy
to overlook. An HTTP 200 response could mean a failed test step. A failed run
could still be a successfully produced command result. A saved step record
didn&amp;rsquo;t mean its API call would never execute again. Those weren&amp;rsquo;t reasons to
redesign the tester during the port; they were behavior to understand before
moving its entry point.&lt;/p&gt;</description></item><item><title>A Rewrite Can Change Data Without Changing the Price</title><link>https://wimdy.works/thoughts/a-rewrite-can-change-data-without-changing-the-price/</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-rewrite-can-change-data-without-changing-the-price/</guid><description>&lt;p&gt;The Go history append preserved the existing JSON. That looked reasonable:
read a stored transition list, add the new transition, leave everything else
alone. Unfortunately, leaving everything else alone wasn&amp;rsquo;t what the PHP
implementation did.&lt;/p&gt;
&lt;p&gt;The shared idempotency updater in my rating engine read history through an
Eloquent array cast, appended a transition, and assigned the resulting value
back to the model. That round trip could turn an old nested &lt;code&gt;{}&lt;/code&gt; into &lt;code&gt;[]&lt;/code&gt;.
The Go translation preserved &lt;code&gt;{}&lt;/code&gt; instead. Both could complete the operation
and return the same price while storing different histories.&lt;/p&gt;</description></item><item><title>A PHP Test File Isn't One Migration Case</title><link>https://wimdy.works/thoughts/a-php-test-file-isnt-one-migration-case/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-php-test-file-isnt-one-migration-case/</guid><description>&lt;p&gt;One PHP file contained two test bodies and eleven migration cases. Counting
it as one file would hide ten of them. Counting its two &lt;code&gt;it(...)&lt;/code&gt; declarations
would still miss most of the work. Copying the first assertion into a Go test
would look plausible and preserve almost nothing of the original matrix.&lt;/p&gt;
&lt;p&gt;The file covered customer lifecycle states in the pricing application. One
body tested eight transitions; another tested three sets of state predicates.
It was small enough to follow completely, which makes it a useful way into
the much larger problem: using an existing PHP suite to constrain a Go port
without pretending that translating test syntax proves equivalent behavior.&lt;/p&gt;</description></item><item><title>The Migration Plan Was Solving the Wrong Problem</title><link>https://wimdy.works/thoughts/the-migration-plan-was-solving-the-wrong-problem/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/the-migration-plan-was-solving-the-wrong-problem/</guid><description>&lt;p&gt;The approved migration plan called for a fresh database schema. I wanted to
keep the existing one. That disagreement wasn&amp;rsquo;t hidden in a difficult Go
implementation; it was written into the planning package before the production
port phase began.&lt;/p&gt;
&lt;p&gt;I had approved that package for planning, not implementation. When I asked
whether the existing migrations and schema would be faithfully replicated,
the answer was no: the documents explicitly described a redesign. The plan
could be internally consistent and still be the wrong plan for this engine.&lt;/p&gt;</description></item><item><title>When the Framework Sets the Latency Floor</title><link>https://wimdy.works/thoughts/when-the-framework-sets-the-latency-floor/</link><pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/when-the-framework-sets-the-latency-floor/</guid><description>&lt;p&gt;By the time I considered moving the pricing backend to Go, the PHP version
already had table-driven pricing, prepared runtime projections, candidate
indexes, and substantial cache work. Those changes mattered. They didn&amp;rsquo;t
remove the request cost I was still seeing before business processing:
roughly 50–100 milliseconds on the conventional PHP/Laravel path, reduced to
roughly 30–40 milliseconds with Octane in the setup I tested.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want to spend the rest of the project squeezing a little more out
of that same path. Go was a better fit for the request and computation work,
particularly as the pricing datasets grew. Performance, resource consumption,
and stability drove the choice. The interesting engineering question became
how to replace the runtime without quietly replacing the application too.&lt;/p&gt;</description></item><item><title>Is Your Benchmark Doing New Work?</title><link>https://wimdy.works/thoughts/is-your-benchmark-doing-new-work/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/is-your-benchmark-doing-new-work/</guid><description>&lt;p&gt;One of the rating engine&amp;rsquo;s early benchmark fixes made the results slower.
That was the improvement. The runner had reused request identities across
invocations, so later runs could return stored results instead of evaluating
new quotes. The endpoint was doing its job. The benchmark wasn&amp;rsquo;t measuring
the job I thought it was.&lt;/p&gt;
&lt;p&gt;This is an awkward mistake because the output can look entirely healthy:
successful HTTP responses, small latencies, no obvious application errors.
It is also worth dealing with before celebrating a faster
&lt;a href="https://wimdy.works/thoughts/stop-scanning-every-tariff-row-for-every-service/"&gt;index&lt;/a&gt;
 or
&lt;a href="https://wimdy.works/thoughts/a-cache-hit-still-has-a-payload/"&gt;cache read&lt;/a&gt;
. A replay can skip the
calculation whose improvement you&amp;rsquo;re trying to measure.&lt;/p&gt;</description></item><item><title>A Cache Hit Still Has a Payload</title><link>https://wimdy.works/thoughts/a-cache-hit-still-has-a-payload/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-cache-hit-still-has-a-payload/</guid><description>&lt;p&gt;The rules were cached. The expensive graph had already been compiled. A warm
request should have been the easy case, yet loading that prepared data was
still taking enough work to deserve its own investigation. The cache was
working; the representation inside it was the problem.&lt;/p&gt;
&lt;p&gt;One attempted fix was to cache a larger, ready-to-use pricing profile and skip
reconstruction. That made cache-hit performance worse. Later, compacting rows
helped, moving their expansion helped, and then keeping one expensive family
expanded helped too. Apparently contradictory results, until you follow where
each version made the request do its work.&lt;/p&gt;</description></item><item><title>Index Tariffs Without Losing Charges</title><link>https://wimdy.works/thoughts/stop-scanning-every-tariff-row-for-every-service/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/stop-scanning-every-tariff-row-for-every-service/</guid><description>&lt;p&gt;Scanning ten thousand tariff rows for each of a hundred candidate services
means a million candidate-row visits before doing much interesting pricing.
Most visits may end with the same answer: this row belongs to another service.
Putting that loop behind a cache doesn&amp;rsquo;t remove it.&lt;/p&gt;
&lt;p&gt;I changed this part of my private logistics engine by preparing an in-memory
index of rows by carrier and service match. This wasn&amp;rsquo;t a database index.
The quote already had a restored runtime projection; the repeated work was
inside that projection. The index moved some predicates out of the per-candidate
loop, which made their preservation a correctness requirement rather than
merely a performance detail.&lt;/p&gt;</description></item><item><title>Compile Published Rules Before the Quote Arrives</title><link>https://wimdy.works/thoughts/compile-published-rules-before-the-quote-arrives/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/compile-published-rules-before-the-quote-arrives/</guid><description>&lt;p&gt;A cache flush shouldn&amp;rsquo;t make the next quote rebuild a price book that was
already published. The shipment is new. The structure of the tariff isn&amp;rsquo;t.
Yet “load the graph, prepare it, cache it” puts both jobs back on the request
as soon as the cache disappears.&lt;/p&gt;
&lt;p&gt;That boundary changed during development of my private rating engine.
First, published rulesets gained a persisted runtime projection. Later,
supporting configuration gained a separate persisted bundle. These changes
didn&amp;rsquo;t precompute shipment prices. They gave repeated preparation somewhere
to survive other than a volatile cache.&lt;/p&gt;</description></item><item><title>The Order of Charges Changes the Price</title><link>https://wimdy.works/thoughts/the-order-of-charges-changes-the-price/</link><pubDate>Wed, 15 Apr 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/the-order-of-charges-changes-the-price/</guid><description>&lt;p&gt;Two row orders can produce the same €35 quote while disagreeing about the
fuel charge. In one, fuel is €2.70; in the other, it&amp;rsquo;s €2.40. A minimum-charge
top-up hides the difference in the total. Assert only that total and the test
passes either way.&lt;/p&gt;
&lt;p&gt;The difference comes from when the fuel row sees a handling charge. The
percentage reads the components already built, not a final basket reconstructed
after every row has run. Scope also limits which of those components it can
use. A shipment percentage doesn&amp;rsquo;t automatically include parcel charges just
because they contribute to the same quote.&lt;/p&gt;</description></item><item><title>One Parcel Doesn't Have One Pricing Weight</title><link>https://wimdy.works/thoughts/one-parcel-doesnt-have-one-pricing-weight/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/one-parcel-doesnt-have-one-pricing-weight/</guid><description>&lt;p&gt;A parcel weighs two kilograms. Its dimensions make its volumetric weight
4.8 kilograms. One service prices the larger measurement, rounded up to a
whole kilogram; another prices actual weight. The same parcel gives those
services pricing weights of five and two kilograms.&lt;/p&gt;
&lt;p&gt;Calculating a weight once and sharing it across both candidates would save
work. It would also calculate the wrong thing. The reusable fact is the
parcel&amp;rsquo;s geometry, not whichever service&amp;rsquo;s policy happened to run first.&lt;/p&gt;</description></item><item><title>The Engine Prices Parcels, Not Copies</title><link>https://wimdy.works/thoughts/the-engine-prices-parcels-not-copies/</link><pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/the-engine-prices-parcels-not-copies/</guid><description>&lt;p&gt;I dropped the package-copy shortcut during the rating engine&amp;rsquo;s development.
If a shipment contained ten parcels, I wanted ten parcel records, not one
record with &lt;code&gt;copies: 10&lt;/code&gt;. Count-based pricing stayed. The alternative way of
describing the things being priced didn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;This was an earlier input-model decision, not another change made during the
Go port. It matters to the rest of the engine because parcels acquire
references: a transport leg carries particular parcels, an item allocation
belongs to a particular parcel, and a package-scoped rule needs that parcel&amp;rsquo;s
facts. A multiplier doesn&amp;rsquo;t answer those questions without more conventions.&lt;/p&gt;</description></item><item><title>A Valid Pricing Row Can Still Be Wrong</title><link>https://wimdy.works/thoughts/a-valid-pricing-row-can-still-be-wrong/</link><pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-valid-pricing-row-can-still-be-wrong/</guid><description>&lt;p&gt;One row charges EUR 12 from zero through five kilograms. Another charges
EUR 18 from four through ten. Give them different names and sequence numbers,
attach them to the right tariff book, and both rows can be perfectly valid
database records. At 4.5 kilograms, they still disagree about the base price.
Adding them gives EUR 30, which nobody specified as the base rate.&lt;/p&gt;
&lt;p&gt;Moving my private rating engine from
&lt;a href="https://wimdy.works/thoughts/not-every-pricing-rule-belongs-in-a-formula/"&gt;expression definitions to explicit rows&lt;/a&gt;

made this problem easier to inspect. It didn&amp;rsquo;t solve it by itself. I needed
to decide which combinations should be impossible, which should remain valid
drafts, and which should stop a ruleset from becoming executable.&lt;/p&gt;</description></item><item><title>Not Every Pricing Rule Belongs in a Formula</title><link>https://wimdy.works/thoughts/not-every-pricing-rule-belongs-in-a-formula/</link><pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/not-every-pricing-rule-belongs-in-a-formula/</guid><description>&lt;p&gt;The first parcel costs EUR 20. Parcels two through ten cost EUR 5 each.
After that, each additional parcel costs EUR 3. Twelve parcels should cost
&lt;code&gt;20 + 9 × 5 + 2 × 3 = 71&lt;/code&gt; euros. There is nothing particularly exotic about
that tariff. Turning it into three small programs still creates more work
than the arithmetic suggests.&lt;/p&gt;
&lt;p&gt;My first pricing model could express those programs. It knew about package
scope, conditions, calculated amounts, and ordered adjustments. The problem
wasn&amp;rsquo;t an inability to calculate the right price. It was that ordinary tariff
structure kept arriving as another expression for the system to interpret.
That affected editing, validation, and eventually execution.&lt;/p&gt;</description></item><item><title>How a Shipping Quote Gets Its Price</title><link>https://wimdy.works/thoughts/a-shipping-price-is-a-calculation-not-a-lookup/</link><pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-shipping-price-is-a-calculation-not-a-lookup/</guid><description>&lt;p&gt;A parcel weighs two kilograms. The tariff has a price for two kilograms.
Returning that price sounds reasonable. It can also be wrong before the
lookup finishes: the tariff may charge for volume, the account may have a
negotiated transport rate, and handling may apply to each parcel rather than
once to the shipment.&lt;/p&gt;
&lt;p&gt;I built a private logistics rating engine around those interactions. It
stayed private rather than becoming a commercial product. What made it
interesting was that most of the difficulty came from pricing itself. I
could work on the model without first accommodating years of
unrelated business exceptions. There was still plenty to get wrong.&lt;/p&gt;</description></item></channel></rss>