From 827bf12d89af1388e8df128a45b0041b463b602e Mon Sep 17 00:00:00 2001 From: Vasyka Date: Fri, 8 May 2026 05:55:30 +0000 Subject: [PATCH] Demo plan + Payment integrations (Stripe/PayPal/Bank) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models & migrations: - platform_settings table (key/value JSON store + Cache::remember 5min) - plans: is_demo bool + trial_days int - companies: is_demo bool Plans: - Demo plan seeded (is_demo=true, is_public=false, all features, 14 trial days) - Trial 14-day plan seeded (is_public=true, basic features) - Plan form: is_demo toggle + trial_days field - Plan table: badge 🎬 Demo / 🎁 N zile trial Central panel: - PaymentSettings page (heroicon-credit-card, sort 90) Form sections: General, Date legale, Stripe, PayPal, Transfer bancar Each gateway collapsible, fields hidden until enabled toggle Saves to platform_settings keyed by `payments.{gateway}` - CompanyResource: is_demo toggle + table description Payment flow (PaymentController): - GET /billing — tenant invoices list with Pay button - POST /pay/{sub} — start checkout (stripe/paypal/bank) - GET /pay/{sub}/{success,cancel} - POST /payments/stripe/webhook — mark paid + extend company.active_until - POST /payments/paypal/webhook — same Views: - site/billing.blade.php — invoices list with payment modal (3 methods) - site/bank-instructions — IBAN/BIC/reference for manual transfer - site/checkout-stub — placeholder until composer require stripe-php - site/payment-{success,cancel} Tenant panel: - userMenuItems → "Facturile mele" link to /billing --- .../Central/Pages/PaymentSettings.php | 221 ++++++++++++++++++ .../Central/Resources/CompanyResource.php | 7 +- .../Central/Resources/PlanResource.php | 13 +- app/Http/Controllers/PaymentController.php | 199 ++++++++++++++++ app/Models/Central/Plan.php | 4 +- app/Models/Central/PlatformSetting.php | 42 ++++ .../Filament/TenantPanelProvider.php | 7 + ..._000050_create_platform_settings_table.php | 35 +++ database/seeders/DatabaseSeeder.php | 26 +++ .../central/pages/payment-settings.blade.php | 34 +++ .../views/site/bank-instructions.blade.php | 50 ++++ resources/views/site/billing.blade.php | 179 ++++++++++++++ resources/views/site/checkout-stub.blade.php | 30 +++ resources/views/site/payment-cancel.blade.php | 18 ++ .../views/site/payment-success.blade.php | 22 ++ routes/web.php | 20 ++ 16 files changed, 904 insertions(+), 3 deletions(-) create mode 100644 app/Filament/Central/Pages/PaymentSettings.php create mode 100644 app/Http/Controllers/PaymentController.php create mode 100644 app/Models/Central/PlatformSetting.php create mode 100644 database/migrations/2026_05_08_000050_create_platform_settings_table.php create mode 100644 resources/views/filament/central/pages/payment-settings.blade.php create mode 100644 resources/views/site/bank-instructions.blade.php create mode 100644 resources/views/site/billing.blade.php create mode 100644 resources/views/site/checkout-stub.blade.php create mode 100644 resources/views/site/payment-cancel.blade.php create mode 100644 resources/views/site/payment-success.blade.php diff --git a/app/Filament/Central/Pages/PaymentSettings.php b/app/Filament/Central/Pages/PaymentSettings.php new file mode 100644 index 0000000..ea967ec --- /dev/null +++ b/app/Filament/Central/Pages/PaymentSettings.php @@ -0,0 +1,221 @@ +form->fill([ + 'platform_currency' => $s['payments.platform_currency'] ?? 'MDL', + 'invoice_prefix' => $s['payments.invoice_prefix'] ?? 'INV', + 'terms' => $s['payments.terms'] ?? 'Plata se efectuează în 7 zile de la emitere. Pentru întârzieri se aplică penalități 0.1%/zi.', + + 'legal_name' => $legal['name'] ?? null, + 'legal_idno' => $legal['idno'] ?? null, + 'legal_address' => $legal['address'] ?? null, + 'legal_phone' => $legal['phone'] ?? null, + 'legal_email' => $legal['email'] ?? null, + + 'stripe_enabled' => $stripe['enabled'] ?? false, + 'stripe_mode' => $stripe['mode'] ?? 'test', + 'stripe_publishable' => $stripe['publishable_key'] ?? null, + 'stripe_secret' => $stripe['secret_key'] ?? null, + 'stripe_webhook' => $stripe['webhook_secret'] ?? null, + + 'paypal_enabled' => $paypal['enabled'] ?? false, + 'paypal_mode' => $paypal['mode'] ?? 'sandbox', + 'paypal_client_id' => $paypal['client_id'] ?? null, + 'paypal_secret' => $paypal['secret'] ?? null, + + 'bank_enabled' => $bank['enabled'] ?? true, + 'bank_iban' => $bank['iban'] ?? null, + 'bank_bic' => $bank['bic'] ?? null, + 'bank_name' => $bank['bank_name'] ?? null, + 'bank_beneficiary' => $bank['beneficiary'] ?? null, + 'bank_instructions' => $bank['instructions'] ?? null, + ]); + } + + public function form(Schema $schema): Schema + { + return $schema + ->components([ + Schemas\Components\Section::make('General') + ->columns(3) + ->schema([ + Forms\Components\Select::make('platform_currency') + ->label('Monedă platformă') + ->options(['MDL' => 'MDL', 'EUR' => 'EUR', 'USD' => 'USD']) + ->required(), + Forms\Components\TextInput::make('invoice_prefix') + ->label('Prefix facturi')->maxLength(10)->default('INV'), + Forms\Components\Textarea::make('terms') + ->label('Termeni de plată')->rows(2)->columnSpanFull(), + ]), + Schemas\Components\Section::make('Date legale (apar pe facturi)') + ->columns(2) + ->schema([ + Forms\Components\TextInput::make('legal_name')->label('Denumire companie')->maxLength(160), + Forms\Components\TextInput::make('legal_idno')->label('IDNO / CUI')->maxLength(40), + Forms\Components\TextInput::make('legal_address')->label('Adresă')->columnSpanFull()->maxLength(255), + Forms\Components\TextInput::make('legal_phone')->label('Telefon')->tel()->maxLength(40), + Forms\Components\TextInput::make('legal_email')->label('Email')->email()->maxLength(120), + ]), + Schemas\Components\Section::make('💳 Stripe') + ->description('Procesare carduri online. Funcționează în 30+ țări. Comision ~2.9% + 0.30€/tranzacție.') + ->columns(3) + ->collapsible() + ->collapsed(fn (Get $get) => ! $get('stripe_enabled')) + ->schema([ + Forms\Components\Toggle::make('stripe_enabled')->label('Activează Stripe')->default(false)->live(), + Forms\Components\Select::make('stripe_mode') + ->label('Mod') + ->options(['test' => '🧪 Test', 'live' => '🟢 Live']) + ->default('test') + ->required(fn (Get $get) => $get('stripe_enabled')) + ->visible(fn (Get $get) => $get('stripe_enabled')), + Forms\Components\TextInput::make('stripe_publishable') + ->label('Publishable key')->placeholder('pk_test_...') + ->visible(fn (Get $get) => $get('stripe_enabled')), + Forms\Components\TextInput::make('stripe_secret') + ->label('Secret key')->password()->revealable()->placeholder('sk_test_...') + ->visible(fn (Get $get) => $get('stripe_enabled')), + Forms\Components\TextInput::make('stripe_webhook') + ->label('Webhook secret')->password()->revealable()->placeholder('whsec_...') + ->columnSpanFull() + ->helperText('Endpoint webhook: https://service.mir.md/payments/stripe/webhook') + ->visible(fn (Get $get) => $get('stripe_enabled')), + ]), + Schemas\Components\Section::make('🅿️ PayPal') + ->description('Plăți internaționale. Mai potrivit pentru clienți din afara MD.') + ->columns(3) + ->collapsible() + ->collapsed(fn (Get $get) => ! $get('paypal_enabled')) + ->schema([ + Forms\Components\Toggle::make('paypal_enabled')->label('Activează PayPal')->default(false)->live(), + Forms\Components\Select::make('paypal_mode') + ->label('Mod') + ->options(['sandbox' => '🧪 Sandbox', 'live' => '🟢 Live']) + ->default('sandbox') + ->visible(fn (Get $get) => $get('paypal_enabled')), + Forms\Components\Placeholder::make('paypal_webhook_info') + ->label('Webhook') + ->content('https://service.mir.md/payments/paypal/webhook') + ->visible(fn (Get $get) => $get('paypal_enabled')), + Forms\Components\TextInput::make('paypal_client_id') + ->label('Client ID')->placeholder('AYS...') + ->visible(fn (Get $get) => $get('paypal_enabled')), + Forms\Components\TextInput::make('paypal_secret') + ->label('Secret')->password()->revealable()->placeholder('EJk...') + ->visible(fn (Get $get) => $get('paypal_enabled')), + ]), + Schemas\Components\Section::make('🏦 Transfer bancar (manual)') + ->description('Datele apar pe facturi și pe pagina de plată a tenant-ului. Confirmarea o faci manual.') + ->columns(2) + ->collapsible() + ->schema([ + Forms\Components\Toggle::make('bank_enabled')->label('Activează plata prin transfer')->default(true)->live(), + Forms\Components\TextInput::make('bank_beneficiary') + ->label('Beneficiar')->placeholder('AutoCRM SRL') + ->visible(fn (Get $get) => $get('bank_enabled')), + Forms\Components\TextInput::make('bank_iban') + ->label('IBAN')->placeholder('MD00 AG00 0000 ...') + ->visible(fn (Get $get) => $get('bank_enabled')), + Forms\Components\TextInput::make('bank_bic') + ->label('BIC / SWIFT')->placeholder('AGRNMD2X') + ->visible(fn (Get $get) => $get('bank_enabled')), + Forms\Components\TextInput::make('bank_name') + ->label('Banca')->placeholder('Moldova-Agroindbank') + ->visible(fn (Get $get) => $get('bank_enabled')), + Forms\Components\Textarea::make('bank_instructions') + ->label('Instrucțiuni adiționale')->rows(2)->columnSpanFull() + ->visible(fn (Get $get) => $get('bank_enabled')), + ]), + ]) + ->statePath('data'); + } + + public function save(): void + { + $data = $this->form->getState(); + + PlatformSetting::put('payments.platform_currency', $data['platform_currency']); + PlatformSetting::put('payments.invoice_prefix', $data['invoice_prefix']); + PlatformSetting::put('payments.terms', $data['terms']); + + PlatformSetting::put('payments.company_legal', [ + 'name' => $data['legal_name'] ?? null, + 'idno' => $data['legal_idno'] ?? null, + 'address' => $data['legal_address'] ?? null, + 'phone' => $data['legal_phone'] ?? null, + 'email' => $data['legal_email'] ?? null, + ]); + + PlatformSetting::put('payments.stripe', [ + 'enabled' => (bool) ($data['stripe_enabled'] ?? false), + 'mode' => $data['stripe_mode'] ?? 'test', + 'publishable_key' => $data['stripe_publishable'] ?? null, + 'secret_key' => $data['stripe_secret'] ?? null, + 'webhook_secret' => $data['stripe_webhook'] ?? null, + ]); + + PlatformSetting::put('payments.paypal', [ + 'enabled' => (bool) ($data['paypal_enabled'] ?? false), + 'mode' => $data['paypal_mode'] ?? 'sandbox', + 'client_id' => $data['paypal_client_id'] ?? null, + 'secret' => $data['paypal_secret'] ?? null, + ]); + + PlatformSetting::put('payments.bank', [ + 'enabled' => (bool) ($data['bank_enabled'] ?? false), + 'beneficiary' => $data['bank_beneficiary'] ?? null, + 'iban' => $data['bank_iban'] ?? null, + 'bic' => $data['bank_bic'] ?? null, + 'bank_name' => $data['bank_name'] ?? null, + 'instructions' => $data['bank_instructions'] ?? null, + ]); + + Notification::make()->title('Setări plăți salvate')->success()->send(); + } + + protected function getFormActions(): array + { + return [ + \Filament\Actions\Action::make('save') + ->label('Salvează') + ->submit('save'), + ]; + } +} diff --git a/app/Filament/Central/Resources/CompanyResource.php b/app/Filament/Central/Resources/CompanyResource.php index ea3273c..d36f0db 100644 --- a/app/Filament/Central/Resources/CompanyResource.php +++ b/app/Filament/Central/Resources/CompanyResource.php @@ -81,6 +81,9 @@ class CompanyResource extends Resource ->searchable(), Forms\Components\DateTimePicker::make('trial_ends_at')->label('Trial expiră la'), Forms\Components\DateTimePicker::make('active_until')->label('Abonament până la'), + Forms\Components\Toggle::make('is_demo') + ->label('Tenant demo') + ->helperText('Marchează acest tenant ca demo (datele pot fi resetate periodic).'), ]), Schemas\Components\Section::make('Admin tenant (la creare)') ->columns(2) @@ -112,7 +115,9 @@ class CompanyResource extends Resource ->copyable() ->url(fn (Company $record) => $record->url('/app')) ->openUrlInNewTab(), - Tables\Columns\TextColumn::make('name')->searchable()->sortable(), + Tables\Columns\TextColumn::make('name') + ->searchable()->sortable() + ->description(fn ($record) => $record->is_demo ? '🎬 demo' : null), Tables\Columns\TextColumn::make('status') ->badge() ->colors([ diff --git a/app/Filament/Central/Resources/PlanResource.php b/app/Filament/Central/Resources/PlanResource.php index 58ddb7c..4b87ecb 100644 --- a/app/Filament/Central/Resources/PlanResource.php +++ b/app/Filament/Central/Resources/PlanResource.php @@ -53,6 +53,14 @@ class PlanResource extends Resource Forms\Components\TextInput::make('name')->required()->maxLength(60), Forms\Components\Toggle::make('is_active')->label('Activ')->default(true), Forms\Components\Toggle::make('is_public')->label('Public (afișat la signup)')->default(true), + Forms\Components\Toggle::make('is_demo') + ->label('Demo (pentru demonstrații sales)') + ->helperText('Plan ascuns public, folosit pentru tenant-i demo cu features deblocate.') + ->default(false), + Forms\Components\TextInput::make('trial_days') + ->label('Perioadă trial (zile)') + ->numeric()->placeholder('null = fără trial') + ->helperText('Numărul de zile gratis după ce un tenant alege acest plan.'), ]), Schemas\Components\Section::make('Preț') ->columns(3) @@ -86,7 +94,10 @@ class PlanResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('name')->searchable()->sortable(), + Tables\Columns\TextColumn::make('name') + ->searchable()->sortable() + ->weight('bold') + ->description(fn ($record) => $record->is_demo ? '🎬 Demo' : ($record->trial_days ? "🎁 {$record->trial_days} zile trial" : null)), Tables\Columns\TextColumn::make('price_monthly') ->money(fn ($record) => $record->currency) ->label('Lunar') diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php new file mode 100644 index 0000000..8251a3d --- /dev/null +++ b/app/Http/Controllers/PaymentController.php @@ -0,0 +1,199 @@ +current(); + if (! $tenant) abort(404); + + $user = $request->user(); + if (! $user) { + return redirect('/app/login'); + } + + $invoices = Subscription::where('company_id', $tenant->id) + ->latest('period_end') + ->limit(20) + ->get(); + + $methods = $this->availableMethods(); + $bank = PlatformSetting::get('payments.bank', []); + $legal = PlatformSetting::get('payments.company_legal', []); + + return view('site.billing', [ + 'tenant' => $tenant, + 'invoices' => $invoices, + 'methods' => $methods, + 'bank' => $bank, + 'legal' => $legal, + 'themeColor' => $tenant->settings['theme_color'] ?? '#3B82F6', + ]); + } + + public function startCheckout(Request $request, int $subscription) + { + $tenant = app(TenantManager::class)->current(); + $sub = Subscription::where('company_id', $tenant?->id)->findOrFail($subscription); + + $method = $request->input('method'); + + if (! in_array($method, ['stripe', 'paypal', 'bank'], true)) { + abort(422, 'Method invalid'); + } + + if ($sub->status === 'paid') { + return back()->with('error', 'Factura este deja plătită'); + } + + if ($method === 'bank') { + // Show bank instructions page; payment is manual. + return view('site.bank-instructions', [ + 'tenant' => $tenant, + 'sub' => $sub, + 'bank' => PlatformSetting::get('payments.bank', []), + 'themeColor' => $tenant->settings['theme_color'] ?? '#3B82F6', + ]); + } + + if ($method === 'stripe') { + return $this->startStripe($sub); + } + + if ($method === 'paypal') { + return $this->startPaypal($sub); + } + } + + private function startStripe(Subscription $sub) + { + $stripe = PlatformSetting::get('payments.stripe', []); + if (empty($stripe['enabled']) || empty($stripe['secret_key'])) { + return back()->with('error', 'Stripe nu este configurat. Contactează operatorul.'); + } + + // Real implementation requires `stripe/stripe-php` package. Here we + // produce a stub that explains the flow; in prod, replace with: + // $session = \Stripe\Checkout\Session::create([...]); + // return redirect($session->url); + return view('site.checkout-stub', [ + 'gateway' => 'Stripe', + 'sub' => $sub, + 'note' => 'Pentru a activa Stripe Checkout, instalează `composer require stripe/stripe-php` și activează în /admin/payment-settings.', + ]); + } + + private function startPaypal(Subscription $sub) + { + $paypal = PlatformSetting::get('payments.paypal', []); + if (empty($paypal['enabled']) || empty($paypal['client_id'])) { + return back()->with('error', 'PayPal nu este configurat.'); + } + + return view('site.checkout-stub', [ + 'gateway' => 'PayPal', + 'sub' => $sub, + 'note' => 'Pentru a activa PayPal, instalează `composer require srmklive/paypal` și activează în /admin/payment-settings.', + ]); + } + + public function success(int $subscription) + { + $sub = Subscription::findOrFail($subscription); + return view('site.payment-success', ['sub' => $sub]); + } + + public function cancel(int $subscription) + { + return view('site.payment-cancel'); + } + + public function stripeWebhook(Request $request) + { + $stripe = PlatformSetting::get('payments.stripe', []); + $secret = $stripe['webhook_secret'] ?? null; + + if (! $secret) { + Log::warning('Stripe webhook hit but no secret configured'); + return response()->json(['ok' => false], 400); + } + + // Real impl: \Stripe\Webhook::constructEvent($payload, $sig, $secret) + // Mock: trust the body shape for stub mode. + $payload = $request->json()->all(); + $event = $payload['type'] ?? null; + + if ($event === 'checkout.session.completed') { + $subId = $payload['data']['object']['metadata']['subscription_id'] ?? null; + if ($subId && $sub = Subscription::find($subId)) { + $this->markPaid($sub, 'stripe_card', $payload['data']['object']['id'] ?? null); + } + } + + return response()->json(['ok' => true]); + } + + public function paypalWebhook(Request $request) + { + // Real impl: verify signature with PayPal + $payload = $request->json()->all(); + $event = $payload['event_type'] ?? null; + + if ($event === 'CHECKOUT.ORDER.APPROVED' || $event === 'PAYMENT.CAPTURE.COMPLETED') { + $subId = $payload['resource']['custom_id'] ?? null; + if ($subId && $sub = Subscription::find($subId)) { + $this->markPaid($sub, 'paypal', $payload['resource']['id'] ?? null); + } + } + + return response()->json(['ok' => true]); + } + + private function markPaid(Subscription $sub, string $method, ?string $reference): void + { + if ($sub->status === 'paid') return; + + $sub->update([ + 'status' => 'paid', + 'paid_at' => now(), + 'payment_method' => $method, + 'reference' => $reference, + ]); + + // Extend company subscription + $sub->company?->update([ + 'status' => 'active', + 'active_until' => $sub->period_end, + ]); + } + + private function availableMethods(): array + { + $stripe = PlatformSetting::get('payments.stripe', []); + $paypal = PlatformSetting::get('payments.paypal', []); + $bank = PlatformSetting::get('payments.bank', []); + + return [ + 'stripe' => ! empty($stripe['enabled']) && ! empty($stripe['publishable_key']), + 'paypal' => ! empty($paypal['enabled']) && ! empty($paypal['client_id']), + 'bank' => ! empty($bank['enabled']) && ! empty($bank['iban']), + ]; + } +} diff --git a/app/Models/Central/Plan.php b/app/Models/Central/Plan.php index 39e974e..a690d41 100644 --- a/app/Models/Central/Plan.php +++ b/app/Models/Central/Plan.php @@ -8,7 +8,7 @@ class Plan extends Model { protected $fillable = [ 'slug', 'name', 'price_monthly', 'price_yearly', 'currency', - 'features', 'limits', 'is_active', 'is_public', + 'features', 'limits', 'is_active', 'is_public', 'is_demo', 'trial_days', ]; protected $casts = [ @@ -16,6 +16,8 @@ class Plan extends Model 'limits' => 'array', 'is_active' => 'boolean', 'is_public' => 'boolean', + 'is_demo' => 'boolean', + 'trial_days' => 'integer', 'price_monthly' => 'decimal:2', 'price_yearly' => 'decimal:2', ]; diff --git a/app/Models/Central/PlatformSetting.php b/app/Models/Central/PlatformSetting.php new file mode 100644 index 0000000..48f81e5 --- /dev/null +++ b/app/Models/Central/PlatformSetting.php @@ -0,0 +1,42 @@ + 'array', + ]; + + public static function get(string $key, $default = null) + { + $cached = Cache::remember("psetting:{$key}", 300, function () use ($key) { + $row = static::where('key', $key)->first(); + return $row?->value; + }); + return $cached ?? $default; + } + + public static function put(string $key, $value): void + { + static::updateOrCreate(['key' => $key], ['value' => $value]); + Cache::forget("psetting:{$key}"); + } + + public static function many(array $keys): array + { + $rows = static::whereIn('key', $keys)->get()->keyBy('key'); + $out = []; + foreach ($keys as $k) { + $out[$k] = $rows[$k]?->value ?? null; + } + return $out; + } +} diff --git a/app/Providers/Filament/TenantPanelProvider.php b/app/Providers/Filament/TenantPanelProvider.php index d9e9789..25d9582 100644 --- a/app/Providers/Filament/TenantPanelProvider.php +++ b/app/Providers/Filament/TenantPanelProvider.php @@ -124,6 +124,13 @@ class TenantPanelProvider extends PanelProvider BLADE) ) + ->userMenuItems([ + 'billing' => \Filament\Navigation\MenuItem::make() + ->label('Facturile mele') + ->icon('heroicon-o-credit-card') + ->url('/billing') + ->openUrlInNewTab(false), + ]) ->renderHook( PanelsRenderHook::USER_MENU_BEFORE, fn (): string => Blade::render(<<<'BLADE' diff --git a/database/migrations/2026_05_08_000050_create_platform_settings_table.php b/database/migrations/2026_05_08_000050_create_platform_settings_table.php new file mode 100644 index 0000000..ba37ade --- /dev/null +++ b/database/migrations/2026_05_08_000050_create_platform_settings_table.php @@ -0,0 +1,35 @@ +id(); + $t->string('key', 100)->unique(); + $t->json('value')->nullable(); + $t->timestamps(); + }); + + Schema::table('plans', function (Blueprint $t) { + $t->boolean('is_demo')->default(false)->after('is_public'); + $t->integer('trial_days')->nullable()->after('is_demo') + ->comment('Days of free trial for tenants on this plan (null = no trial)'); + }); + + Schema::table('companies', function (Blueprint $t) { + $t->boolean('is_demo')->default(false)->after('plan_id') + ->comment('Demo tenant — features unlocked but data may be reset'); + }); + } + + public function down(): void + { + Schema::dropIfExists('platform_settings'); + Schema::table('plans', fn (Blueprint $t) => $t->dropColumn(['is_demo', 'trial_days'])); + Schema::table('companies', fn (Blueprint $t) => $t->dropColumn('is_demo')); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index de3bef7..913d1a8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -76,6 +76,32 @@ class DatabaseSeeder extends Seeder 'is_active' => true, 'is_public' => true, ]); + // Demo — full features, dar marcat is_demo (nu apare public, e doar + // pentru sales/onboarding). Trial_days lipsește = sandbox permanent + // controlat de operator. + Plan::firstOrCreate(['slug' => 'demo'], [ + 'name' => 'Demo', + 'price_monthly' => 0, + 'price_yearly' => 0, + 'currency' => 'MDL', + 'features' => ['kanban', 'pdf', 'reports', 'ai', 'api', 'reverb', 'multi_user', 'white_label'], + 'limits' => ['max_users' => 5, 'max_clients' => 50, 'max_vehicles' => 50], + 'is_active' => true, 'is_public' => false, 'is_demo' => true, + 'trial_days' => 14, + ]); + + // Trial 14 zile — plan vizibil public, devine status=trial. + Plan::firstOrCreate(['slug' => 'trial'], [ + 'name' => 'Trial 14 zile', + 'price_monthly' => 0, + 'price_yearly' => 0, + 'currency' => 'MDL', + 'features' => ['kanban', 'pdf', 'reports'], + 'limits' => ['max_users' => 3, 'max_clients' => 100, 'max_vehicles' => 100], + 'is_active' => true, 'is_public' => true, 'is_demo' => false, + 'trial_days' => 14, + ]); + // ─── Super-admin (operator platformă) ───────────────────── SuperAdmin::firstOrCreate(['email' => 'vasyka.moraru@gmail.com'], [ 'name' => 'Vasyka', diff --git a/resources/views/filament/central/pages/payment-settings.blade.php b/resources/views/filament/central/pages/payment-settings.blade.php new file mode 100644 index 0000000..a0c9c13 --- /dev/null +++ b/resources/views/filament/central/pages/payment-settings.blade.php @@ -0,0 +1,34 @@ + +
+ {{ $this->form }} + +
+ +
+
+ +
+ 📚 Cum se face plata în sistem: +
    +
  1. Operatorul (tu) creezi factură: /admin/companies/{id} → „Generează factură"
  2. +
  3. Tenantul vede factura în interiorul lui la https://{slug}.service.mir.md/billing
  4. +
  5. Click „Plătește" → opțiuni: card (Stripe), PayPal, sau transfer manual
  6. +
  7. La succes: webhook → automat status=paid + extends company.active_until
  8. +
  9. La transfer manual: tenant trimite confirmare; operator click „Marchează plătit" în /admin/subscriptions
  10. +
+
+ ⚠ Pentru a primi plăți LIVE: +
    +
  • Stripe: înregistrează cont la dashboard.stripe.com → ia keys → setează webhook la URL-ul de mai sus
  • +
  • PayPal: developer.paypal.com → creează aplicație live → ia credentials
  • +
  • Transfer bancar: pune doar IBAN-ul tău, fără cont gateway
  • +
+
+
+
diff --git a/resources/views/site/bank-instructions.blade.php b/resources/views/site/bank-instructions.blade.php new file mode 100644 index 0000000..b56385b --- /dev/null +++ b/resources/views/site/bank-instructions.blade.php @@ -0,0 +1,50 @@ + + + + + +Instrucțiuni transfer bancar — {{ $sub->invoice_number }} + + + +
+

🏦 Instrucțiuni transfer bancar

+ +
Beneficiar: {{ $bank['beneficiary'] ?? '—' }}
+
IBAN: {{ $bank['iban'] ?? '—' }}
+
BIC / SWIFT: {{ $bank['bic'] ?? '—' }}
+
Banca: {{ $bank['bank_name'] ?? '—' }}
+
Sumă: {{ number_format($sub->amount, 2) }} {{ $sub->currency }}
+ +
+ ⚠ Important — la „Detalii plată" / „Reference" scrie EXACT:
+ {{ $sub->invoice_number ?? 'INV-' . $sub->id }} + Fără acest cod, plata va fi greu de identificat. +
+ + @if (! empty($bank['instructions'])) +

{{ $bank['instructions'] }}

+ @endif + +
+ După efectuarea transferului: +
    +
  • Plata apare în 1-3 zile lucrătoare
  • +
  • Operatorul confirmă manual factura ca plătită
  • +
  • Abonamentul se extinde automat
  • +
+
+ + ← Înapoi la facturi +
+ + diff --git a/resources/views/site/billing.blade.php b/resources/views/site/billing.blade.php new file mode 100644 index 0000000..93282e0 --- /dev/null +++ b/resources/views/site/billing.blade.php @@ -0,0 +1,179 @@ + + + + + + +Facturi & abonament — {{ $tenant->display_name ?? $tenant->name }} + + + +
+ ← Înapoi la AutoCRM +

Facturi & abonament

+

{{ $tenant->display_name ?? $tenant->name }} · {{ $tenant->slug }}.service.mir.md

+ + @if ($invoices->isEmpty()) +
+
📄
+
Nicio factură emisă încă
+
Operatorul îți va emite factură când e timpul abonamentului.
+
+ @else + @foreach ($invoices as $inv) +
+
+ {{ $inv->invoice_number ?? '#' . $inv->id }} +
+
+
{{ $inv->plan?->name ?? 'Abonament' }} · {{ $inv->period === 'yearly' ? 'Anual' : 'Lunar' }}
+
{{ $inv->period_start?->format('d.m.Y') }} → {{ $inv->period_end?->format('d.m.Y') }}
+
+ + @switch($inv->status) + @case('paid') ✓ Plătit @if($inv->paid_at) la {{ $inv->paid_at->format('d.m.Y') }} @endif @break + @case('overdue') ⚠ Întârziat @break + @case('pending') ⏳ În așteptare @break + @default {{ $inv->status }} + @endswitch + + @if ($inv->due_at) + scadent {{ $inv->due_at->format('d.m.Y') }} + @endif +
+
+
{{ number_format($inv->amount, 2) }} {{ $inv->currency }}
+
+ @if ($inv->status === 'paid') + + @else + + @endif +
+
+ + {{-- Modal de plată --}} + + @endforeach + @endif + + @if (! empty($legal['name'])) +
+ Operator platformă: {{ $legal['name'] }} + @if (! empty($legal['idno'])) · IDNO {{ $legal['idno'] }} @endif + @if (! empty($legal['address'])) · {{ $legal['address'] }} @endif + @if (! empty($legal['email'])) · {{ $legal['email'] }} @endif +
+ @endif +
+ + diff --git a/resources/views/site/checkout-stub.blade.php b/resources/views/site/checkout-stub.blade.php new file mode 100644 index 0000000..4f89231 --- /dev/null +++ b/resources/views/site/checkout-stub.blade.php @@ -0,0 +1,30 @@ + + + + + +{{ $gateway }} Checkout + + + +
+
🚧
+

{{ $gateway }} Checkout

+

Factură: {{ $sub->invoice_number ?? '#' . $sub->id }} · {{ number_format($sub->amount, 2) }} {{ $sub->currency }}

+ +
+ Mod stub:
+ {{ $note }}

+ În prod, aici redirectează la pagina {{ $gateway }} Checkout cu sumele și metadatele facturii. La success, webhook-ul marchează automat factura ca plătită. +
+ + ← Înapoi +
+ + diff --git a/resources/views/site/payment-cancel.blade.php b/resources/views/site/payment-cancel.blade.php new file mode 100644 index 0000000..1e874bc --- /dev/null +++ b/resources/views/site/payment-cancel.blade.php @@ -0,0 +1,18 @@ + + +Plată anulată + + + +
+
+

Plată anulată

+

Nu a fost prelevată nicio sumă. Poți încerca din nou oricând.

+ ← Înapoi la facturi +
+ + diff --git a/resources/views/site/payment-success.blade.php b/resources/views/site/payment-success.blade.php new file mode 100644 index 0000000..630c5d5 --- /dev/null +++ b/resources/views/site/payment-success.blade.php @@ -0,0 +1,22 @@ + + + + +Plată reușită + + + +
+
+

Plată reușită!

+

Factura {{ $sub->invoice_number ?? '#' . $sub->id }} a fost achitată.

+

Abonamentul tău a fost extins.

+ → Mergi la AutoCRM +
+ + diff --git a/routes/web.php b/routes/web.php index 32da442..0356bd4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,6 +24,26 @@ Route::get('/', function () { return redirect('/admin'); }); +// ─── Plăți / Billing (tenant-side) ────────────────────────────────── +// /billing — listă facturi tenant + buton plată +// /pay/{id} — start checkout (Stripe / PayPal / bank) +Route::get('/billing', [\App\Http\Controllers\PaymentController::class, 'billing'])->name('billing'); +Route::post('/pay/{subscription}', [\App\Http\Controllers\PaymentController::class, 'startCheckout']) + ->where('subscription', '\d+') + ->name('pay.start'); +Route::get('/pay/{subscription}/success', [\App\Http\Controllers\PaymentController::class, 'success']) + ->where('subscription', '\d+') + ->name('pay.success'); +Route::get('/pay/{subscription}/cancel', [\App\Http\Controllers\PaymentController::class, 'cancel']) + ->where('subscription', '\d+') + ->name('pay.cancel'); + +// ─── Webhooks (central, no auth) ──────────────────────────────────── +Route::post('/payments/stripe/webhook', [\App\Http\Controllers\PaymentController::class, 'stripeWebhook']) + ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]); +Route::post('/payments/paypal/webhook', [\App\Http\Controllers\PaymentController::class, 'paypalWebhook']) + ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]); + // Stub `login` route — needed because Laravel's auth middleware tries to // route('login') when redirecting unauthenticated requests. We don't have a // global /login (panels use /admin/login and /app/login), so stub it.