diff --git a/AGENT.md b/AGENT.md
new file mode 100644
index 000000000..55f578b4f
--- /dev/null
+++ b/AGENT.md
@@ -0,0 +1,112 @@
+# 🤖 AFAQCM Agent Guide
+
+Welcome to the **AFAQCM Assessment Platform**.
+
+This guide is intended for **assessment agents** who are responsible for managing company assessments, sending quotations, and ensuring that clients are guided through the evaluation journey using professional standards like the "الرباعية العبدلية" framework.
+
+---
+
+## 🧱 System Overview
+
+- **Backend:** Laravel 12
+- **Admin Panel:** FilamentPHP 3.2
+- **Frontend:** React + TypeScript (via Inertia.js)
+
+AFAQCM allows companies to assess departments using industry-standard tools built around defined **domains**, **categories**, and **criteria**. One such example is the **Abdulliya Quadrant** (`الرباعية العبدلية`), used to evaluate marketing readiness.
+
+---
+
+## 🧑💻 Your Role as Agent
+
+Agents are responsible for:
+
+- Monitoring incoming **assessment requests**
+- Communicating with requesting companies
+- Sending tailored **tool quotations**
+- Assisting with onboarding companies to use the assessment tools
+- Following up with free and premium users
+
+---
+
+## 🔍 Understanding the Tool Structure
+
+Each tool consists of:
+
+1. **Domain** (e.g., Structure, Process, Innovation & Tech, Impact)
+2. **Categories** under each domain (e.g., Team Structure, Strategic Planning)
+3. **Criteria** that users evaluate or are evaluated against
+
+All of these are created and managed from the **Filament Admin Panel**.
+
+---
+
+## 🧾 Handling a New Request
+
+1. Go to the **Filament Admin Panel**
+2. Navigate to:
+ `Tool Requests → View`
+3. Review user details:
+ - Tool requested
+ - Company name and contact info
+ - User type: Free or Premium
+
+---
+
+## 📬 Sending a Quotation
+
+If the user is not yet Premium:
+
+1. Prepare a quotation with course/tool price and duration.
+2. Use the `SendToolQuotation` feature to generate and email a quote.
+3. Update the tool request status to `Quoted`.
+
+Make sure to:
+- Personalize the message
+- Attach any helpful documents (PDFs, brochures, etc.)
+
+---
+
+## 🛠 Using the Assessment Tool
+
+Users (free or premium) complete assessments directly on the **React frontend**.
+
+You can:
+- View user progress in the admin panel
+- See which tool, domain, and category they’re interacting with
+- Generate a **readiness report** if needed
+
+---
+
+## 📌 Tips for Agents
+
+- Understand the assessment framework used in each tool (e.g., `الرباعية العبدلية`)
+- Guide clients through tool expectations if needed
+- Follow up with leads who requested but haven’t submitted assessments
+- Mark tool requests with clear status updates: `Pending`, `Quoted`, `Completed`
+
+---
+
+## 📈 Example: Abdulliya Quadrant Tool (الرباعية العبدلية)
+
+This tool includes 4 domains:
+1. **Structure** – team roles, resources, policies
+2. **Process** – marketing strategy, execution, data use
+3. **Innovation & Tech** – CRM, automation, tech readiness
+4. **Impact** – ROI, brand influence, customer satisfaction
+
+Each domain includes weighted criteria and scoring. Results determine if a company is:
+- **Beginner**
+- **Intermediate**
+- **Mature**
+
+---
+
+## 📞 Need Help?
+
+- Platform issues? → `support@afaqcm.com`
+- Quotation questions? → Contact the administrator
+- PDF references → See attached SADARAH-OMI-011 for tool criteria
+
+---
+
+**Thank you for representing AFAQCM professionally and helping clients improve through structured assessments.**
diff --git a/GartnerPDFGenerator.php b/GartnerPDFGenerator.php
new file mode 100644
index 000000000..577607752
--- /dev/null
+++ b/GartnerPDFGenerator.php
@@ -0,0 +1,952 @@
+set([
+ 'isRemoteEnabled' => true, // Allow external resources
+ 'isHtml5ParserEnabled' => true, // Better HTML parsing
+ 'isFontSubsettingEnabled' => true, // Optimize fonts
+ 'defaultFont' => 'DejaVu Sans', // Reliable default font
+ 'dpi' => 96, // Standard web DPI
+ 'fontHeightRatio' => 1.1, // Better line spacing
+ 'debugKeepTemp' => false, // Set true for debugging
+ 'chroot' => realpath(''), // Security measure
+ ]);
+
+ $this->dompdf = new Dompdf($options);
+
+ // Sample data structure matching the original document
+ $this->data = [
+ 'title' => 'The CMO Value Playbook',
+ 'subtitle' => '5 strategies to boost influence and showcase marketing\'s value',
+ 'statistics' => [
+ 'unable_to_prove' => 48,
+ 'able_to_prove' => 52,
+ 'cfo_skeptical' => 40,
+ 'ceo_skeptical' => 39
+ ],
+ 'steps' => [
+ [
+ 'number' => 1,
+ 'title' => 'Focus on marketing\'s long-term, holistic impact',
+ 'metrics' => [
+ 'holistic_long_term' => 68,
+ 'holistic_short_term' => 51,
+ 'individual_long_term' => 49,
+ 'individual_short_term' => 30
+ ]
+ ],
+ [
+ 'number' => 2,
+ 'title' => 'Build a narrative about marketing\'s value for all stakeholders',
+ 'description' => 'CEOs and CFOs are the most skeptical of marketing\'s value.'
+ ],
+ [
+ 'number' => 3,
+ 'title' => 'Increase variety and sophistication of metric types',
+ 'metrics' => [
+ 'no_high_complexity' => 37,
+ 'high_complexity' => 56
+ ]
+ ],
+ [
+ 'number' => 4,
+ 'title' => 'Expand leadership involvement in D&A activity',
+ 'description' => 'More leadership participation in D&A activity is advantageous.'
+ ],
+ [
+ 'number' => 5,
+ 'title' => 'Invest in marketing talent to close gaps',
+ 'barriers' => [
+ 'soft_skills' => 39,
+ 'analytical_talent' => 34,
+ 'technical_talent' => 33
+ ]
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Generate the complete PDF document
+ */
+ public function generate()
+ {
+ $html = $this->buildCompleteHTML();
+
+ $this->dompdf->loadHtml($html);
+ $this->dompdf->setPaper('A4', 'portrait');
+ $this->dompdf->render();
+
+ return $this->dompdf->output();
+ }
+
+ /**
+ * Build the complete HTML structure
+ */
+ private function buildCompleteHTML()
+ {
+ $css = $this->getStyles();
+ $coverPage = $this->getCoverPage();
+ $summaryPage = $this->getSummaryPage();
+ $contentPages = $this->getContentPages();
+
+ return "
+
+
+
+
+
+
Gartner
+
{$this->data['title']}
+
{$this->data['subtitle']}
+
";
+ }
+
+ /**
+ * Generate the summary page with key statistics
+ */
+ private function getSummaryPage()
+ {
+ $stats = $this->data['statistics'];
+
+ return "
+
+
Only half of marketing leaders can prove value and receive credit
+
+
The 2024 Gartner Marketing Analytics and Technology Survey indicates that proving the value of marketing and receiving credit for business outcomes is a common challenge, with nearly half of marketing leaders feeling it's out of reach.
+
+
+
+
+
{$stats['unable_to_prove']}%
+
Unable to prove value and/or don't get credit
+
+
+
{$stats['able_to_prove']}%
+
Able to prove value and get credit
+
+
+
+
+
+
+
{$stats['unable_to_prove']}%
+
+
+
+
5 steps to success for CMOs
+
+
";
+
+ foreach ($this->data['steps'] as $step) {
+ $html .= "
+
";
+ }
+
+ $html .= "
+
+
";
+
+ return $html;
+ }
+
+ /**
+ * Generate content pages for each step
+ */
+ private function getContentPages()
+ {
+ $html = '';
+
+ foreach ($this->data['steps'] as $index => $step) {
+ $html .= '
+ ";
+
+ // Add specific content based on step
+ switch ($currentStep) {
+ case 1:
+ $html .= $this->getStep1Content($step);
+ break;
+ case 2:
+ $html .= $this->getStep2Content($step);
+ break;
+ case 3:
+ $html .= $this->getStep3Content($step);
+ break;
+ case 4:
+ $html .= $this->getStep4Content($step);
+ break;
+ case 5:
+ $html .= $this->getStep5Content($step);
+ break;
+ }
+
+ $html .= "
+
";
+
+ return $html;
+ }
+
+ /**
+ * Step 1 specific content with metrics
+ */
+ private function getStep1Content($step)
+ {
+ $metrics = $step['metrics'];
+
+ return "
+
+
🚀 UNLOCK COMPREHENSIVE ANALYSIS
+
Upgrade to Premium for detailed category breakdowns, advanced analytics, AI-powered recommendations, action plans, and unlimited assessments.
+
+
+
+
+ ';
+
+ return $html;
+ }
+
+ /**
+ * Get compact recommendations
+ */
+ private function getCompactRecommendations(float $overallScore, array $domainResults): string
+ {
+ $recommendations = '';
+
+ if ($overallScore >= 80) {
+ $recommendations = '
+
+
🎯 Priority Domains Requiring Attention:
+
The following domains scored below 70%: ' . $domainList . '
+
Consider developing domain-specific improvement plans with targeted actions and timelines.
+
';
+ }
+ }
+
+ return $recommendations;
+ }
+
+ /**
+ * Get assessment status based on score
+ */
+ private function getAssessmentStatus(float $score): array
+ {
+ if ($score >= 80) {
+ return [
+ 'label' => '✅ EXCELLENT',
+ 'description' => 'Outstanding results! Your organization demonstrates excellent capabilities across all assessed areas.',
+ 'bg_color' => '#dcfce7',
+ 'text_color' => '#166534',
+ ];
+ } elseif ($score >= 70) {
+ return [
+ 'label' => '✅ GOOD',
+ 'description' => 'Solid results with good foundations. Strong capabilities with some enhancement opportunities.',
+ 'bg_color' => '#dbeafe',
+ 'text_color' => '#1e40af',
+ ];
+ } elseif ($score >= 60) {
+ return [
+ 'label' => '⚠️ ACCEPTABLE',
+ 'description' => 'Acceptable baseline with opportunities for improvement. Focus on enhancing key areas.',
+ 'bg_color' => '#fef3c7',
+ 'text_color' => '#92400e',
+ ];
+ } elseif ($score >= 40) {
+ return [
+ 'label' => '⚠️ NEEDS IMPROVEMENT',
+ 'description' => 'Performance below optimal levels. Immediate attention required in multiple areas.',
+ 'bg_color' => '#fed7d7',
+ 'text_color' => '#c53030',
+ ];
+ } else {
+ return [
+ 'label' => '❌ CRITICAL',
+ 'description' => 'Significant performance gaps identified. Urgent comprehensive review required.',
+ 'bg_color' => '#fecaca',
+ 'text_color' => '#991b1b',
+ ];
+ }
+ }
+
+ /**
+ * Get score color based on percentage
+ */
+ private function getScoreColor(float $score): string
+ {
+ if ($score >= 80) return '#10b981'; // emerald
+ if ($score >= 70) return '#3b82f6'; // blue
+ if ($score >= 60) return '#f59e0b'; // amber
+ if ($score >= 40) return '#f97316'; // orange
+ return '#ef4444'; // red
+ }
+
+ /**
+ * Generate filename for the PDF
+ */
+ private function generateFilename(Assessment $assessment): string
+ {
+ $toolName = str_replace([' ', '/', '\\', ':', '*', '?', '"', '<', '>', '|'], '_', $assessment->tool->name_en);
+ $date = now()->format('Y-m-d');
+ $assessmentId = $assessment->id;
+
+ return "Assessment_Report_{$toolName}_{$assessmentId}_{$date}.pdf";
+ }
+}
diff --git a/app/Http/Controllers/GuestAssessmentController.php b/app/Http/Controllers/GuestAssessmentController.php
new file mode 100644
index 000000000..d14c49d95
--- /dev/null
+++ b/app/Http/Controllers/GuestAssessmentController.php
@@ -0,0 +1,756 @@
+user();
+
+
+
+ Log::info('Index method called', ['user_id' => $user?->id ?? 'guest']);
+
+ // If user is authenticated, redirect them to their appropriate page
+ if ($user) {
+ try {
+ // Load relationships safely
+ $user->load(['subscription', 'details', 'roles']);
+ Log::info('User relationships loaded', ['user_id' => $user->id]);
+
+ // Create default subscription/details if missing
+ if (!$user->subscription || !$user->details) {
+ Log::info('Creating default subscription/details', ['user_id' => $user->id]);
+ $user->createDefaultSubscriptionAndDetails();
+ $user->refresh();
+ }
+
+ // Redirect based on user type - FIXED ROUTES
+ if ($user->isAdmin()) {
+ Log::info('Redirecting admin to dashboard', ['user_id' => $user->id]);
+ return redirect()->route('dashboard');
+ } elseif ($user->isPremium()) {
+ Log::info('Redirecting premium to dashboard', ['user_id' => $user->id]);
+ return redirect()->route('dashboard');
+ } else {
+ Log::info('Redirecting free user to assessment tools', ['user_id' => $user->id]);
+ return redirect()->route('assessment-tools');
+ }
+ } catch (Exception $e) {
+ // Log error and fallback to assessment tools
+ Log::error('Home redirect failed', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ return redirect()->route('assessment-tools');
+ }
+ }
+
+ // Show welcome page for guests
+ Log::info('Showing welcome page for guest');
+ return $this->renderWelcomePage();
+ }
+
+ /**
+ * Show the home2 page (/home) - For free users to register/subscribe
+ */
+ public function index2()
+ {
+ $user = auth()->user();
+
+ Log::info('Index2 method called', ['user_id' => $user?->id ?? 'guest']);
+
+ try {
+ // If user is authenticated, show them the welcome page with user info
+ if ($user) {
+ Log::info('Authenticated user accessing home2', ['user_id' => $user->id]);
+
+ // Load relationships safely
+ $user->load(['subscription', 'details', 'roles']);
+
+ // Create default subscription/details if missing
+ if (!$user->subscription || !$user->details) {
+ Log::info('Creating default subscription/details for user', ['user_id' => $user->id]);
+ $user->createDefaultSubscriptionAndDetails();
+ $user->refresh();
+ }
+
+ // Show welcome page with user authentication info (NO BLOCKING)
+ return $this->renderWelcomePage([
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'subscription' => $user->subscription ? [
+ 'plan_type' => $user->subscription->plan_type,
+ 'status' => $user->subscription->status,
+ ] : null,
+ ]
+ ]);
+
+ } else {
+ Log::info('Guest user accessing home2');
+ // Show welcome page for guests
+ return $this->renderWelcomePage();
+ }
+
+ } catch (Exception $e) {
+ // Log detailed error information
+ Log::error('Index2 failed', [
+ 'user_id' => $user?->id ?? 'guest',
+ 'error' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ // Fallback - show basic welcome page
+ return $this->renderWelcomePage($user ? [
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ ]
+ ] : null);
+ }
+ }
+
+ /**
+ * Render welcome page with consistent structure
+ */
+ private function renderWelcomePage($authData = null)
+ {
+ $data = [
+ 'auth' => $authData ? $authData : ['user' => null],
+ 'locale' => app()->getLocale()
+ ];
+
+ Log::info('Rendering Welcome2 page', [
+ 'auth_data' => $authData ? 'provided' : 'null',
+ 'locale' => $data['locale']
+ ]);
+
+ try {
+ return Inertia::render('Welcome2', $data);
+ } catch (Exception $e) {
+ Log::error('Failed to render Welcome2', [
+ 'error' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine()
+ ]);
+
+ // Ultimate fallback - simple response
+ return response()->json([
+ 'error' => 'Page rendering failed',
+ 'debug' => [
+ 'message' => $e->getMessage(),
+ 'file' => basename($e->getFile()),
+ 'line' => $e->getLine()
+ ]
+ ], 500);
+ }
+ }
+
+ /**
+ * Start a new assessment
+ */
+ public function start(Request $request)
+ {
+ Log::info('Assessment start requested', $request->all());
+
+ $validator = Validator::make($request->all(), [
+ 'tool_id' => 'required|exists:tools,id',
+ 'name' => 'required|string|max:255',
+ 'email' => 'required|email|max:255',
+ 'organization' => 'nullable|string|max:255',
+ ]);
+
+ if ($validator->fails()) {
+ Log::warning('Assessment start validation failed', $validator->errors()->toArray());
+ return back()->withErrors($validator)->withInput();
+ }
+
+ try {
+ $tool = Tool::with(['domains.categories.criteria'])->findOrFail($request->tool_id);
+
+ // Create assessment
+ $assessment = Assessment::create([
+ 'user_id' => Auth::id(), // This will be null for guests
+ 'tool_id' => $tool->id,
+ 'name' => $request->name,
+ 'email' => $request->email,
+ 'organization' => $request->organization,
+ 'status' => 'in_progress',
+ 'started_at' => now(),
+ ]);
+
+ Log::info('Assessment created', ['assessment_id' => $assessment->id]);
+
+ // Create guest session tracking
+ if (!Auth::check()) {
+ GuestSession::createFromRequest($request, $assessment);
+ Log::info('Guest session created', ['assessment_id' => $assessment->id]);
+ }
+
+ return redirect()->route('assessment.take', ['assessment' => $assessment->id]);
+
+ } catch (Exception $e) {
+ Log::error('Assessment start failed', [
+ 'error' => $e->getMessage(),
+ 'request_data' => $request->all()
+ ]);
+
+ return back()->withErrors(['general' => 'Failed to start assessment. Please try again.']);
+ }
+ }
+
+ /**
+ * Take the assessment
+ */
+ public function take(Assessment $assessment)
+ {
+ Log::info('Assessment take requested', ['assessment_id' => $assessment->id]);
+
+ try {
+ // Load assessment with all related data
+ $assessment->load([
+ 'tool.domains.categories.criteria' => function ($query) {
+ $query->orderBy('order');
+ },
+ 'responses'
+ ]);
+
+ // Check if user can access this assessment
+ if (Auth::check() && $assessment->user_id && $assessment->user_id !== Auth::id()) {
+ Log::warning('Unauthorized assessment access attempt', [
+ 'assessment_id' => $assessment->id,
+ 'user_id' => Auth::id(),
+ 'assessment_user_id' => $assessment->user_id
+ ]);
+ abort(403);
+ }
+
+ // Get existing responses
+ $existingResponses = $assessment->responses->keyBy('criterion_id');
+
+ return Inertia::render('assessment/Take', [
+ 'assessment' => $assessment,
+ 'tool' => $assessment->tool,
+ 'existingResponses' => $existingResponses,
+ 'completionPercentage' => $assessment->getCompletionPercentage(),
+ ]);
+
+ } catch (Exception $e) {
+ Log::error('Assessment take failed', [
+ 'assessment_id' => $assessment->id,
+ 'error' => $e->getMessage()
+ ]);
+
+ return back()->withErrors(['general' => 'Failed to load assessment. Please try again.']);
+ }
+ }
+
+ /**
+ * Save assessment response
+ */
+ public function saveResponse(Request $request, Assessment $assessment)
+ {
+ Log::info('Save response requested', [
+ 'assessment_id' => $assessment->id,
+ 'criterion_id' => $request->criterion_id
+ ]);
+
+ try {
+ $validator = Validator::make($request->all(), [
+ 'criterion_id' => 'required|exists:criteria,id',
+ 'response' => 'required|in:yes,no,na',
+ 'notes' => 'nullable|string|max:2000',
+ 'attachment' => 'nullable|file|max:10240|mimes:pdf,doc,docx,jpg,jpeg,png,txt',
+ ]);
+
+ if ($validator->fails()) {
+ Log::warning('Save response validation failed', $validator->errors()->toArray());
+ return back()->withErrors($validator);
+ }
+
+ // Check if user can access this assessment
+ if (Auth::check() && $assessment->user_id && $assessment->user_id !== Auth::id()) {
+ Log::warning('Unauthorized response save attempt');
+ abort(403);
+ }
+
+ $responseData = [
+ 'assessment_id' => $assessment->id,
+ 'criterion_id' => $request->criterion_id,
+ 'response' => $request->response,
+ 'notes' => $request->input('notes'),
+ ];
+
+ // Handle file upload
+ if ($request->hasFile('attachment')) {
+ $file = $request->file('attachment');
+ $fileName = time() . '_' . $request->criterion_id . '_' . $file->getClientOriginalName();
+ $filePath = $file->storeAs('assessments/' . $assessment->id, $fileName, 'public');
+ $responseData['attachment'] = $filePath;
+ Log::info('File uploaded', ['file_path' => $filePath]);
+ }
+
+ // Save or update response
+ AssessmentResponse::updateOrCreate(
+ [
+ 'assessment_id' => $assessment->id,
+ 'criterion_id' => $request->criterion_id,
+ ],
+ $responseData
+ );
+
+ // Recalculate completion percentage
+ $completionPercentage = $assessment->getCompletionPercentage();
+ $isComplete = $assessment->isComplete();
+
+ // Update assessment status if needed
+ if ($isComplete && $assessment->status !== 'completed') {
+ $assessment->update([
+ 'status' => 'completed',
+ 'completed_at' => now(),
+ ]);
+ // Calculate results
+ $assessment->calculateResults();
+ Log::info('Assessment completed and results calculated', ['assessment_id' => $assessment->id]);
+ }
+
+ // Return back with success message for Inertia
+ return back()->with('success', 'Response saved successfully');
+
+ } catch (Exception $e) {
+ Log::error('Error saving assessment response', [
+ 'assessment_id' => $assessment->id,
+ 'criterion_id' => $request->criterion_id ?? null,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ return back()->withErrors(['response' => 'An error occurred while saving your response']);
+ }
+ }
+
+ /**
+ * Show limited assessment results for guests
+ */
+ public function results(Assessment $assessment)
+ {
+ Log::info('Results requested', ['assessment_id' => $assessment->id]);
+
+ try {
+ // Check if user can access this assessment
+ if (Auth::check() && $assessment->user_id && $assessment->user_id !== Auth::id()) {
+ Log::warning('Unauthorized results access attempt');
+ abort(403);
+ }
+
+ if (!$assessment->isComplete()) {
+ Log::info('Assessment not complete, redirecting to take', ['assessment_id' => $assessment->id]);
+ return redirect()->route('assessment.take', $assessment)
+ ->with('error', 'Please complete the assessment to view results.');
+ }
+
+ // Load necessary relationships
+ $assessment->load(['tool', 'results.domain', 'results.category']);
+
+ // Get basic results (limited for guests)
+ $results = $assessment->results()->with(['domain', 'category'])->get();
+ $domainResults = $results->where('category_id', null);
+
+ // Get guest session data
+ $guestSession = GuestSession::where('assessment_id', $assessment->id)->first();
+
+ // Format basic results for guests (limited information)
+ $basicResults = [
+ 'overall_percentage' => (float) $domainResults->avg('score_percentage') ?? 0,
+ 'total_criteria' => $domainResults->sum('total_criteria'),
+ 'applicable_criteria' => $domainResults->sum('applicable_criteria'),
+ 'yes_count' => $domainResults->sum('yes_count'),
+ 'no_count' => $domainResults->sum('no_count'),
+ 'na_count' => $domainResults->sum('na_count'),
+ 'domain_count' => $domainResults->count(),
+ ];
+
+ // Format assessment data for frontend
+ $assessmentData = [
+ 'id' => $assessment->id,
+ 'name' => $assessment->name,
+ 'email' => $assessment->email,
+ 'organization' => $assessment->organization,
+ 'status' => $assessment->status,
+ 'completed_at' => $assessment->completed_at ? $assessment->completed_at->toISOString() : null,
+ 'created_at' => $assessment->created_at->toISOString(),
+ 'tool' => [
+ 'id' => $assessment->tool->id,
+ 'name_en' => $assessment->tool->name_en,
+ 'name_ar' => $assessment->tool->name_ar,
+ ]
+ ];
+
+ // Format session data
+ $sessionData = null;
+ if ($guestSession) {
+ $sessionData = [
+ 'device_type' => $guestSession->device_type,
+ 'browser' => $guestSession->browser,
+ 'operating_system' => $guestSession->operating_system,
+ 'location' => $guestSession->country . ', ' . $guestSession->city,
+ 'ip_address' => $guestSession->ip_address,
+ ];
+ }
+
+ Log::info('Results prepared successfully', ['assessment_id' => $assessment->id]);
+
+ return Inertia::render('assessment/GuestResults', [
+ 'assessment' => $assessmentData,
+ 'results' => $basicResults,
+ 'sessionData' => $sessionData,
+ ]);
+
+ } catch (Exception $e) {
+ Log::error('Results display failed', [
+ 'assessment_id' => $assessment->id,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ return back()->withErrors(['general' => 'Failed to load results. Please try again.']);
+ }
+ }
+
+ /**
+ * Send email with results to guest
+ */
+ public function sendEmail(Assessment $assessment)
+ {
+ Log::info('Send email requested', ['assessment_id' => $assessment->id]);
+
+ try {
+ $results = $assessment->results()->with(['domain'])->get();
+ $domainResults = $results->where('category_id', null);
+
+ $basicResults = [
+ 'overall_percentage' => (float) $domainResults->avg('score_percentage') ?? 0,
+ 'total_criteria' => $domainResults->sum('total_criteria'),
+ 'applicable_criteria' => $domainResults->sum('applicable_criteria'),
+ 'yes_count' => $domainResults->sum('yes_count'),
+ 'no_count' => $domainResults->sum('no_count'),
+ 'na_count' => $domainResults->sum('na_count'),
+ ];
+
+ // Generate results URL
+ $resultsUrl = route('guest.assessment.results', $assessment->id);
+
+ // Send email
+ Mail::to($assessment->email)->send(
+ new GuestAssessmentCompleted($assessment, $resultsUrl, $basicResults)
+ );
+
+ Log::info('Email sent successfully', ['assessment_id' => $assessment->id]);
+
+ return response()->json(['success' => true]);
+
+ } catch (Exception $e) {
+ Log::error('Error sending guest assessment email', [
+ 'assessment_id' => $assessment->id,
+ 'email' => $assessment->email,
+ 'error' => $e->getMessage()
+ ]);
+
+ return response()->json(['error' => 'Failed to send email'], 500);
+ }
+ }
+
+ /**
+ * Update guest session with additional device information
+ */
+ public function updateSession(Request $request, Assessment $assessment)
+ {
+ Log::info('Update session requested', ['assessment_id' => $assessment->id]);
+
+ try {
+ $guestSession = GuestSession::where('assessment_id', $assessment->id)->first();
+
+ if ($guestSession) {
+ $sessionData = $guestSession->session_data ?? [];
+ $sessionData = array_merge($sessionData, [
+ 'device_info' => $request->input('device_info'),
+ 'completed_at' => $request->input('completed_at'),
+ 'updated_at' => now(),
+ ]);
+
+ $guestSession->update([
+ 'session_data' => $sessionData,
+ 'completed_at' => $request->input('completed_at') ?
+ \Carbon\Carbon::parse($request->input('completed_at')) : now(),
+ ]);
+
+ Log::info('Session updated successfully', ['assessment_id' => $assessment->id]);
+ }
+
+ return response()->json(['success' => true]);
+
+ } catch (Exception $e) {
+ Log::error('Error updating guest session', [
+ 'assessment_id' => $assessment->id,
+ 'error' => $e->getMessage()
+ ]);
+
+ return response()->json(['error' => 'Failed to update session'], 500);
+ }
+ }
+
+ /**
+ * Show assessment form with pre-filled data for authenticated users
+ */
+ public function create(Tool $tool)
+ {
+ $user = Auth::user();
+
+ Log::info('Assessment create requested', [
+ 'tool_id' => $tool->id,
+ 'user_id' => $user?->id ?? 'guest'
+ ]);
+
+ try {
+ return Inertia::render('assessment/Create', [
+ 'tool' => $tool->load(['domains.categories']),
+ 'prefillData' => $user ? [
+ 'name' => $user->name,
+ 'email' => $user->email,
+ ] : null,
+ ]);
+ } catch (Exception $e) {
+ Log::error('Assessment create failed', [
+ 'tool_id' => $tool->id,
+ 'error' => $e->getMessage()
+ ]);
+
+ return back()->withErrors(['general' => 'Failed to load assessment form.']);
+ }
+ }
+
+ /**
+ * Update assessment details (notes and attachment)
+ */
+ public function updateDetails(Request $request, Assessment $assessment)
+ {
+ Log::info('Update details requested', ['assessment_id' => $assessment->id]);
+
+ $validator = Validator::make($request->all(), [
+ 'notes' => 'nullable|string|max:2000',
+ 'attachment' => 'nullable|file|max:10240|mimes:pdf,doc,docx,jpg,jpeg,png,txt',
+ ]);
+
+ if ($validator->fails()) {
+ Log::warning('Update details validation failed', $validator->errors()->toArray());
+ return response()->json(['errors' => $validator->errors()], 422);
+ }
+
+ try {
+ // Check if user can access this assessment
+ if (Auth::check() && $assessment->user_id && $assessment->user_id !== Auth::id()) {
+ Log::warning('Unauthorized details update attempt');
+ abort(403);
+ }
+
+ $updateData = ['notes' => $request->input('notes')];
+
+ // Handle file upload
+ if ($request->hasFile('attachment')) {
+ $file = $request->file('attachment');
+ $fileName = time() . '_' . $file->getClientOriginalName();
+ $filePath = $file->storeAs('assessments/' . $assessment->id, $fileName, 'public');
+ $updateData['attachment'] = $filePath;
+ Log::info('Attachment uploaded', ['file_path' => $filePath]);
+ }
+
+ $assessment->update($updateData);
+
+ Log::info('Details updated successfully', ['assessment_id' => $assessment->id]);
+
+ return response()->json(['success' => true]);
+
+ } catch (Exception $e) {
+ Log::error('Update details failed', [
+ 'assessment_id' => $assessment->id,
+ 'error' => $e->getMessage()
+ ]);
+
+ return response()->json(['error' => 'Failed to update details'], 500);
+ }
+ }
+
+ /**
+ * Submit assessment
+ */
+ public function submit(Request $request, Assessment $assessment)
+ {
+ Log::info('Assessment submit requested', [
+ 'assessment_id' => $assessment->id,
+ 'responses_count' => count($request->input('responses', []))
+ ]);
+
+ // Check if user can access this assessment
+ if (Auth::check() && $assessment->user_id && $assessment->user_id !== Auth::id()) {
+ Log::warning('Unauthorized submit attempt');
+ abort(403);
+ }
+
+ // Validate the request data
+ $request->validate([
+ 'responses' => 'required|array',
+ ]);
+
+ try {
+ DB::beginTransaction();
+
+ // Process responses from the frontend
+ if (isset($request->responses)) {
+ foreach ($request->responses as $criterionId => $responseData) {
+ // Handle different response formats
+ if (is_array($responseData)) {
+ $response = $responseData['response'] ?? null;
+ $notes = $responseData['notes'] ?? null;
+ $attachment = $responseData['attachment'] ?? null;
+ } else {
+ $response = $responseData;
+ $notes = null;
+ $attachment = null;
+ }
+
+ if (!$response) continue;
+
+ $data = [
+ 'assessment_id' => $assessment->id,
+ 'criterion_id' => $criterionId,
+ 'response' => $response,
+ 'notes' => $notes,
+ ];
+
+ // Handle file upload if present
+ if ($attachment && $attachment instanceof \Illuminate\Http\UploadedFile) {
+ $filename = time() . '_' . $criterionId . '_' . $attachment->getClientOriginalName();
+ $path = $attachment->storeAs('assessment_attachments', $filename, 'public');
+ $data['attachment'] = $path;
+ Log::info('Response attachment uploaded', ['file_path' => $path]);
+ }
+
+ // Update or create response
+ AssessmentResponse::updateOrCreate(
+ [
+ 'assessment_id' => $assessment->id,
+ 'criterion_id' => $criterionId,
+ ],
+ $data
+ );
+ }
+ }
+
+ // Mark assessment as completed
+ $assessment->markAsCompleted();
+
+ // Update guest session completion
+ $guestSession = GuestSession::where('assessment_id', $assessment->id)->first();
+ if ($guestSession) {
+ $guestSession->update(['completed_at' => now()]);
+ }
+
+ DB::commit();
+
+ Log::info('Assessment submitted successfully', ['assessment_id' => $assessment->id]);
+
+ // Redirect to guest results page (limited view)
+ return redirect()->route('guest.assessment.results', $assessment)
+ ->with('success', 'Assessment submitted successfully!');
+
+ } catch (Exception $e) {
+ DB::rollBack();
+
+ Log::error('Assessment submission error', [
+ 'assessment_id' => $assessment->id,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ return back()->withErrors(['assessment' => 'There was an error submitting your assessment. Please try again.']);
+ }
+ }
+
+ /**
+ * Get analytics data for admin dashboard
+ */
+ public function getAnalytics(Request $request)
+ {
+ Log::info('Analytics requested');
+
+ // Only allow authenticated admin users
+ if (!Auth::check() || !Auth::user()->isAdmin()) {
+ Log::warning('Unauthorized analytics access attempt');
+ abort(403);
+ }
+
+ try {
+ $startDate = $request->input('start_date', now()->subDays(30));
+ $endDate = $request->input('end_date', now());
+
+ $guestSessions = GuestSession::with(['assessment.tool'])
+ ->whereBetween('created_at', [$startDate, $endDate])
+ ->get();
+
+ $analytics = [
+ 'total_sessions' => $guestSessions->count(),
+ 'unique_emails' => $guestSessions->unique('email')->count(),
+ 'completed_assessments' => $guestSessions->whereNotNull('completed_at')->count(),
+ 'device_breakdown' => $guestSessions->groupBy('device_type')->map->count(),
+ 'browser_breakdown' => $guestSessions->groupBy('browser')->map->count(),
+ 'country_breakdown' => $guestSessions->groupBy('country')->map->count(),
+ 'top_locations' => $guestSessions->groupBy('city')
+ ->map->count()
+ ->sortDesc()
+ ->take(10),
+ 'conversion_rate' => $guestSessions->count() > 0 ?
+ ($guestSessions->whereNotNull('completed_at')->count() / $guestSessions->count()) * 100 : 0,
+ 'popular_tools' => $guestSessions->groupBy('assessment.tool.name_en')
+ ->map->count()
+ ->sortDesc()
+ ->take(5),
+ ];
+
+ Log::info('Analytics generated successfully');
+
+ return response()->json($analytics);
+
+ } catch (Exception $e) {
+ Log::error('Analytics generation failed', [
+ 'error' => $e->getMessage()
+ ]);
+
+ return response()->json(['error' => 'Failed to generate analytics'], 500);
+ }
+ }
+}
diff --git a/app/Http/Controllers/NavigationController.php b/app/Http/Controllers/NavigationController.php
new file mode 100644
index 000000000..05c532b91
--- /dev/null
+++ b/app/Http/Controllers/NavigationController.php
@@ -0,0 +1,149 @@
+user();
+
+ if (!$user) {
+ return response()->json([
+ 'items' => [],
+ 'user_access_level' => 'guest',
+ 'user_stats' => null,
+ ]);
+ }
+
+ $accessLevel = $user->getAccessLevel();
+ $navigationItems = $this->getNavigationByAccessLevel($accessLevel);
+ $userStats = $this->getUserStats($user);
+
+ return response()->json([
+ 'items' => $navigationItems,
+ 'user_access_level' => $accessLevel,
+ 'user_stats' => $userStats,
+ ]);
+ }
+
+ /**
+ * Get navigation items based on access level
+ */
+ private function getNavigationByAccessLevel(string $accessLevel): array
+ {
+ switch ($accessLevel) {
+ case 'admin':
+ return [
+ [
+ 'title' => 'Dashboard',
+ 'href' => route('dashboard'),
+ 'icon' => 'LayoutGrid',
+ 'active' => request()->routeIs('dashboard'),
+ ],
+ [
+ 'title' => 'Assessment Tools',
+ 'href' => route('assessment-tools'),
+ 'icon' => 'ClipboardCheck',
+ 'active' => request()->routeIs('assessment-tools*'),
+ ],
+ [
+ 'title' => 'My Assessments',
+ 'href' => route('assessments.index'),
+ 'icon' => 'FileText',
+ 'active' => request()->routeIs('assessments*'),
+ ],
+ [
+ 'title' => 'Admin Panel',
+ 'href' => '/admin', // Filament admin URL
+ 'icon' => 'Settings',
+ 'active' => request()->is('admin*'),
+ ],
+ ];
+
+ case 'premium':
+ return [
+ [
+ 'title' => 'Dashboard',
+ 'href' => route('dashboard'),
+ 'icon' => 'LayoutGrid',
+ 'active' => request()->routeIs('dashboard'),
+ ],
+ [
+ 'title' => 'Assessment Tools',
+ 'href' => route('assessment-tools'),
+ 'icon' => 'ClipboardCheck',
+ 'active' => request()->routeIs('assessment-tools*'),
+ ],
+ [
+ 'title' => 'My Assessments',
+ 'href' => route('assessments.index'),
+ 'icon' => 'FileText',
+ 'active' => request()->routeIs('assessments*'),
+ ],
+ [
+ 'title' => 'Tool Subscriptions',
+ 'href' => route('tools.subscriptions'),
+ 'icon' => 'Crown',
+ 'active' => request()->routeIs('tools.subscriptions*'),
+ ],
+ ];
+
+ case 'free':
+ default:
+ $user = auth()->user();
+ return [
+ [
+ 'title' => 'Free Assessment',
+ 'href' => route('free-assessment.index'),
+ 'icon' => 'FileText',
+ 'active' => request()->routeIs('free-assessment*'),
+ 'badge' => $user && $user->canTakeFreeAssessment() ? 'Available' : null,
+ 'badge_variant' => 'success',
+ ],
+ [
+ 'title' => 'Browse Tools',
+ 'href' => route('tools.discover'),
+ 'icon' => 'ShoppingCart',
+ 'active' => request()->routeIs('tools.discover*'),
+ ],
+ [
+ 'title' => 'Contact Sales',
+ 'href' => route('contact.sales.show'),
+ 'icon' => 'Info',
+ 'active' => request()->routeIs('contact.sales*'),
+ ],
+ ];
+ }
+ }
+
+ /**
+ * Get user statistics
+ */
+ private function getUserStats($user): array
+ {
+ return [
+ 'has_free_assessment' => $user->hasTakenFreeAssessment(),
+ 'can_take_free' => $user->canTakeFreeAssessment(),
+ 'tool_subscriptions_count' => $user->toolSubscriptions()->where('status', 'active')->count(),
+ 'total_assessments' => $user->assessments()->count(),
+ ];
+ }
+}
diff --git a/app/Http/Controllers/PDFController.php b/app/Http/Controllers/PDFController.php
new file mode 100644
index 000000000..6c269e63d
--- /dev/null
+++ b/app/Http/Controllers/PDFController.php
@@ -0,0 +1,284 @@
+pdfService = new GartnerPDFService();
+ }
+
+ /**
+ * Generate and download the Gartner PDF replica
+ * Route: GET /pdf/gartner/download
+ *
+ * This method creates the PDF and immediately sends it to the user as a download.
+ * Perfect for "Generate Report" buttons in your application.
+ */
+ public function downloadGartnerPDF(Request $request): Response
+ {
+ try {
+ // You can customize the data based on request parameters
+ // For example, if you want different statistics based on user input:
+ if ($request->has('custom_data')) {
+ $customData = $request->input('custom_data');
+ $this->pdfService->setData($customData);
+ }
+
+ // Generate the PDF and return it as a download
+ $response = $this->pdfService->generatePDF();
+
+ // Add additional headers if needed
+ $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
+ $response->headers->set('Pragma', 'no-cache');
+ $response->headers->set('Expires', '0');
+
+ return $response;
+
+ } catch (\Exception $e) {
+ // Handle errors gracefully - show user-friendly message
+ return response()->json([
+ 'error' => 'Failed to generate PDF',
+ 'message' => 'Please try again or contact support if the problem persists.'
+ ], 500);
+ }
+ }
+
+ /**
+ * Save PDF to storage and return the file path
+ * Route: POST /pdf/gartner/save
+ *
+ * This method saves the PDF to your Laravel storage system.
+ * Useful when you want to email PDFs later or keep them for records.
+ */
+ public function saveGartnerPDF(Request $request)
+ {
+ try {
+ // Optional: validate request data
+ $validated = $request->validate([
+ 'filename' => 'sometimes|string|max:255',
+ 'data' => 'sometimes|array'
+ ]);
+
+ // Apply custom data if provided
+ if (isset($validated['data'])) {
+ $this->pdfService->setData($validated['data']);
+ }
+
+ // Generate filename if not provided
+ $filename = $validated['filename'] ?? 'gartner_playbook_' . now()->format('Y-m-d_H-i-s') . '.pdf';
+
+ // Save the PDF and get the storage path
+ $filePath = $this->pdfService->savePDF($filename);
+
+ // Return success response with file information
+ return response()->json([
+ 'success' => true,
+ 'message' => 'PDF generated and saved successfully',
+ 'file_path' => $filePath,
+ 'download_url' => Storage::url($filePath),
+ 'filename' => $filename
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'error' => 'Failed to save PDF',
+ 'message' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * Preview the PDF content in the browser
+ * Route: GET /pdf/gartner/preview
+ *
+ * This shows the HTML version in the browser so you can see exactly
+ * how the PDF will look before generating it. Great for testing and debugging.
+ */
+ public function previewGartnerPDF(Request $request)
+ {
+ try {
+ // Apply any custom data
+ if ($request->has('data')) {
+ $this->pdfService->setData($request->input('data'));
+ }
+
+ // Get the HTML content that would be converted to PDF
+ $htmlContent = $this->pdfService->generatePreview();
+
+ // Return the HTML directly to the browser
+ return response($htmlContent)
+ ->header('Content-Type', 'text/html');
+
+ } catch (\Exception $e) {
+ return response()->view('errors.500', [
+ 'message' => 'Failed to generate preview: ' . $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * API endpoint to get PDF as base64 encoded string
+ * Route: GET /api/pdf/gartner
+ *
+ * This is useful when you need to embed the PDF in other applications
+ * or send it via API to frontend applications.
+ */
+ public function getGartnerPDFAPI(Request $request)
+ {
+ try {
+ // Validate API request
+ $validated = $request->validate([
+ 'format' => 'sometimes|in:download,base64,url',
+ 'data' => 'sometimes|array'
+ ]);
+
+ $format = $validated['format'] ?? 'base64';
+
+ // Apply custom data if provided
+ if (isset($validated['data'])) {
+ $this->pdfService->setData($validated['data']);
+ }
+
+ switch ($format) {
+ case 'download':
+ return $this->pdfService->generatePDF();
+
+ case 'url':
+ $filename = 'gartner_api_' . now()->timestamp . '.pdf';
+ $filePath = $this->pdfService->savePDF($filename);
+ return response()->json([
+ 'url' => Storage::url($filePath),
+ 'filename' => $filename
+ ]);
+
+ case 'base64':
+ default:
+ // Generate PDF content and encode as base64
+ $pdfContent = $this->pdfService->generatePDF()->getContent();
+ $base64PDF = base64_encode($pdfContent);
+
+ return response()->json([
+ 'pdf_base64' => $base64PDF,
+ 'mime_type' => 'application/pdf',
+ 'filename' => 'gartner_cmo_playbook.pdf'
+ ]);
+ }
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'error' => 'PDF generation failed',
+ 'message' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * Bulk generate PDFs with different data sets
+ * Route: POST /pdf/gartner/bulk
+ *
+ * This allows you to generate multiple PDFs at once with different data.
+ * Useful for creating personalized reports for different departments or clients.
+ */
+ public function bulkGenerateGartnerPDFs(Request $request)
+ {
+ try {
+ $validated = $request->validate([
+ 'datasets' => 'required|array|min:1|max:10', // Limit to prevent server overload
+ 'datasets.*.name' => 'required|string',
+ 'datasets.*.data' => 'required|array'
+ ]);
+
+ $results = [];
+
+ foreach ($validated['datasets'] as $dataset) {
+ try {
+ // Create a new service instance for each dataset
+ $service = new GartnerPDFService();
+ $service->setData($dataset['data']);
+
+ // Generate filename based on dataset name
+ $filename = 'gartner_' . \Str::slug($dataset['name']) . '_' . now()->timestamp . '.pdf';
+
+ // Save the PDF
+ $filePath = $service->savePDF($filename);
+
+ $results[] = [
+ 'name' => $dataset['name'],
+ 'success' => true,
+ 'file_path' => $filePath,
+ 'download_url' => Storage::url($filePath)
+ ];
+
+ } catch (\Exception $e) {
+ $results[] = [
+ 'name' => $dataset['name'],
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ];
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Bulk PDF generation completed',
+ 'results' => $results,
+ 'successful' => count(array_filter($results, fn($r) => $r['success'])),
+ 'failed' => count(array_filter($results, fn($r) => !$r['success']))
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'error' => 'Bulk generation failed',
+ 'message' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * Debug preview with visible page boundaries
+ * Route: GET /pdf/gartner/debug
+ *
+ * This shows the HTML with visual indicators for page boundaries,
+ * helping you verify that content fits properly before generating PDF.
+ */
+ public function debugGartnerPDF(Request $request)
+ {
+ try {
+ // Apply any custom data
+ if ($request->has('data')) {
+ $this->pdfService->setData($request->input('data'));
+ }
+
+ // Get the debug HTML with visual page boundaries
+ $htmlContent = $this->pdfService->generateDebugPreview();
+
+ // Return the HTML directly to the browser
+ return response($htmlContent)
+ ->header('Content-Type', 'text/html');
+
+ } catch (\Exception $e) {
+ return response()->view('errors.500', [
+ 'message' => 'Failed to generate debug preview: ' . $e->getMessage()
+ ], 500);
+ }
+ }
+}
diff --git a/app/Http/Controllers/PaddleWebhookController.php b/app/Http/Controllers/PaddleWebhookController.php
new file mode 100644
index 000000000..9e85fb4ac
--- /dev/null
+++ b/app/Http/Controllers/PaddleWebhookController.php
@@ -0,0 +1,84 @@
+ $payload]);
+ return;
+ }
+
+ try {
+ DB::transaction(function () use ($payload, $customData) {
+ $user = User::find($customData['user_id']);
+ $tool = Tool::find($customData['tool_id']);
+
+ if ($user && $tool) {
+ $subscription = $user->toolSubscriptions()->updateOrCreate(
+ ['tool_id' => $tool->id],
+ [
+ 'plan_type' => 'premium',
+ 'status' => 'active',
+ 'started_at' => now(),
+ 'expires_at' => now()->addYear(),
+ 'paddle_customer_id' => $payload['customer_id'] ?? null,
+ 'amount' => $payload['amount'] ?? 49.99,
+ 'currency' => $payload['currency'] ?? 'USD',
+ 'paddle_data' => $payload,
+ 'features' => [
+ 'assessments_limit' => null,
+ 'pdf_reports' => 'detailed',
+ 'advanced_analytics' => true,
+ 'support' => 'priority',
+ ]
+ ]
+ );
+
+ Log::info('Tool subscription activated via Paddle webhook', [
+ 'user_id' => $user->id,
+ 'tool_id' => $tool->id,
+ 'subscription_id' => $subscription->id,
+ ]);
+ }
+ });
+
+ } catch (\Exception $e) {
+ Log::error('Paddle webhook processing failed', [
+ 'error' => $e->getMessage(),
+ 'payload' => $payload
+ ]);
+ }
+ }
+
+ /**
+ * Handle a payment failed webhook
+ */
+ public function handlePaymentFailed(array $payload)
+ {
+ Log::warning('Paddle payment failed', $payload);
+
+ // Handle failed payment logic here
+ // You might want to notify the user or retry payment
+ }
+}
diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php
new file mode 100644
index 000000000..52f74088c
--- /dev/null
+++ b/app/Http/Controllers/PostController.php
@@ -0,0 +1,54 @@
+latest('published_at')
+ ->get();
+
+ return Inertia::render('posts/Index', [
+ 'posts' => $posts,
+ ]);
+ }
+
+ public function show(Post $post)
+ {
+ if ($post->status !== 'published') {
+ abort(404);
+ }
+
+
+ // Only display approved comments in newest-first order
+ $comments = $post->comments()
+ ->where('approved', true)
+ ->latest()
+ ->get();
+
+ return Inertia::render('posts/Show', [
+ 'post' => $post,
+ 'comments' => $comments,
+ ]);
+ }
+
+ public function storeComment(Request $request, Post $post)
+ {
+ $data = $request->validate([
+ 'author_name' => 'required|string|max:255',
+ 'content' => 'required|string',
+ ]);
+
+ // Always create a pending comment awaiting approval
+ $post->comments()->create($data + ['approved' => false]);
+
+ return redirect()->back();
+ }
+}
diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php
new file mode 100644
index 000000000..da2d16dc4
--- /dev/null
+++ b/app/Http/Controllers/ReportController.php
@@ -0,0 +1,201 @@
+getReportData();
+
+ // Render the enhanced Blade template
+// $html = view('reports.cmo-playbook-professional', compact('data'))->render();
+ $html = view('reports.cmo-playbook-professional', compact('data'))->render();
+
+ // Configure mPDF with ONLY valid settings
+ $mpdf = new Mpdf([
+ 'mode' => 'utf-8',
+ 'format' => 'A4',
+ 'orientation' => 'L',
+ 'margin_left' => 10,
+ 'margin_right' => 10,
+ 'margin_top' => 10,
+ 'margin_bottom' => 10,
+ 'margin_header' => 5,
+ 'margin_footer' => 5,
+ 'tempDir' => sys_get_temp_dir(),
+ 'default_font' => 'sans-serif', // Using built-in font for better compatibility
+ 'default_font_size' => 12,
+ ]);
+
+ // These are the CORRECT ways to enhance mPDF's CSS support
+ $mpdf->showImageErrors = true;
+ $mpdf->autoScriptToLang = true;
+ $mpdf->autoLangToFont = true;
+
+
+ $mpdf->WriteHTML($html);
+
+ return response($mpdf->Output('CMO-Value-Playbook-Professional.pdf', 'D'))
+ ->header('Content-Type', 'application/pdf');
+ }
+
+ public function previewReport()
+ {
+ $data = $this->getReportData();
+ return view('reports.cmo-playbook-professional', compact('data'));
+ }
+
+
+
+
+ /**
+ * Enhanced data structure with more detailed information for better visualizations
+ */
+ private function getReportData()
+ {
+ return [
+ 'title' => 'The CMO Value Playbook',
+ 'subtitle' => '5 strategies to boost influence and showcase marketing\'s value',
+ 'survey_info' => [
+ 'year' => '2024',
+ 'source' => 'Gartner Marketing Analytics and Technology Survey',
+ 'sample_size' => 377,
+ 'respondent_type' => 'senior marketing leaders'
+ ],
+ 'key_statistics' => [
+ 'prove_value_and_credit' => 52,
+ 'unable_to_prove' => 48,
+ 'holistic_long_term_success' => 68,
+ 'individual_short_term_success' => 30,
+ 'high_complexity_metrics_improvement' => 1.5,
+ 'da_activity_improvement' => 1.4
+ ],
+ 'stakeholder_data' => [
+ 'most_skeptical' => [
+ ['role' => 'Chief Financial Officer (CFO)', 'percentage' => 40, 'color' => '#f59e0b'],
+ ['role' => 'Chief Executive Officer (CEO)', 'percentage' => 39, 'color' => '#3b82f6']
+ ],
+ 'ceo_beliefs' => [
+ 'Marketing is crucial, but measuring its direct impact on our bottom line can be challenging',
+ 'Marketing must move from being a cost center to a profit driver',
+ 'Marketing should demonstrate its impact on the overall business strategy'
+ ],
+ 'cfo_beliefs' => [
+ 'Marketing\'s broad range of activities makes it challenging to pinpoint financial accountability',
+ 'Investments in marketing dollars need to show a clear and directly measurable impact on revenue and growth',
+ 'Marketing\'s impact is often subtle and temporary, as many of its efforts are geared toward indirectly influencing engagement and perception'
+ ]
+ ],
+ 'strategies' => [
+ [
+ 'number' => 1,
+ 'title' => 'Focus on marketing\'s long-term, holistic impact',
+ 'description' => 'CMOs who adopt a long-term, holistic view are more successful in proving marketing\'s value and gaining credit. Only 30% of those focusing on short-term initiatives reported success.',
+ 'key_stat' => '68% success rate with holistic, long-term focus',
+ 'actions' => [
+ 'Measure long-term value using advanced approaches like marketing mix modeling (MMM)',
+ 'Create a holistic view across six value vectors: Connection to Strategy, ROI Story, Critical-Project Impact, Insight Engine, Empowering Others and Optimizing Resources'
+ ],
+ 'icon' => '📊'
+ ],
+ [
+ 'number' => 2,
+ 'title' => 'Build a narrative about marketing\'s value for all stakeholders',
+ 'description' => 'CEOs and CFOs are the most skeptical of marketing\'s value. CMOs must understand their priorities and craft compelling narratives that resonate.',
+ 'key_stat' => '40% of CFOs and 39% of CEOs are skeptical',
+ 'actions' => [
+ 'Address CEO concerns about measuring direct impact on bottom line',
+ 'Help CFOs understand clear financial accountability and measurable revenue impact'
+ ],
+ 'icon' => '🎯'
+ ],
+ [
+ 'number' => 3,
+ 'title' => 'Increase variety and sophistication of metric types',
+ 'description' => 'Using high-complexity metrics drives significant improvement in proving value. Variety and quality of metrics both matter.',
+ 'key_stat' => '1.5x increase in likelihood to prove value',
+ 'actions' => [
+ 'Implement relationship metrics: Customer LTV, CAC, LTV:CAC ratio',
+ 'Use return on transactional metrics: CPA, ROAS, ROI',
+ 'Track operational metrics: stakeholder satisfaction, resource productivity'
+ ],
+ 'icon' => '📈'
+ ],
+ [
+ 'number' => 4,
+ 'title' => 'Expand leadership involvement in data and analytics activity',
+ 'description' => 'Marketing leaders engaged in D&A activities have a clear advantage. Regular meetings with stakeholders and hands-on involvement are crucial.',
+ 'key_stat' => '1.4x more likely to prove value',
+ 'actions' => [
+ 'Manage teams of marketing data analysts',
+ 'Create marketing dashboards and custom reports',
+ 'Develop measurement strategies for marketing activities'
+ ],
+ 'icon' => '⚡'
+ ],
+ [
+ 'number' => 5,
+ 'title' => 'Invest in marketing talent to close gaps',
+ 'description' => 'Talent gaps present the biggest barriers to proving marketing value. CMOs must develop adaptive capabilities across multiple skill areas.',
+ 'key_stat' => 'Top 3 barriers are all talent-related',
+ 'actions' => [
+ 'Develop soft skills and competencies for storytelling',
+ 'Invest in analytical talent for data analysis and insights',
+ 'Focus on technical talent for data integration and advanced technologies'
+ ],
+ 'icon' => '👥'
+ ]
+ ],
+ 'barriers' => [
+ ['description' => 'Lack of necessary soft skills/competencies', 'percentage' => 39, 'color' => '#ef4444'],
+ ['description' => 'Lack of analytical talent to analyze data and generate insight', 'percentage' => 34, 'color' => '#f97316'],
+ ['description' => 'Lack of technical talent to integrate and analyze data', 'percentage' => 33, 'color' => '#eab308']
+ ],
+ 'success_matrix' => [
+ 'holistic_long_term' => ['label' => 'Holistic, longer-term focus', 'value' => 68, 'category' => 'best'],
+ 'holistic_short_term' => ['label' => 'Holistic, shorter-term focus', 'value' => 51, 'category' => 'good'],
+ 'individual_long_term' => ['label' => 'Individual, longer-term focus', 'value' => 49, 'category' => 'moderate'],
+ 'individual_short_term' => ['label' => 'Individual, shorter-term focus', 'value' => 30, 'category' => 'worst']
+ ]
+ ];
+ }
+
+
+
+
+ public function landscapeChart()
+ {
+
+ $data = [
+ 'bars' => [
+ ['label' => 'Marketing', 'value' => 60],
+ ['label' => 'Sales', 'value' => 40],
+ ['label' => 'Support', 'value' => 30],
+ ],
+ 'text' => 'Key takeaways from the performance data include increased marketing investment and noticeable under-resourcing of support.',
+ 'list' => [
+ 'Rebalance budgets toward support',
+ 'Double down on ROI in marketing',
+ 'Explore AI-driven sales enablement tools'
+ ]
+ ];
+
+ $html = view('reports.half-chart', compact('data'))->render();
+
+ $mpdf = new Mpdf([
+ 'format' => 'A4-L', // A4 Landscape
+ 'margin_top' => 10,
+ 'margin_bottom' => 10,
+ 'margin_left' => 10,
+ 'margin_right' => 10,
+ ]);
+
+ $mpdf->WriteHTML($html);
+ return response($mpdf->Output('half_chart.pdf', 'I'))->header('Content-Type', 'application/pdf');
+ }
+
+}
diff --git a/app/Http/Controllers/ToolDiscoveryController.php b/app/Http/Controllers/ToolDiscoveryController.php
new file mode 100644
index 000000000..98f3848f4
--- /dev/null
+++ b/app/Http/Controllers/ToolDiscoveryController.php
@@ -0,0 +1,138 @@
+user();
+
+ $tools = Tool::where('status', 'active')
+ ->withCount(['assessments', 'domains'])
+ ->with(['domains' => function($query) {
+ $query->withCount(['categories' => function($q) {
+ $q->withCount('criteria');
+ }]);
+ }])
+ ->orderBy('name_en')
+ ->get()
+ ->map(function ($tool) use ($user) {
+ // Calculate total criteria
+ $totalCriteria = $tool->domains->sum(function($domain) {
+ return $domain->categories->sum('criteria_count');
+ });
+
+ $estimatedTime = max(10, ceil($totalCriteria * 0.5)); // Minimum 10 minutes
+
+ // Check user access to this tool
+ $hasAccess = $user ? $user->hasAccessToTool($tool->id) : false;
+ $subscriptionType = 'none';
+
+ if ($user && $hasAccess) {
+ $subscription = $user->getToolSubscription($tool->id);
+ $subscriptionType = $subscription ? $subscription->plan_type : 'none';
+ }
+
+ return [
+ 'id' => $tool->id,
+ 'name_en' => $tool->name_en,
+ 'name_ar' => $tool->name_ar,
+ 'description_en' => $tool->description_en,
+ 'description_ar' => $tool->description_ar,
+ 'image' => $tool->image,
+ 'total_domains' => $tool->domains_count,
+ 'total_criteria' => $totalCriteria,
+ 'estimated_time' => $estimatedTime,
+ 'assessments_count' => $tool->assessments_count,
+ 'has_access' => $hasAccess,
+ 'subscription_type' => $subscriptionType,
+ 'has_free_plan' => $tool->has_free_plan ?? false,
+ 'premium_price' => $tool->premium_price,
+ 'currency' => $tool->currency ?? 'USD',
+ ];
+ });
+
+ $userInfo = $user ? [
+ 'access_level' => $user->getAccessLevel(),
+ 'name_ar' => $user->name_ar,
+ 'name_en' => $user->name,
+ 'current_assessments' => $user->assessments()->count(),
+ 'tool_subscriptions' => $user->toolSubscriptions()
+ ->where('status', 'active')
+ ->pluck('tool_id', 'tool_id')
+ ->toArray(),
+ ] : [
+ 'access_level' => 'free User',
+ 'current_assessments' => 0,
+ 'tool_subscriptions' => [],
+ ];
+
+
+ return Inertia::render('ToolDiscover', [
+ 'tools' => $tools,
+ 'user' => $userInfo,
+ 'locale' => app()->getLocale(),
+ ]);
+ }
+
+ /**
+ * Show individual tool details
+ */
+ public function show(Tool $tool): Response
+ {
+ $user = auth()->user();
+
+ $tool->load([
+ 'domains.categories.criteria' => function ($query) {
+ $query->orderBy('order');
+ }
+ ]);
+
+ $toolData = [
+ 'id' => $tool->id,
+ 'name_en' => $tool->name_en,
+ 'name_ar' => $tool->name_ar,
+ 'description_en' => $tool->description_en,
+ 'description_ar' => $tool->description_ar,
+ 'image' => $tool->image,
+ 'has_free_plan' => $tool->has_free_plan ?? false,
+ 'premium_price' => $tool->premium_price,
+ 'currency' => $tool->currency ?? 'USD',
+ 'domains' => $tool->domains->map(function ($domain) {
+ return [
+ 'id' => $domain->id,
+ 'name_en' => $domain->name_en,
+ 'name_ar' => $domain->name_ar,
+ 'categories_count' => $domain->categories->count(),
+ 'criteria_count' => $domain->categories->sum(function($category) {
+ return $category->criteria->count();
+ }),
+ ];
+ }),
+ ];
+
+ $userAccess = $user ? [
+ 'has_access' => $user->hasAccessToTool($tool->id),
+ 'subscription_type' => $user->getToolSubscription($tool->id)?->plan_type ?? 'none',
+ 'access_level' => $user->getAccessLevel(),
+ ] : [
+ 'has_access' => false,
+ 'subscription_type' => 'none',
+ 'access_level' => 'guest',
+ ];
+
+ return Inertia::render('ToolDetails', [
+ 'tool' => $toolData,
+ 'user_access' => $userAccess,
+ 'locale' => app()->getLocale(),
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/ToolRequestController.php b/app/Http/Controllers/ToolRequestController.php
new file mode 100644
index 000000000..2728d5d37
--- /dev/null
+++ b/app/Http/Controllers/ToolRequestController.php
@@ -0,0 +1,51 @@
+ [
+ 'id' => $tool->id,
+ 'name_en' => $tool->name_en,
+ 'name_ar' => $tool->name_ar,
+ 'description_en' => $tool->description_en,
+ 'description_ar' => $tool->description_ar,
+ 'image' => $tool->image,
+ ],
+ 'user' => auth()->user() ? [
+ 'name' => auth()->user()->name,
+ 'email' => auth()->user()->email,
+ 'organization' => auth()->user()->getCompanyName(),
+ ] : null,
+ ]);
+ }
+
+ public function store(StoreToolRequestRequest $request): RedirectResponse
+ {
+ $data = $request->validated();
+ $data['user_id'] = auth()->id();
+ $toolRequest = ToolRequest::create($data);
+
+ $admins = User::whereHas('roles', function ($q) {
+ $q->whereIn('name', ['admin', 'super_admin']);
+ })->get();
+
+ Notification::make()
+ ->title('New Tool Request')
+ ->body($toolRequest->name.' requested access to '.$toolRequest->tool->name_en)
+ ->sendToDatabase($admins);
+
+ return redirect()->back()->with('success', 'Request submitted successfully');
+ }
+}
diff --git a/app/Http/Controllers/ToolSubscriptionController.php b/app/Http/Controllers/ToolSubscriptionController.php
new file mode 100644
index 000000000..dfbaa42cf
--- /dev/null
+++ b/app/Http/Controllers/ToolSubscriptionController.php
@@ -0,0 +1,507 @@
+user();
+
+ $subscriptions = $user->toolSubscriptions()
+ ->with('tool')
+ ->orderBy('created_at', 'desc')
+ ->get()
+ ->map(function ($subscription) {
+ return [
+ 'id' => $subscription->id,
+ 'tool' => [
+ 'id' => $subscription->tool->id,
+ 'name_en' => $subscription->tool->name_en,
+ 'name_ar' => $subscription->tool->name_ar,
+ 'image' => $subscription->tool->image,
+ ],
+ 'plan_type' => $subscription->plan_type,
+ 'status' => $subscription->status,
+ 'started_at' => $subscription->started_at,
+ 'expires_at' => $subscription->expires_at,
+ 'features' => $subscription->features,
+ 'paddle_subscription_id' => $subscription->paddle_subscription_id,
+ ];
+ });
+
+ return Inertia::render('MyToolSubscriptions', [
+ 'subscriptions' => $subscriptions,
+ ]);
+ }
+
+ /**
+ * Show tool subscription page with pricing options
+ */
+ public function show(Tool $tool)
+ {
+ $user = auth()->user();
+
+ // Load tool with pricing information
+ $tool->load(['domains.categories.criteria']);
+
+ // Check current subscription status for this tool
+ $currentSubscription = $user->getToolSubscription($tool->id);
+
+ // Calculate tool metrics
+ $totalCriteria = $tool->domains->sum(function($domain) {
+ return $domain->categories->sum(function($category) {
+ return $category->criteria->count();
+ });
+ });
+
+ return Inertia::render('ToolSubscription', [
+ 'tool' => [
+ 'id' => $tool->id,
+ 'name_en' => $tool->name_en,
+ 'name_ar' => $tool->name_ar,
+ 'description_en' => $tool->description_en,
+ 'description_ar' => $tool->description_ar,
+ 'image' => $tool->image,
+ 'total_criteria' => $totalCriteria,
+ 'estimated_time' => max(10, ceil($totalCriteria * 0.5)),
+ ],
+ 'currentSubscription' => $currentSubscription ? [
+ 'plan_type' => $currentSubscription->plan_type,
+ 'status' => $currentSubscription->status,
+ 'expires_at' => $currentSubscription->expires_at,
+ 'features' => $currentSubscription->features,
+ ] : null,
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'is_premium' => $user->isPremium(),
+ 'is_admin' => $user->isAdmin(),
+ ],
+ 'pricing' => [
+ 'free' => [
+ 'price' => 0,
+ 'currency' => 'USD',
+ 'features' => [
+ 'assessments_limit' => 1,
+ 'pdf_reports' => 'basic',
+ 'advanced_analytics' => false,
+ 'support' => 'community',
+ ]
+ ],
+ 'premium' => [
+ 'price' => 49.99,
+ 'currency' => 'USD',
+ 'features' => [
+ 'assessments_limit' => null, // Unlimited
+ 'pdf_reports' => 'detailed',
+ 'advanced_analytics' => true,
+ 'support' => 'priority',
+ ]
+ ]
+ ],
+ 'paddle' => [
+ 'vendor_id' => config('paddle.vendor_id'),
+ 'client_side_token' => config('paddle.client_side_token'),
+ 'environment' => config('paddle.sandbox') ? 'sandbox' : 'production',
+ ],
+ ]);
+ }
+
+ /**
+ * Handle subscription request (both free and premium)
+ */
+ public function subscribe(Request $request, Tool $tool)
+ {
+ $request->validate([
+ 'plan_type' => 'required|in:free,premium',
+ ]);
+
+ $user = auth()->user();
+
+ // Check if user already has a subscription for this tool
+ $existingSubscription = $user->getToolSubscription($tool->id);
+ if ($existingSubscription && $existingSubscription->status === 'active') {
+ return response()->json([
+ 'success' => false,
+ 'error' => 'You already have an active subscription for this tool.',
+ ], 400);
+ }
+
+ if ($request->plan_type === 'free') {
+ return $this->subscribeFree($user, $tool);
+ } else {
+ return $this->createPaddleCheckout($user, $tool);
+ }
+ }
+
+ /**
+ * Create Paddle checkout for premium subscription
+ */
+ private function createPaddleCheckout($user, $tool)
+ {
+ try {
+ // Prepare checkout data for Paddle API
+ $checkoutData = [
+ 'items' => [
+ [
+ 'price_id' => config('paddle.products.tool_premium'), // Set this in config
+ 'quantity' => 1,
+ ]
+ ],
+ 'customer' => [
+ 'email' => $user->email,
+ 'name' => $user->name,
+ ],
+ 'custom_data' => [
+ 'user_id' => $user->id,
+ 'tool_id' => $tool->id,
+ 'plan_type' => 'premium',
+ 'tool_name' => $tool->name_en,
+ ],
+ 'return_url' => route('tools.payment.success', ['tool' => $tool->id]),
+ 'discount_id' => null, // Add discount logic if needed
+ ];
+
+ // Create checkout via Paddle API
+ $response = Http::withHeaders([
+ 'Authorization' => 'Bearer ' . config('paddle.vendor_auth_code'),
+ 'Content-Type' => 'application/json',
+ ])->post('https://sandbox-api.paddle.com/transactions', $checkoutData);
+
+ if ($response->successful()) {
+ $checkout = $response->json();
+
+ return response()->json([
+ 'success' => true,
+ 'checkout_url' => $checkout['data']['checkout']['url'],
+ 'checkout_id' => $checkout['data']['id'],
+ ]);
+ } else {
+ Log::error('Paddle checkout creation failed', [
+ 'user_id' => $user->id,
+ 'tool_id' => $tool->id,
+ 'response' => $response->body(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Checkout creation failed. Please try again.',
+ ], 500);
+ }
+
+ } catch (\Exception $e) {
+ Log::error('Paddle checkout exception', [
+ 'user_id' => $user->id,
+ 'tool_id' => $tool->id,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Checkout creation failed. Please try again.',
+ ], 500);
+ }
+ }
+
+ /**
+ * Handle successful payment callback
+ */
+ public function paymentSuccess(Request $request, Tool $tool)
+ {
+ $user = auth()->user();
+ $transactionId = $request->query('_ptxn');
+
+ if (!$transactionId) {
+ return redirect()->route('tools.discover')
+ ->with('error', 'Payment verification failed.');
+ }
+
+ // Check if subscription was created (webhook should have handled this)
+ $subscription = $user->getToolSubscription($tool->id);
+
+ if ($subscription && $subscription->status === 'active') {
+ return redirect()->route('assessment.start', $tool->id)
+ ->with('success', "Payment successful! You now have premium access to {$tool->name_en}.");
+ } else {
+ // Subscription not found, redirect to try again
+ return redirect()->route('tools.subscribe', $tool->id)
+ ->with('error', 'Payment processed but subscription setup is pending. Please contact support if this persists.');
+ }
+ }
+
+ /**
+ * Handle free subscription
+ */
+ private function subscribeFree($user, $tool)
+ {
+ try {
+ DB::beginTransaction();
+
+ $subscription = $user->toolSubscriptions()->updateOrCreate(
+ ['tool_id' => $tool->id],
+ [
+ 'plan_type' => 'free',
+ 'status' => 'active',
+ 'started_at' => now(),
+ 'expires_at' => null, // Free subscriptions don't expire
+ 'features' => [
+ 'assessments_limit' => 1,
+ 'pdf_reports' => 'basic',
+ 'advanced_analytics' => false,
+ 'support' => 'community',
+ ]
+ ]
+ );
+
+ DB::commit();
+
+ return response()->json([
+ 'success' => true,
+ 'message' => "You now have free access to {$tool->name_en}!",
+ 'redirect_url' => route('assessment.start', $tool->id),
+ ]);
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+
+ Log::error('Free subscription failed', [
+ 'user_id' => $user->id,
+ 'tool_id' => $tool->id,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Subscription failed. Please try again.',
+ ], 500);
+ }
+ }
+
+ /**
+ * Cancel tool subscription
+ */
+ public function cancel(ToolSubscription $subscription)
+ {
+ $user = auth()->user();
+
+ // Ensure user owns this subscription
+ if ($subscription->user_id !== $user->id) {
+ abort(403, 'Unauthorized to cancel this subscription.');
+ }
+
+ try {
+ DB::beginTransaction();
+
+ // If it's a premium subscription with Paddle, cancel it there too
+ if ($subscription->plan_type === 'premium' && $subscription->paddle_subscription_id) {
+ $this->cancelPaddleSubscription($subscription->paddle_subscription_id);
+ }
+
+ // Update local subscription status
+ $subscription->update([
+ 'status' => 'canceled',
+ 'canceled_at' => now(),
+ ]);
+
+ DB::commit();
+
+ return redirect()->back()
+ ->with('success', 'Subscription canceled successfully.');
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+
+ Log::error('Subscription cancellation failed', [
+ 'subscription_id' => $subscription->id,
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return redirect()->back()
+ ->with('error', 'Failed to cancel subscription. Please try again.');
+ }
+ }
+
+ /**
+ * Cancel subscription in Paddle
+ */
+ private function cancelPaddleSubscription($paddleSubscriptionId)
+ {
+ try {
+ $response = Http::withHeaders([
+ 'Authorization' => 'Bearer ' . config('paddle.vendor_auth_code'),
+ 'Content-Type' => 'application/json',
+ ])->post("https://sandbox-api.paddle.com/subscriptions/{$paddleSubscriptionId}/cancel", [
+ 'effective_from' => 'immediately',
+ ]);
+
+ if (!$response->successful()) {
+ Log::warning('Failed to cancel Paddle subscription', [
+ 'paddle_subscription_id' => $paddleSubscriptionId,
+ 'response' => $response->body(),
+ ]);
+ }
+
+ } catch (\Exception $e) {
+ Log::error('Exception canceling Paddle subscription', [
+ 'paddle_subscription_id' => $paddleSubscriptionId,
+ 'error' => $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * Webhook handler for Paddle events
+ */
+ public function webhook(Request $request)
+ {
+ $signature = $request->header('Paddle-Signature');
+ $webhookSecret = config('paddle.webhook_secret');
+
+ // Verify webhook signature
+ if (!$this->verifyWebhookSignature($request->getContent(), $signature, $webhookSecret)) {
+ Log::warning('Invalid webhook signature', [
+ 'signature' => $signature,
+ ]);
+ return response('Invalid signature', 400);
+ }
+
+ $payload = $request->json()->all();
+ $eventType = $payload['event_type'] ?? null;
+
+ try {
+ switch ($eventType) {
+ case 'transaction.completed':
+ $this->handleTransactionCompleted($payload);
+ break;
+
+ case 'subscription.canceled':
+ $this->handleSubscriptionCanceled($payload);
+ break;
+
+ case 'subscription.updated':
+ $this->handleSubscriptionUpdated($payload);
+ break;
+
+ default:
+ Log::info('Unhandled webhook event', ['event_type' => $eventType]);
+ }
+
+ return response('OK', 200);
+
+ } catch (\Exception $e) {
+ Log::error('Webhook processing failed', [
+ 'event_type' => $eventType,
+ 'error' => $e->getMessage(),
+ 'payload' => $payload,
+ ]);
+
+ return response('Processing failed', 500);
+ }
+ }
+
+ /**
+ * Handle transaction completed webhook
+ */
+ private function handleTransactionCompleted($payload)
+ {
+ $customData = $payload['data']['custom_data'] ?? [];
+ $userId = $customData['user_id'] ?? null;
+ $toolId = $customData['tool_id'] ?? null;
+
+ if (!$userId || !$toolId) {
+ Log::warning('Missing custom data in transaction webhook', $customData);
+ return;
+ }
+
+ $user = \App\Models\User::find($userId);
+ $tool = Tool::find($toolId);
+
+ if (!$user || !$tool) {
+ Log::warning('User or tool not found for webhook', [
+ 'user_id' => $userId,
+ 'tool_id' => $toolId,
+ ]);
+ return;
+ }
+
+ // Create premium subscription
+ $user->toolSubscriptions()->updateOrCreate(
+ ['tool_id' => $toolId],
+ [
+ 'plan_type' => 'premium',
+ 'status' => 'active',
+ 'started_at' => now(),
+ 'expires_at' => null, // One-time payment, no expiry
+ 'paddle_transaction_id' => $payload['data']['id'],
+ 'features' => [
+ 'assessments_limit' => null, // Unlimited
+ 'pdf_reports' => 'detailed',
+ 'advanced_analytics' => true,
+ 'support' => 'priority',
+ ]
+ ]
+ );
+
+ Log::info('Premium tool subscription created via webhook', [
+ 'user_id' => $userId,
+ 'tool_id' => $toolId,
+ 'transaction_id' => $payload['data']['id'],
+ ]);
+ }
+
+ /**
+ * Handle subscription canceled webhook
+ */
+ private function handleSubscriptionCanceled($payload)
+ {
+ $subscriptionId = $payload['data']['id'] ?? null;
+
+ if ($subscriptionId) {
+ ToolSubscription::where('paddle_subscription_id', $subscriptionId)
+ ->update([
+ 'status' => 'canceled',
+ 'canceled_at' => now(),
+ ]);
+ }
+ }
+
+ /**
+ * Handle subscription updated webhook
+ */
+ private function handleSubscriptionUpdated($payload)
+ {
+ $subscriptionId = $payload['data']['id'] ?? null;
+ $status = $payload['data']['status'] ?? null;
+
+ if ($subscriptionId && $status) {
+ ToolSubscription::where('paddle_subscription_id', $subscriptionId)
+ ->update(['status' => $status]);
+ }
+ }
+
+ /**
+ * Verify Paddle webhook signature
+ */
+ private function verifyWebhookSignature($payload, $signature, $secret)
+ {
+ // Implement Paddle signature verification
+ // This is a simplified version - use Paddle's official method
+ $computedSignature = hash_hmac('sha256', $payload, $secret);
+ return hash_equals($signature, $computedSignature);
+ }
+}
diff --git a/app/Http/Controllers/UserRegistrationController.php b/app/Http/Controllers/UserRegistrationController.php
new file mode 100644
index 000000000..53e3b8093
--- /dev/null
+++ b/app/Http/Controllers/UserRegistrationController.php
@@ -0,0 +1,289 @@
+ $request->except(['password', 'password_confirmation']),
+ 'ip' => $request->ip(),
+ 'user_agent' => $request->userAgent()
+ ]);
+
+ try {
+ // Validate the request
+ $validated = $request->validate([
+ 'name' => 'required|string|max:255',
+ 'email' => 'required|string|email|max:255|unique:users,email',
+ 'password' => 'required|string|min:8|confirmed',
+ 'company_name' => 'required|string|max:255',
+ 'marketing_emails' => 'nullable|boolean',
+ 'newsletter_subscription' => 'nullable|boolean',
+ ], [
+ 'name.required' => 'Full name is required.',
+ 'email.required' => 'Email address is required.',
+ 'email.email' => 'Please enter a valid email address.',
+ 'email.unique' => 'This email address is already registered. Please use a different email or try signing in.',
+ 'password.required' => 'Password is required.',
+ 'password.min' => 'Password must be at least 8 characters long.',
+ 'password.confirmed' => 'Password confirmation does not match.',
+ 'company_name.required' => 'Company name is required.',
+ 'password.confirmed' => 'Password confirmation does not match.',
+ 'company_name.required' => 'Company name is required.',
+ ]);
+
+ Log::info('Validation passed for user registration', ['email' => $validated['email']]);
+
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ Log::warning('Validation failed for user registration', [
+ 'errors' => $e->errors(),
+ 'email' => $request->email ?? 'not provided'
+ ]);
+ throw $e;
+ }
+
+ try {
+ DB::beginTransaction();
+
+ // Create the user
+ $user = User::create([
+ 'name' => $validated['name'],
+ 'email' => $validated['email'],
+ 'password' => Hash::make($validated['password']),
+ 'email_verified_at' => now(), // Auto-verify for free users
+ ]);
+
+ Log::info('User created successfully', ['user_id' => $user->id, 'email' => $user->email]);
+
+ // Try to create user details (if the relationship exists)
+ try {
+ if (method_exists($user, 'details')) {
+ $user->details()->create([
+ 'company' => $validated['company_name'],
+ 'company_name' => $validated['company_name'],
+ 'preferred_language' => app()->getLocale(),
+ 'marketing_emails' => (bool) ($validated['marketing_emails'] ?? false),
+ 'newsletter_subscription' => (bool) ($validated['newsletter_subscription'] ?? false),
+ 'marketing_emails' => true,
+ 'newsletter_subscription' => false,
+ 'profile_completed' => false,
+ ]);
+ Log::info('User details created', ['user_id' => $user->id]);
+ }
+ } catch (Exception $e) {
+ Log::warning('Failed to create user details, but continuing', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage()
+ ]);
+ // Don't fail registration if details creation fails
+ }
+
+ // Try to create free subscription (if the relationship exists)
+ try {
+ if (method_exists($user, 'subscriptions')) {
+ $user->subscriptions()->create([
+ 'plan_type' => 'free',
+ 'status' => 'active',
+ 'started_at' => now(),
+ 'features' => [
+ 'assessments_limit' => 1,
+ 'pdf_reports' => 'basic',
+ 'advanced_analytics' => false,
+ 'team_management' => false,
+ 'api_access' => false,
+ 'priority_support' => false,
+ 'custom_branding' => false,
+ ]
+ ]);
+ Log::info('User subscription created', ['user_id' => $user->id]);
+ }
+ } catch (Exception $e) {
+ Log::warning('Failed to create user subscription, but continuing', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage()
+ ]);
+ }
+
+ // Try to assign free role (if using Spatie Permission)
+ try {
+ if (method_exists($user, 'assignRole')) {
+ $user->assignRole('free');
+ Log::info('Role assigned to user', ['user_id' => $user->id, 'role' => 'free']);
+ }
+ } catch (Exception $e) {
+ Log::warning('Could not assign role to user', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage()
+ ]);
+ }
+
+ DB::commit();
+ Log::info('User registration transaction committed', ['user_id' => $user->id]);
+
+ // Send admin notification (non-blocking)
+ try {
+ $this->sendAdminNotification($user);
+ Log::info('New free user notification sent', ['user_id' => $user->id]);
+ } catch (Exception $e) {
+ Log::error('Failed to send new user notification', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage()
+ ]);
+ }
+
+ // Log them in
+ auth()->login($user);
+ Log::info('User logged in after registration', ['user_id' => $user->id]);
+
+ // Redirect to tools discovery page
+ return redirect()->route('tools.discover')->with('success',
+ 'Registration successful! Welcome to our platform. Browse available assessment tools below.'
+ );
+
+ } catch (Exception $e) {
+ DB::rollBack();
+
+ Log::error('Free user registration failed', [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ 'data' => $request->except(['password', 'password_confirmation'])
+ ]);
+
+ // Return specific error message
+ if ($e instanceof \Illuminate\Database\QueryException) {
+ if (str_contains($e->getMessage(), 'users_email_unique')) {
+ throw ValidationException::withMessages([
+ 'email' => 'This email address is already registered. Please use a different email or try signing in.'
+ ]);
+ }
+ }
+
+ throw ValidationException::withMessages([
+ 'email' => 'Registration failed due to a system error. Please try again in a few moments.'
+ ]);
+ }
+ }
+
+ /**
+ * Complete user profile after initial registration
+ */
+ public function completeProfile(Request $request)
+ {
+ $data = $request->validate([
+ 'company_name_ar' => ['required', 'string', 'max:255'],
+ 'company_name_en' => ['required', 'string', 'max:255'],
+ 'company_type' => ['required', 'integer', 'in:1,2,3'],
+ 'region' => ['required', 'string', 'max:255'],
+ 'city' => ['required', 'string', 'max:255'],
+ 'employee_name_ar' => ['required', 'string', 'max:255'],
+ 'employee_name_en' => ['required', 'string', 'max:255'],
+ 'employee_type' => ['required', 'integer', 'in:1,2,3,4,5,6,7'],
+ 'phone' => ['required', 'string', 'max:255'],
+ 'website' => ['nullable', 'url', 'max:255'],
+ 'notes' => ['nullable', 'string'],
+ 'how_did_you_hear' => ['nullable', 'string', 'max:255'],
+ 'phone' => ['required', 'string', 'max:255'],
+ 'address' => ['required', 'string'],
+ ]);
+
+ $user = $request->user();
+
+ if ($user && $user->details) {
+ $user->details->update([
+ 'company_name_ar' => $data['company_name_ar'],
+ 'company_name_en' => $data['company_name_en'],
+ 'company_type' => $data['company_type'],
+ 'region' => $data['region'],
+ 'city' => $data['city'],
+ 'employee_name_ar' => $data['employee_name_ar'],
+ 'employee_name_en' => $data['employee_name_en'],
+ 'employee_type' => $data['employee_type'],
+ 'phone' => $data['phone'],
+ 'website' => $data['website'] ?? null,
+ 'notes' => $data['notes'] ?? null,
+ 'how_did_you_hear' => $data['how_did_you_hear'] ?? null,
+ 'phone' => $data['phone'],
+ 'address' => $data['address'],
+ 'profile_completed' => true,
+ ]);
+ }
+
+ return redirect()->route('dashboard');
+ }
+
+ /**
+ * Send admin notification about new user
+ */
+ private function sendAdminNotification(User $user)
+ {
+ try {
+ $adminEmail = env('ADMIN_EMAIL', 'admin@example.com');
+ $subject = 'New Free User Registration - ' . $user->name;
+
+ $message = "New free user registered:\n\n" .
+ "Name: {$user->name}\n" .
+ "Email: {$user->email}\n" .
+ "Registered At: {$user->created_at}\n";
+
+ // Simple mail function - you can replace with Laravel Mail facade if needed
+ if (function_exists('mail')) {
+ mail($adminEmail, $subject, $message);
+ }
+
+ } catch (Exception $e) {
+ Log::error('Failed to send admin notification', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * Show subscription upgrade page
+ */
+ public function showSubscription()
+ {
+ $user = auth()->user();
+
+ if (!$user) {
+ return redirect()->route('login');
+ }
+
+ // Load relationships safely
+ try {
+ $user->load(['details', 'subscription']);
+ } catch (Exception $e) {
+ Log::warning('Could not load user relationships', [
+ 'user_id' => $user->id,
+ 'error' => $e->getMessage()
+ ]);
+ }
+
+ return Inertia::render('SubscriptionUpgrade', [
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'details' => $user->details ?? null,
+ 'subscription' => $user->subscription ?? null,
+ ],
+ 'currentPlan' => $user->subscription?->plan_type ?? 'free'
+ ]);
+ }
+}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
new file mode 100644
index 000000000..02efd8603
--- /dev/null
+++ b/app/Http/Kernel.php
@@ -0,0 +1,77 @@
+
+ */
+ protected $middleware = [
+ // \App\Http\Middleware\TrustHosts::class,
+ \App\Http\Middleware\TrustProxies::class,
+ \Illuminate\Http\Middleware\HandleCors::class,
+ \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
+ \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
+ \App\Http\Middleware\TrimStrings::class,
+ \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
+ ];
+
+ /**
+ * The application's route middleware groups.
+ *
+ * @var array