feat: Complete checkout overhaul with Stripe PaymentIntent integration and abandoned cart recovery
This commit is contained in:
@@ -1,198 +1,3 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Mail\Welcome;
|
||||
use App\Models\Package;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Response;
|
||||
use Throwable;
|
||||
|
||||
class CheckoutController extends Controller
|
||||
{
|
||||
/**
|
||||
* Render the checkout wizard using the legacy marketing controller for now.
|
||||
*/
|
||||
public function show(Request $request, Package $package): Response
|
||||
{
|
||||
$marketingController = app(MarketingController::class);
|
||||
|
||||
return $marketingController->purchaseWizard($request, $package->getKey());
|
||||
}
|
||||
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
app()->setLocale($request->input('locale', app()->getLocale()));
|
||||
|
||||
$request->authenticate();
|
||||
$request->session()->regenerate();
|
||||
|
||||
$user = $request->user()?->fresh();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'user' => $this->transformUser($user),
|
||||
]);
|
||||
}
|
||||
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
app()->setLocale($request->input('locale', app()->getLocale()));
|
||||
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'username' => ['required', 'string', 'max:255', 'unique:'.User::class],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'address' => ['required', 'string', 'max:500'],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
'privacy_consent' => ['accepted'],
|
||||
'package_id' => ['nullable', 'integer'],
|
||||
]);
|
||||
} catch (ValidationException $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$shouldAutoVerify = app()->environment(['local', 'testing']);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$user = User::create([
|
||||
'username' => $validated['username'],
|
||||
'email' => $validated['email'],
|
||||
'first_name' => $validated['first_name'],
|
||||
'last_name' => $validated['last_name'],
|
||||
'address' => $validated['address'],
|
||||
'phone' => $validated['phone'],
|
||||
'password' => bcrypt($validated['password']),
|
||||
'role' => 'user',
|
||||
'pending_purchase' => !empty($validated['package_id']),
|
||||
]);
|
||||
|
||||
if ($user->pending_purchase) {
|
||||
$request->session()->put('pending_user_id', $user->id);
|
||||
}
|
||||
|
||||
if ($shouldAutoVerify) {
|
||||
$user->forceFill(['email_verified_at' => now()])->save();
|
||||
}
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $validated['first_name'].' '.$validated['last_name'],
|
||||
'slug' => Str::slug($validated['first_name'].' '.$validated['last_name'].'-'.now()->timestamp),
|
||||
'email' => $validated['email'],
|
||||
'is_active' => true,
|
||||
'is_suspended' => false,
|
||||
'event_credits_balance' => 0,
|
||||
'subscription_tier' => 'free',
|
||||
'subscription_expires_at' => null,
|
||||
'settings' => json_encode([
|
||||
'branding' => [
|
||||
'logo_url' => null,
|
||||
'primary_color' => '#3B82F6',
|
||||
'secondary_color' => '#1F2937',
|
||||
'font_family' => 'Inter, sans-serif',
|
||||
],
|
||||
'features' => [
|
||||
'photo_likes_enabled' => false,
|
||||
'event_checklist' => false,
|
||||
'custom_domain' => false,
|
||||
'advanced_analytics' => false,
|
||||
],
|
||||
'custom_domain' => null,
|
||||
'contact_email' => $validated['email'],
|
||||
'event_default_type' => 'general',
|
||||
]),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
$request->session()->regenerate();
|
||||
|
||||
DB::commit();
|
||||
|
||||
Mail::to($user)->queue(new Welcome($user));
|
||||
|
||||
$redirect = $shouldAutoVerify ? route('dashboard') : route('verification.notice');
|
||||
$pendingPurchase = $user->pending_purchase;
|
||||
|
||||
if (!empty($validated['package_id'])) {
|
||||
$package = Package::find($validated['package_id']);
|
||||
|
||||
if (!$package) {
|
||||
throw ValidationException::withMessages([
|
||||
'package_id' => __('validation.exists', ['attribute' => 'package'])
|
||||
]);
|
||||
}
|
||||
|
||||
if ((float) $package->price <= 0.0) {
|
||||
TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'price' => 0,
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||
'price' => 0,
|
||||
'purchased_at' => now(),
|
||||
'provider_id' => 'free',
|
||||
]);
|
||||
|
||||
$tenant->update(['subscription_status' => 'active']);
|
||||
$user->update(['role' => 'tenant_admin', 'pending_purchase' => false]);
|
||||
$pendingPurchase = false;
|
||||
$redirect = $shouldAutoVerify ? route('dashboard') : route('verification.notice');
|
||||
} else {
|
||||
$pendingPurchase = true;
|
||||
$redirect = route('buy.packages', $package->id);
|
||||
}
|
||||
}
|
||||
|
||||
$freshUser = $user->fresh();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'user' => $this->transformUser($freshUser),
|
||||
'pending_purchase' => $pendingPurchase,
|
||||
'redirect' => $redirect,
|
||||
]);
|
||||
} catch (ValidationException $validationException) {
|
||||
DB::rollBack();
|
||||
throw $validationException;
|
||||
} catch (Throwable $throwable) {
|
||||
DB::rollBack();
|
||||
report($throwable);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => __('auth.registration_failed'),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function transformUser(?User $user): ?array
|
||||
{
|
||||
if (!$user) {
|
||||
@@ -207,18 +12,64 @@ class CheckoutController extends Controller
|
||||
'email_verified_at' => $user->email_verified_at,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an abandoned checkout for reminder emails
|
||||
*/
|
||||
public function trackAbandonedCheckout(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'package_id' => 'required|integer|exists:packages,id',
|
||||
'last_step' => 'required|integer|min:1|max:4',
|
||||
'user_id' => 'nullable|integer|exists:users,id',
|
||||
'email' => 'nullable|email',
|
||||
]);
|
||||
|
||||
try {
|
||||
$userId = $request->user_id;
|
||||
$email = $request->email;
|
||||
|
||||
// Wenn kein user_id aber email, versuche User zu finden
|
||||
if (!$userId && $email) {
|
||||
$user = User::where('email', $email)->first();
|
||||
$userId = $user?->id;
|
||||
}
|
||||
|
||||
// Nur tracken wenn wir einen User haben
|
||||
if (!$userId) {
|
||||
return response()->json(['success' => false, 'message' => 'No user found to track']);
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'message' => 'User not found']);
|
||||
}
|
||||
|
||||
// Erstelle oder update abandoned checkout
|
||||
AbandonedCheckout::updateOrCreate(
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'package_id' => $request->package_id,
|
||||
],
|
||||
[
|
||||
'email' => $user->email,
|
||||
'checkout_state' => null, // Später erweitern
|
||||
'last_step' => $request->last_step,
|
||||
'abandoned_at' => now(),
|
||||
'reminded_at' => null,
|
||||
'reminder_stage' => 'none',
|
||||
'expires_at' => now()->addDays(7), // 7 Tage gültig
|
||||
'converted' => false,
|
||||
]
|
||||
);
|
||||
|
||||
Log::info("Abandoned checkout tracked for user {$userId}, package {$request->package_id}, step {$request->last_step}");
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to track abandoned checkout: ' . $e->getMessage());
|
||||
return response()->json(['success' => false, 'message' => 'Failed to track checkout'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,11 +18,10 @@ class LocaleController extends Controller
|
||||
Session::put('locale', $locale);
|
||||
}
|
||||
|
||||
$previousUrl = $request->header('Referer') ?? '/';
|
||||
$currentPath = parse_url($previousUrl, PHP_URL_PATH);
|
||||
// Remove prefix if present for redirect to prefix-free
|
||||
$currentPath = preg_replace('/^\/(de|en)/', '', $currentPath);
|
||||
|
||||
return redirect($currentPath);
|
||||
// Return JSON response for fetch requests
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'locale' => App::getLocale(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -123,32 +123,7 @@ class MarketingController extends Controller
|
||||
return $this->checkout($request, $packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the purchase wizard.
|
||||
*/
|
||||
public function purchaseWizard(Request $request, $packageId)
|
||||
{
|
||||
$package = Package::findOrFail($packageId)->append(['features', 'limits']);
|
||||
$packageOptions = Package::where('type', $package->type)
|
||||
->orderBy('price')
|
||||
->get()
|
||||
->map(function ($candidate) {
|
||||
return $candidate->append(['features', 'limits']);
|
||||
});
|
||||
$stripePublishableKey = config('services.stripe.key');
|
||||
$privacyHtml = view('legal.datenschutz-partial', ['locale' => app()->getLocale()])->render();
|
||||
|
||||
$csp = "default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:5173 https://js.stripe.com https://js.stripe.network; style-src 'self' 'unsafe-inline' data: https:; img-src 'self' data: https: blob:; font-src 'self' data: https:; connect-src 'self' http://localhost:5173 ws://localhost:5173 https://api.stripe.com https://api.stripe.network wss://*.stripe.network; media-src data: blob: 'self' https: https://js.stripe.com https://*.stripe.com; frame-src 'self' https://js.stripe.com https://*.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self';";
|
||||
|
||||
$response = Inertia::render('marketing/PurchaseWizard', [
|
||||
'package' => $package,
|
||||
'packageOptions' => $packageOptions,
|
||||
'stripePublishableKey' => $stripePublishableKey,
|
||||
'privacyHtml' => $privacyHtml,
|
||||
])->toResponse($request);
|
||||
$response->headers->set('Content-Security-Policy', $csp);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checkout for Stripe with auth metadata.
|
||||
|
||||
85
app/Http/Controllers/StripePaymentController.php
Normal file
85
app/Http/Controllers/StripePaymentController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\PaymentIntent;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
|
||||
class StripePaymentController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function createPaymentIntent(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'package_id' => 'required|integer|exists:packages,id',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
if (!$user) {
|
||||
return response()->json(['error' => 'Nicht authentifiziert'], 401);
|
||||
}
|
||||
|
||||
$tenant = $user->tenant;
|
||||
if (!$tenant) {
|
||||
return response()->json(['error' => 'Kein Tenant gefunden'], 403);
|
||||
}
|
||||
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
|
||||
// Kostenlose Pakete brauchen kein Payment Intent
|
||||
if ($package->price <= 0) {
|
||||
return response()->json([
|
||||
'type' => 'free',
|
||||
'message' => 'Kostenloses Paket - kein Payment Intent nötig'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$paymentIntent = PaymentIntent::create([
|
||||
'amount' => (int)($package->price * 100), // In Cent
|
||||
'currency' => 'eur',
|
||||
'metadata' => [
|
||||
'package_id' => $package->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
'user_id' => $user->id,
|
||||
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||
],
|
||||
'automatic_payment_methods' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
'description' => "Paket: {$package->name}",
|
||||
'receipt_email' => $user->email,
|
||||
]);
|
||||
|
||||
Log::info('Payment Intent erstellt', [
|
||||
'payment_intent_id' => $paymentIntent->id,
|
||||
'package_id' => $package->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
'amount' => $package->price
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'clientSecret' => $paymentIntent->client_secret,
|
||||
'paymentIntentId' => $paymentIntent->id,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Stripe Payment Intent Fehler', [
|
||||
'error' => $e->getMessage(),
|
||||
'package_id' => $request->package_id,
|
||||
'user_id' => $user->id
|
||||
]);
|
||||
|
||||
return response()->json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\TenantPackage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Webhook;
|
||||
|
||||
class StripeWebhookController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request)
|
||||
{
|
||||
$payload = $request->getContent();
|
||||
$sigHeader = $request->header('Stripe-Signature');
|
||||
$endpointSecret = config('services.stripe.webhook_secret');
|
||||
|
||||
try {
|
||||
$event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Stripe webhook signature verification failed: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Invalid signature'], 400);
|
||||
}
|
||||
|
||||
switch ($event['type']) {
|
||||
case 'checkout.session.completed':
|
||||
$session = $event['data']['object'];
|
||||
if ($session['mode'] === 'subscription' && isset($session['metadata']['subscription']) && $session['metadata']['subscription'] === 'true') {
|
||||
$this->handleSubscriptionStarted($session);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
$paymentIntent = $event['data']['object'];
|
||||
$this->handlePaymentIntentSucceeded($paymentIntent);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_succeeded':
|
||||
$invoice = $event['data']['object'];
|
||||
$this->handleInvoicePaymentSucceeded($invoice);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_failed':
|
||||
$invoice = $event['data']['object'];
|
||||
$this->handleInvoicePaymentFailed($invoice);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('Unhandled Stripe event type: ' . $event['type']);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
private function handlePaymentIntentSucceeded($paymentIntent)
|
||||
{
|
||||
$metadata = $paymentIntent['metadata'];
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe payment intent: ' . $paymentIntent['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
$type = $metadata['type'] ?? 'endcustomer_event';
|
||||
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe payment intent: ' . $paymentIntent['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate user if pending purchase
|
||||
$user = \App\Models\User::where('id', $tenant->user_id ?? $userId)->first();
|
||||
if ($user && $user->pending_purchase) {
|
||||
$user->update([
|
||||
'email_verified_at' => now(),
|
||||
'role' => 'tenant_admin',
|
||||
'pending_purchase' => false,
|
||||
]);
|
||||
Log::info('User activated after purchase: ' . $user->id);
|
||||
}
|
||||
|
||||
// Create PackagePurchase for one-off payment
|
||||
\App\Models\PackagePurchase::create([
|
||||
$purchase = \App\Models\PackagePurchase::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $paymentIntent['id'],
|
||||
@@ -109,162 +9,13 @@ class StripeWebhookController extends Controller
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
if ($type === 'endcustomer_event') {
|
||||
// For event packages, assume event_id from metadata or handle separately
|
||||
// TODO: Link to specific event if provided
|
||||
}
|
||||
|
||||
Log::info('Package purchase created via Stripe payment intent: ' . $paymentIntent['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentSucceeded($invoice)
|
||||
{
|
||||
$subscription = $invoice['subscription'];
|
||||
$metadata = $invoice['metadata'];
|
||||
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe invoice: ' . $invoice['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
// Send purchase confirmation email
|
||||
try {
|
||||
$tenant = \App\Models\Tenant::find($tenantId);
|
||||
if ($tenant && $tenant->user) {
|
||||
Mail::to($tenant->user)->queue(new PurchaseConfirmation($purchase));
|
||||
Log::info('Purchase confirmation email sent for payment intent: ' . $paymentIntent['id']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe invoice: ' . $invoice['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update or create TenantPackage for subscription
|
||||
\App\Models\TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
],
|
||||
[
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(), // Renew annually
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
// Create or update PackagePurchase
|
||||
\App\Models\PackagePurchase::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $subscription,
|
||||
],
|
||||
[
|
||||
'price' => $invoice['amount_paid'] / 100,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]
|
||||
);
|
||||
|
||||
Log::info('Subscription renewed via Stripe invoice: ' . $invoice['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentFailed($invoice)
|
||||
{
|
||||
$subscription = $invoice['subscription'];
|
||||
Log::warning('Stripe invoice payment failed: ' . $invoice['id'] . ' for subscription ' . $subscription);
|
||||
|
||||
// TODO: Deactivate package or notify tenant
|
||||
// e.g., TenantPackage::where('provider_id', $subscription)->update(['active' => false]);
|
||||
}
|
||||
|
||||
private function handleSubscriptionStarted($session)
|
||||
{
|
||||
$metadata = $session['metadata'];
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe checkout session: ' . $session['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
$type = $metadata['type'] ?? 'reseller_subscription';
|
||||
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe checkout session: ' . $session['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriptionId = $session['subscription']['id'] ?? null;
|
||||
if (!$subscriptionId) {
|
||||
Log::error('No subscription ID in checkout session: ' . $session['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate user if pending purchase
|
||||
$tenant = \App\Models\Tenant::find($tenantId);
|
||||
$user = $tenant ? $tenant->user : null;
|
||||
if ($user && $user->pending_purchase) {
|
||||
$user->update([
|
||||
'email_verified_at' => now(),
|
||||
'role' => 'tenant_admin',
|
||||
'pending_purchase' => false,
|
||||
]);
|
||||
Log::info('User activated after subscription purchase: ' . $user->id);
|
||||
}
|
||||
|
||||
// Activate TenantPackage for initial subscription
|
||||
\App\Models\TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
],
|
||||
[
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
// Create initial PackagePurchase
|
||||
\App\Models\PackagePurchase::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $subscriptionId,
|
||||
'price' => $session['amount_total'] / 100,
|
||||
'type' => $type,
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
// Update tenant subscription fields if needed
|
||||
$tenant = \App\Models\Tenant::find($tenantId);
|
||||
if ($tenant) {
|
||||
$tenant->update([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'subscription_status' => 'active',
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info('Initial subscription activated via Stripe checkout session: ' . $session['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send purchase confirmation email: ' . $e->getMessage());
|
||||
}
|
||||
Reference in New Issue
Block a user