name('webhook.midtrans'); // Dynamic favicon + PWA manifest — public, no auth Route::get('/favicon.ico', [AppSettingsController::class, 'favicon'])->name('favicon'); Route::get('/manifest.webmanifest', [AppSettingsController::class, 'manifest'])->name('manifest'); // Main domain routes — marketing, legal, aggregator marketplace (no auth, no tenant) Route::domain(config('app.aggregator_domain'))->group(function () { // Root: landing page for guests, redirect for authenticated users Route::get('/', [\App\Http\Controllers\Marketing\MarketingController::class, 'landing'])->name('landing'); // Public marketing pages Route::get('/about', [\App\Http\Controllers\Marketing\MarketingController::class, 'about'])->name('marketing.about'); Route::get('/pricing', [\App\Http\Controllers\Marketing\MarketingController::class, 'pricing'])->name('marketing.pricing'); // Link-in-bio page (Instagram profile link) Route::get('/link', [MarketingController::class, 'linkInBio'])->name('link-in-bio'); // Legal pages Route::get('/terms', function () { $prices = app(\App\Services\PlanPriceService::class)->getAllPrices(); return view('legal.terms', compact('prices')); })->name('legal.terms'); Route::get('/refund-policy', fn() => view('legal.refund'))->name('legal.refund'); // Aggregator marketplace Route::get('/listing/{id}', [AggregatorController::class, 'show'])->name('aggregator.show'); Route::get('/sitemap.xml', [AggregatorController::class, 'sitemap'])->name('aggregator.sitemap'); Route::get('/listings', [AggregatorController::class, 'index'])->name('aggregator.index'); }); // Public marketplace routes — tenant-scoped, no auth required, no Inertia Route::middleware(['tenant', 'tenant.active']) ->withoutMiddleware([\App\Http\Middleware\HandleInertiaRequests::class]) ->group(function () { Route::get('/', [PublicListingController::class, 'index'])->name('public.index'); Route::get('/listing/{id}', [PublicListingController::class, 'show'])->name('public.show'); Route::post('/listing/{id}/inquiry', [PublicListingController::class, 'inquiry'])->name('public.inquiry'); Route::get('/about', [PublicAboutController::class, 'show'])->name('public.about'); Route::get('/sitemap.xml', [\App\Http\Controllers\Public\PublicSitemapController::class, 'sitemap'])->name('public.sitemap'); Route::get('/robots.txt', [\App\Http\Controllers\Public\PublicSitemapController::class, 'robots'])->name('public.robots'); }); // Health check — no auth, no tenant scope (for uptime monitors) Route::get('/health', [HealthController::class, 'check'])->name('health'); // PWA offline fallback — no auth, no tenant Route::get('/offline', fn() => view('pwa.offline'))->name('pwa.offline'); // Slug availability check — public, no auth Route::get('/check-slug', \App\Http\Controllers\Auth\SlugCheckController::class) ->name('check-slug'); // City search — public, no auth, accessible from all domains Route::get('/ajax/cities/search', [CitySearchController::class, 'search'])->name('cities.search'); // US-7703: Location tree endpoints for aggregator filter Route::get('/ajax/cities/provinces', [CitySearchController::class, 'provinces'])->name('cities.provinces'); Route::get('/ajax/cities/{id}/children', [CitySearchController::class, 'children'])->name('cities.children'); // Authenticated + tenant routes Route::domain(config('app.app_domain'))->middleware(['auth', 'verified', 'tenant.user', 'tenant.active'])->group(function () { Route::get('/dashboard', DashboardController::class)->name('dashboard'); // Mobile listing create flow — disabled, use full form instead // Route::get('/listings/create/mobile', [ListingController::class, 'mobileCreate']) // ->name('listings.create.mobile'); // Listing CRUD Route::resource('listings', ListingController::class) ->only(['index', 'create', 'edit', 'update', 'destroy']); Route::post('/listings', [ListingController::class, 'store']) ->middleware('plan:listings') ->name('listings.store'); Route::post('/listings/{listing}/mark-as-sold', [ListingController::class, 'markAsSold']) ->name('listings.mark-as-sold'); // Media management (AJAX/JSON) Route::post('/listings/{listing}/media', [ListingMediaController::class, 'store']) ->name('listings.media.store'); Route::delete('/listings/{listing}/media/{media}', [ListingMediaController::class, 'destroy']) ->name('listings.media.destroy'); Route::post('/listings/{listing}/media/reorder', [ListingMediaController::class, 'reorder']) ->name('listings.media.reorder'); Route::post('/listings/{listing}/media/{media}/set-primary', [ListingMediaController::class, 'setPrimary']) ->name('listings.media.set-primary'); // AJAX theme endpoints — all authenticated tenant users Route::get('/ajax/templates', [AjaxThemeController::class, 'templates'])->name('ajax.templates'); Route::get('/ajax/templates/{template}/themes', [AjaxThemeController::class, 'themes'])->name('ajax.themes'); // AI AJAX endpoints Route::post('/ajax/ai/listing/generate', [AiListingController::class, 'generate'])->middleware('plan:ai')->name('ajax.ai.listing.generate'); Route::post('/ajax/ai/listing/rewrite', [AiListingController::class, 'rewrite'])->middleware('plan:ai')->name('ajax.ai.listing.rewrite'); Route::post('/ajax/ai/social/generate', [AiSocialController::class, 'generate'])->middleware('plan:ai')->name('ajax.ai.social.generate'); // Listing photos ZIP download Route::get('/ajax/listings/{listing}/photos/download', [ListingMediaController::class, 'downloadZip']) ->name('ajax.listings.photos.download'); // Watermark apply/undo/status (AJAX) Route::get('/ajax/listings/{listing}/watermark/status', [WatermarkApplyController::class, 'status'])->name('ajax.watermark.status'); Route::post('/ajax/listings/{listing}/watermark/apply', [WatermarkApplyController::class, 'apply'])->name('ajax.watermark.apply'); Route::post('/ajax/listings/{listing}/watermark/undo/{media}', [WatermarkApplyController::class, 'undo'])->name('ajax.watermark.undo'); // AI watermark generate (AJAX) Route::post('/ajax/ai/watermark/generate', [AiWatermarkController::class, 'generate'])->middleware('plan:ai')->name('ajax.ai.watermark.generate'); }); // Owner-only routes Route::domain(config('app.app_domain'))->middleware(['auth', 'verified', 'tenant.user', 'tenant.active', 'role:owner'])->group(function () { Route::get('/analytics', [AnalyticsController::class, 'index'])->name('analytics.index'); // Background Jobs Route::get('/jobs', [BackgroundJobController::class, 'index'])->name('jobs.index'); Route::get('/ajax/jobs/{id}/status', [BackgroundJobController::class, 'status'])->name('ajax.jobs.status'); Route::get('/ajax/jobs/list', [BackgroundJobController::class, 'list'])->name('ajax.jobs.list'); Route::post('/ajax/jobs/{id}/retry', [BackgroundJobController::class, 'retry'])->name('ajax.jobs.retry'); // AI Reports Route::prefix('analytics')->name('analytics.')->group(function () { Route::get('/reports', [AiReportController::class, 'index'])->name('reports.index'); Route::post('/reports/generate', [AiReportController::class, 'generate']) ->name('reports.generate') ->middleware('plan:ai'); Route::get('/reports/{id}', [AiReportController::class, 'show'])->name('reports.show'); Route::delete('/reports/{id}', [AiReportController::class, 'destroy'])->name('reports.destroy'); }); // Analytics AJAX endpoints Route::prefix('ajax/analytics')->name('ajax.analytics.')->group(function () { Route::get('revenue-summary', [AnalyticsController::class, 'revenueSummary'])->name('revenue-summary'); Route::get('avg-days-to-sell', [AnalyticsController::class, 'avgDaysToSell'])->name('avg-days-to-sell'); Route::get('max-margin-vehicles', [AnalyticsController::class, 'maxMarginVehicles'])->name('max-margin-vehicles'); Route::get('units-in-out', [AnalyticsController::class, 'unitsInOut'])->name('units-in-out'); Route::get('aging-inventory', [AnalyticsController::class, 'agingInventory'])->name('aging-inventory'); Route::get('stock-turnover-rate', [AnalyticsController::class, 'stockTurnoverRate'])->name('stock-turnover-rate'); Route::get('inventory-value', [AnalyticsController::class, 'inventoryValue'])->name('inventory-value'); Route::get('best-selling-brand-model', [AnalyticsController::class, 'bestSellingBrandModel'])->name('best-selling-brand-model'); Route::get('price-range-distribution', [AnalyticsController::class, 'priceRangeDistribution'])->name('price-range-distribution'); Route::get('transmission-fuel', [AnalyticsController::class, 'transmissionFuelBreakdown'])->name('transmission-fuel'); Route::get('inquiry-conversion-rate', [AnalyticsController::class, 'inquiryConversionRate'])->name('inquiry-conversion-rate'); Route::get('most-viewed-unsold', [AnalyticsController::class, 'mostViewedUnsold'])->name('most-viewed-unsold'); Route::get('views-inquiries-funnel', [AnalyticsController::class, 'viewsInquiriesFunnel'])->name('views-inquiries-funnel'); Route::get('ai-adoption-rate', [AnalyticsController::class, 'aiAdoptionRate'])->name('ai-adoption-rate'); Route::get('ai-vs-manual-performance', [AnalyticsController::class, 'aiVsManualPerformance'])->name('ai-vs-manual-performance'); Route::get('export', [AnalyticsController::class, 'export'])->name('export'); Route::get('sales-by-channel', [AnalyticsController::class, 'salesByChannel'])->name('sales-by-channel'); }); Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index'); Route::get('/settings/pricing', [SettingsController::class, 'pricing'])->name('settings.pricing'); Route::post('/settings/aggregator', [SettingsController::class, 'updateAggregator'])->name('settings.aggregator'); Route::put('/settings/profile', [SettingsController::class, 'update'])->name('settings.profile.update'); Route::post('/settings/logo', [SettingsController::class, 'uploadLogo'])->name('settings.logo.upload'); Route::delete('/settings/logo', [SettingsController::class, 'deleteLogo'])->name('settings.logo.delete'); Route::get('/ajax/billing/header-status', BillingHeaderController::class)->name('billing.header-status'); Route::post('/ajax/billing/snap-token', [BillingController::class, 'snapToken'])->name('ajax.billing.snap-token'); Route::post('/ajax/billing/snap-token-pending', [BillingController::class, 'snapTokenFromPending'])->name('ajax.billing.snap-token-pending'); Route::get('/ajax/billing/plan-status', [BillingController::class, 'planStatus'])->name('ajax.billing.plan-status'); Route::get('/ajax/billing/prorate-preview', [BillingController::class, 'proratePreview'])->name('ajax.billing.prorate-preview'); Route::post('/ajax/billing/process-order', [BillingController::class, 'processOrder'])->name('ajax.billing.process-order'); Route::post('/ajax/promo/validate', [PromoCodeController::class, 'validate'])->name('ajax.promo.validate'); Route::get('/settings/billing/renew', [BillingController::class, 'renew'])->name('settings.billing.renew'); Route::get('/settings/invoices', [BillingController::class, 'invoices'])->name('settings.invoices'); Route::get('/settings/invoices/{subscription}/download', [BillingController::class, 'downloadInvoice'])->name('settings.invoices.download'); Route::get('/settings/refunds', [RefundController::class, 'index'])->name('settings.refunds'); Route::post('/settings/refunds', [RefundController::class, 'store'])->name('settings.refunds.store'); // Scheduled downgrade Route::post('/settings/downgrade', [\App\Http\Controllers\Tenant\DowngradeController::class, 'schedule'])->name('settings.downgrade.schedule'); Route::delete('/settings/downgrade', [\App\Http\Controllers\Tenant\DowngradeController::class, 'cancel'])->name('settings.downgrade.cancel'); Route::get('/theme', [TenantThemeController::class, 'show'])->name('theme.show'); Route::post('/theme', [TenantThemeController::class, 'update'])->name('theme.update'); Route::delete('/theme', [TenantThemeController::class, 'reset'])->name('theme.reset'); // Team member management Route::get('/settings/team', [TeamController::class, 'index'])->name('settings.team.index'); Route::post('/settings/team', [TeamController::class, 'store'])->middleware('plan:users')->name('settings.team.store'); Route::delete('/settings/team/{user}', [TeamController::class, 'destroy'])->name('settings.team.destroy'); // AI Usage History — hidden from tenant, accessible via admin only // Route::get('/settings/ai-usage', [AiUsageController::class, 'index'])->name('settings.ai-usage'); // About page editor Route::get('/about-editor', [AboutController::class, 'show'])->name('about.edit'); Route::put('/about-editor', [AboutController::class, 'update'])->name('about.update'); Route::post('/ajax/about/upload-image', [AboutController::class, 'uploadImage'])->name('about.upload-image'); // Watermark template CRUD Route::post('/settings/watermark/templates', [WatermarkTemplateController::class, 'store'])->name('watermark.templates.store'); Route::put('/settings/watermark/templates/{template}', [WatermarkTemplateController::class, 'update'])->name('watermark.templates.update'); Route::delete('/settings/watermark/templates/{template}', [WatermarkTemplateController::class, 'destroy'])->name('watermark.templates.destroy'); Route::post('/settings/watermark/templates/{template}/set-active', [WatermarkTemplateController::class, 'setActive'])->name('watermark.templates.set-active'); Route::put('/settings/watermark/auto-apply', [SettingsController::class, 'updateWatermarkAutoApply'])->name('settings.watermark.auto-apply'); }); Route::domain(config('app.app_domain'))->middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); // Admin routes — no tenant middleware, role:admin required Route::domain(config('app.app_domain'))->middleware(['auth', 'verified', 'role:admin'])->prefix('admin')->name('admin.')->group(function () { Route::get('/themes', [AdminThemeController::class, 'index'])->name('themes.index'); Route::get('/themes/generate', [AdminThemeController::class, 'generatePage'])->name('themes.generate'); // Route::post('/themes/templates', [AdminThemeController::class, 'storeTemplate'])->name('themes.templates.store'); Route::put('/themes/templates/{template}', [AdminThemeController::class, 'updateTemplate'])->name('themes.templates.update'); // Route::delete('/themes/templates/{template}', [AdminThemeController::class, 'destroyTemplate'])->name('themes.templates.destroy'); Route::post('/themes/templates/{template}/toggle-publish', [AdminThemeController::class, 'toggleTemplatePublish'])->name('themes.templates.toggle-publish'); Route::post('/themes/themes', [AdminThemeController::class, 'storeTheme'])->name('themes.themes.store'); Route::put('/themes/themes/{theme}', [AdminThemeController::class, 'updateTheme'])->name('themes.themes.update'); Route::delete('/themes/themes/{theme}', [AdminThemeController::class, 'destroyTheme'])->name('themes.themes.destroy'); Route::post('/themes/themes/{theme}/toggle-publish', [AdminThemeController::class, 'toggleThemePublish'])->name('themes.themes.toggle-publish'); Route::get('/tenants', [AdminTenantController::class, 'index'])->name('tenants.index'); Route::get('/tenants/{tenant}', [AdminTenantController::class, 'show'])->name('tenants.show'); Route::post('/tenants/{tenant}/override-plan', [AdminTenantOverrideController::class, 'overridePlan'])->name('tenants.override-plan'); Route::get('/promo-codes', [AdminPromoCodeController::class, 'index'])->name('promo-codes.index'); Route::post('/promo-codes', [AdminPromoCodeController::class, 'store'])->name('promo-codes.store'); Route::get('/promo-codes/{promo_code}', [AdminPromoCodeController::class, 'show'])->name('promo-codes.show'); Route::put('/promo-codes/{promo_code}', [AdminPromoCodeController::class, 'update'])->name('promo-codes.update'); Route::delete('/promo-codes/{promo_code}', [AdminPromoCodeController::class, 'destroy'])->name('promo-codes.destroy'); Route::get('/subscriptions', [AdminSubscriptionController::class, 'index'])->name('subscriptions.index'); Route::get('/revenue', [AdminRevenueController::class, 'index'])->name('revenue.index'); // App Settings Route::get('/settings', [AppSettingsController::class, 'index'])->name('settings.index'); Route::post('/settings/logo', [AppSettingsController::class, 'uploadLogo'])->name('settings.logo.upload'); Route::delete('/settings/logo', [AppSettingsController::class, 'deleteLogo'])->name('settings.logo.delete'); Route::post('/settings/plans', [AdminPlanPriceController::class, 'update'])->name('settings.plans.update'); Route::post('/settings/ai-report-limits', [AppSettingsController::class, 'updateAiReportLimits'])->name('settings.ai-report-limits.update'); Route::post('/settings/plan-limits', [AppSettingsController::class, 'updatePlanLimits'])->name('settings.plan-limits.update'); Route::post('/settings/sync-exchange-rate', [AppSettingsController::class, 'syncExchangeRate'])->name('settings.exchange-rate.sync'); Route::post('/settings/ai-cost-rates', [AppSettingsController::class, 'storeAiCostRate'])->name('settings.ai-cost-rates.store'); Route::patch('/settings/ai-cost-rates/{rate}', [AppSettingsController::class, 'updateAiCostRate'])->name('settings.ai-cost-rates.update'); // Admin AI Usage Route::get('/ai-usage', [AdminAiUsageController::class, 'index'])->name('ai-usage.index'); // Profitability Simulator Route::get('/simulator', AdminSimulatorController::class)->name('simulator'); // Plan Feature Management Route::get('/plans', [AdminPlanController::class, 'index'])->name('plans.index'); Route::post('/plans/{plan}', [AdminPlanController::class, 'update'])->name('plans.update'); // Refund Requests Route::get('/refunds', [AdminRefundController::class, 'index'])->name('refunds.index'); Route::get('/refunds/{id}', [AdminRefundController::class, 'show'])->name('refunds.show'); Route::post('/refunds/{id}/approve', [AdminRefundController::class, 'approve'])->name('refunds.approve'); Route::post('/refunds/{id}/reject', [AdminRefundController::class, 'reject'])->name('refunds.reject'); Route::post('/refunds/{id}/process', [AdminRefundController::class, 'process'])->name('refunds.process'); Route::post('/refunds/{id}/retry', [AdminRefundController::class, 'retry'])->name('refunds.retry'); Route::post('/refunds/{id}/confirm-manual', [AdminRefundController::class, 'confirmManual'])->name('refunds.confirm-manual'); }); Route::domain(config('app.app_domain')) ->middleware(['auth', 'verified', 'role:admin']) ->post('/ajax/ai/theme/generate', [AdminThemeController::class, 'generateTheme']) ->name('ajax.ai.theme.generate'); // Dev-only: simulate plan upgrade without webhook (local testing) if (app()->environment('local')) { Route::domain(config('app.app_domain')) ->middleware(['auth', 'verified', 'tenant.user', 'tenant.active', 'role:owner']) ->post('/dev/upgrade-plan', function (\Illuminate\Http\Request $request) { $plan = $request->input('plan', 'pro'); $currentExpiry = tenant()->plan_expires_at; $baseDate = ($currentExpiry && $currentExpiry->isFuture()) ? $currentExpiry : now(); tenant()->update([ 'plan' => $plan, 'plan_renewed_at' => now(), 'plan_expires_at' => $baseDate->copy()->addMonth(), ]); return back()->with('success', "[DEV] Plan upgraded to {$plan}."); })->name('dev.upgrade-plan'); // Simulate Midtrans webhook by fetching transaction status from sandbox API Route::domain(config('app.app_domain')) ->middleware(['auth', 'verified', 'tenant.user', 'tenant.active', 'role:owner']) ->post('/dev/simulate-webhook', function (\Illuminate\Http\Request $request, \App\Services\BillingService $billingService) { $orderId = $request->input('order_id'); if (! $orderId) { return response()->json(['message' => 'order_id required'], 422); } $serverKey = config('midtrans.server_key'); $baseUrl = 'https://api.sandbox.midtrans.com/v2'; $response = \Illuminate\Support\Facades\Http::withBasicAuth($serverKey, '') ->get("{$baseUrl}/{$orderId}/status"); if ($response->failed()) { return response()->json(['message' => 'Failed to fetch transaction from Midtrans', 'detail' => $response->body()], 502); } $payload = $response->json(); $transactionStatus = $payload['transaction_status'] ?? ''; $fraudStatus = $payload['fraud_status'] ?? ''; if (($transactionStatus === 'capture' && $fraudStatus === 'accept') || $transactionStatus === 'settlement') { $billingService->handlePaymentSuccess($payload); return response()->json(['message' => 'Payment success processed', 'plan' => tenant()->fresh()->plan->value]); } return response()->json(['message' => 'Transaction not in success state', 'status' => $transactionStatus]); })->name('dev.simulate-webhook'); // Dev-only: reset AI report usage for current tenant this month Route::domain(config('app.app_domain')) ->middleware(['auth', 'verified', 'tenant.user', 'tenant.active', 'role:owner']) ->post('/dev/reset-ai-report-usage', function (\App\Services\AiReportLimitService $limitService) { $deleted = $limitService->resetUsage(tenant()->id); return back()->with('success', "[DEV] Reset {$deleted} laporan AI bulan ini."); })->name('dev.reset-ai-report-usage'); // Dev-only: reset all subscriptions for current tenant + downgrade to starter Route::domain(config('app.app_domain')) ->middleware(['auth', 'verified', 'tenant.user', 'tenant.active', 'role:owner']) ->post('/dev/reset-subscriptions', function () { $tenant = tenant(); $deleted = \App\Models\Subscription::where('tenant_id', $tenant->id)->delete(); $tenant->update([ 'plan' => \App\Enums\PlanType::Starter, 'plan_renewed_at' => null, 'plan_expires_at' => null, 'grace_period_ends_at' => null, ]); return back()->with('success', "[DEV] Reset {$deleted} subscription. Plan kembali ke Starter."); })->name('dev.reset-subscriptions'); } Route::domain(config('app.app_domain'))->group(function () { require __DIR__.'/auth.php'; });