From 25ce2bc9de0a5d9ebda9562aa3aace570db927df Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 24 Jul 2026 14:15:34 +0000 Subject: [PATCH] Retire Events subscriptions and owner payment gateways. Make Ladill Events free for all accounts, remove plan upgrade UI, force Ladill Pay only for tickets and contributions, and reinstate standard platform fees. --- app/Http/Controllers/Events/ProController.php | 114 +++--------------- app/Http/Middleware/EnsurePro.php | 9 +- app/Services/Events/SubscriptionService.php | 7 +- .../Payments/MerchantGatewayService.php | 15 +-- config/events.php | 19 ++- resources/views/events/payouts.blade.php | 4 +- resources/views/events/pro/index.blade.php | 51 ++------ resources/views/partials/sidebar.blade.php | 12 -- resources/views/qr/account/settings.blade.php | 28 +---- tests/Feature/EventsProTest.php | 103 ++++------------ 10 files changed, 72 insertions(+), 290 deletions(-) diff --git a/app/Http/Controllers/Events/ProController.php b/app/Http/Controllers/Events/ProController.php index 85086ad..e67a966 100644 --- a/app/Http/Controllers/Events/ProController.php +++ b/app/Http/Controllers/Events/ProController.php @@ -3,134 +3,52 @@ namespace App\Http\Controllers\Events; use App\Http\Controllers\Controller; -use App\Models\Events\ProSubscription; -use App\Models\User; use App\Services\Billing\BillingClient; use App\Services\Events\SubscriptionService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\View\View; +/** + * Paid Events plans are retired. Routes remain so old bookmarks and emails + * do not 404; every action redirects to the dashboard. + */ class ProController extends Controller { public function __construct(private readonly SubscriptionService $subscriptions) {} - public function index(Request $request): View + public function index(Request $request): RedirectResponse { - $user = ladill_account() ?? $request->user(); - $planKey = $this->subscriptions->planKey($user); - - return view('events.pro.index', [ - 'subscription' => $this->subscriptions->subscriptionFor($user), - 'planKey' => $planKey, - 'isPro' => $planKey === ProSubscription::PLAN_PRO, - 'isEnterprise' => $planKey === ProSubscription::PLAN_ENTERPRISE, - 'hasPaidPlan' => $this->subscriptions->hasPaidPlan($user), - 'gatingActive' => $this->subscriptions->gatingActive(), - 'proPriceMinor' => $this->subscriptions->priceMinor(), - 'enterprisePriceMinor' => $this->subscriptions->enterprisePriceMinor(), - 'prepaidMonths' => (array) config('events.prepaid_months', [6, 12, 24]), - 'currency' => (string) config('events.pro.currency', 'GHS'), - ]); + return $this->retired(); } public function subscribe(Request $request): RedirectResponse { - [$ok, $message] = $this->subscriptions->subscribe(ladill_account() ?? $request->user()); - - return redirect()->route('events.pro.index')->with($ok ? 'success' : 'error', $message); + return $this->retired(); } public function subscribeEnterprise(Request $request): RedirectResponse { - [$ok, $message] = $this->subscriptions->subscribeEnterprise(ladill_account() ?? $request->user()); - - return redirect()->route('events.pro.index')->with($ok ? 'success' : 'error', $message); + return $this->retired(); } public function subscribePrepaid(Request $request, BillingClient $billing): RedirectResponse { - $validated = $request->validate([ - 'plan' => ['required', 'in:pro,enterprise'], - 'months' => ['required', 'integer', 'in:6,12,24'], - ]); - - $user = ladill_account() ?? $request->user(); - if ($this->subscriptions->hasPaidPlan($user)) { - return redirect()->route('events.pro.index') - ->with('success', 'You already have an active subscription.'); - } - - $plan = $validated['plan']; - $months = (int) $validated['months']; - $monthlyMinor = $plan === 'enterprise' - ? $this->subscriptions->enterprisePriceMinor() - : $this->subscriptions->priceMinor(); - - try { - $checkout = $billing->initiatePlanCheckout( - $user, - $plan, - $months, - $monthlyMinor * $months, - route('events.pro.paystack.callback'), - ['user_id' => $user->id], - ); - } catch (\Throwable) { - return redirect()->route('events.pro.index') - ->with('error', 'Could not start Paystack checkout. Please try again.'); - } - - $url = trim((string) ($checkout['checkout_url'] ?? '')); - if ($url === '') { - return redirect()->route('events.pro.index') - ->with('error', 'Paystack did not return a checkout URL.'); - } - - return redirect()->away($url); + return $this->retired(); } public function paystackCallback(Request $request, BillingClient $billing): RedirectResponse { - $reference = (string) $request->query('reference', ''); - if ($reference === '') { - return redirect()->route('events.pro.index')->with('error', 'Missing payment reference.'); - } - - try { - $result = $billing->verifyPlanCheckout($reference); - } catch (\Throwable) { - return redirect()->route('events.pro.index') - ->with('error', 'Could not verify payment. Contact support with reference: '.$reference); - } - - if (! ($result['paid'] ?? false)) { - return redirect()->route('events.pro.index') - ->with('error', 'Payment was not completed.'); - } - - $metadata = (array) ($result['metadata'] ?? []); - $user = User::query()->find((int) ($metadata['user_id'] ?? 0)); - $account = ladill_account() ?? $request->user(); - if (! $user || ! $account || $user->id !== $account->id) { - return redirect()->route('events.pro.index') - ->with('error', 'Account not found for this payment.'); - } - - $plan = (string) ($result['plan'] ?? 'pro'); - $months = (int) ($result['months'] ?? 0); - $this->subscriptions->activatePrepaid($user, $plan, $months); - - $label = $plan === 'enterprise' ? 'Events Business' : 'Events Pro'; - - return redirect()->route('events.pro.index') - ->with('success', "{$label} is active for {$months} months."); + return $this->retired(); } public function cancel(Request $request): RedirectResponse { - [$ok, $message] = $this->subscriptions->cancel(ladill_account() ?? $request->user()); + return $this->retired(); + } - return redirect()->route('events.pro.index')->with($ok ? 'success' : 'error', $message); + private function retired(): RedirectResponse + { + return redirect()->route('events.dashboard') + ->with('success', 'Ladill Events is free — paid subscriptions are no longer offered. Ticket payments use Ladill Pay with the standard platform fee.'); } } diff --git a/app/Http/Middleware/EnsurePro.php b/app/Http/Middleware/EnsurePro.php index 1016a17..befb8fa 100644 --- a/app/Http/Middleware/EnsurePro.php +++ b/app/Http/Middleware/EnsurePro.php @@ -13,12 +13,7 @@ class EnsurePro public function handle(Request $request, Closure $next): Response { - $user = ladill_account() ?? $request->user(); - if ($user && $this->subscriptions->isPro($user)) { - return $next($request); - } - - return redirect()->route('events.pro.index') - ->with('upsell', 'This feature is part of Ladill Events Pro or Business.'); + // Paid Events plans are retired — all authenticated users have full access. + return $next($request); } } diff --git a/app/Services/Events/SubscriptionService.php b/app/Services/Events/SubscriptionService.php index 069cd35..f1e90d4 100644 --- a/app/Services/Events/SubscriptionService.php +++ b/app/Services/Events/SubscriptionService.php @@ -17,7 +17,8 @@ class SubscriptionService public function gatingActive(): bool { - return (bool) config('events.pro.enabled', true) && $this->billing->configured(); + // Paid Events subscriptions are retired — product is free for all accounts. + return false; } public function priceMinor(): int @@ -74,10 +75,10 @@ class SubscriptionService return $this->hasPaidPlan($user); } - /** Own payment gateway (Paystack / Flutterwave / Hubtel) requires Pro or Business. */ + /** Custom / owner payment gateways are retired — Ladill Pay only. */ public function canUsePaymentGateway(User $user): bool { - return $this->hasPaidPlan($user); + return false; } public function liveEventCount(User $user): int diff --git a/app/Services/Payments/MerchantGatewayService.php b/app/Services/Payments/MerchantGatewayService.php index 38633bc..1bf520b 100644 --- a/app/Services/Payments/MerchantGatewayService.php +++ b/app/Services/Payments/MerchantGatewayService.php @@ -24,8 +24,9 @@ class MerchantGatewayService public function shouldUseOwnerGateway(User|string $owner): bool { - return $this->ownerMayUseGateway($owner) - && (bool) $this->settingFor($owner)?->isConfigured(); + // Custom / owner payment gateways are retired. All checkouts use Ladill Pay + // with platform fees. + return false; } /** @@ -88,15 +89,7 @@ class MerchantGatewayService protected function ownerMayUseGateway(User|string $owner): bool { - $user = $owner instanceof User - ? $owner - : User::query()->where('public_id', (string) $owner)->first(); - - if (! $user) { - return false; - } - - return app(SubscriptionService::class)->canUsePaymentGateway($user); + return false; } /** diff --git a/config/events.php b/config/events.php index c7d2449..05bee3a 100644 --- a/config/events.php +++ b/config/events.php @@ -8,30 +8,27 @@ return [ 'meet_app_url' => env('LADILL_MEET_APP_URL', 'https://meet.ladill.com'), + /* + | Paid Events subscriptions (Pro / Business) are retired. Keep pro.* keys for + | legacy renew command / DB rows; gating is always off via SubscriptionService. + */ 'pro' => [ - 'enabled' => filter_var(env('EVENTS_PRO_ENABLED', true), FILTER_VALIDATE_BOOLEAN), + 'enabled' => false, 'price_minor' => (int) env('EVENTS_PRO_PRICE_MINOR', 49000), 'currency' => env('EVENTS_PRO_CURRENCY', 'GHS'), 'period_days' => (int) env('EVENTS_PRO_PERIOD_DAYS', 30), 'grace_days' => (int) env('EVENTS_PRO_GRACE_DAYS', 3), ], - 'plans' => [ - 'pro' => ['price_minor' => (int) env('EVENTS_PRO_PRICE_MINOR', 49000)], - 'enterprise' => ['price_minor' => (int) env('EVENTS_ENTERPRISE_PRICE_MINOR', 149000)], - ], + 'plans' => [], - 'prepaid_months' => [6, 12, 24], + 'prepaid_months' => [], 'free' => [ 'max_live_events' => (int) env('EVENTS_FREE_MAX_LIVE_EVENTS', 2), 'max_tickets_per_month' => (int) env('EVENTS_FREE_MAX_TICKETS_PER_MONTH', 100), ], - 'upgrade_banner' => [ - 'title' => 'Unlock Events Pro or Business', - 'description' => 'Unlimited events & tickets, with your own payment gateway — from GHS 490/mo.', - 'route' => 'events.pro.index', - ], + 'upgrade_banner' => null, ]; diff --git a/resources/views/events/payouts.blade.php b/resources/views/events/payouts.blade.php index c58681a..dfc5518 100644 --- a/resources/views/events/payouts.blade.php +++ b/resources/views/events/payouts.blade.php @@ -16,7 +16,7 @@

Payments & Payouts

-

Paid tickets and contributions settle directly to your connected gateway (0% Ladill fee). This wallet is for app subscriptions and older balances only.

+

Paid tickets and contributions settle via Ladill Pay (standard platform fee). Net proceeds land in your Ladill wallet for withdrawal.

@@ -97,7 +97,7 @@

Event payments

-

Legacy wallet credits from before BYO gateway settlement. New ticket revenue does not land here.

+

Ticket and contribution net proceeds after the Ladill Pay platform fee.

@if($transactions->isEmpty()) diff --git a/resources/views/events/pro/index.blade.php b/resources/views/events/pro/index.blade.php index 74cd148..20bf93c 100644 --- a/resources/views/events/pro/index.blade.php +++ b/resources/views/events/pro/index.blade.php @@ -36,7 +36,7 @@
- {{ $isEnterprise ? 'Business' : 'Pro' }} active + Business active @if ($subscription->current_period_end) until {{ $subscription->current_period_end->format('d M Y') }} @endif @@ -52,7 +52,7 @@ @elseif ($subscription->status === 'canceled') -
+ @csrf
@@ -61,7 +61,7 @@
@endif -
+

Free

GHS 0

@@ -69,54 +69,25 @@
  • {{ (int) config('events.free.max_live_events', 2) }} live events
  • {{ (int) config('events.free.max_tickets_per_month', 100) }} tickets / month
  • -
  • Your own Paystack / Flutterwave / Hubtel
  • -
  • 0% Ladill platform fee
  • +
  • Ladill Pay (zero-config)
  • +
  • 100 emails + 50 SMS / month
@if ($planKey === 'free')

Current plan

@endif
-
-

Pro

- -
    -
  • Unlimited events
  • -
  • Unlimited tickets
  • -
  • BYO payment gateway
  • -
  • Priority support
  • -
-
- @if ($planKey === 'pro') -

Current plan

- @elseif ($gatingActive && ! $hasPaidPlan) -
- @csrf - -
- @foreach ($prepaidMonths as $months) -
- @csrf - - - -
- @endforeach - @endif -
-
- -
+

Business

    -
  • Everything in Pro
  • -
  • Team seats
  • -
  • Advanced attendee tools
  • -
  • Dedicated onboarding
  • +
  • Unlimited events & tickets
  • +
  • Programmes & badges
  • +
  • Unlimited email & SMS
  • +
  • Team seats & dedicated onboarding
- @if ($planKey === 'enterprise') + @if (in_array($planKey, ['enterprise', 'pro'], true))

Current plan

@elseif ($gatingActive && ! $hasPaidPlan)
diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index fd2c67a..5336ef1 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -40,17 +40,5 @@ Settings - @php $proActive = request()->routeIs('events.pro.*'); @endphp - @if (! empty($hasPaidPlan)) - - - {{ ! empty($isEnterprise) ? 'Events Business' : 'Events Pro' }} - - @else - - - Upgrade to Pro - - @endif
diff --git a/resources/views/qr/account/settings.blade.php b/resources/views/qr/account/settings.blade.php index ce327e0..dc7ae68 100644 --- a/resources/views/qr/account/settings.blade.php +++ b/resources/views/qr/account/settings.blade.php @@ -272,31 +272,9 @@

Online payments

-

Ladill Pay is the recommended default and needs no API keys. Pro and Business may optionally select an owner gateway.

+

All ticket and contribution checkouts use Ladill Pay — zero-config, no owner API keys.

-

Ladill Pay is active on all plans and remains the automatic fallback.

- @if ($canUsePaymentGateway ?? false) - -
- - -
- - -

Incomplete or invalid credentials automatically fall back to Ladill Pay.

- @else -

Free uses Ladill Pay only. Upgrade to Pro to optionally connect your own gateway.

- @endif +

Ladill Pay is the only payment option. Standard platform fees apply on paid tickets and contributions and settle to your Ladill wallet.