whisper-money/resources/js/components
Víctor Falcón b6bc7756bc
feat(subscriptions): end the trial experiment and make the trial length per plan (#762)
Ends the trial/pricing A/B/C experiment. Everyone gets the control offer
— a free trial — and the trial length becomes a per-plan setting.

## Trial length

| Plan | Before | Now | Env override |
|---|---|---|---|
| Yearly | 15 days | **15 days** | `STRIPE_PRO_YEARLY_TRIAL_DAYS` |
| Monthly | 15 days | **7 days** | `STRIPE_PRO_MONTHLY_TRIAL_DAYS` |

**Note that monthly 15 → 7 is a new bet, not a rollback.** The control
arm was 15 days on both plans, and 7 days on monthly is a value the
experiment never tested (`reduced_trial` was monthly 3 / yearly 7). The
rationale is that the longer commitment earns the longer trial; it ships
here at the same time as the instrument that could measure it is
removed, so it will not be measurable as an isolated effect.

## Final experiment numbers

Archived here because `stats:experiment-funnel` and its collector are
deleted by this PR and the purge migration's `down()` is a no-op.

| Variant | Assigned | Subscribed | Active | Refunded |
|---|---|---|---|---|
| control | 597 | 46 | 13 | 0 |
| reduced_trial | 590 | 50 | 10 | 0 |
| pay_now | 609 | 41 | 21 | 18 |
| legacy | 94 | 55 | 31 | 0 |

## What is deleted

- `App\Features\SubscriptionExperiment` (the Pennant A/B/C assignment)
and the `ExperimentOffer` service.
- The `pay_now` self-service refund: `RefundSelfServe`, the
`settings.billing.refund` route, the controller actions and Discord
embeds, the money-back card in billing settings, and the
`stripe:verify-refund` sandbox command.
- The weekly `stats:experiment-funnel` report, its collector, and the
`ProportionSignificance` / `BinomialProportion` helpers it was the only
caller of, plus its schedule entry.
- The `subscriptions.experiment.*` config block and the orphaned
`es`/`fr` translation strings.
- A data migration purges the ~1,890 stored Pennant assignments.

`subscriptions.refunded_at` is deliberately **kept**: nothing reads it
anymore, but it is the only record of the 18 refunds the experiment
issued. The migration carries a comment saying so.

## Fixes found in review

- **The surviving funnel report was mis-scoring conversions.**
`SubscriptionFunnelCollector` compared every cancellation to one global
trial length. With trials now diverging per plan, a monthly subscriber
who was billed and cancelled on day 10 was scored as never having paid.
It now reads each subscription's own `trial_ends_at`, and the longest
plan trial is used only for deciding when a cohort is old enough to
score. Covered by two new tests.
- **The trial length swapped silently.** It lived on a single line under
the plan selector, which rewrote itself when the user switched plan. Now
that the plans genuinely differ, each plan card shows its own length.
- The report legend no longer quotes a single trial length for both
plans, and warns that the experiment weeks are still inside its window.

## Before merging

- [x] **Unset `SUBSCRIPTION_EXPERIMENT_STARTED_AT` in production** so no
new `pay_now` assignment happens while this waits. Anyone who checks out
under `pay_now` between now and the deploy is charged upfront and then
loses the one-tap refund they were promised at the point of payment.
Checked just before opening this PR: **0 `pay_now` subscriptions
currently inside the 3-day window**, so nobody is stranded today.
- [x] Drop the now-orphaned `SUBSCRIPTION_EXPERIMENT_*` variables from
the production env with the deploy.
- [x] If old containers are still serving while the purge migration
runs, a few assignments can be re-resolved and reappear. Harmless —
re-run `php artisan pennant:purge "App\Features\SubscriptionExperiment"`
once the deploy settles if you want the table clean.

Support note: a manual Stripe refund for a former `pay_now` user will
not disconnect their bank connections, which the automated flow used to
do.

## Demo


https://github.com/user-attachments/assets/3614d488-05c6-405d-a687-bbf45746879a


<!-- PLACEHOLDER: drag the QA video here -->

## QA

Browser-tested against the running app:

- Paywall: annual card shows "15 days free", monthly card "7 days free";
the terms line under the selector follows the selected plan (15 ↔ 7);
mobile viewport renders fine.
- Billing settings: no money-back card for a free user or an active
subscriber; `POST /settings/billing/refund` returns 404.
- No console or network errors on any screen.
- `stats:subscription-funnel` still renders and posts.
- The purge migration leaves 0 `SubscriptionExperiment` rows.

Full suite green (2045 tests) apart from the known local-only
`DashboardTest` 409; `pint`, `lint`, `format` and `build` all clean.
2026-08-12 10:59:55 +02:00
..
accounts feat(accounts): count shared accounts at the owner's percentage (#750) 2026-08-11 13:26:54 +00:00
automation-rules fix(security): scope job-status endpoints to owner + feature-area fixes (#627) 2026-07-03 14:49:32 +02:00
budgets feat: add catch-all budgets (#527) 2026-06-15 16:07:19 +00:00
cashflow feat(analysis): per-category 12-month spending drawer (#519) 2026-06-11 09:52:53 +02:00
categories refactor(frontend): call backend routes through Wayfinder instead of literal URLs (#772) 2026-08-11 13:26:10 +00:00
charts feat(cashflow): expand income categories and fix Sankey label overlap (#672) 2026-07-13 07:28:14 +00:00
dashboard refactor(frontend): call backend routes through Wayfinder instead of literal URLs (#772) 2026-08-11 13:26:10 +00:00
icons Subscriptions (#15) 2025-12-06 19:09:56 +01:00
integration-requests feat(integration-requests): add done status and fix review command crash on orphaned author (#601) 2026-06-27 14:42:09 +00:00
labels refactor(js): use InputError for inline form errors (#483) 2026-06-03 19:01:21 +02:00
landing feat(landing): stop auto-redirecting signed-in visitors to the dashboard (#777)a 2026-08-11 14:02:18 +00:00
onboarding fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
open-banking feat: reuse the upgrade modal at more upsell points and attribute revenue (#699) 2026-07-18 12:53:20 +00:00
partials feat(roadmap): add a public roadmap page mirrored from UserJot (#778) 2026-08-11 14:14:59 +00:00
shared refactor(settings): move the last two settings tables onto the shared table and hook (#775) 2026-08-11 13:54:18 +00:00
subscription feat(subscriptions): end the trial experiment and make the trial length per plan (#762) 2026-08-12 10:59:55 +02:00
transactions fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
ui fix(dashboard): render negative net worth as a downward bar (#722) 2026-07-22 09:32:18 +00:00
alert-error.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
app-content.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
app-error-boundary.test.tsx fix(ui): show a recoverable screen instead of a blank page when render throws (#758) 2026-08-10 15:02:39 +00:00
app-error-boundary.tsx fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
app-logo-icon.tsx refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
app-logo.tsx Add animated logo icon that transitions based on encryption key state 2025-12-12 11:24:42 +01:00
app-shell.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
app-sidebar-header.tsx fix: keep iOS content below the notch (#280) 2026-04-13 13:59:05 +01:00
app-sidebar.tsx fix: move community link to user menu (#442) 2026-05-27 17:39:26 +02:00
appearance-tabs.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
bank-logo.tsx feat: Integrate EnableBanking as open banking provider (#106) 2026-02-12 09:09:28 +01:00
breadcrumbs.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
delete-user.tsx fix(account): block deletion while subscription or trial is active (#531) 2026-06-14 20:46:44 +02:00
encryption-key-button.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
heading-small.tsx Add Budgeting Feature to Track and Manage Spending (#36) 2026-01-21 15:25:50 +01:00
heading.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
icon.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
input-error.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
mobile-back-button.tsx fix(haptics): use a local WebHaptics wrapper (#225) 2026-03-16 11:26:04 +00:00
nav-footer.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
nav-main.tsx fix(haptics): use a local WebHaptics wrapper (#225) 2026-03-16 11:26:04 +00:00
nav-user.tsx feat(integration-requests): community board to request & vote bank integrations (#550) 2026-06-17 12:50:51 +00:00
pro-badge.tsx feat(mcp): read-only MCP server for Pro accounts (#689) 2026-07-17 16:54:15 +02:00
sortable-grid.test.tsx fix(accounts): stop second long-press haptic on drag handle (#578) 2026-06-22 11:20:57 +02:00
sortable-grid.tsx fix(accounts): stop second long-press haptic on drag handle (#578) 2026-06-22 11:20:57 +02:00
support-dialog.tsx feat(support): add support link with community-first help modal (#542) 2026-06-16 13:16:04 +02:00
text-link.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
two-factor-recovery-codes.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
two-factor-setup-modal.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
unlock-message-dialog.tsx feat(auth): show/hide toggle on password fields (#499) 2026-06-06 11:42:20 +02:00
user-info.tsx feat: Update facehash and enable blink 2026-02-24 15:54:49 +01:00
user-menu-content.test.tsx feat(feedback): move feedback and roadmap links from Canny to UserJot (#748) 2026-08-10 06:54:21 +00:00
user-menu-content.tsx feat(feedback): move feedback and roadmap links from Canny to UserJot (#748) 2026-08-10 06:54:21 +00:00