diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index b68f0982..d8372a8f 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -77,13 +77,19 @@ class SubscriptionController extends Controller $priceId = $this->resolvePriceIdByLookupKey($plan['stripe_lookup_key']); - return $request->user() + $subscriptionBuilder = $request->user() ->newSubscription('default', $priceId) - ->allowPromotionCodes() - ->checkout([ - 'success_url' => route('subscribe.success'), - 'cancel_url' => route('subscribe.cancel'), - ]); + ->allowPromotionCodes(); + + $trialDays = (int) ($plan['trial_days'] ?? 0); + if ($trialDays > 0) { + $subscriptionBuilder->trialDays($trialDays); + } + + return $subscriptionBuilder->checkout([ + 'success_url' => route('subscribe.success'), + 'cancel_url' => route('subscribe.cancel'), + ]); } /** diff --git a/config/subscriptions.php b/config/subscriptions.php index d05ef109..da46dcba 100644 --- a/config/subscriptions.php +++ b/config/subscriptions.php @@ -52,6 +52,7 @@ return [ 'original_price' => null, 'stripe_lookup_key' => env('STRIPE_PRO_MONTHLY_LOOKUP_KEY', 'whisper_pro_monthly'), 'billing_period' => 'month', + 'trial_days' => (int) env('STRIPE_PRO_MONTHLY_TRIAL_DAYS', 15), 'features' => [ 'Unlimited accounts', 'Unlimited transactions', @@ -68,6 +69,7 @@ return [ 'original_price' => 47.88, 'stripe_lookup_key' => env('STRIPE_PRO_YEARLY_LOOKUP_KEY', 'whisper_pro_yearly'), 'billing_period' => 'year', + 'trial_days' => (int) env('STRIPE_PRO_YEARLY_TRIAL_DAYS', 15), 'features' => [ 'Unlimited accounts', 'Unlimited transactions', diff --git a/tests/Feature/SubscriptionTest.php b/tests/Feature/SubscriptionTest.php index 16f71af2..4a402553 100644 --- a/tests/Feature/SubscriptionTest.php +++ b/tests/Feature/SubscriptionTest.php @@ -8,6 +8,9 @@ use App\Models\Category; use App\Models\Transaction; use App\Models\User; use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\Cache; +use Laravel\Cashier\Checkout; +use Laravel\Cashier\SubscriptionBuilder; beforeEach(function () { config([ @@ -196,6 +199,7 @@ test('pricing config includes all plan details', function () { ->where('original_price', null) ->has('stripe_lookup_key') ->where('billing_period', 'month') + ->where('trial_days', 15) ->has('features') ) ->has('pricing.plans.yearly', fn ($plan) => $plan @@ -204,6 +208,7 @@ test('pricing config includes all plan details', function () { ->where('original_price', 47.88) ->has('stripe_lookup_key') ->where('billing_period', 'year') + ->where('trial_days', 15) ->has('features') ) ->has('pricing.promo', fn ($promo) => $promo @@ -298,3 +303,61 @@ test('billing portal skips stripe customer creation when user already has a stri $this->get(route('settings.billing.portal'))->assertRedirect(); }); + +test('checkout applies configured trial days to the subscription builder', function () { + config([ + 'subscriptions.plans.monthly.trial_days' => 15, + 'subscriptions.plans.monthly.stripe_lookup_key' => 'test_monthly_lookup', + ]); + Cache::put('stripe_price_id:test_monthly_lookup', 'price_test_monthly', now()->addHour()); + + $checkout = Mockery::mock(Checkout::class); + $checkout->shouldReceive('toResponse')->andReturn(new RedirectResponse('https://stripe.test/session')); + + $builder = Mockery::mock(SubscriptionBuilder::class); + $builder->shouldReceive('allowPromotionCodes')->once()->andReturnSelf(); + $builder->shouldReceive('trialDays')->once()->with(15)->andReturnSelf(); + $builder->shouldReceive('checkout')->once()->andReturn($checkout); + + $user = Mockery::mock(User::class)->shouldIgnoreMissing(); + $user->shouldReceive('hasVerifiedEmail')->andReturn(true); + $user->shouldReceive('hasProPlan')->andReturn(false); + $user->shouldReceive('newSubscription') + ->once() + ->with('default', 'price_test_monthly') + ->andReturn($builder); + + $this->withoutMiddleware(HandleInertiaRequests::class); + $this->actingAs($user); + + $this->get(route('subscribe.checkout', ['plan' => 'monthly']))->assertRedirect(); +}); + +test('checkout skips trial days when configured to zero', function () { + config([ + 'subscriptions.plans.monthly.trial_days' => 0, + 'subscriptions.plans.monthly.stripe_lookup_key' => 'test_monthly_lookup', + ]); + Cache::put('stripe_price_id:test_monthly_lookup', 'price_test_monthly', now()->addHour()); + + $checkout = Mockery::mock(Checkout::class); + $checkout->shouldReceive('toResponse')->andReturn(new RedirectResponse('https://stripe.test/session')); + + $builder = Mockery::mock(SubscriptionBuilder::class); + $builder->shouldReceive('allowPromotionCodes')->once()->andReturnSelf(); + $builder->shouldNotReceive('trialDays'); + $builder->shouldReceive('checkout')->once()->andReturn($checkout); + + $user = Mockery::mock(User::class)->shouldIgnoreMissing(); + $user->shouldReceive('hasVerifiedEmail')->andReturn(true); + $user->shouldReceive('hasProPlan')->andReturn(false); + $user->shouldReceive('newSubscription') + ->once() + ->with('default', 'price_test_monthly') + ->andReturn($builder); + + $this->withoutMiddleware(HandleInertiaRequests::class); + $this->actingAs($user); + + $this->get(route('subscribe.checkout', ['plan' => 'monthly']))->assertRedirect(); +});