fix(pricing): count a player's age forwards, and hold the picker to the same ceiling as the engine
Carbon 3 signs its differences: `$a->diffIn*($b)` answers `$b − $a`. The
pricing engine asked it the other way round —
`now()->diffInYears($birthday)` — so every participant it priced arrived
at the rules with a negative age and a negative membership duration.
The damage ran in both directions at once. A rule with a `min` never
matched anyone again: the loyalty and annual recipes both ask for twelve
months, and −25 is not twelve. A rule with a `max` matched the entire
academy: the juniors recipe is `max: 6`, and −34 is comfortably under
six, so one click in the rule builder would have taken 10% off every
price in the club. The registration wizard computed age correctly in its
own provisional context, which is why the desk saw one price at
registration and another at renewal.
Age and tenure now read from the older moment forward, through two named
helpers that say why, and a date in the future is no age rather than a
negative one.
Alongside it, the discount picker: `selectedDiscountIds` is a public
Livewire property, so it is a list the browser sends, and the total was
summed from whatever ids arrived. applyDiscount() refuses a blocked rule
and diverts an above-ceiling one into an approval request; neither guard
survived to where the money was worked out. The engine's verdict is
re-read there now, the academy's global discount ceiling applies to a
hand-assembled total exactly as it does at step 8, a manual discount
above the actor's cap reaches neither the total nor the invoice
snapshot, and the picker's state is #[Locked] — it is driven entirely
by wire:click, so nothing needed to arrive from the browser at all.
Also here, found while reading for the above:
- POSTerminal::updateQuantity() did not check the index exists, so an
invented one wrote a cart line made of a quantity and nothing else.
- The same reversed diff in four other places: overdue invoices and
renewals reported negative days on the dashboard and in reminder
messages, expiring memberships reported negative days remaining, and
a product's months-active pinned to 1, inflating its average monthly
movement to its entire lifetime sales.
- validateCoupon() still carried a comment promising academy-wide
coupons, three commits after branch_owns_the_catalogue removed them.
Verified: 379 tests green on SQLite and against the restored OC-Sport
tenant. That tenant carries one pricing rule (sibling_order, 4 EGP) and
no invoice with a discount snapshot, so there is no historical billing
to correct — the bug was waiting on the first age or loyalty rule.
Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
Please register or sign in to comment