chore: remove legacy plan tiers (starter/plus/pro/max)
All customers were migrated to the single Workspace plan and the old plan rows were deleted in production, so drop the now-dead legacy tiers from the code: reduce the Plan Slug enum to Workspace only and default the PlanFactory to it. Rework StripeEventListenerTest around the single Workspace plan (its monthly and yearly price ids still exercise plan-by-price mapping, trial clearing, deletion, and previous-plan propagation), and point the remaining tests that referenced the starter/pro slugs at the seeded Workspace plan.
This commit is contained in:
parent
11319cb45a
commit
21e45b4847
9 changed files with 87 additions and 187 deletions
|
|
@ -6,19 +6,11 @@
|
|||
|
||||
enum Slug: string
|
||||
{
|
||||
case Starter = 'starter';
|
||||
case Plus = 'plus';
|
||||
case Pro = 'pro';
|
||||
case Max = 'max';
|
||||
case Workspace = 'workspace';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Starter => 'Starter',
|
||||
self::Plus => 'Plus',
|
||||
self::Pro => 'Pro',
|
||||
self::Max => 'Max',
|
||||
self::Workspace => 'Workspace',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ class PlanFactory extends Factory
|
|||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'slug' => fake()->randomElement(Slug::cases()),
|
||||
'name' => fake()->word(),
|
||||
'slug' => Slug::Workspace,
|
||||
'name' => 'Workspace',
|
||||
'stripe_monthly_price_id' => null,
|
||||
'stripe_yearly_price_id' => null,
|
||||
'monthly_credits_limit' => 2500,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id,
|
||||
'plan_id' => Plan::query()->where('slug', 'workspace')->first()?->id,
|
||||
]);
|
||||
$this->user = User::factory()->create(['account_id' => $this->account->id]);
|
||||
$this->account->update(['owner_id' => $this->user->id]);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id,
|
||||
'plan_id' => Plan::query()->where('slug', 'workspace')->first()?->id,
|
||||
]);
|
||||
$this->user = User::factory()->create(['account_id' => $this->account->id]);
|
||||
$this->account->update(['owner_id' => $this->user->id]);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id,
|
||||
'plan_id' => Plan::query()->where('slug', 'workspace')->first()?->id,
|
||||
]);
|
||||
$this->user = User::factory()->create(['account_id' => $this->account->id]);
|
||||
$this->account->update(['owner_id' => $this->user->id]);
|
||||
|
|
|
|||
|
|
@ -15,17 +15,12 @@
|
|||
beforeEach(function () {
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
Plan::factory()->create([
|
||||
'slug' => Slug::Starter,
|
||||
'name' => 'Starter',
|
||||
'stripe_monthly_price_id' => 'price_starter_monthly',
|
||||
'stripe_yearly_price_id' => 'price_starter_yearly',
|
||||
]);
|
||||
Plan::factory()->create([
|
||||
'slug' => Slug::Pro,
|
||||
'name' => 'Pro',
|
||||
'stripe_monthly_price_id' => 'price_pro_monthly',
|
||||
'stripe_yearly_price_id' => 'price_pro_yearly',
|
||||
// Use the seeded Workspace plan; set deterministic price ids so the
|
||||
// assertions don't depend on `.env.testing`.
|
||||
$this->plan = Plan::where('slug', Slug::Workspace)->firstOrFail();
|
||||
$this->plan->update([
|
||||
'stripe_monthly_price_id' => 'price_workspace_monthly',
|
||||
'stripe_yearly_price_id' => 'price_workspace_yearly',
|
||||
]);
|
||||
|
||||
$this->account = Account::factory()->create(['stripe_id' => 'cus_test123']);
|
||||
|
|
@ -51,7 +46,6 @@
|
|||
});
|
||||
|
||||
test('subscription created clears the generic trial_ends_at on the account', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['trial_ends_at' => now()->addDays(3)]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
|
|
@ -60,7 +54,7 @@
|
|||
'customer' => 'cus_test123',
|
||||
'id' => 'sub_123',
|
||||
'status' => 'active',
|
||||
'items' => ['data' => [['price' => ['id' => $starter->stripe_monthly_price_id]]]],
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
|
|
@ -184,8 +178,7 @@
|
|||
});
|
||||
|
||||
test('subscription deleted dispatches TrackBilling with subscription.cancelled event', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
$this->account->update(['plan_id' => $this->plan->id]);
|
||||
|
||||
Bus::fake([TrackBilling::class]);
|
||||
|
||||
|
|
@ -236,116 +229,81 @@
|
|||
});
|
||||
|
||||
// ========================================
|
||||
// Plan sync — domain logic
|
||||
// Plan mapping by Stripe price id
|
||||
// ========================================
|
||||
//
|
||||
// Tests below override the seeded plans' Stripe price ids with deterministic
|
||||
// values so the assertions don't depend on `.env.testing` having
|
||||
// `STRIPE_*_MONTHLY/YEARLY` set.
|
||||
|
||||
beforeEach(function () {
|
||||
Plan::query()->where('slug', 'starter')->update([
|
||||
'stripe_monthly_price_id' => 'price_starter_monthly',
|
||||
'stripe_yearly_price_id' => 'price_starter_yearly',
|
||||
]);
|
||||
Plan::query()->where('slug', 'pro')->update([
|
||||
'stripe_monthly_price_id' => 'price_pro_monthly',
|
||||
'stripe_yearly_price_id' => 'price_pro_yearly',
|
||||
]);
|
||||
});
|
||||
|
||||
test('subscription updated swaps account plan_id when price matches a different plan', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$pro = Plan::query()->where('slug', 'pro')->firstOrFail();
|
||||
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [
|
||||
['price' => ['id' => 'price_pro_monthly']],
|
||||
]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($pro->id);
|
||||
});
|
||||
|
||||
test('subscription updated leaves plan_id alone when price already matches current plan', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [
|
||||
['price' => ['id' => 'price_starter_monthly']],
|
||||
]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($starter->id);
|
||||
});
|
||||
|
||||
test('subscription updated ignores unknown price ids without erroring', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [
|
||||
['price' => ['id' => 'price_unknown_xyz']],
|
||||
]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($starter->id);
|
||||
});
|
||||
|
||||
test('subscription updated matches yearly price ids too', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$pro = Plan::query()->where('slug', 'pro')->firstOrFail();
|
||||
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [
|
||||
['price' => ['id' => 'price_pro_yearly']],
|
||||
]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($pro->id);
|
||||
});
|
||||
|
||||
test('subscription created syncs plan from price ids on first activation', function () {
|
||||
$pro = Plan::query()->where('slug', 'pro')->firstOrFail();
|
||||
test('subscription created syncs the plan from the price id on first activation', function () {
|
||||
$this->account->update(['plan_id' => null]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.created',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [
|
||||
['price' => ['id' => 'price_pro_monthly']],
|
||||
]],
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($pro->id);
|
||||
expect($this->account->fresh()->plan_id)->toBe($this->plan->id);
|
||||
});
|
||||
|
||||
test('subscription updated maps the plan by its monthly price id', function () {
|
||||
$this->account->update(['plan_id' => null]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($this->plan->id);
|
||||
});
|
||||
|
||||
test('subscription updated maps the plan by its yearly price id too', function () {
|
||||
$this->account->update(['plan_id' => null]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_yearly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($this->plan->id);
|
||||
});
|
||||
|
||||
test('subscription updated leaves plan_id alone when the price already matches', function () {
|
||||
$this->account->update(['plan_id' => $this->plan->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($this->plan->id);
|
||||
});
|
||||
|
||||
test('subscription updated ignores unknown price ids without erroring', function () {
|
||||
$this->account->update(['plan_id' => $this->plan->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_unknown_xyz']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($this->plan->id);
|
||||
});
|
||||
|
||||
test('subscription deleted clears the account plan_id', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
$this->account->update(['plan_id' => $this->plan->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.deleted',
|
||||
|
|
@ -377,8 +335,7 @@
|
|||
// ========================================
|
||||
|
||||
test('subscription updated forwards the previous plan name to TrackBilling', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
$this->account->update(['plan_id' => $this->plan->id]);
|
||||
|
||||
Bus::fake([TrackBilling::class]);
|
||||
|
||||
|
|
@ -386,19 +343,18 @@
|
|||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_pro_monthly']]]],
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
Bus::assertDispatched(
|
||||
TrackBilling::class,
|
||||
fn ($job) => $job->event === BillingEvent::Updated && $job->previousPlan === $starter->name,
|
||||
fn ($job) => $job->event === BillingEvent::Updated && $job->previousPlan === $this->plan->name,
|
||||
);
|
||||
});
|
||||
|
||||
test('subscription deleted forwards the previous plan name to TrackBilling', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
$this->account->update(['plan_id' => $this->plan->id]);
|
||||
|
||||
Bus::fake([TrackBilling::class]);
|
||||
|
||||
|
|
@ -409,7 +365,7 @@
|
|||
|
||||
Bus::assertDispatched(
|
||||
TrackBilling::class,
|
||||
fn ($job) => $job->event === BillingEvent::Cancelled && $job->previousPlan === $starter->name,
|
||||
fn ($job) => $job->event === BillingEvent::Cancelled && $job->previousPlan === $this->plan->name,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -422,7 +378,7 @@
|
|||
'type' => 'customer.subscription.created',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_starter_monthly']]]],
|
||||
'items' => ['data' => [['price' => ['id' => 'price_workspace_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
|
|
@ -431,51 +387,3 @@
|
|||
fn ($job) => $job->event === BillingEvent::Created && $job->previousPlan === null,
|
||||
);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Plan mapping by Stripe price id
|
||||
// ========================================
|
||||
|
||||
test('subscription updated maps the plan by price id', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$pro = Plan::query()->where('slug', 'pro')->firstOrFail();
|
||||
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_pro_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($pro->id);
|
||||
});
|
||||
|
||||
test('subscription updated maps an archived legacy plan by price id', function () {
|
||||
$pro = Plan::query()->where('slug', 'pro')->firstOrFail();
|
||||
$pro->update(['is_archived' => true]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.updated',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'items' => ['data' => [['price' => ['id' => 'price_pro_monthly']]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBe($pro->id);
|
||||
});
|
||||
|
||||
test('subscription deleted clears the plan_id', function () {
|
||||
$starter = Plan::query()->where('slug', 'starter')->firstOrFail();
|
||||
$this->account->update(['plan_id' => $starter->id]);
|
||||
|
||||
$this->listener->handle(new WebhookReceived([
|
||||
'type' => 'customer.subscription.deleted',
|
||||
'data' => ['object' => ['customer' => 'cus_test123']],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->plan_id)->toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@
|
|||
use App\Models\Plan;
|
||||
|
||||
test('plan can be created with factory', function () {
|
||||
Plan::where('slug', Slug::Pro)->delete();
|
||||
Plan::where('slug', Slug::Workspace)->delete();
|
||||
|
||||
$plan = Plan::factory()->create([
|
||||
'slug' => Slug::Pro,
|
||||
'name' => 'Pro',
|
||||
'slug' => Slug::Workspace,
|
||||
'name' => 'Workspace',
|
||||
]);
|
||||
|
||||
expect($plan)->toBeInstanceOf(Plan::class)
|
||||
->and($plan->slug)->toBe(Slug::Pro)
|
||||
->and($plan->name)->toBe('Pro')
|
||||
->and($plan->slug)->toBe(Slug::Workspace)
|
||||
->and($plan->name)->toBe('Workspace')
|
||||
->and($plan->is_archived)->toBeFalse();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@
|
|||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => null,
|
||||
'stripe_id' => 'cus_test_'.fake()->uuid(),
|
||||
'plan_id' => Plan::where('slug', Slug::Starter)->value('id'),
|
||||
'plan_id' => Plan::where('slug', Slug::Workspace)->value('id'),
|
||||
]);
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
Queue::fake();
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$plan = Plan::query()->where('slug', 'starter')->first();
|
||||
$plan = Plan::query()->where('slug', 'workspace')->first();
|
||||
$account = Account::factory()->create(['plan_id' => $plan?->id]);
|
||||
|
||||
$service = new PostHogService;
|
||||
|
|
|
|||
Loading…
Reference in a new issue