Add SUBSCRIPTION_EXPERIMENT_FORCE_VARIANT: a Pennant before() override that
serves one variant to every user, so once a winner is chosen the split ends
with an env flip (no deploy, stored assignments untouched). Invalid values are
ignored and fall through to normal assignment.
- Refund is now idempotent and crash-safe: stamp refunded_at right after the
refund succeeds, then cancel + disconnect best-effort in a try/catch (logged),
and bail if already refunded. Drop the no-op DB::transaction.
- Funnel report attributes users by the variant Pennant actually served (eager
loaded per chunk) instead of recomputing the formula, so it can't drift from
what users experienced.
- Paywall states the exact amount charged today for pay_now (yearly upfront).
- Throttle + block-demo middleware on the refund route; drop the inline check.
- resolve(?User) nullable to match sibling features; resolve the variant once in
offerFor; add French translations.
Resolve trial days per plan from the user's experiment variant instead of
the static plan config, so reduced_trial gets the shorter trial and pay_now
is charged immediately (zero trial). Expose the offer descriptor on the
paywall so the frontend can show the trial/money-back terms.
Add the A/B/C experiment foundation: a Pennant feature that buckets users
who register on or after subscriptions.experiment.started_at evenly into
control / reduced_trial / pay_now via a stable hash, leaving earlier users
"legacy". The ExperimentOffer service turns a variant into the concrete
offer (trial days per plan, pay-now flag, refund window, self-refund
eligibility) for the checkout, paywall and billing screens to share.
While started_at is null the experiment is off and everyone behaves like
control, so this is inert until activated via env.