Add Shop::mrr(), activeSubscribers(), mrrByProduct(), subscriptionMetrics()
and subscriberStatuses() to ShopService. Counts active/past_due/trialing
subscribers (not `ends_at IS NULL`, which silently drops still-billing
cancel-at-period-end subs and under-reports MRR), normalizes every interval
to a monthly run-rate (year/12, quarter/3, week & day by the average
days-per-month constant so they match Stripe), and returns `unpriced_items`
so a stale or incomplete price mirror is surfaced, never a silent undercount.
Additive only: no existing method changed. Covered by MrrMetricsTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a generic conditional-pricing mechanic: a ProductPrice may carry
meta.requires (e.g. {"role":"seat"}) so it only applies to buyers who
already hold a prerequisite (loyalty / bundle pricing). Product::resolvePriceFor
returns the cheapest price the buyer is eligible for, delegating the
'does the buyer satisfy this requirement' test to a host-bound
EntitlementChecker contract. Defaults to DenyAllEntitlementChecker so an
unconfigured host never leaks a discount. Adds requires()/isConditional()/
requiresLabel()/requiresSatisfiedBy() and conditional()/standalone() scopes.
Blax\Shop\Services\TaxService::rates($exempt, $rates=null) is one source of
truth that turns "which rate(s)" (config shop.tax.rates or an explicit list)
and "is this customer exempt" into the array Stripe expects. It fails loud
(TaxRateNotConfiguredException) when shop.tax.require is set and no rate is
configured for a non-exempt charge, instead of silently billing 0% VAT.
New config('shop.tax') block (SHOP_TAX_RATES / SHOP_TAX_REQUIRE). Unit tested.
Consumers (learn-atc #1509) delegate getApplicableTaxRates() here so no path
can apply the rate twice (Stripe rejects duplicate tax rates) or drop VAT to 0%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A seat-based product (meta.seat_based) sold at quantity N provisions N
assignable LicenseSeat rows instead of granting the buyer. Assigning a seat
runs the product's existing ProductAction grants against the seat holder via
an explicit `grantee` (resolution: grantee ?? purchaser ?? subscription->user),
so a seat confers exactly the product's access with zero duplicated grant logic.
- LicenseSeat model: assign / reassign / reclaim / retire / refreshGrant
- SeatService: idempotent provisioning for one-time purchases and subscriptions
(sizes the pool to quantity; renewal extends expiry + re-grants held seats)
- SeatAssigned / SeatReassigned / SeatRevoked events
- Product::isSeatBased(); ProductPurchase + Subscription provision on
completion / lifecycle hooks
- license_seats migration + factory; config `shop.seats` block
- 15 new tests (purchase, subscription, renewal, retire, delegation, gating)
Backward compatible: with no `grantee` the buyer grant is unchanged, and seat
behavior is gated behind the per-product meta flag + config `shop.seats.enabled`.
Full package suite green: 1425 tests / 3812 assertions / 0 failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Added strict types declaration to multiple traits for better type safety.
- Updated method signatures in traits to use nullable types where applicable.
- Improved documentation for traits, including host-model contracts and method descriptions.
- Added new tests to ensure correct behavior of loan checkout and stock management.
- Fixed regression in order number generation to ensure proper string formatting.
- Ensured that currency codes sent to Stripe are consistently lowercased.