whisper-money/app/Console/Commands
Víctor Falcón 3db03de86d
fix(stats): correct the trial/pricing experiment funnel report (#679)
## Summary

Audited `stats:experiment-funnel` end-to-end (data acquisition, queries,
per-row counting, financial math, and statistics) and fixed the issues
that made it misleading for the win/no-win decision. The report used to
rank variants by an **absolute** contribution-margin total that
mechanically favoured whichever variant matured fastest, and it declared
statistical significance with a normal approximation that is invalid at
the small conversion counts this experiment has.

## What changed

**Presentation & comparability**
- Replace `A2P%` (numerator not a subset of the denominator → could
exceed 100%) with `Conv%` = conversions ÷ matured-assigned — always
≤100% and comparable across variants.
- Print `MatU` (matured cohort size) so the rate denominators are
visible and the table reconciles; revive `ARPU`.
- Reframe the guidance: compare on per-user `Conv%`/`ARPU`, not the
absolute `MRR`/`Cost`/`Burn`/`CM` totals (which scale with `MatU`).

**Data correctness**
- Count soft-deleted users (`withTrashed`) — they were assigned a
variant and their connections incurred real cost.
- Attribute by the deterministic `SubscriptionExperiment::bucket()`
instead of the resolved Pennant flag, so setting `force_variant` (the
winner rollout switch) no longer collapses the whole report onto one
variant. Also stops writing Pennant rows as a side effect.
- Resolve `MRR` for subscriptions on rotated/archived Stripe price ids
(fetch prices by product), warn on any net-active sub whose price is
unmapped, round yearly ÷ 12, and skip foreign-currency prices.
- `Burn` counts only users who never earned net revenue (no
subscription, or paid-then-refunded) — a paid-then-churned user is no
longer booked as connect-and-leave leak.

**Statistics**
- Measure conversion as "ever charged, net of refund" (time-invariant)
instead of a live active-now snapshot that biases older cohorts (which
have had longer to churn).
- Decide significance with **Fisher's exact test** (exact at any sample
size), Bonferroni-corrected over the three arms, instead of the
normal-approx z that overstates evidence when expected cell counts fall
below 5. Add a Newcombe difference-of-proportions CI and a small-sample
caveat.
- Extract the inference into `App\Services\Stats\ProportionSignificance`
(+ a `BinomialProportion` value object), with unit tests that pin the
exact interval and p-value numbers.

## Validation

Reconstructed every column in raw SQL against a production dump — all
reconcile **to the cent / to the row**. Independent recomputation of the
statistics (Wilson, Fisher exact, Newcombe, z) matches the command's
output.

## Testing

26 tests (20 feature + 6 unit, 94 assertions). `pint` and
`phpstan`/larastan green.

## Note

This branch also carries two small pre-existing commits unrelated to the
funnel (`fix(categories): fall back to gray…`, `chore(schedule): stop
scheduling the stuck cohort report`). Happy to split them into their own
PR if preferred.
2026-07-15 09:17:32 +02:00
..
Concerns fix(encryption): never email or delete users who are still being billed (#656) 2026-07-07 16:04:27 +00:00
AgentDatabaseCommand.php feat(console): add agent:db command for querying local and prod DB (#522) 2026-06-12 18:35:14 +02:00
ApplyRealEstateRevaluationCommand.php fix(real-estate): compound annual revaluation monthly (#337) 2026-04-27 07:35:51 +01:00
BackfillAccountIbans.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
BackfillSpaces.php feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
BackfillUserCurrencyCode.php Add Budgeting Feature to Track and Manage Spending (#36) 2026-01-21 15:25:50 +01:00
CancelFreeEnableBankingConnectionsCommand.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
CategorizeBackfillCommand.php AI auto-categorization: open to pro + consent, nudge free users (#561) 2026-06-19 14:08:40 +00:00
CheckBankLogosCommand.php Add weekly bank logo audit command (#211) 2026-03-06 11:05:13 +00:00
DeleteEncryptedDataAccountsCommand.php fix(encryption): never email or delete users who are still being billed (#656) 2026-07-07 16:04:27 +00:00
DeleteUserCommand.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
DisconnectBankingConnectionsCommand.php feat(banking): add command to disconnect connections by id (#497) 2026-06-06 11:16:01 +02:00
E2eBankingFixtureCommand.php test(open-banking): e2e coverage for Enable Banking connection flows (#509) 2026-06-09 11:58:50 +02:00
EnsureLaunchCouponsCommand.php feat(leads): cohort-based launch invitations with per-user Stripe coupons (#333) 2026-04-30 15:10:28 +01:00
FeatureDisableCommand.php Add Budgeting Feature to Track and Manage Spending (#36) 2026-01-21 15:25:50 +01:00
FeatureEnableCommand.php feat(features): support percentage rollouts in feature:enable (#592) 2026-06-25 10:46:30 +00:00
GenerateBudgetPeriods.php fix: Budget period not found on last day of period (#91) 2026-02-01 11:33:10 +01:00
GenerateLandingAuthLinkCommand.php feat(landing): add signed auth links (#312) 2026-04-21 08:28:59 +01:00
GenerateMonthlyLoanBalances.php feat(accounts): add loan amortization projections for loan accounts (#246) 2026-03-26 15:06:09 +01:00
GenerateStripePromotionCodesCommand.php feat(stripe): add promo code generator (#311) 2026-04-20 18:15:28 +01:00
NotifyEncryptedDataRemovalCommand.php fix(encryption): never email or delete users who are still being billed (#656) 2026-07-07 16:04:27 +00:00
ResendLeadVerificationEmailsCommand.php feat: resend verification emails to unverified leads (#287) 2026-04-15 09:13:27 +01:00
ResendSyncCommand.php feat: Sync new users to Resend contacts (#85) 2026-01-28 21:25:58 +01:00
ResendSyncLeadsCommand.php perf(resend): default sync-leads to last 24h window (#354) 2026-05-05 09:57:25 +01:00
ResetDemoAccountCommand.php fix: skip demo reset when demo account is disabled (#626) 2026-07-03 07:27:43 +00:00
ResetUserCategories.php Refactor: Optimize category management in CreateDefaultCategories and ResetUserCategories 2025-12-01 15:37:53 +01:00
RetryLeadFailedJobsCommand.php feat: selective retry of failed lead email jobs (#286) 2026-04-15 08:00:29 +01:00
ReviewIntegrationRequestsCommand.php feat(integration-requests): add done status and fix review command crash on orphaned author (#601) 2026-06-27 14:42:09 +00:00
SendAiCohortReportCommand.php feat(stats): add --no-discord to the remaining report commands (#607) 2026-06-29 15:32:31 +02:00
SendAiConsentFollowUpEmailsCommand.php feat(email): follow up after post-onboarding AI consent (#596) 2026-06-26 17:56:06 +00:00
SendDailyStatsReportCommand.php feat(stats): add --no-discord to the remaining report commands (#607) 2026-06-29 15:32:31 +02:00
SendExperimentFunnelReportCommand.php fix(stats): correct the trial/pricing experiment funnel report (#679) 2026-07-15 09:17:32 +02:00
SendPaywallFollowUpEmailsCommand.php feat(drip): email users stuck on the paywall a day after onboarding (#562) 2026-06-19 14:11:12 +00:00
SendStuckCohortReportCommand.php feat(stats): add --no-discord to the remaining report commands (#607) 2026-06-29 15:32:31 +02:00
SendSubscriptionCancelledEmailCommand.php Add subscription cancellation email with manual trigger command (#47) 2025-12-30 20:32:44 +01:00
SendSubscriptionFunnelReportCommand.php feat(stats): add --no-discord to the remaining report commands (#607) 2026-06-29 15:32:31 +02:00
SendTestEmails.php chore(commands): unify command signatures 2025-12-30 07:22:18 +01:00
SendUpdateEmailCommand.php fix: delay emails to avoid reaching daily resend limit 2026-01-09 11:11:38 +01:00
SendUserLeadInvitations.php fix: keep lead invite command aliases (#406) 2026-05-20 12:26:21 +02:00
SendUserLeadReInvitations.php feat(leads): add user lead re-invite campaign (#432) 2026-05-26 08:35:31 +02:00
SetBankLogoCommand.php fix(banks:set-logo): add JPEG support test coverage and prompt for missing arguments (#214) 2026-03-07 16:33:54 +00:00
SetupMainUser.php fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183) 2026-03-02 12:22:30 +00:00
StripeSubscriptionStatsCommand.php feat: add Discord admin feed for daily stats and Stripe events (#458) 2026-05-30 18:14:46 +02:00
SuggestRulesCommand.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
SyncBankingConnections.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
SyncStripePricesCommand.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
TestScheduledDripEmailCommand.php Test mail drip command 2025-12-30 07:22:18 +01:00
VerifyRefundFlowCommand.php feat(subscriptions): trial/pricing A/B/C experiment (#600) 2026-06-27 18:00:15 +02:00