whisper-money/app/Models
Víctor Falcón 33fc058f3c
fix(demo): stop demo:reset from colliding on the fake Stripe subscription id (#756)
Fixes
[PHP-LARAVEL-5B](https://whisper-money.sentry.io/issues/PHP-LARAVEL-5B).

## What broke

`demo:reset` seeds a fabricated Stripe subscription so the demo account
gets Pro access without touching Stripe. The id was the hardcoded
literal `sub_demo_free_forever`, and `subscriptions.stripe_id` is
unique.

`--email` (#753, shipped this morning) made a second seeded account
possible. `createSubscription()` deletes only the *current* user's
subscriptions before inserting, so the first `demo:reset --email` run
after the public demo account existed died on:

```
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
'sub_demo_free_forever' for key 'subscriptions.subscriptions_stripe_id_unique'
```

`createSubscription()` is the last step of `handle()`, so the reviewer
account was left fully seeded but with **no subscription at all** — an
app-store reviewer signing in lands on the paywall instead of the Pro
app.

## The fix

Derive the fake id from the user: `sub_demo_{$user->id}`. Matches what
`E2eBankingFixtureCommand` already does (`sub_e2e_.$user->id`). Nothing
reads the old literal — entitlement goes through Cashier's
`subscribed('default')`, which only looks at `stripe_status`. Production
is self-healing: the next run replaces the old row, and the demo account
keeps working until then.

## Second commit: seeded accounts and the Stripe billing paths

Fixing the collision means reviewer accounts now genuinely have
`hasProPlan() === true`, which switches on paths that were unreachable
while they were paywalled. `isDemoAccount()` is equality against
`config('app.demo.email')`, so an `--email` account is **not** a demo
account and skipped every existing guard:

- `SubscriptionController::billingPortal` would call
`createAsStripeCustomer()` — creating a real Stripe customer in
production — and drop the reviewer on an empty portal.
- Bucketed into `PAY_NOW`, `canSelfRefund()` returns true, so the refund
box renders; `RefundSelfServe::handle` then calls
`$subscription->latestPayment()` on `sub_demo_<uuid>` → Stripe 404,
thrown above the `try`, 500 plus a false `🔴 Self-service refund FAILED —
the user may have been charged without a refund` Discord alert.

`User::hasSeededSubscription()` keys off the fabricated id prefix rather
than one hardcoded email, so it also covers the e2e fixture account.
Real Stripe ids are `sub_` + alphanumerics with no further underscore,
so no paying user can match it.

## Tests

- `demo:reset subscribes a named account even when the public demo
account already exists` — seeds the public demo account, then a named
one. Verified it fails on the pre-fix code with the **identical**
SQLSTATE 1062 message from the Sentry event, and asserts the two ids
differ so a regression back to a shared literal is caught.
- `a seeded reviewer account cannot reach the billing portal`, `blocks a
self-refund on a seeded demo subscription`.

Local: `ResetDemoAccountCommandTest` 7/7, `SelfServeRefundTest` +
`DemoAccountRestrictionsTest` + `SubscriptionTest` 59/59.

## Not fixed here (found during review, out of scope)

- `ResetDemoAccountCommand` falls back to
`Bank::factory()->create(['user_id' => null])` when a named bank is
missing, permanently adding randomly-named **global** banks visible to
every user. `firstOrCreate(['name' => …, 'user_id' => null])` would fix
it.
- Seeded accounts count as paid in `SubscriptionFunnelCollector` /
`ExperimentFunnelCollector` unless their email is in
`AI_SUGGESTIONS_REPORT_EXCLUDED_EMAILS`.
- `handle()` runs without a transaction, so a failure mid-reseed still
leaves a half-seeded account.
2026-08-10 12:48:13 +00:00
..
Concerns feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
Account.php feat(import): persist per-account import configuration on the backend (#698) 2026-07-18 16:10:40 +02:00
AccountBalance.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
AccountImportConfig.php feat(import): persist per-account import configuration on the backend (#698) 2026-07-18 16:10:40 +02:00
AiConsent.php feat(email): follow up after post-onboarding AI consent (#596) 2026-06-26 17:56:06 +00:00
AutomationRule.php fix(banking): strip the ISO 20022 remittance tag from transaction descriptions (#744) 2026-08-09 18:11:05 +02:00
AutomationRuleLabel.php feat: add transaction labels feature (#24) 2025-12-13 13:02:19 +01:00
Bank.php refactor(api): standardize serialization via model $hidden (#492) 2026-06-05 13:57:34 +02:00
BankingConnection.php fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746) 2026-08-09 18:41:35 +02:00
BankingSyncLog.php fix(banking): retry failed sync connections and log every sync attempt (#251) 2026-03-31 11:34:35 +01:00
Budget.php feat(budgets): add per-budget email notifications (#731) 2026-07-24 12:52:03 +02:00
BudgetPeriod.php feat(budgets): add per-budget email notifications (#731) 2026-07-24 12:52:03 +02:00
BudgetTransaction.php fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183) 2026-03-02 12:22:30 +00:00
Category.php feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
CategoryCorrection.php feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
EncryptedMessage.php fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183) 2026-03-02 12:22:30 +00:00
ExchangeRate.php Preload exchange rates (#362) 2026-05-06 16:35:49 +01:00
IntegrationRequest.php feat(integration-requests): add not-doable status with a public comment (#552) 2026-06-17 16:36:32 +02:00
IntegrationRequestVote.php feat(integration-requests): community board to request & vote bank integrations (#550) 2026-06-17 12:50:51 +00:00
Label.php feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
LabelTransaction.php feat: add transaction labels feature (#24) 2025-12-13 13:02:19 +01:00
LoanDetail.php refactor(api): standardize serialization via model $hidden (#492) 2026-06-05 13:57:34 +02:00
RealEstateDetail.php refactor(api): standardize serialization via model $hidden (#492) 2026-06-05 13:57:34 +02:00
RuleSuggestion.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
SavedFilter.php feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
Space.php feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
SpaceInvitation.php feat(spaces): phase 0 — multi-tenant Space foundation (no behaviour change) (#650) 2026-07-09 14:26:07 +02:00
StuckCohortSnapshot.php feat(stats): weekly paywall stuck-cohort report to Discord (#563) 2026-06-19 14:12:51 +00:00
SuggestionRun.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
Transaction.php feat(transactions): allow editing all fields of manual transactions (#683) 2026-07-16 08:59:10 +02:00
User.php fix(demo): stop demo:reset from colliding on the fake Stripe subscription id (#756) 2026-08-10 12:48:13 +00:00
UserLead.php refactor: remove HIDE_AUTH_BUTTONS launch gate and waitlist apparatus (#717) 2026-07-22 08:51:48 +02:00
UserMailLog.php feat: Send custom emails to users (#52) 2026-01-09 09:33:19 +01:00
UserSetting.php feat(budgets): add per-budget email notifications (#731) 2026-07-24 12:52:03 +02:00