fix(stats): count only revenue-less users as connection burn
Burn incremented for every matured user who was not currently net-active, which wrongly booked users who paid and later churned as connect-and-leave leak (and mislabelled it 'never converted'). Restrict burn to users who never earned net revenue: no subscription at all, or paid then refunded. A paid-then- canceled user converted, so their connection cost is no longer burn.
This commit is contained in:
parent
af03a179af
commit
a6024d35ee
|
|
@ -127,7 +127,7 @@ class SendExperimentFunnelReportCommand extends Command
|
|||
[
|
||||
'name' => 'Legend',
|
||||
'value' => sprintf(
|
||||
'Assg = signups · Actd = activated (connected a bank or enabled AI = cost triggered) · Card = completed checkout (card on file) · MatU = matured assigned (cohort old enough to score for this variant) · Paid = live non-refunded subs among MatU · Conv%% = Paid ÷ MatU (net conversion, always ≤100%%, comparable across variants) · ARPU = MRR ÷ MatU (revenue per matured user) · MRR = monthly run-rate of paid subs (yearly ÷ 12) · Cost = est. connection cost of MatU (%s/connection) · Burn = connection cost of matured users not currently paying · CM = MRR − Cost · `pend`/`—` = no matured data yet.',
|
||||
'Assg = signups · Actd = activated (connected a bank or enabled AI = cost triggered) · Card = completed checkout (card on file) · MatU = matured assigned (cohort old enough to score for this variant) · Paid = live non-refunded subs among MatU · Conv%% = Paid ÷ MatU (net conversion, always ≤100%%, comparable across variants) · ARPU = MRR ÷ MatU (revenue per matured user) · MRR = monthly run-rate of paid subs (yearly ÷ 12) · Cost = est. connection cost of MatU (%s/connection) · Burn = connection cost of matured users who never earned net revenue (connected a bank but never paid, or paid then refunded) · CM = MRR − Cost · `pend`/`—` = no matured data yet.',
|
||||
$this->money($report['costPerConnectionCents'], $report['currency']),
|
||||
),
|
||||
'inline' => false,
|
||||
|
|
|
|||
|
|
@ -174,7 +174,12 @@ class ExperimentFunnelCollector
|
|||
}
|
||||
|
||||
$row['mrrCents'] += (int) ($monthlyEquiv[$priceId] ?? 0);
|
||||
} else {
|
||||
} elseif ($subscription === null || $subscription->refunded_at !== null) {
|
||||
// Burn = connections of matured users who never earned
|
||||
// net revenue: connected a bank but never carded, or
|
||||
// paid and got refunded. A user who paid and later
|
||||
// churned (canceled, not refunded) did convert, so
|
||||
// their connection cost is not burn.
|
||||
$row['wastedCostCents'] += $connectionCostCents;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,6 +248,25 @@ it('reports a matured-cohort conversion capped at 100% and prints the matured de
|
|||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('excludes churned payers from burn but keeps refunds as burn', function () {
|
||||
$signup = CarbonImmutable::parse('2026-06-05');
|
||||
|
||||
// Paid then canceled (not refunded): converted, so its cost is NOT burn.
|
||||
experimentUser(SubscriptionExperiment::CONTROL, $signup, [
|
||||
'status' => 'canceled', 'at' => $signup, 'endsAt' => $signup->addDays(20),
|
||||
], connections: 2);
|
||||
// Paid then refunded: zero net revenue, so its cost IS burn.
|
||||
experimentUser(SubscriptionExperiment::CONTROL, $signup, [
|
||||
'status' => 'canceled', 'at' => $signup, 'endsAt' => $signup->addDay(), 'refundedAt' => $signup->addDay(),
|
||||
], connections: 3);
|
||||
|
||||
$control = app(ExperimentFunnelCollector::class)->collect()['variants'][SubscriptionExperiment::CONTROL];
|
||||
|
||||
expect($control['costCents'])->toBe(200) // (2 + 3) × 40, all mature connections
|
||||
->and($control['wastedCostCents'])->toBe(120) // only the refunded user's 3 × 40
|
||||
->and($control['refunded'])->toBe(1);
|
||||
});
|
||||
|
||||
it('scales connection cost by the cost-per-connection argument', function () {
|
||||
$signup = CarbonImmutable::parse('2026-06-05');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue