From b8e1f9c2d857e66e64dd655300fb948a88686079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Wed, 15 Jul 2026 07:40:18 +0200 Subject: [PATCH] fix(stats): count soft-deleted users in the experiment funnel The base user query dropped soft-deleted accounts (SoftDeletes scope), so their variant assignment, connection cost and burn vanished from the report - biased asymmetrically (prod: 8/9/10 deleted across control/reduced/pay_now) and precisely on the cohort that most embodies connect-and-leave, since deleting the account is the strongest form of it. Add withTrashed(). --- app/Services/Stats/ExperimentFunnelCollector.php | 4 ++++ .../SendExperimentFunnelReportCommandTest.php | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/Services/Stats/ExperimentFunnelCollector.php b/app/Services/Stats/ExperimentFunnelCollector.php index 6dec2525..2fbed267 100644 --- a/app/Services/Stats/ExperimentFunnelCollector.php +++ b/app/Services/Stats/ExperimentFunnelCollector.php @@ -94,6 +94,10 @@ class ExperimentFunnelCollector $monthlyEquiv = $this->monthlyEquivByPriceId(); User::query() + // Soft-deleted accounts still count: they were assigned a variant and + // their bank connections incurred real cost, and deleting the account + // is itself an experiment outcome (the strongest "connect and leave"). + ->withTrashed() ->where('users.created_at', '>=', $startedAt) ->when($excluded !== [], fn ($query) => $query->whereNotIn('email', $excluded)) ->with(['subscriptions' => fn ($query) => $query->where('type', 'default')]) diff --git a/tests/Feature/SendExperimentFunnelReportCommandTest.php b/tests/Feature/SendExperimentFunnelReportCommandTest.php index 0f83638b..e26d74a6 100644 --- a/tests/Feature/SendExperimentFunnelReportCommandTest.php +++ b/tests/Feature/SendExperimentFunnelReportCommandTest.php @@ -173,6 +173,21 @@ it('computes connection cost, wasted burn and contribution margin', function () ->and($control['activationToPaidRate'])->toBe(0.5); // 1 paid ÷ 2 activated }); +it('still counts soft-deleted users so their assignment and connection cost survive', function () { + $signup = CarbonImmutable::parse('2026-06-05'); + + $user = experimentUser(SubscriptionExperiment::CONTROL, $signup, null, connections: 2); + $user->delete(); // soft delete the account + + $control = app(ExperimentFunnelCollector::class)->collect()['variants'][SubscriptionExperiment::CONTROL]; + + expect($control['assigned'])->toBe(1) + ->and($control['assignedMature'])->toBe(1) + ->and($control['activated'])->toBe(1) // 2 connections + ->and($control['costCents'])->toBe(80) // 2 × 40, still charged + ->and($control['wastedCostCents'])->toBe(80); // never paid → pure burn +}); + it('reports a matured-cohort conversion capped at 100% and prints the matured denominator', function () { $signup = CarbonImmutable::parse('2026-06-05'); // control matures by the test clock