From cc34c9aca349bcf942e38aafa4810e101cb58667 Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Fri, 28 Nov 2025 06:03:43 -0800 Subject: [PATCH] chore: add payment routes for our UPS --- app/Http/Controllers/PaymentController.php | 99 +++++++++++++++++ routes/payment.php | 8 +- routes/web.php | 3 +- tests/Feature/PricingCheckoutTest.php | 121 +++++++++++++++++++++ 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/PricingCheckoutTest.php diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index adf1bf8..fab53d8 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -254,6 +254,105 @@ class PaymentController extends Controller } } + /** + * Handle enhanced checkout with provider selection + */ + public function enhancedCheckout(Request $request, int $plan, string $provider) + { + try { + $user = $request->user(); + $planModel = Plan::with(['planProviders', 'trialConfiguration'])->findOrFail($plan); + + // Validate provider support + if (! $planModel->supportsProvider($provider)) { + session()->flash('error', "Provider '{$provider}' is not supported for this plan."); + + return redirect()->route('dashboard'); + } + + // Create checkout session via orchestrator + $result = $this->orchestrator->createCheckoutSession($user, $planModel, $provider, [ + 'success_url' => route('checkout.success'), + 'cancel_url' => route('checkout.cancel'), + 'is_trial' => false, + ]); + + // Redirect to provider's checkout page + if (isset($result['redirect_url'])) { + return redirect($result['redirect_url']); + } + + // Fallback to session-based checkout + if (isset($result['session_url'])) { + return redirect($result['session_url']); + } + + session()->flash('error', 'Unable to create checkout session. Please try again.'); + + return redirect()->route('dashboard'); + + } catch (\Exception $e) { + session()->flash('error', 'Checkout error: '.$e->getMessage()); + + return redirect()->route('dashboard'); + } + } + + /** + * Handle trial-specific checkout flow + */ + public function trialCheckout(Request $request, int $plan, string $provider) + { + try { + $user = $request->user(); + $planModel = Plan::with(['trialConfiguration', 'planProviders'])->findOrFail($plan); + + // Validate trial availability + if (! $planModel->hasTrial()) { + session()->flash('error', 'This plan does not offer trials.'); + + return redirect()->route('dashboard'); + } + + // Validate provider support + if (! $planModel->supportsProvider($provider)) { + session()->flash('error', "Provider '{$provider}' is not supported for this plan."); + + return redirect()->route('dashboard'); + } + + $trialConfig = $planModel->getTrialConfig(); + + // Create trial checkout session + $result = $this->orchestrator->createCheckoutSession($user, $planModel, $provider, [ + 'success_url' => route('checkout.success'), + 'cancel_url' => route('checkout.cancel'), + 'is_trial' => true, + 'trial_duration_days' => $trialConfig?->trial_duration_days ?? 14, + 'trial_requires_payment_method' => $trialConfig?->trial_requires_payment_method ?? true, + ]); + + // Redirect to provider's checkout page + if (isset($result['redirect_url'])) { + return redirect($result['redirect_url']); + } + + // Fallback to session-based checkout + if (isset($result['session_url'])) { + return redirect($result['session_url']); + } + + session()->flash('error', 'Unable to create trial checkout session. Please try again.'); + + return redirect()->route('dashboard'); + + } catch (\Exception $e) { + session()->flash('error', 'Trial checkout error: '.$e->getMessage()); + + return redirect()->route('dashboard'); + } + } + /** * Handle successful payment redirect */ diff --git a/routes/payment.php b/routes/payment.php index 171c87a..b9f5f70 100644 --- a/routes/payment.php +++ b/routes/payment.php @@ -15,13 +15,19 @@ Route::prefix('payment')->name('payment.')->group(function () { Route::get('/success', [PaymentController::class, 'success'])->name('success'); Route::get('/cancel', [PaymentController::class, 'cancel'])->name('cancel'); - // Payment processing endpoints + // UNIFIED: Payment processing endpoints (new unified payment system) Route::post('/checkout', [PaymentController::class, 'createCheckout'])->name('checkout'); Route::post('/subscribe', [PaymentController::class, 'createSubscription'])->name('subscribe'); Route::get('/methods', [PaymentController::class, 'getPaymentMethods'])->name('methods'); Route::get('/history', [PaymentController::class, 'getHistory'])->name('history'); }); +// UNIFIED: Enhanced checkout routes with provider selection +Route::middleware(['auth', 'verified'])->prefix('checkout')->name('checkout.')->group(function () { + Route::get('/enhanced/{plan}/{provider}', [PaymentController::class, 'enhancedCheckout'])->name('enhanced'); + Route::get('/trial/{plan}/{provider}', [PaymentController::class, 'trialCheckout'])->name('trial'); +}); + Route::prefix('webhook')->name('webhook.')->group(function () { // Unified webhook handler Route::post('/{provider}', [WebhookController::class, 'handle'])->name('unified'); diff --git a/routes/web.php b/routes/web.php index 43bca80..6922447 100644 --- a/routes/web.php +++ b/routes/web.php @@ -69,7 +69,7 @@ Route::middleware(['auth', 'verified', CheckUserBanned::class])->group(function Route::get('dashboard/compose-email', Dashboard::class)->name('dashboard.compose'); Route::get('dashboard/support', Support::class)->name('dashboard.support'); - // Checkout Routes + // LEGACY: Old Stripe Cashier checkout route (deprecated - use unified payment system) Route::get('checkout/{plan}', function ($pricing_id) { $plans = config('app.plans'); $pricingData = []; @@ -91,6 +91,7 @@ Route::middleware(['auth', 'verified', CheckUserBanned::class])->group(function abort(404); })->name('checkout'); + // LEGACY: Payment status routes (used by both legacy and unified systems) Route::get('dashboard/success', [Dashboard::class, 'paymentStatus'])->name('checkout.success')->defaults('status', 'success'); Route::get('dashboard/cancel', [Dashboard::class, 'paymentStatus'])->name('checkout.cancel')->defaults('status', 'cancel'); diff --git a/tests/Feature/PricingCheckoutTest.php b/tests/Feature/PricingCheckoutTest.php new file mode 100644 index 0000000..3bce9d9 --- /dev/null +++ b/tests/Feature/PricingCheckoutTest.php @@ -0,0 +1,121 @@ +create(); + $plan = Plan::factory()->create([ + 'name' => 'Test Plan', + 'price' => 99.99, + 'monthly_billing' => true, + ]); + + // Enable polar provider for this plan + $plan->planProviders()->create([ + 'provider' => 'polar', + 'is_enabled' => true, + ]); + + Livewire::actingAs($user) + ->test(\App\Livewire\Dashboard\Pricing::class) + ->assertSet('plans') + ->assertSet('planTiers') + ->assertSee($plan->name); + } + + /** @test */ + public function it_can_redirect_to_enhanced_checkout_for_polar_provider() + { + $user = User::factory()->create(); + $plan = Plan::factory()->create([ + 'name' => 'Test Plan', + 'price' => 99.99, + 'monthly_billing' => true, + ]); + + // Enable polar provider for this plan + $plan->planProviders()->create([ + 'provider' => 'polar', + 'is_enabled' => true, + ]); + + Livewire::actingAs($user) + ->test(\App\Livewire\Dashboard\Pricing::class) + ->call('choosePlan', $plan->id, 'polar') + ->assertRedirect(route('checkout.enhanced', [ + 'plan' => $plan->id, + 'provider' => 'polar', + ])); + } + + /** @test */ + public function it_can_redirect_to_trial_checkout_when_plan_supports_trials() + { + $user = User::factory()->create(); + $plan = Plan::factory()->create([ + 'name' => 'Test Plan with Trial', + 'price' => 99.99, + 'monthly_billing' => true, + ]); + + // Enable polar provider for this plan + $plan->planProviders()->create([ + 'provider' => 'polar', + 'is_enabled' => true, + ]); + + // Create trial configuration + $plan->trialConfiguration()->create([ + 'trial_duration_days' => 14, + 'trial_requires_payment_method' => true, + 'trial_auto_converts' => true, + ]); + + Livewire::actingAs($user) + ->test(\App\Livewire\Dashboard\Pricing::class) + ->call('startTrial', $plan->id, 'polar') + ->assertRedirect(route('checkout.trial', [ + 'plan' => $plan->id, + 'provider' => 'polar', + ])); + } + + /** @test */ + public function it_shows_error_when_provider_is_not_supported() + { + $user = User::factory()->create(); + $plan = Plan::factory()->create(); + + Livewire::actingAs($user) + ->test(\App\Livewire\Dashboard\Pricing::class) + ->call('choosePlan', $plan->id, 'unsupported_provider') + ->assertSessionHas('error', "This plan doesn't support unsupported_provider payments."); + } + + /** @test */ + public function it_shows_error_when_trying_trial_on_plan_without_trial() + { + $user = User::factory()->create(); + $plan = Plan::factory()->create(); + + // Enable provider but no trial configuration + $plan->planProviders()->create([ + 'provider' => 'polar', + 'is_enabled' => true, + ]); + + Livewire::actingAs($user) + ->test(\App\Livewire\Dashboard\Pricing::class) + ->call('startTrial', $plan->id, 'polar') + ->assertSessionHas('error', "This plan doesn't offer trials."); + } +}