Merge pull request #38 from trypostit/feat/trial-without-card
feat(signup): no-card 7-day trial on Starter plan
This commit is contained in:
commit
a9e8efffea
20 changed files with 415 additions and 28 deletions
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\Enums\Plan\Slug;
|
||||
use App\Jobs\PostHog\SyncUser;
|
||||
use App\Models\Account;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\PostHogService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -24,6 +26,8 @@ public static function execute(array $data, array $utmParameters = []): User
|
|||
$account = Account::create([
|
||||
'name' => data_get($data, 'name')."'s Account",
|
||||
'billing_email' => data_get($data, 'email'),
|
||||
'plan_id' => Plan::where('slug', Slug::Starter)->value('id'),
|
||||
'trial_ends_at' => now()->addDays(config('cashier.trial_days')),
|
||||
]);
|
||||
|
||||
$user = User::create(array_merge([
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ public function subscribe(Request $request): Response|RedirectResponse
|
|||
|
||||
return Inertia::render('billing/Subscribe', [
|
||||
'plans' => Plan::active()->orderBy('sort')->get(),
|
||||
'trialDays' => config('cashier.trial_days'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +62,7 @@ public function checkout(Request $request, Plan $plan): SymfonyResponse|Redirect
|
|||
]);
|
||||
|
||||
$subscription = $account->newSubscription(Account::SUBSCRIPTION_NAME, $priceId)
|
||||
->allowPromotionCodes()
|
||||
->trialDays(config('cashier.trial_days'));
|
||||
->allowPromotionCodes();
|
||||
|
||||
$checkoutSession = $subscription->checkout([
|
||||
'success_url' => route('app.billing.processing').'?session_id={CHECKOUT_SESSION_ID}',
|
||||
|
|
@ -138,8 +136,8 @@ public function index(Request $request): Response|RedirectResponse
|
|||
|
||||
return Inertia::render('settings/account/Billing', [
|
||||
'hasSubscription' => $account->subscribed(Account::SUBSCRIPTION_NAME),
|
||||
'onTrial' => $subscription?->onTrial() ?? false,
|
||||
'trialEndsAt' => $subscription?->trial_ends_at,
|
||||
'onTrial' => $account->isOnTrial(),
|
||||
'trialEndsAt' => $account->activeTrialEndsAt(),
|
||||
'subscription' => $subscription?->only([
|
||||
'stripe_status',
|
||||
'ends_at',
|
||||
|
|
|
|||
|
|
@ -24,8 +24,15 @@ public function handle(Request $request, Closure $next): Response
|
|||
|
||||
$account = $user->account;
|
||||
|
||||
if (! config('trypost.self_hosted') && (! $account || ! $account->subscribed(Account::SUBSCRIPTION_NAME))) {
|
||||
return redirect()->route('app.subscribe');
|
||||
if (! config('trypost.self_hosted')) {
|
||||
$hasAccess = $account && (
|
||||
$account->subscribed(Account::SUBSCRIPTION_NAME)
|
||||
|| $account->isOnTrial()
|
||||
);
|
||||
|
||||
if (! $hasAccess) {
|
||||
return redirect()->route('app.subscribe');
|
||||
}
|
||||
}
|
||||
|
||||
if (! $user->workspaces()->exists()) {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ public function share(Request $request): array
|
|||
'selfHosted' => $isSelfHosted,
|
||||
'googleAuthEnabled' => config('trypost.google_auth_enabled'),
|
||||
'githubAuthEnabled' => config('trypost.github_auth_enabled'),
|
||||
'trialDays' => config('cashier.trial_days'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ protected function handleSubscriptionCreated(Account $account, array $payload):
|
|||
$previousPlan = $account->plan?->name;
|
||||
|
||||
if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) {
|
||||
$account->update(['plan_id' => $plan->id]);
|
||||
$account->update([
|
||||
'plan_id' => $plan->id,
|
||||
'trial_ends_at' => null,
|
||||
]);
|
||||
$account->forgetPlanFeatureCache();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Features\SocialAccountLimit;
|
||||
use App\Features\WorkspaceLimit;
|
||||
use App\Models\Traits\HasUsage;
|
||||
use Carbon\CarbonInterface;
|
||||
use Database\Factories\AccountFactory;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -30,6 +31,11 @@ class Account extends Model
|
|||
'name',
|
||||
'billing_email',
|
||||
'plan_id',
|
||||
'trial_ends_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'trial_ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function forgetPlanFeatureCache(): void
|
||||
|
|
@ -78,7 +84,22 @@ public function hasActiveSubscription(): bool
|
|||
|
||||
public function isOnTrial(): bool
|
||||
{
|
||||
return $this->subscription(self::SUBSCRIPTION_NAME)?->onTrial() ?? false;
|
||||
if ($this->onGenericTrial()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) $this->subscription(self::SUBSCRIPTION_NAME)?->onTrial();
|
||||
}
|
||||
|
||||
public function activeTrialEndsAt(): ?CarbonInterface
|
||||
{
|
||||
$subscription = $this->subscription(self::SUBSCRIPTION_NAME);
|
||||
|
||||
return match (true) {
|
||||
(bool) $subscription?->onTrial() => $subscription->trial_ends_at,
|
||||
$this->onGenericTrial() => $this->trial_ends_at,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -135,6 +135,6 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'trial_days' => env('CASHIER_TRIAL_DAYS', 8),
|
||||
'trial_days' => env('CASHIER_TRIAL_DAYS', 7),
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@
|
|||
'page_title' => 'Choose your plan',
|
||||
'eyebrow' => 'Pricing',
|
||||
'title' => 'Choose the right plan for you',
|
||||
'description' => 'Start with a 7-day free trial. No charge until your trial ends.',
|
||||
'trial_info' => '7-day free trial, then billed automatically',
|
||||
'description' => 'Pick the plan that fits you. Billed monthly or annually.',
|
||||
'monthly' => 'Monthly',
|
||||
'yearly' => 'Yearly',
|
||||
'per_month' => 'monthly',
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
'everything_in' => 'Everything in :plan, plus:',
|
||||
'save_months' => '2 months free',
|
||||
'popular' => 'Most popular',
|
||||
'start_trial' => 'Start 7-day free trial',
|
||||
'subscribe_cta' => 'Subscribe',
|
||||
'prices' => [
|
||||
'starter' => ['monthly' => '$19', 'yearly_per_month' => '$16', 'yearly' => '$190'],
|
||||
'plus' => ['monthly' => '$29', 'yearly_per_month' => '$24', 'yearly' => '$290'],
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@
|
|||
'page_title' => 'Elige tu plan',
|
||||
'eyebrow' => 'Precios',
|
||||
'title' => 'Elige el plan ideal para ti',
|
||||
'description' => 'Comienza con 7 días gratis. Sin cargo hasta que termine tu prueba.',
|
||||
'trial_info' => 'Prueba gratuita de 7 días, luego se cobra automáticamente',
|
||||
'description' => 'Elige el plan que te queda. Facturación mensual o anual.',
|
||||
'monthly' => 'Mensual',
|
||||
'yearly' => 'Anual',
|
||||
'per_month' => 'mensual',
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
'everything_in' => 'Todo lo de :plan, más:',
|
||||
'save_months' => '2 meses gratis',
|
||||
'popular' => 'Más popular',
|
||||
'start_trial' => 'Comenzar prueba de 7 días',
|
||||
'subscribe_cta' => 'Suscribirse',
|
||||
'prices' => [
|
||||
'starter' => ['monthly' => '$19', 'yearly_per_month' => '$16', 'yearly' => '$190'],
|
||||
'plus' => ['monthly' => '$29', 'yearly_per_month' => '$24', 'yearly' => '$290'],
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -26,8 +26,7 @@
|
|||
'page_title' => 'Escolha seu plano',
|
||||
'eyebrow' => 'Preços',
|
||||
'title' => 'Escolha o plano ideal pra você',
|
||||
'description' => 'Comece com 7 dias grátis. Sem cobrança até o fim do seu teste.',
|
||||
'trial_info' => '7 dias grátis, depois cobrança automática',
|
||||
'description' => 'Escolha o plano que combina com você. Cobrança mensal ou anual.',
|
||||
'monthly' => 'Mensal',
|
||||
'yearly' => 'Anual',
|
||||
'per_month' => 'mensal',
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
'everything_in' => 'Tudo do :plan, mais:',
|
||||
'save_months' => '2 meses grátis',
|
||||
'popular' => 'Mais popular',
|
||||
'start_trial' => 'Iniciar teste de 7 dias',
|
||||
'subscribe_cta' => 'Assinar',
|
||||
'prices' => [
|
||||
'starter' => ['monthly' => 'R$ 95', 'yearly_per_month' => 'R$ 79', 'yearly' => 'R$ 950'],
|
||||
'plus' => ['monthly' => 'R$ 145', 'yearly_per_month' => 'R$ 121', 'yearly' => 'R$ 1450'],
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ interface Highlight {
|
|||
|
||||
defineProps<{
|
||||
plans: Plan[];
|
||||
trialDays: number;
|
||||
}>();
|
||||
|
||||
const isYearly = ref(true);
|
||||
|
|
@ -120,7 +119,7 @@ const planTones: Record<PlanSlug, string> = {
|
|||
{{ $t('billing.subscribe.title') }}
|
||||
</h1>
|
||||
<p class="mx-auto max-w-2xl text-balance text-base text-muted-foreground sm:text-lg">
|
||||
{{ trans('billing.subscribe.description', { days: String(trialDays) }) }}
|
||||
{{ $t('billing.subscribe.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -228,7 +227,7 @@ const planTones: Record<PlanSlug, string> = {
|
|||
]"
|
||||
@click="selectPlan(plan)"
|
||||
>
|
||||
{{ trans('billing.subscribe.start_trial', { days: String(trialDays) }) }}
|
||||
{{ $t('billing.subscribe.subscribe_cta') }}
|
||||
<IconArrowRight class="size-4" />
|
||||
</button>
|
||||
|
||||
|
|
|
|||
63
tests/Feature/Auth/TrialOnSignupTest.php
Normal file
63
tests/Feature/Auth/TrialOnSignupTest.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\User\CreateUser;
|
||||
use App\Enums\Plan\Slug;
|
||||
use App\Models\Plan;
|
||||
use Carbon\Carbon;
|
||||
use Database\Seeders\PlanSeeder;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
$this->seed(PlanSeeder::class);
|
||||
});
|
||||
|
||||
test('new signup gets a 7-day trial without card', function () {
|
||||
Carbon::setTestNow('2026-05-14 12:00:00');
|
||||
|
||||
$user = CreateUser::execute([
|
||||
'name' => 'Alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'password123',
|
||||
'timezone' => 'UTC',
|
||||
'registration_ip' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
$starterPlan = Plan::where('slug', Slug::Starter)->firstOrFail();
|
||||
|
||||
expect($user->account->plan_id)->toBe($starterPlan->id);
|
||||
expect($user->account->trial_ends_at?->toDateTimeString())->toBe('2026-05-21 12:00:00');
|
||||
expect($user->account->stripe_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('account during generic trial is recognized as on trial', function () {
|
||||
Carbon::setTestNow('2026-05-14 12:00:00');
|
||||
|
||||
$user = CreateUser::execute([
|
||||
'name' => 'Alice',
|
||||
'email' => 'alice2@example.com',
|
||||
'password' => 'password123',
|
||||
'timezone' => 'UTC',
|
||||
'registration_ip' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
expect($user->account->isOnTrial())->toBeTrue();
|
||||
expect($user->account->onGenericTrial())->toBeTrue();
|
||||
});
|
||||
|
||||
test('account whose generic trial expired is not on trial', function () {
|
||||
Carbon::setTestNow('2026-05-14 12:00:00');
|
||||
|
||||
$user = CreateUser::execute([
|
||||
'name' => 'Alice',
|
||||
'email' => 'alice3@example.com',
|
||||
'password' => 'password123',
|
||||
'timezone' => 'UTC',
|
||||
'registration_ip' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
Carbon::setTestNow('2026-05-21 12:01:00');
|
||||
|
||||
expect($user->account->fresh()->isOnTrial())->toBeFalse();
|
||||
});
|
||||
|
|
@ -38,7 +38,6 @@
|
|||
$response->assertInertia(fn ($page) => $page
|
||||
->component('billing/Subscribe', false)
|
||||
->has('plans')
|
||||
->has('trialDays')
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -93,6 +92,73 @@
|
|||
);
|
||||
});
|
||||
|
||||
test('billing index exposes onTrial=true and trialEndsAt for generic-trial-only account', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$endsAt = now()->addDays(7)->startOfSecond();
|
||||
$this->account->update(['trial_ends_at' => $endsAt]);
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->get(route('app.billing.index'));
|
||||
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('settings/account/Billing', false)
|
||||
->where('hasSubscription', false)
|
||||
->where('onTrial', true)
|
||||
->where('trialEndsAt', $endsAt->toIso8601ZuluString('microsecond'))
|
||||
);
|
||||
});
|
||||
|
||||
test('billing index exposes onTrial=true and trialEndsAt for subscription-trial account', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$subscriptionEndsAt = now()->addDays(5)->startOfSecond();
|
||||
$this->account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'trialing',
|
||||
'stripe_price' => 'price_123',
|
||||
'trial_ends_at' => $subscriptionEndsAt,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->get(route('app.billing.index'));
|
||||
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('hasSubscription', true)
|
||||
->where('onTrial', true)
|
||||
->where('trialEndsAt', $subscriptionEndsAt->toIso8601ZuluString('microsecond'))
|
||||
);
|
||||
});
|
||||
|
||||
test('billing index exposes onTrial=false and trialEndsAt=null for paying subscribed user', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$this->account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'active',
|
||||
'stripe_price' => 'price_123',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->get(route('app.billing.index'));
|
||||
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('hasSubscription', true)
|
||||
->where('onTrial', false)
|
||||
->where('trialEndsAt', null)
|
||||
);
|
||||
});
|
||||
|
||||
test('subscribe page does not expose trialDays prop anymore', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.subscribe'));
|
||||
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('billing/Subscribe', false)
|
||||
->missing('trialDays')
|
||||
);
|
||||
});
|
||||
|
||||
test('billing index redirects to calendar in self hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,23 @@
|
|||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
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([
|
||||
'type' => 'customer.subscription.created',
|
||||
'data' => ['object' => [
|
||||
'customer' => 'cus_test123',
|
||||
'id' => 'sub_123',
|
||||
'status' => 'active',
|
||||
'items' => ['data' => [['price' => ['id' => $starter->stripe_monthly_price_id]]]],
|
||||
]],
|
||||
]));
|
||||
|
||||
expect($this->account->fresh()->trial_ends_at)->toBeNull();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// customer.subscription.updated
|
||||
// ========================================
|
||||
|
|
|
|||
92
tests/Feature/Middleware/TrialMiddlewareAccessTest.php
Normal file
92
tests/Feature/Middleware/TrialMiddlewareAccessTest.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\User\CreateUser;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Carbon\Carbon;
|
||||
use Database\Seeders\PlanSeeder;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
$this->seed(PlanSeeder::class);
|
||||
});
|
||||
|
||||
test('user on generic trial can access the app', function () {
|
||||
Carbon::setTestNow('2026-05-14 12:00:00');
|
||||
|
||||
$user = CreateUser::execute([
|
||||
'name' => 'Alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'password123',
|
||||
'timezone' => 'UTC',
|
||||
'registration_ip' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $user->account_id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
|
||||
$user->update(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user->fresh())->get(route('app.accounts'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('user whose trial expired is redirected to subscribe', function () {
|
||||
Carbon::setTestNow('2026-05-14 12:00:00');
|
||||
|
||||
$user = CreateUser::execute([
|
||||
'name' => 'Alice',
|
||||
'email' => 'alice2@example.com',
|
||||
'password' => 'password123',
|
||||
'timezone' => 'UTC',
|
||||
'registration_ip' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $user->account_id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
|
||||
$user->update(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
Carbon::setTestNow('2026-05-21 12:01:00');
|
||||
|
||||
$response = $this->actingAs($user->fresh())->get(route('app.accounts'));
|
||||
|
||||
$response->assertRedirect(route('app.subscribe'));
|
||||
});
|
||||
|
||||
test('user on trialing subscription (legacy trial-with-card) can access the app', function () {
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => null,
|
||||
'stripe_id' => 'cus_test_'.fake()->uuid(),
|
||||
]);
|
||||
$user = User::factory()->create(['account_id' => $account->id]);
|
||||
$account->update(['owner_id' => $user->id]);
|
||||
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'trialing',
|
||||
'stripe_price' => 'price_123',
|
||||
'trial_ends_at' => now()->addDays(5),
|
||||
]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $account->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
|
||||
$user->update(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user->fresh())->get(route('app.accounts'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
|
@ -107,7 +107,6 @@
|
|||
$response->assertInertia(fn ($page) => $page
|
||||
->component('billing/Subscribe', false)
|
||||
->has('plans', $activePlanCount)
|
||||
->has('trialDays')
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
122
tests/Unit/Models/AccountTest.php
Normal file
122
tests/Unit/Models/AccountTest.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\Plan\Slug;
|
||||
use App\Models\Account;
|
||||
use App\Models\Plan;
|
||||
use Carbon\Carbon;
|
||||
use Database\Seeders\PlanSeeder;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(PlanSeeder::class);
|
||||
Carbon::setTestNow('2026-05-14 12:00:00');
|
||||
});
|
||||
|
||||
test('isOnTrial returns true for account on generic trial', function () {
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => now()->addDays(7),
|
||||
]);
|
||||
|
||||
expect($account->isOnTrial())->toBeTrue();
|
||||
});
|
||||
|
||||
test('isOnTrial returns false when generic trial has expired and there is no subscription', function () {
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
expect($account->isOnTrial())->toBeFalse();
|
||||
});
|
||||
|
||||
test('isOnTrial returns false for account without trial or subscription', function () {
|
||||
$account = Account::factory()->create(['trial_ends_at' => null]);
|
||||
|
||||
expect($account->isOnTrial())->toBeFalse();
|
||||
});
|
||||
|
||||
test('isOnTrial returns true via subscription trial when account has no generic trial', function () {
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => null,
|
||||
'stripe_id' => 'cus_test_'.fake()->uuid(),
|
||||
]);
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'trialing',
|
||||
'stripe_price' => 'price_123',
|
||||
'trial_ends_at' => now()->addDays(5),
|
||||
]);
|
||||
|
||||
expect($account->isOnTrial())->toBeTrue();
|
||||
});
|
||||
|
||||
test('activeTrialEndsAt returns null when not on any trial', function () {
|
||||
$account = Account::factory()->create(['trial_ends_at' => null]);
|
||||
|
||||
expect($account->activeTrialEndsAt())->toBeNull();
|
||||
});
|
||||
|
||||
test('activeTrialEndsAt returns generic trial date when only generic is active', function () {
|
||||
$endsAt = now()->addDays(7);
|
||||
$account = Account::factory()->create(['trial_ends_at' => $endsAt]);
|
||||
|
||||
expect($account->activeTrialEndsAt()?->toDateTimeString())
|
||||
->toBe($endsAt->toDateTimeString());
|
||||
});
|
||||
|
||||
test('activeTrialEndsAt returns subscription date when only subscription trial is active', function () {
|
||||
$subscriptionEndsAt = now()->addDays(5);
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => null,
|
||||
'stripe_id' => 'cus_test_'.fake()->uuid(),
|
||||
]);
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'trialing',
|
||||
'stripe_price' => 'price_123',
|
||||
'trial_ends_at' => $subscriptionEndsAt,
|
||||
]);
|
||||
|
||||
expect($account->activeTrialEndsAt()?->toDateTimeString())
|
||||
->toBe($subscriptionEndsAt->toDateTimeString());
|
||||
});
|
||||
|
||||
test('activeTrialEndsAt prefers subscription date over generic when both active', function () {
|
||||
$genericEndsAt = now()->addDays(7);
|
||||
$subscriptionEndsAt = now()->addDays(14);
|
||||
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => $genericEndsAt,
|
||||
'stripe_id' => 'cus_test_'.fake()->uuid(),
|
||||
]);
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'trialing',
|
||||
'stripe_price' => 'price_123',
|
||||
'trial_ends_at' => $subscriptionEndsAt,
|
||||
]);
|
||||
|
||||
expect($account->activeTrialEndsAt()?->toDateTimeString())
|
||||
->toBe($subscriptionEndsAt->toDateTimeString());
|
||||
});
|
||||
|
||||
test('activeTrialEndsAt returns null for paying customer post-trial', function () {
|
||||
$account = Account::factory()->create([
|
||||
'trial_ends_at' => null,
|
||||
'stripe_id' => 'cus_test_'.fake()->uuid(),
|
||||
'plan_id' => Plan::where('slug', Slug::Starter)->value('id'),
|
||||
]);
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_test_'.fake()->uuid(),
|
||||
'stripe_status' => 'active',
|
||||
'stripe_price' => 'price_123',
|
||||
'trial_ends_at' => null,
|
||||
]);
|
||||
|
||||
expect($account->activeTrialEndsAt())->toBeNull();
|
||||
expect($account->isOnTrial())->toBeFalse();
|
||||
});
|
||||
Loading…
Reference in a new issue