Demo plan + Payment integrations (Stripe/PayPal/Bank)
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
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Central\PlatformSetting;
|
||||
use App\Models\Central\Subscription;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Payment flow:
|
||||
* - GET /billing → tenant invoices list (auth required)
|
||||
* - POST /pay/{subscription} → start checkout (Stripe/PayPal/Bank)
|
||||
* - GET /pay/{subscription}/success → marked paid via webhook in real flow
|
||||
* - GET /pay/{subscription}/cancel → user aborted
|
||||
* - POST /payments/stripe/webhook → Stripe → mark paid
|
||||
* - POST /payments/paypal/webhook → PayPal → mark paid
|
||||
*/
|
||||
class PaymentController
|
||||
{
|
||||
public function billing(Request $request)
|
||||
{
|
||||
$tenant = app(TenantManager::class)->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']),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user