--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Http\Controllers\Admin\StripeConfigController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Setting; // Assuming your Setting model is in App\Models
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;

class StripeConfigController extends Controller
{
    protected $settingKeys = [
        'stripe_enabled',
        'stripe_publishable_key',
        'stripe_secret_key',
        'stripe_webhook_secret',
        // Add 'stripe_mode' (live/test) if you want to manage this via settings
    ];

    public function edit()
    {
        if (!function_exists('setting')) {
            return redirect()->route('admin.dashboard')->with('error', 'Settings helper function not found.');
        }

        $settings = [];
        foreach ($this->settingKeys as $key) {
            $settings[$key] = setting($key);
        }
        return view('stripegateway::admin.config', compact('settings'));
    }

    public function update(Request $request)
    {
        if (!function_exists('setting')) {
            return redirect()->route('admin.dashboard')->with('error', 'Settings helper function not found.');
        }

        $validated = $request->validate([
            'stripe_enabled' => 'nullable|boolean',
            'stripe_publishable_key' => 'nullable|string|max:255',
            'stripe_secret_key' => 'nullable|string|max:255',
            'stripe_webhook_secret' => 'nullable|string|max:255',
        ]);

        try {
            foreach ($this->settingKeys as $key) {
                $value = $request->has($key) ? ($key === 'stripe_enabled' ? (bool)$request->input($key) : $request->input($key)) : ($key === 'stripe_enabled' ? '0' : null);
                
                // For boolean 'stripe_enabled', ensure '0' or '1' is stored if it's a checkbox
                if ($key === 'stripe_enabled') {
                    $valueToStore = $request->has('stripe_enabled') ? '1' : '0';
                } else {
                    $valueToStore = $request->input($key);
                }

                Setting::updateOrCreate(
                    ['key' => $key],
                    ['value' => $valueToStore]
                );
            }

            // Clear cache for settings to take effect immediately
            Artisan::call('cache:clear');
            Artisan::call('config:clear');


            return redirect()->back()->with('success', 'Stripe settings updated successfully.');

        } catch (\Exception $e) {
            Log::error('Error updating Stripe settings: ' . $e->getMessage());
            return redirect()->back()->with('error', 'Failed to update Stripe settings. Please check the logs.');
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Http\Controllers\StripeGatewayController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StripeGatewayController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return view('stripegateway::index');
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('stripegateway::create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request) {}

    /**
     * Show the specified resource.
     */
    public function show($id)
    {
        return view('stripegateway::show');
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id)
    {
        return view('stripegateway::edit');
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, $id) {}

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($id) {}
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Http\Controllers\StripeSubscriptionController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Subscription;
use App\Models\SubscriptionPlan;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Modules\StripeGateway\Services\StripeService;
use Stripe\Checkout\Session as StripeCheckoutSession;

class StripeSubscriptionController extends Controller
{
    protected StripeService $stripeService;

    public function __construct(StripeService $stripeService)
    {
        $this->stripeService = $stripeService;
    }

    public function checkout(Request $request, SubscriptionPlan $plan)
    {
        if (setting('stripe_enabled', '0') != '1') {
            return redirect()->route('subscription.plans')->with('error', 'Stripe payments are currently disabled.');
        }

        /** @var User $user */
        $user = Auth::user();

        // Check if user already has an active subscription to this plan or any plan
        // (You might want more sophisticated logic here, e.g., allow upgrades/downgrades)
        $activeSubscription = $user->subscriptions()
                                ->whereIn('status', ['active', 'trialing'])
                                ->first();

        if ($activeSubscription) {
            // If they are trying to subscribe to the same plan they already have and it's active/trialing
            if ($activeSubscription->subscription_plan_id == $plan->id) {
                 return redirect()->route('subscription.plans')->with('info', 'You are already subscribed to this plan.');
            }
            // For now, prevent new subscriptions if one is active.
            // Later, you can implement upgrade/downgrade logic.
            return redirect()->route('subscription.plans')->with('error', 'You already have an active subscription. Please manage it from your profile.');
        }


        try {
            $checkoutSession = $this->stripeService->createCheckoutSession($user, $plan);
            return redirect($checkoutSession->url);
        } catch (\Exception $e) {
            Log::error("Stripe Checkout Error for user {$user->id}, plan {$plan->id}: " . $e->getMessage());
            return redirect()->route('subscription.plans')->with('error', 'Could not initiate Stripe checkout. ' . $e->getMessage());
        }
    }

    public function handleSuccess(Request $request)
    {
        $sessionId = $request->query('session_id');
        if (!$sessionId) {
            return redirect()->route('subscription.plans')->with('error', 'Invalid Stripe session.');
        }

        try {
            if (!setting('stripe_secret_key')) {
                 throw new \Exception('Stripe secret key is not configured.');
            }
            \Stripe\Stripe::setApiKey(setting('stripe_secret_key'));
            $session = StripeCheckoutSession::retrieve($sessionId, ['expand' => ['subscription', 'line_items.data.price.product']]);

            $user = User::find($session->metadata->user_id);
            $plan = SubscriptionPlan::find($session->metadata->plan_id);

            if (!$user || !$plan) {
                Log::error("Stripe success: User or Plan not found from session metadata.", ['session_id' => $sessionId, 'metadata' => $session->metadata]);
                return redirect()->route('subscription.plans')->with('error', 'Subscription details could not be verified.');
            }

            // Check if a subscription record already exists for this Stripe subscription ID
            // This can happen if the webhook processed it first
            $existingSubscription = Subscription::where('gateway_subscription_id', $session->subscription->id)->first();

            if ($existingSubscription) {
                Log::info("Stripe success: Subscription already processed by webhook for Stripe sub ID: " . $session->subscription->id);
                 // Potentially update status if it was pending, though webhook should handle this.
                if ($existingSubscription->status === 'pending') {
                    $existingSubscription->status = $session->subscription->status; // e.g., 'active' or 'trialing'
                    $existingSubscription->starts_at = $session->subscription->current_period_start ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_start) : now();
                    $existingSubscription->ends_at = $session->subscription->current_period_end ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_end) : null;
                    if ($session->subscription->trial_end) {
                        $existingSubscription->trial_ends_at = \Carbon\Carbon::createFromTimestamp($session->subscription->trial_end);
                    }
                    $existingSubscription->save();
                }
            } else {
                // Create new local subscription record
                Subscription::create([
                    'user_id' => $user->id,
                    'subscription_plan_id' => $plan->id,
                    'payment_gateway' => 'stripe',
                    'gateway_subscription_id' => $session->subscription->id,
                    'status' => $session->subscription->status, // e.g., 'active' or 'trialing'
                    'starts_at' => $session->subscription->current_period_start ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_start) : now(),
                    'ends_at' => $session->subscription->current_period_end ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_end) : null,
                    'trial_ends_at' => $session->subscription->trial_end ? \Carbon\Carbon::createFromTimestamp($session->subscription->trial_end) : null,
                ]);
            }
            
            // Update user's Stripe customer ID if it's not set (though StripeService should do this)
            if ($user && $session->customer && !$user->stripe_customer_id) {
                $user->stripe_customer_id = $session->customer;
                $user->save();
            }


            return redirect()->route('dashboard')->with('success', 'Subscription successful! Welcome to ' . $plan->name . '.');

        } catch (\Exception $e) {
            Log::error("Stripe Success Error: " . $e->getMessage(), ['session_id' => $sessionId]);
            return redirect()->route('subscription.plans')->with('error', 'There was an issue confirming your subscription: ' . $e->getMessage());
        }
    }

    public function handleCancel(Request $request)
    {
        return redirect()->route('subscription.plans')->with('info', 'Your subscription process was cancelled.');
    }
    
    public function redirectToCustomerPortal(Request $request)
    {
        /** @var User $user */
        $user = Auth::user();
        if (!$user->stripe_customer_id) {
            return redirect()->back()->with('error', 'Stripe customer profile not found.');
        }

        try {
            $portalSession = $this->stripeService->createBillingPortalSession($user);
            return redirect($portalSession->url);
        } catch (\Exception $e) {
            Log::error("Stripe Billing Portal Error for user {$user->id}: " . $e->getMessage());
            return redirect()->back()->with('error', 'Could not access the billing portal: ' . $e->getMessage());
        }
    }

    public function cancelActiveSubscription(Request $request)
    {
        /** @var User $user */
        $user = Auth::user();
        $activeSubscription = $user->subscriptions()
            ->where('payment_gateway', 'stripe')
            ->whereIn('status', ['active', 'trialing'])
            ->orderBy('created_at', 'desc')
            ->first();

        if (!$activeSubscription) {
            return redirect()->back()->with('info', 'No active Stripe subscription found to cancel.');
        }

        try {
            $cancelledAtGateway = $this->stripeService->cancelSubscriptionAtGateway($activeSubscription);
            if ($cancelledAtGateway) {
                // Webhook should ideally handle the final status update,
                // but we can mark it as 'cancelled' locally for immediate feedback.
                $activeSubscription->status = 'cancelled'; // Or 'pending_cancellation'
                $activeSubscription->cancelled_at = now();
                // ends_at will be updated by webhook when Stripe confirms cancellation at period end
                $activeSubscription->save();
                return redirect()->back()->with('success', 'Your subscription has been scheduled for cancellation at the end of the current billing period.');
            } else {
                return redirect()->back()->with('error', 'Failed to cancel subscription at Stripe. Please try again or contact support.');
            }
        } catch (\Exception $e) {
            Log::error("Error cancelling active Stripe subscription for user {$user->id}, sub ID {$activeSubscription->gateway_subscription_id}: " . $e->getMessage());
            return redirect()->back()->with('error', 'An error occurred while trying to cancel your subscription: ' . $e->getMessage());
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Http\Controllers\StripeWebhookController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Modules\StripeGateway\Services\StripeService;
use Stripe\Event as StripeEvent;
use Stripe\Subscription as StripeSubscriptionObject; // Alias to avoid conflict

class StripeWebhookController extends Controller
{
    protected StripeService $stripeService;

    public function __construct(StripeService $stripeService)
    {
        $this->stripeService = $stripeService;
    }

    public function handleWebhook(Request $request)
    {
        $payload = $request->getContent();
        $sigHeader = $request->header('Stripe-Signature');
        $event = null;

        try {
            $event = $this->stripeService->verifyWebhookSignature($payload, $sigHeader);
        } catch (\UnexpectedValueException $e) {
            // Invalid payload
            Log::error('Stripe Webhook Error: Invalid payload.', ['exception' => $e->getMessage()]);
            return response()->json(['error' => 'Invalid payload'], 400);
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            // Invalid signature
            Log::error('Stripe Webhook Error: Invalid signature.', ['exception' => $e->getMessage()]);
            return response()->json(['error' => 'Invalid signature'], 400);
        } catch (\Exception $e) {
            Log::error('Stripe Webhook Error: Could not verify signature.', ['exception' => $e->getMessage()]);
            return response()->json(['error' => 'Signature verification error'], 400);
        }

        // Handle the event
        switch ($event->type) {
            case 'checkout.session.completed':
                $session = $event->data->object; // contains a \Stripe\Checkout\Session
                // This is often handled by the success redirect, but good to have a fallback
                // or if you need to do something specific when checkout is fully completed.
                // Ensure you get the subscription ID from the session: $session->subscription
                Log::info('Stripe Webhook: checkout.session.completed', ['session_id' => $session->id, 'subscription_id' => $session->subscription]);
                if ($session->subscription && $session->payment_status == 'paid') {
                    $this->handleSubscriptionUpdate($session->subscription, $session->customer);
                }
                break;

            case 'customer.subscription.created':
            case 'customer.subscription.updated':
            case 'customer.subscription.resumed': // Stripe specific for resumed subscriptions
                $stripeSubscription = $event->data->object; // contains a \Stripe\Subscription
                Log::info('Stripe Webhook: ' . $event->type, ['subscription_id' => $stripeSubscription->id]);
                $this->handleSubscriptionUpdate($stripeSubscription->id, $stripeSubscription->customer);
                break;

            case 'customer.subscription.trial_will_end':
                $stripeSubscription = $event->data->object;
                Log::info('Stripe Webhook: customer.subscription.trial_will_end', ['subscription_id' => $stripeSubscription->id]);
                // Send a notification to the user
                break;

            case 'customer.subscription.deleted': // Occurs when a subscription is canceled immediately or at period end.
                $stripeSubscription = $event->data->object; // contains a \Stripe\Subscription
                Log::info('Stripe Webhook: customer.subscription.deleted', ['subscription_id' => $stripeSubscription->id]);
                $this->handleSubscriptionCancellation($stripeSubscription->id);
                break;

            case 'invoice.payment_succeeded':
                $invoice = $event->data->object; // contains an \Stripe\Invoice
                Log::info('Stripe Webhook: invoice.payment_succeeded', ['invoice_id' => $invoice->id, 'subscription_id' => $invoice->subscription]);
                if ($invoice->subscription) {
                    // This event confirms a recurring payment was successful.
                    // The customer.subscription.updated event usually handles the date changes.
                    $this->handleSubscriptionUpdate($invoice->subscription, $invoice->customer);
                }
                break;

            case 'invoice.payment_failed':
                $invoice = $event->data->object; // contains an \Stripe\Invoice
                Log::error('Stripe Webhook: invoice.payment_failed', ['invoice_id' => $invoice->id, 'subscription_id' => $invoice->subscription]);
                if ($invoice->subscription) {
                    $this->handleSubscriptionPaymentFailure($invoice->subscription);
                }
                break;
            
            // ... handle other event types
            default:
                Log::info('Stripe Webhook: Received unhandled event type ' . $event->type);
        }

        return response()->json(['status' => 'success']);
    }

    protected function handleSubscriptionUpdate($stripeSubscriptionId, $stripeCustomerId)
    {
        try {
            if (!setting('stripe_secret_key')) {
                 throw new \Exception('Stripe secret key is not configured for webhook processing.');
            }
            \Stripe\Stripe::setApiKey(setting('stripe_secret_key'));
            $stripeSubscription = StripeSubscriptionObject::retrieve($stripeSubscriptionId); // Use aliased StripeSubscriptionObject

            $localSubscription = Subscription::where('gateway_subscription_id', $stripeSubscription->id)->first();

            $user = User::where('stripe_customer_id', $stripeCustomerId)->first();
            if (!$user && $localSubscription) { // Fallback if customer ID wasn't set on user yet
                $user = $localSubscription->user;
            }
            
            if (!$user) {
                Log::error("Stripe Webhook: User not found for Stripe Customer ID: {$stripeCustomerId}");
                return;
            }
            
            // Ensure user has stripe_customer_id if it's missing
            if ($user && $stripeCustomerId && !$user->stripe_customer_id) {
                $user->stripe_customer_id = $stripeCustomerId;
                $user->save();
            }

            $plan = null;
            if (isset($stripeSubscription->items->data[0]->price->product)) {
                // This assumes you store Stripe Product ID or a mapping if your plan slugs don't match Stripe Product IDs
                // For simplicity, let's assume plan_id was stored in metadata or you can find it
                // A more robust way is to store Stripe Price ID on your SubscriptionPlan model.
                // For now, we'll rely on existing local subscription or metadata if available.
            }
            if (!$plan && $localSubscription) {
                $plan = $localSubscription->plan;
            }
            // If creating a new subscription from webhook (e.g. checkout.session.completed without success redirect hitting first)
            // You'd need to get plan_id from $stripeSubscription->metadata or items.
            // For now, this primarily updates existing subscriptions.

            if ($localSubscription) {
                $localSubscription->status = $stripeSubscription->status;
                $localSubscription->starts_at = \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start);
                $localSubscription->ends_at = \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end);
                $localSubscription->trial_ends_at = $stripeSubscription->trial_end ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end) : null;
                $localSubscription->cancelled_at = $stripeSubscription->cancel_at_period_end ? ($stripeSubscription->canceled_at ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->canceled_at) : now()) : null;
                $localSubscription->save();
                Log::info("Stripe Webhook: Updated local subscription ID {$localSubscription->id} to status {$stripeSubscription->status}");
            } elseif ($user && $plan) { // Create if it doesn't exist (e.g. from checkout.session.completed)
                 Subscription::create([
                    'user_id' => $user->id,
                    'subscription_plan_id' => $plan->id, // This needs to be reliably determined
                    'payment_gateway' => 'stripe',
                    'gateway_subscription_id' => $stripeSubscription->id,
                    'status' => $stripeSubscription->status,
                    'starts_at' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start),
                    'ends_at' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end),
                    'trial_ends_at' => $stripeSubscription->trial_end ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end) : null,
                ]);
                Log::info("Stripe Webhook: Created new local subscription for user {$user->id}, Stripe sub ID {$stripeSubscription->id}");
            } else {
                 Log::warning("Stripe Webhook: Could not create or update local subscription for Stripe sub ID {$stripeSubscription->id}. User or Plan missing.");
            }

        } catch (\Exception $e) {
            Log::error("Stripe Webhook handleSubscriptionUpdate Error for sub ID {$stripeSubscriptionId}: " . $e->getMessage());
        }
    }

    protected function handleSubscriptionCancellation($stripeSubscriptionId)
    {
        $localSubscription = Subscription::where('gateway_subscription_id', $stripeSubscriptionId)->first();
        if ($localSubscription) {
            $localSubscription->status = 'cancelled'; // Or 'ended' depending on Stripe's final status
            $localSubscription->cancelled_at = now();
            // ends_at might already be set if it was cancel_at_period_end
            if (!$localSubscription->ends_at || $localSubscription->ends_at->isFuture()) {
                 // If Stripe cancels it immediately, set ends_at to now.
                 // If it was cancel_at_period_end, ends_at should reflect that.
                 // The customer.subscription.updated event with status 'canceled' often provides the final end date.
            }
            $localSubscription->save();
            Log::info("Stripe Webhook: Cancelled local subscription ID {$localSubscription->id}");
        } else {
            Log::warning("Stripe Webhook: Received cancellation for unknown Stripe subscription ID: {$stripeSubscriptionId}");
        }
    }

    protected function handleSubscriptionPaymentFailure($stripeSubscriptionId)
    {
        $localSubscription = Subscription::where('gateway_subscription_id', $stripeSubscriptionId)->first();
        if ($localSubscription) {
            // Stripe might set status to 'past_due' or 'unpaid'
            // You might want to fetch the latest status from Stripe API if not provided directly in event
            if (!setting('stripe_secret_key')) {
                 Log::error('Stripe secret key not set for fetching subscription status on payment failure.');
                 return;
            }
            \Stripe\Stripe::setApiKey(setting('stripe_secret_key'));
            try {
                $stripeSub = StripeSubscriptionObject::retrieve($stripeSubscriptionId);
                $localSubscription->status = $stripeSub->status; // e.g., 'past_due'
                $localSubscription->save();
                Log::info("Stripe Webhook: Updated local subscription ID {$localSubscription->id} to status {$stripeSub->status} due to payment failure.");
                // TODO: Notify user about payment failure
            } catch (\Exception $e) {
                Log::error("Stripe Webhook: Error fetching Stripe subscription {$stripeSubscriptionId} on payment failure: " . $e->getMessage());
            }
        } else {
            Log::warning("Stripe Webhook: Received payment failure for unknown Stripe subscription ID: {$stripeSubscriptionId}");
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Providers\EventServiceProvider.php ---
<?php

namespace Modules\StripeGateway\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event handler mappings for the application.
     *
     * @var array<string, array<int, string>>
     */
    protected $listen = [];

    /**
     * Indicates if events should be discovered.
     *
     * @var bool
     */
    protected static $shouldDiscoverEvents = true;

    /**
     * Configure the proper event listeners for email verification.
     */
    protected function configureEmailVerification(): void {}
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Providers\RouteServiceProvider.php ---
<?php

namespace Modules\StripeGateway\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    protected string $name = 'ModuleName'; // Replace ModuleName

    public function boot(): void
    {
        parent::boot();
    }

    public function map(): void
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
        // $this->mapAdminRoutes(); // <--- THIS LINE SHOULD BE COMMENTED OUT OR REMOVED
    }

    protected function mapWebRoutes(): void
    {
        Route::middleware('web')
             ->group(module_path($this->name, '/Routes/web.php'));
    }

    protected function mapApiRoutes(): void
    {
        Route::middleware('api')
             ->prefix('api') // Or 'api/' . strtolower($this->name) if you want module-specific API prefixes
             ->name('api.')   // Or 'api.' . strtolower($this->name) . '.'
             ->group(module_path($this->name, '/Routes/api.php'));
    }

    // This entire method should be removed or its call in map() removed
    // if the main app's RouteServiceProvider handles module admin routes.
    /*
    protected function mapAdminRoutes(): void
    {
        Route::middleware(['web', \App\Http\Middleware\IsAdminMiddleware::class])
             ->prefix('admin/' . strtolower($this->name))
             ->name('admin.' . strtolower($this->name) . '.')
             ->group(module_path($this->name, '/Routes/admin.php'));
    }
    */
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\app\Providers\StripeGatewayServiceProvider.php ---
<?php

namespace Modules\StripeGateway\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;

class StripeGatewayServiceProvider extends ServiceProvider
{
    /**
     * @var string $moduleName
     */
    protected string $moduleName = 'StripeGateway';

    /**
     * @var string $moduleNameLower
     */
    protected string $moduleNameLower = 'stripegateway';

    public function boot(): void
    {
        $this->registerConfig();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
        // $this->registerTranslations(); // Add if you have them
        // $this->registerViews();      // Add if you have them

        // Route loading is handled by the main app's RouteServiceProvider loop for modules.
        // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/web.php'));
        // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/admin.php')); // Note: path should be Routes/admin.php
        // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/api.php'));
    }

    public function register(): void
    {
        // Route registration is handled by the main app's RouteServiceProvider loop for modules.
        // if (file_exists(module_path($this->moduleName, 'Providers/RouteServiceProvider.php'))) {
        //     $this->app->register(module_path($this->moduleName, 'Providers/RouteServiceProvider.php'));
        // }
    }

    protected function registerConfig(): void
    {
        $this->publishes([
            module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower . '.php'),
        ], 'config');
        $this->mergeConfigFrom(
            module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower
        );
    }

    public function provides(): array
    {
        return [];
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\config\config.php ---
<?php

return [
    'name' => 'StripeGateway',
];


--- File: D:\projects\digitalvocano\Modules\StripeGateway\database\seeders\StripeGatewayDatabaseSeeder.php ---
<?php

namespace Modules\StripeGateway\Database\Seeders;

use Illuminate\Database\Seeder;

class StripeGatewayDatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // $this->call([]);
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Http\Controllers\Admin\StripeConfigController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Setting; // Assuming your Setting model is in App\Models
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;

class StripeConfigController extends Controller
{
    protected $settingKeys = [
        'stripe_enabled',
        'stripe_publishable_key',
        'stripe_secret_key',
        'stripe_webhook_secret',
        // Add 'stripe_mode' (live/test) if you want to manage this via settings
    ];

    public function edit()
    {
        if (!function_exists('setting')) {
            return redirect()->route('admin.dashboard')->with('error', 'Settings helper function not found.');
        }

        $settings = [];
        foreach ($this->settingKeys as $key) {
            $settings[$key] = setting($key);
        }
        return view('stripegateway::admin.config', compact('settings'));
    }

    public function update(Request $request)
    {
        if (!function_exists('setting')) {
            return redirect()->route('admin.dashboard')->with('error', 'Settings helper function not found.');
        }

        $validated = $request->validate([
            'stripe_enabled' => 'nullable|boolean',
            'stripe_publishable_key' => 'nullable|string|max:255',
            'stripe_secret_key' => 'nullable|string|max:255',
            'stripe_webhook_secret' => 'nullable|string|max:255',
        ]);

        try {
            foreach ($this->settingKeys as $key) {
                $value = $request->has($key) ? ($key === 'stripe_enabled' ? (bool)$request->input($key) : $request->input($key)) : ($key === 'stripe_enabled' ? '0' : null);
                
                // For boolean 'stripe_enabled', ensure '0' or '1' is stored if it's a checkbox
                if ($key === 'stripe_enabled') {
                    $valueToStore = $request->has('stripe_enabled') ? '1' : '0';
                } else {
                    $valueToStore = $request->input($key);
                }

                Setting::updateOrCreate(
                    ['key' => $key],
                    ['value' => $valueToStore]
                );
            }

            // Clear cache for settings to take effect immediately
            Artisan::call('cache:clear');
            Artisan::call('config:clear');


            return redirect()->back()->with('success', 'Stripe settings updated successfully.');

        } catch (\Exception $e) {
            Log::error('Error updating Stripe settings: ' . $e->getMessage());
            return redirect()->back()->with('error', 'Failed to update Stripe settings. Please check the logs.');
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Http\Controllers\StripeGatewayController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StripeGatewayController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return view('stripegateway::index');
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('stripegateway::create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request) {}

    /**
     * Show the specified resource.
     */
    public function show($id)
    {
        return view('stripegateway::show');
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id)
    {
        return view('stripegateway::edit');
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, $id) {}

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($id) {}
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Http\Controllers\StripeSubscriptionController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Subscription;
use App\Models\SubscriptionPlan;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Modules\StripeGateway\Services\StripeService;
use Stripe\Checkout\Session as StripeCheckoutSession;

class StripeSubscriptionController extends Controller
{
    protected StripeService $stripeService;

    public function __construct(StripeService $stripeService)
    {
        $this->stripeService = $stripeService;
    }

    public function checkout(Request $request, SubscriptionPlan $plan)
    {
        if (setting('stripe_enabled', '0') != '1') {
            return redirect()->route('subscription.plans')->with('error', 'Stripe payments are currently disabled.');
        }

        /** @var User $user */
        $user = Auth::user();

        // Check if user already has an active subscription to this plan or any plan
        // (You might want more sophisticated logic here, e.g., allow upgrades/downgrades)
        $activeSubscription = $user->subscriptions()
                                ->whereIn('status', ['active', 'trialing'])
                                ->first();

        if ($activeSubscription) {
            // If they are trying to subscribe to the same plan they already have and it's active/trialing
            if ($activeSubscription->subscription_plan_id == $plan->id) {
                 return redirect()->route('subscription.plans')->with('info', 'You are already subscribed to this plan.');
            }
            // For now, prevent new subscriptions if one is active.
            // Later, you can implement upgrade/downgrade logic.
            return redirect()->route('subscription.plans')->with('error', 'You already have an active subscription. Please manage it from your profile.');
        }


        try {
            $checkoutSession = $this->stripeService->createCheckoutSession($user, $plan);
            return redirect($checkoutSession->url);
        } catch (\Exception $e) {
            Log::error("Stripe Checkout Error for user {$user->id}, plan {$plan->id}: " . $e->getMessage());
            return redirect()->route('subscription.plans')->with('error', 'Could not initiate Stripe checkout. ' . $e->getMessage());
        }
    }

    public function handleSuccess(Request $request)
    {
        $sessionId = $request->query('session_id');
        if (!$sessionId) {
            return redirect()->route('subscription.plans')->with('error', 'Invalid Stripe session.');
        }

        try {
            if (!setting('stripe_secret_key')) {
                 throw new \Exception('Stripe secret key is not configured.');
            }
            \Stripe\Stripe::setApiKey(setting('stripe_secret_key'));
            $session = StripeCheckoutSession::retrieve($sessionId, ['expand' => ['subscription', 'line_items.data.price.product']]);

            $user = User::find($session->metadata->user_id);
            $plan = SubscriptionPlan::find($session->metadata->plan_id);

            if (!$user || !$plan) {
                Log::error("Stripe success: User or Plan not found from session metadata.", ['session_id' => $sessionId, 'metadata' => $session->metadata]);
                return redirect()->route('subscription.plans')->with('error', 'Subscription details could not be verified.');
            }

            // Check if a subscription record already exists for this Stripe subscription ID
            // This can happen if the webhook processed it first
            $existingSubscription = Subscription::where('gateway_subscription_id', $session->subscription->id)->first();

            if ($existingSubscription) {
                Log::info("Stripe success: Subscription already processed by webhook for Stripe sub ID: " . $session->subscription->id);
                 // Potentially update status if it was pending, though webhook should handle this.
                if ($existingSubscription->status === 'pending') {
                    $existingSubscription->status = $session->subscription->status; // e.g., 'active' or 'trialing'
                    $existingSubscription->starts_at = $session->subscription->current_period_start ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_start) : now();
                    $existingSubscription->ends_at = $session->subscription->current_period_end ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_end) : null;
                    if ($session->subscription->trial_end) {
                        $existingSubscription->trial_ends_at = \Carbon\Carbon::createFromTimestamp($session->subscription->trial_end);
                    }
                    $existingSubscription->save();
                }
            } else {
                // Create new local subscription record
                Subscription::create([
                    'user_id' => $user->id,
                    'subscription_plan_id' => $plan->id,
                    'payment_gateway' => 'stripe',
                    'gateway_subscription_id' => $session->subscription->id,
                    'status' => $session->subscription->status, // e.g., 'active' or 'trialing'
                    'starts_at' => $session->subscription->current_period_start ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_start) : now(),
                    'ends_at' => $session->subscription->current_period_end ? \Carbon\Carbon::createFromTimestamp($session->subscription->current_period_end) : null,
                    'trial_ends_at' => $session->subscription->trial_end ? \Carbon\Carbon::createFromTimestamp($session->subscription->trial_end) : null,
                ]);
            }
            
            // Update user's Stripe customer ID if it's not set (though StripeService should do this)
            if ($user && $session->customer && !$user->stripe_customer_id) {
                $user->stripe_customer_id = $session->customer;
                $user->save();
            }


            return redirect()->route('dashboard')->with('success', 'Subscription successful! Welcome to ' . $plan->name . '.');

        } catch (\Exception $e) {
            Log::error("Stripe Success Error: " . $e->getMessage(), ['session_id' => $sessionId]);
            return redirect()->route('subscription.plans')->with('error', 'There was an issue confirming your subscription: ' . $e->getMessage());
        }
    }

    public function handleCancel(Request $request)
    {
        return redirect()->route('subscription.plans')->with('info', 'Your subscription process was cancelled.');
    }
    
    public function redirectToCustomerPortal(Request $request)
    {
        /** @var User $user */
        $user = Auth::user();
        if (!$user->stripe_customer_id) {
            return redirect()->back()->with('error', 'Stripe customer profile not found.');
        }

        try {
            $portalSession = $this->stripeService->createBillingPortalSession($user);
            return redirect($portalSession->url);
        } catch (\Exception $e) {
            Log::error("Stripe Billing Portal Error for user {$user->id}: " . $e->getMessage());
            return redirect()->back()->with('error', 'Could not access the billing portal: ' . $e->getMessage());
        }
    }

    public function cancelActiveSubscription(Request $request)
    {
        /** @var User $user */
        $user = Auth::user();
        $activeSubscription = $user->subscriptions()
            ->where('payment_gateway', 'stripe')
            ->whereIn('status', ['active', 'trialing'])
            ->orderBy('created_at', 'desc')
            ->first();

        if (!$activeSubscription) {
            return redirect()->back()->with('info', 'No active Stripe subscription found to cancel.');
        }

        try {
            $cancelledAtGateway = $this->stripeService->cancelSubscriptionAtGateway($activeSubscription);
            if ($cancelledAtGateway) {
                // Webhook should ideally handle the final status update,
                // but we can mark it as 'cancelled' locally for immediate feedback.
                $activeSubscription->status = 'cancelled'; // Or 'pending_cancellation'
                $activeSubscription->cancelled_at = now();
                // ends_at will be updated by webhook when Stripe confirms cancellation at period end
                $activeSubscription->save();
                return redirect()->back()->with('success', 'Your subscription has been scheduled for cancellation at the end of the current billing period.');
            } else {
                return redirect()->back()->with('error', 'Failed to cancel subscription at Stripe. Please try again or contact support.');
            }
        } catch (\Exception $e) {
            Log::error("Error cancelling active Stripe subscription for user {$user->id}, sub ID {$activeSubscription->gateway_subscription_id}: " . $e->getMessage());
            return redirect()->back()->with('error', 'An error occurred while trying to cancel your subscription: ' . $e->getMessage());
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Http\Controllers\StripeWebhookController.php ---
<?php

namespace Modules\StripeGateway\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Modules\StripeGateway\Services\StripeService;
use Stripe\Event as StripeEvent;
use Stripe\Subscription as StripeSubscriptionObject; // Alias to avoid conflict

class StripeWebhookController extends Controller
{
    protected StripeService $stripeService;

    public function __construct(StripeService $stripeService)
    {
        $this->stripeService = $stripeService;
    }

    public function handleWebhook(Request $request)
    {
        $payload = $request->getContent();
        $sigHeader = $request->header('Stripe-Signature');
        $event = null;

        try {
            $event = $this->stripeService->verifyWebhookSignature($payload, $sigHeader);
        } catch (\UnexpectedValueException $e) {
            // Invalid payload
            Log::error('Stripe Webhook Error: Invalid payload.', ['exception' => $e->getMessage()]);
            return response()->json(['error' => 'Invalid payload'], 400);
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            // Invalid signature
            Log::error('Stripe Webhook Error: Invalid signature.', ['exception' => $e->getMessage()]);
            return response()->json(['error' => 'Invalid signature'], 400);
        } catch (\Exception $e) {
            Log::error('Stripe Webhook Error: Could not verify signature.', ['exception' => $e->getMessage()]);
            return response()->json(['error' => 'Signature verification error'], 400);
        }

        // Handle the event
        switch ($event->type) {
            case 'checkout.session.completed':
                $session = $event->data->object; // contains a \Stripe\Checkout\Session
                // This is often handled by the success redirect, but good to have a fallback
                // or if you need to do something specific when checkout is fully completed.
                // Ensure you get the subscription ID from the session: $session->subscription
                Log::info('Stripe Webhook: checkout.session.completed', ['session_id' => $session->id, 'subscription_id' => $session->subscription]);
                if ($session->subscription && $session->payment_status == 'paid') {
                    $this->handleSubscriptionUpdate($session->subscription, $session->customer);
                }
                break;

            case 'customer.subscription.created':
            case 'customer.subscription.updated':
            case 'customer.subscription.resumed': // Stripe specific for resumed subscriptions
                $stripeSubscription = $event->data->object; // contains a \Stripe\Subscription
                Log::info('Stripe Webhook: ' . $event->type, ['subscription_id' => $stripeSubscription->id]);
                $this->handleSubscriptionUpdate($stripeSubscription->id, $stripeSubscription->customer);
                break;

            case 'customer.subscription.trial_will_end':
                $stripeSubscription = $event->data->object;
                Log::info('Stripe Webhook: customer.subscription.trial_will_end', ['subscription_id' => $stripeSubscription->id]);
                // Send a notification to the user
                break;

            case 'customer.subscription.deleted': // Occurs when a subscription is canceled immediately or at period end.
                $stripeSubscription = $event->data->object; // contains a \Stripe\Subscription
                Log::info('Stripe Webhook: customer.subscription.deleted', ['subscription_id' => $stripeSubscription->id]);
                $this->handleSubscriptionCancellation($stripeSubscription->id);
                break;

            case 'invoice.payment_succeeded':
                $invoice = $event->data->object; // contains an \Stripe\Invoice
                Log::info('Stripe Webhook: invoice.payment_succeeded', ['invoice_id' => $invoice->id, 'subscription_id' => $invoice->subscription]);
                if ($invoice->subscription) {
                    // This event confirms a recurring payment was successful.
                    // The customer.subscription.updated event usually handles the date changes.
                    $this->handleSubscriptionUpdate($invoice->subscription, $invoice->customer);
                }
                break;

            case 'invoice.payment_failed':
                $invoice = $event->data->object; // contains an \Stripe\Invoice
                Log::error('Stripe Webhook: invoice.payment_failed', ['invoice_id' => $invoice->id, 'subscription_id' => $invoice->subscription]);
                if ($invoice->subscription) {
                    $this->handleSubscriptionPaymentFailure($invoice->subscription);
                }
                break;
            
            // ... handle other event types
            default:
                Log::info('Stripe Webhook: Received unhandled event type ' . $event->type);
        }

        return response()->json(['status' => 'success']);
    }

    protected function handleSubscriptionUpdate($stripeSubscriptionId, $stripeCustomerId)
    {
        try {
            if (!setting('stripe_secret_key')) {
                 throw new \Exception('Stripe secret key is not configured for webhook processing.');
            }
            \Stripe\Stripe::setApiKey(setting('stripe_secret_key'));
            $stripeSubscription = StripeSubscriptionObject::retrieve($stripeSubscriptionId); // Use aliased StripeSubscriptionObject

            $localSubscription = Subscription::where('gateway_subscription_id', $stripeSubscription->id)->first();

            $user = User::where('stripe_customer_id', $stripeCustomerId)->first();
            if (!$user && $localSubscription) { // Fallback if customer ID wasn't set on user yet
                $user = $localSubscription->user;
            }
            
            if (!$user) {
                Log::error("Stripe Webhook: User not found for Stripe Customer ID: {$stripeCustomerId}");
                return;
            }
            
            // Ensure user has stripe_customer_id if it's missing
            if ($user && $stripeCustomerId && !$user->stripe_customer_id) {
                $user->stripe_customer_id = $stripeCustomerId;
                $user->save();
            }

            $plan = null;
            if (isset($stripeSubscription->items->data[0]->price->product)) {
                // This assumes you store Stripe Product ID or a mapping if your plan slugs don't match Stripe Product IDs
                // For simplicity, let's assume plan_id was stored in metadata or you can find it
                // A more robust way is to store Stripe Price ID on your SubscriptionPlan model.
                // For now, we'll rely on existing local subscription or metadata if available.
            }
            if (!$plan && $localSubscription) {
                $plan = $localSubscription->plan;
            }
            // If creating a new subscription from webhook (e.g. checkout.session.completed without success redirect hitting first)
            // You'd need to get plan_id from $stripeSubscription->metadata or items.
            // For now, this primarily updates existing subscriptions.

            if ($localSubscription) {
                $localSubscription->status = $stripeSubscription->status;
                $localSubscription->starts_at = \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start);
                $localSubscription->ends_at = \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end);
                $localSubscription->trial_ends_at = $stripeSubscription->trial_end ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end) : null;
                $localSubscription->cancelled_at = $stripeSubscription->cancel_at_period_end ? ($stripeSubscription->canceled_at ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->canceled_at) : now()) : null;
                $localSubscription->save();
                Log::info("Stripe Webhook: Updated local subscription ID {$localSubscription->id} to status {$stripeSubscription->status}");
            } elseif ($user && $plan) { // Create if it doesn't exist (e.g. from checkout.session.completed)
                 Subscription::create([
                    'user_id' => $user->id,
                    'subscription_plan_id' => $plan->id, // This needs to be reliably determined
                    'payment_gateway' => 'stripe',
                    'gateway_subscription_id' => $stripeSubscription->id,
                    'status' => $stripeSubscription->status,
                    'starts_at' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start),
                    'ends_at' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end),
                    'trial_ends_at' => $stripeSubscription->trial_end ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end) : null,
                ]);
                Log::info("Stripe Webhook: Created new local subscription for user {$user->id}, Stripe sub ID {$stripeSubscription->id}");
            } else {
                 Log::warning("Stripe Webhook: Could not create or update local subscription for Stripe sub ID {$stripeSubscription->id}. User or Plan missing.");
            }

        } catch (\Exception $e) {
            Log::error("Stripe Webhook handleSubscriptionUpdate Error for sub ID {$stripeSubscriptionId}: " . $e->getMessage());
        }
    }

    protected function handleSubscriptionCancellation($stripeSubscriptionId)
    {
        $localSubscription = Subscription::where('gateway_subscription_id', $stripeSubscriptionId)->first();
        if ($localSubscription) {
            $localSubscription->status = 'cancelled'; // Or 'ended' depending on Stripe's final status
            $localSubscription->cancelled_at = now();
            // ends_at might already be set if it was cancel_at_period_end
            if (!$localSubscription->ends_at || $localSubscription->ends_at->isFuture()) {
                 // If Stripe cancels it immediately, set ends_at to now.
                 // If it was cancel_at_period_end, ends_at should reflect that.
                 // The customer.subscription.updated event with status 'canceled' often provides the final end date.
            }
            $localSubscription->save();
            Log::info("Stripe Webhook: Cancelled local subscription ID {$localSubscription->id}");
        } else {
            Log::warning("Stripe Webhook: Received cancellation for unknown Stripe subscription ID: {$stripeSubscriptionId}");
        }
    }

    protected function handleSubscriptionPaymentFailure($stripeSubscriptionId)
    {
        $localSubscription = Subscription::where('gateway_subscription_id', $stripeSubscriptionId)->first();
        if ($localSubscription) {
            // Stripe might set status to 'past_due' or 'unpaid'
            // You might want to fetch the latest status from Stripe API if not provided directly in event
            if (!setting('stripe_secret_key')) {
                 Log::error('Stripe secret key not set for fetching subscription status on payment failure.');
                 return;
            }
            \Stripe\Stripe::setApiKey(setting('stripe_secret_key'));
            try {
                $stripeSub = StripeSubscriptionObject::retrieve($stripeSubscriptionId);
                $localSubscription->status = $stripeSub->status; // e.g., 'past_due'
                $localSubscription->save();
                Log::info("Stripe Webhook: Updated local subscription ID {$localSubscription->id} to status {$stripeSub->status} due to payment failure.");
                // TODO: Notify user about payment failure
            } catch (\Exception $e) {
                Log::error("Stripe Webhook: Error fetching Stripe subscription {$stripeSubscriptionId} on payment failure: " . $e->getMessage());
            }
        } else {
            Log::warning("Stripe Webhook: Received payment failure for unknown Stripe subscription ID: {$stripeSubscriptionId}");
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Providers\EventServiceProvider.php ---
<?php

namespace Modules\StripeGateway\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event handler mappings for the application.
     *
     * @var array<string, array<int, string>>
     */
    protected $listen = [];

    /**
     * Indicates if events should be discovered.
     *
     * @var bool
     */
    protected static $shouldDiscoverEvents = true;

    /**
     * Configure the proper event listeners for email verification.
     */
    protected function configureEmailVerification(): void {}
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Providers\RouteServiceProvider.php ---
<?php

namespace Modules\StripeGateway\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    protected string $name = 'StripeGateway'; // Corrected Module name
    protected string $moduleNamespace = 'Modules\StripeGateway\Http\Controllers';

    /**
     * Called before routes are registered.
     *
     * Register any model bindings or pattern based filters.
     */
    public function boot(): void
    {
        parent::boot();
    }

    /**
     * Define the routes for the application.
     */
    public function map(): void
    {
        $this->mapWebRoutes();
        // $this->mapAdminRoutes(); // Admin routes are loaded by the main app's RouteServiceProvider
        $this->mapApiRoutes();
    }

    /**
     * Define the "web" routes for the application.
     *
     * These routes all receive session state, CSRF protection, etc.
     */
    protected function mapWebRoutes(): void
    {
        // The main app's RouteServiceProvider already applies the 'web' middleware.
        // This ensures controllers in web.php are correctly namespaced.
        Route::middleware('web') // Ensures the group call is valid and context is clear
             ->namespace($this->moduleNamespace)
             ->group(module_path($this->name, '/routes/web.php')); // Use lowercase 'routes'
    }

    /**
     * Define the "admin" routes for the application.
     */
    // protected function mapAdminRoutes(): void // This method is not needed as main RSP handles admin routes
    // {
        // The main app's RouteServiceProvider handles loading admin routes,
        // applying 'web' and 'IsAdminMiddleware', prefixing with 'admin/stripegateway',
        // naming with 'admin.stripegateway.', and namespacing to 'Modules\StripeGateway\Http\Controllers\Admin'.
    // }

    /**
     * Define the "api" routes for the application.
     */
    protected function mapApiRoutes(): void
    {
        // The main app's RouteServiceProvider applies 'api' middleware,
        // 'api/stripegateway' prefix, 'api.stripegateway.' name prefix,
        // and the base 'Modules\StripeGateway\Http\Controllers' namespace.
        // This method ensures controllers in api.php are correctly namespaced
        // relative to $this->moduleNamespace.
        Route::namespace($this->moduleNamespace) // Assuming API controllers are directly under Http/Controllers
             ->group(module_path($this->name, '/routes/api.php')); // Use lowercase 'routes'
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Providers\StripeGatewayServiceProvider.php ---
<?php

namespace Modules\StripeGateway\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Nwidart\Modules\Traits\PathNamespace;

class StripeGatewayServiceProvider extends ServiceProvider
{
    /**
     * @var string $moduleName
     */
    protected string $moduleName = 'StripeGateway';

    /**
     * @var string $moduleNameLower
     */
    protected string $moduleNameLower = 'stripegateway';

    use PathNamespace; // Add this trait

    public function boot(): void
    {
        $this->registerConfig();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
        $this->registerTranslations(); // Uncomment and implement
        $this->registerViews();      // Uncomment and implement
        // Route loading is handled by the main app's RouteServiceProvider loop for modules.
        // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/web.php'));
        // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/admin.php')); // Note: path should be Routes/admin.php
        // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/api.php'));
    }
// In Modules\StripeGateway\Providers\StripeGatewayServiceProvider.php
public function register(): void
{
    $this->app->register(RouteServiceProvider::class);
}

    protected function registerConfig(): void
    {
        $this->publishes([
            module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower . '.php'),
        ], 'config');
        $this->mergeConfigFrom(
            module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower
        );
    }

    public function registerViews(): void
    {
        $viewPath = resource_path('views/modules/'.$this->moduleNameLower);
        $sourcePath = module_path($this->moduleName, 'resources/views');

        $this->publishes([$sourcePath => $viewPath], ['views', $this->moduleNameLower.'-module-views']);

        $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);

        Blade::componentNamespace(config('modules.namespace').'\\' . $this->moduleName . '\\View\\Components', $this->moduleNameLower);
    }

    public function registerTranslations(): void
    {
        $langPath = resource_path('lang/modules/' . $this->moduleNameLower);

        if (is_dir($langPath)) {
            $this->loadTranslationsFrom($langPath, $this->moduleNameLower);
            $this->loadJsonTranslationsFrom($langPath);
        } else {
            $this->loadTranslationsFrom(module_path($this->moduleName, 'lang'), $this->moduleNameLower);
            $this->loadJsonTranslationsFrom(module_path($this->moduleName, 'lang'));
        }
    }

    public function provides(): array
    {
        return [];
    }

    private function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach (config('view.paths') as $path) {
            if (is_dir($path.'/modules/'.$this->moduleNameLower)) {
                $paths[] = $path.'/modules/'.$this->moduleNameLower;
            }
        }
        return $paths;
    }
}


--- File: D:\projects\digitalvocano\Modules\StripeGateway\resources\views\admin\config.blade.php ---
@extends('layouts.admin') {{-- Use your admin layout --}}

@section('title', 'Stripe Gateway Settings')
@section('header_title', 'Stripe Gateway Configuration')

@section('content')
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        <h1 class="text-2xl font-medium text-gray-900 dark:text-white mb-6">
            Stripe Settings
        </h1>

        @include('admin.partials.alerts') {{-- For success/error messages --}}

        <form action="{{ route('admin.stripegateway.settings.update') }}" method="POST">
            @csrf
            @method('PUT')

            <div class="space-y-6">
                <!-- Enable Stripe -->
                <div>
                    <label for="stripe_enabled" class="flex items-center">
                        <input type="checkbox" name="stripe_enabled" id="stripe_enabled" value="1"
                               {{ old('stripe_enabled', $settings['stripe_enabled'] ?? '0') == '1' ? 'checked' : '' }}
                               class="rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:bg-gray-700 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800">
                        <span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Enable Stripe Gateway</span>
                    </label>
                    @error('stripe_enabled') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
                </div>

                <!-- Stripe Publishable Key -->
                <div>
                    <label for="stripe_publishable_key" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Stripe Publishable Key</label>
                    <input type="text" name="stripe_publishable_key" id="stripe_publishable_key" value="{{ old('stripe_publishable_key', $settings['stripe_publishable_key'] ?? '') }}"
                           class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm">
                    @error('stripe_publishable_key') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
                </div>

                <!-- Stripe Secret Key -->
                <div>
                    <label for="stripe_secret_key" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Stripe Secret Key</label>
                    <input type="password" name="stripe_secret_key" id="stripe_secret_key" value="{{ old('stripe_secret_key', $settings['stripe_secret_key'] ?? '') }}"
                           class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
                           placeholder="Leave blank to keep current secret">
                    <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enter a new secret key to update it. Leave blank to keep the existing one.</p>
                    @error('stripe_secret_key') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
                </div>

                <!-- Stripe Webhook Signing Secret -->
                <div>
                    <label for="stripe_webhook_secret" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Stripe Webhook Signing Secret</label>
                    <input type="password" name="stripe_webhook_secret" id="stripe_webhook_secret" value="{{ old('stripe_webhook_secret', $settings['stripe_webhook_secret'] ?? '') }}"
                           class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
                           placeholder="Leave blank to keep current secret">
                    <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enter a new webhook secret to update it. Leave blank to keep the existing one.</p>
                    @error('stripe_webhook_secret') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
                </div>

            </div>

            <div class="mt-8 flex justify-end">
                <button type="submit"
                        class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition ease-in-out duration-150">
                    Save Settings
                </button>
            </div>
        </form>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\StripeGateway\resources\views\components\layouts\master.blade.php ---
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">

        <title>StripeGateway Module - {{ config('app.name', 'Laravel') }}</title>

        <meta name="description" content="{{ $description ?? '' }}">
        <meta name="keywords" content="{{ $keywords ?? '' }}">
        <meta name="author" content="{{ $author ?? '' }}">

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

        {{-- Vite CSS --}}
        {{-- {{ module_vite('build-stripegateway', 'resources/assets/sass/app.scss') }} --}}
    </head>

    <body>
        {{ $slot }}

        {{-- Vite JS --}}
        {{-- {{ module_vite('build-stripegateway', 'resources/assets/js/app.js') }} --}}
    </body>


--- File: D:\projects\digitalvocano\Modules\StripeGateway\resources\views\checkout_redirect.blade.php ---


--- File: D:\projects\digitalvocano\Modules\StripeGateway\resources\views\index.blade.php ---
<x-stripegateway::layouts.master>
    <h1>Hello World</h1>

    <p>Module: {!! config('stripegateway.name') !!}</p>
</x-stripegateway::layouts.master>


--- File: D:\projects\digitalvocano\Modules\StripeGateway\routes\admin.php ---
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Admin Routes
|--------------------------------------------------------------------------
|
| Here is where you can register admin routes for your module. These
| routes are loaded by the main application's RouteServiceProvider within
| a group which LIES WITHIN the "admin" prefix and "admin." naming
| convention.
|
*/

// The main app/Providers/RouteServiceProvider.php already applies the
// 'Modules\StripeGateway\Http\Controllers\Admin' namespace.
// Routes defined here will use that namespace.

// This route will be accessible via URL: admin/stripegateway/settings
// And named: admin.stripegateway.settings.edit
Route::get('settings', 'StripeConfigController@edit')->name('settings.edit');
Route::post('settings', 'StripeConfigController@update')->name('settings.update');

// Add other admin routes for StripeGateway here
// Example: Route::get('transactions', 'TransactionController@index')->name('transactions.index');
// This would be admin.stripegateway.transactions.index


--- File: D:\projects\digitalvocano\Modules\StripeGateway\routes\api.php ---
<?php

use Illuminate\Support\Facades\Route;
use Modules\StripeGateway\Http\Controllers\StripeGatewayController;

Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
    Route::apiResource('stripegateways', StripeGatewayController::class)->names('stripegateway');
});


--- File: D:\projects\digitalvocano\Modules\StripeGateway\routes\web.php ---
<?php

use Illuminate\Support\Facades\Route;
use Modules\StripeGateway\Http\Controllers\StripeSubscriptionController;
use Modules\StripeGateway\Http\Controllers\StripeWebhookController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the ServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

// Webhook - should generally be exempt from CSRF if not already handled by VerifyCsrfToken exceptions
Route::post('webhooks/stripe', [StripeWebhookController::class, 'handleWebhook'])->name('webhooks.stripe.handle');


Route::middleware(['web', 'auth']) // User must be authenticated
     ->prefix('subscribe/stripe')
     ->name('subscription.stripe.') // Changed name for consistency
     ->group(function () {
         // The {subscriptionPlan:slug} will use route model binding if your SubscriptionPlan model uses 'slug' as its route key name
         Route::post('checkout/{subscriptionPlan:slug}', [StripeSubscriptionController::class, 'checkout'])->name('checkout');
         Route::get('success', [StripeSubscriptionController::class, 'handleSuccess'])->name('success');
         Route::get('cancel', [StripeSubscriptionController::class, 'handleCancel'])->name('cancel');
         Route::post('portal', [StripeSubscriptionController::class, 'redirectToCustomerPortal'])->name('customer.portal');
         Route::post('cancel-active', [StripeSubscriptionController::class, 'cancelActiveSubscription'])->name('cancel.active');
     });


--- File: D:\projects\digitalvocano\Modules\StripeGateway\Services\StripeService.php ---
<?php

namespace Modules\StripeGateway\Services;

use App\Models\Subscription;
use App\Models\SubscriptionPlan;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Stripe\Stripe;
use Stripe\Customer as StripeCustomer;
use Stripe\Checkout\Session as StripeCheckoutSession;
use Stripe\Subscription as StripeSubscription;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook as StripeWebhook;
use Stripe\BillingPortal\Session as StripeBillingPortalSession;


class StripeService
{
    protected $secretKey;
    protected $webhookSecret;

    public function __construct(?string $secretKey, ?string $webhookSecret)
    {
        $this->secretKey = $secretKey;
        $this->webhookSecret = $webhookSecret;
        if ($this->secretKey) {
            Stripe::setApiKey($this->secretKey);
            Stripe::setAppInfo(
                config('app.name', 'Laravel App'), // Your application's name
                // app()->version(), // Optional: Your application's version
                // url('/') // Optional: Your application's URL
            );
        }
    }

    protected function ensureApiKeysAreSet()
    {
        if (!$this->secretKey || !setting('stripe_publishable_key')) {
            Log::error('Stripe API keys are not configured.');
            throw new \Exception('Stripe API keys are not configured. Please set them in the admin settings.');
        }
    }

    public function getOrCreateStripeCustomer(User $user): StripeCustomer
    {
        $this->ensureApiKeysAreSet();

        if ($user->stripe_customer_id) {
            try {
                return StripeCustomer::retrieve($user->stripe_customer_id);
            } catch (\Stripe\Exception\InvalidRequestException $e) {
                // Customer might have been deleted on Stripe's end, create a new one
                Log::warning("Stripe customer ID {$user->stripe_customer_id} for user {$user->id} not found on Stripe. Creating a new one.");
            }
        }

        $customer = StripeCustomer::create([
            'email' => $user->email,
            'name' => $user->name,
            'metadata' => [
                'user_id' => $user->id,
                'app_name' => config('app.name')
            ]
        ]);

        // Save the Stripe customer ID to your user model
        // You'll need to add a 'stripe_customer_id' column to your users table
        // php artisan make:migration add_stripe_customer_id_to_users_table --table=users
        // In migration: $table->string('stripe_customer_id')->nullable()->index();
        $user->stripe_customer_id = $customer->id;
        $user->save();

        return $customer;
    }

    public function createCheckoutSession(User $user, SubscriptionPlan $plan): StripeCheckoutSession
    {
        $this->ensureApiKeysAreSet();
        $stripeCustomer = $this->getOrCreateStripeCustomer($user);

        $lineItems = [
            [
                'price_data' => [
                    'currency' => strtolower($plan->currency),
                    'product_data' => [
                        'name' => $plan->name,
                        'description' => $plan->description,
                    ],
                    'unit_amount' => (int) ($plan->price * 100), // Price in cents
                    'recurring' => [
                        'interval' => $plan->interval, // 'month', 'year', 'day', 'week'
                        'interval_count' => $plan->interval_count,
                    ],
                ],
                'quantity' => 1,
            ],
        ];
        
        $checkoutSessionParams = [
            'customer' => $stripeCustomer->id,
            'payment_method_types' => ['card'],
            'line_items' => $lineItems,
            'mode' => 'subscription',
            'success_url' => route('subscription.stripe.success') . '?session_id={CHECKOUT_SESSION_ID}',
            'cancel_url' => route('subscription.stripe.cancel'),
            'metadata' => [
                'user_id' => $user->id,
                'plan_id' => $plan->id,
                'plan_slug' => $plan->slug,
            ],
        ];

        if ($plan->trial_period_days > 0) {
            $checkoutSessionParams['subscription_data'] = [
                'trial_period_days' => $plan->trial_period_days,
            ];
        }

        return StripeCheckoutSession::create($checkoutSessionParams);
    }
    
    public function createBillingPortalSession(User $user)
    {
        $this->ensureApiKeysAreSet();
        if (!$user->stripe_customer_id) {
            // This should ideally not happen if user has active subscriptions
            // but as a fallback, try to create/retrieve customer
            $this->getOrCreateStripeCustomer($user);
            if (!$user->stripe_customer_id) {
                 throw new \Exception('Stripe customer ID not found for this user.');
            }
        }

        return StripeBillingPortalSession::create([
            'customer' => $user->stripe_customer_id,
            'return_url' => route('profile.show'), // Or your desired return URL
        ]);
    }


    public function verifyWebhookSignature(string $payload, string $signatureHeader): \Stripe\Event
    {
        if (!$this->webhookSecret) {
            Log::error('Stripe webhook secret is not configured.');
            throw new \Exception('Stripe webhook secret is not configured.');
        }
        return StripeWebhook::constructEvent($payload, $signatureHeader, $this->webhookSecret);
    }

    public function cancelSubscriptionAtGateway(Subscription $localSubscription): bool
    {
        $this->ensureApiKeysAreSet();
        if (!$localSubscription->gateway_subscription_id) {
            Log::error("Cannot cancel Stripe subscription for local subscription ID {$localSubscription->id}: missing gateway_subscription_id.");
            return false;
        }

        try {
            $stripeSubscription = StripeSubscription::retrieve($localSubscription->gateway_subscription_id);
            // $stripeSubscription->cancel(); // Immediate cancellation
            $stripeSubscription->update($localSubscription->gateway_subscription_id, ['cancel_at_period_end' => true]); // Cancel at period end
            
            Log::info("Stripe subscription {$localSubscription->gateway_subscription_id} scheduled for cancellation at period end.");
            return true;
        } catch (\Exception $e) {
            Log::error("Error cancelling Stripe subscription {$localSubscription->gateway_subscription_id}: " . $e->getMessage());
            return false;
        }
    }
}


