whisper-money/app/Features/SubscriptionExperiment.php

71 lines
2.2 KiB
PHP

<?php
namespace App\Features;
use App\Models\User;
use Carbon\CarbonImmutable;
/**
* A/B/C assignment for the trial/pricing experiment.
*
* Users who registered before `subscriptions.experiment.started_at` (or any user
* while it is null) are "legacy" and behave like the control group. Everyone who
* registered on or after the start is split evenly into the three variants by a
* stable hash of their id, so the bucket never changes for a given user.
*
* The split is deterministic (crc32(id) % 3) and persisted by Pennant; the funnel
* report reads that persisted value so it always matches what the user was served.
*
* @api
*/
class SubscriptionExperiment
{
public const LEGACY = 'legacy';
public const CONTROL = 'control';
public const REDUCED_TRIAL = 'reduced_trial';
public const PAY_NOW = 'pay_now';
/**
* In-memory override that pins every user to the winning variant once the
* experiment is decided. Returning non-null skips both storage and resolve,
* so flipping SUBSCRIPTION_EXPERIMENT_FORCE_VARIANT rolls the winner out to
* everyone without a deploy and without rewriting stored assignments.
*/
public function before(?User $user): ?string
{
$forced = config('subscriptions.experiment.force_variant');
return in_array($forced, [self::CONTROL, self::REDUCED_TRIAL, self::PAY_NOW], true)
? $forced
: null;
}
public function resolve(?User $user): string
{
$startedAt = config('subscriptions.experiment.started_at');
if ($user === null || $startedAt === null || $user->created_at?->lt(CarbonImmutable::parse($startedAt))) {
return self::LEGACY;
}
return self::bucket((string) $user->getKey());
}
/**
* Deterministic, evenly-split bucket for a post-start user. The funnel report
* mirrors this in PHP to attribute users without reading Pennant per row, so
* keep the formula here as the single source of truth.
*/
public static function bucket(string $key): string
{
return match (crc32($key) % 3) {
0 => self::CONTROL,
1 => self::REDUCED_TRIAL,
default => self::PAY_NOW,
};
}
}