Retire Events subscriptions and owner payment gateways.
Deploy Ladill Events / deploy (push) Successful in 55s
Deploy Ladill Events / deploy (push) Successful in 55s
Make Ladill Events free for all accounts, remove plan upgrade UI, force Ladill Pay only for tickets and contributions, and reinstate standard platform fees.
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
-11
@@ -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,
|
||||
|
||||
];
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Payments & Payouts</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Paid tickets and contributions settle directly to your connected gateway (0% Ladill fee). This wallet is for app subscriptions and older balances only.</p>
|
||||
<p class="mt-1 text-sm text-slate-500">Paid tickets and contributions settle via Ladill Pay (standard platform fee). Net proceeds land in your Ladill wallet for withdrawal.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
@@ -97,7 +97,7 @@
|
||||
<div class="rounded-2xl border border-slate-200 bg-white">
|
||||
<div class="border-b border-slate-100 px-5 py-4">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Event payments</h2>
|
||||
<p class="mt-0.5 text-xs text-slate-500">Legacy wallet credits from before BYO gateway settlement. New ticket revenue does not land here.</p>
|
||||
<p class="mt-0.5 text-xs text-slate-500">Ticket and contribution net proceeds after the Ladill Pay platform fee.</p>
|
||||
</div>
|
||||
|
||||
@if($transactions->isEmpty())
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="rounded-2xl border border-slate-200 bg-white px-6 py-5">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-slate-600">
|
||||
<strong>{{ $isEnterprise ? 'Business' : 'Pro' }}</strong> active
|
||||
<strong>Business</strong> active
|
||||
@if ($subscription->current_period_end)
|
||||
until <strong>{{ $subscription->current_period_end->format('d M Y') }}</strong>
|
||||
@endif
|
||||
@@ -52,7 +52,7 @@
|
||||
<button class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50">Cancel auto-renew</button>
|
||||
</form>
|
||||
@elseif ($subscription->status === 'canceled')
|
||||
<form method="post" action="{{ route($isEnterprise ? 'events.pro.subscribe-enterprise' : 'events.pro.subscribe') }}">
|
||||
<form method="post" action="{{ route('events.pro.subscribe-enterprise') }}">
|
||||
@csrf
|
||||
<button class="btn-primary">Resume subscription</button>
|
||||
</form>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-5 lg:grid-cols-3">
|
||||
<div class="grid gap-5 lg:grid-cols-2">
|
||||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-white p-6 {{ $planKey === 'free' ? 'ring-2 ring-indigo-500' : '' }}">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Free</p>
|
||||
<p class="mt-2 text-3xl font-bold text-slate-900">GHS 0</p>
|
||||
@@ -69,54 +69,25 @@
|
||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
||||
<li>{{ (int) config('events.free.max_live_events', 2) }} live events</li>
|
||||
<li>{{ (int) config('events.free.max_tickets_per_month', 100) }} tickets / month</li>
|
||||
<li>Your own Paystack / Flutterwave / Hubtel</li>
|
||||
<li>0% Ladill platform fee</li>
|
||||
<li>Ladill Pay (zero-config)</li>
|
||||
<li>100 emails + 50 SMS / month</li>
|
||||
</ul>
|
||||
@if ($planKey === 'free')
|
||||
<p class="mt-6 text-sm font-medium text-indigo-700">Current plan</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-white p-6 {{ $planKey === 'pro' ? 'ring-2 ring-indigo-500' : '' }}">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-600">Pro</p>
|
||||
<x-plan-tier-price :currency="$currency" :monthly-minor="$proPriceMinor" />
|
||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
||||
<li>Unlimited events</li>
|
||||
<li>Unlimited tickets</li>
|
||||
<li>BYO payment gateway</li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
<div class="mt-6">
|
||||
@if ($planKey === 'pro')
|
||||
<p class="text-sm text-slate-600">Current plan</p>
|
||||
@elseif ($gatingActive && ! $hasPaidPlan)
|
||||
<form x-show="billing === 'monthly'" method="post" action="{{ route('events.pro.subscribe') }}">
|
||||
@csrf
|
||||
<button class="btn-primary w-full">Upgrade — monthly wallet</button>
|
||||
</form>
|
||||
@foreach ($prepaidMonths as $months)
|
||||
<form x-show="billing == {{ $months }}" method="post" action="{{ route('events.pro.subscribe-prepaid') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="plan" value="pro">
|
||||
<input type="hidden" name="months" value="{{ $months }}">
|
||||
<button type="submit" class="btn-primary w-full">Pay {{ $months }} months via Paystack</button>
|
||||
</form>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-gradient-to-b from-slate-900 to-slate-800 p-6 text-white {{ $planKey === 'enterprise' ? 'ring-2 ring-amber-400' : '' }}">
|
||||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-gradient-to-b from-slate-900 to-slate-800 p-6 text-white {{ in_array($planKey, ['enterprise', 'pro'], true) ? 'ring-2 ring-amber-400' : '' }}">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-amber-300">Business</p>
|
||||
<x-plan-tier-price :currency="$currency" :monthly-minor="$enterprisePriceMinor" dark />
|
||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-200">
|
||||
<li>Everything in Pro</li>
|
||||
<li>Team seats</li>
|
||||
<li>Advanced attendee tools</li>
|
||||
<li>Dedicated onboarding</li>
|
||||
<li>Unlimited events & tickets</li>
|
||||
<li>Programmes & badges</li>
|
||||
<li>Unlimited email & SMS</li>
|
||||
<li>Team seats & dedicated onboarding</li>
|
||||
</ul>
|
||||
<div class="mt-6">
|
||||
@if ($planKey === 'enterprise')
|
||||
@if (in_array($planKey, ['enterprise', 'pro'], true))
|
||||
<p class="text-sm text-slate-200">Current plan</p>
|
||||
@elseif ($gatingActive && ! $hasPaidPlan)
|
||||
<form x-show="billing === 'monthly'" method="post" action="{{ route('events.pro.subscribe-enterprise') }}">
|
||||
|
||||
@@ -40,17 +40,5 @@
|
||||
<span class="flex-1 truncate">Settings</span>
|
||||
</a>
|
||||
|
||||
@php $proActive = request()->routeIs('events.pro.*'); @endphp
|
||||
@if (! empty($hasPaidPlan))
|
||||
<a href="{{ route('events.pro.index') }}" class="group mt-0.5 flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $proActive ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
||||
<svg class="h-[18px] w-[18px] shrink-0 text-amber-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25c.3 0 .58.18.7.46l2.36 5.5 5.96.5a.75.75 0 0 1 .43 1.31l-4.53 3.9 1.36 5.83a.75.75 0 0 1-1.12.81L12 17.77l-5.16 3a.75.75 0 0 1-1.12-.81l1.36-5.83-4.53-3.9a.75.75 0 0 1 .43-1.31l5.96-.5 2.36-5.5c.12-.28.4-.46.7-.46Z"/></svg>
|
||||
<span>{{ ! empty($isEnterprise) ? 'Events Business' : 'Events Pro' }}</span>
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('events.pro.index') }}" class="group mt-0.5 flex items-center gap-3 rounded-lg bg-gradient-to-r from-indigo-600 to-emerald-500 px-3 py-2 text-[13px] font-semibold text-white shadow-sm transition hover:opacity-95">
|
||||
<svg class="h-[18px] w-[18px] shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25c.3 0 .58.18.7.46l2.36 5.5 5.96.5a.75.75 0 0 1 .43 1.31l-4.53 3.9 1.36 5.83a.75.75 0 0 1-1.12.81L12 17.77l-5.16 3a.75.75 0 0 1-1.12-.81l1.36-5.83-4.53-3.9a.75.75 0 0 1 .43-1.31l5.96-.5 2.36-5.5c.12-.28.4-.46.7-.46Z"/></svg>
|
||||
<span>Upgrade to Pro</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -272,31 +272,9 @@
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 space-y-4">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-slate-900">Online payments</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Ladill Pay is the recommended default and needs no API keys. Pro and Business may optionally select an owner gateway.</p>
|
||||
<p class="mt-1 text-sm text-slate-500">All ticket and contribution checkouts use Ladill Pay — zero-config, no owner API keys.</p>
|
||||
</div>
|
||||
<p class="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">Ladill Pay is active on all plans and remains the automatic fallback.</p>
|
||||
@if ($canUsePaymentGateway ?? false)
|
||||
<select name="gateway_provider" class="w-full rounded-lg border-slate-200 text-sm">
|
||||
@php $selectedGateway = old('gateway_provider', $gateway?->is_active ? $gateway?->provider : ''); @endphp
|
||||
<option value="">Ladill Pay (recommended)</option>
|
||||
@foreach (['paystack' => 'Paystack', 'flutterwave' => 'Flutterwave', 'hubtel' => 'Hubtel'] as $value => $label)
|
||||
<option value="{{ $value }}" @selected($selectedGateway === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input name="gateway_public_key" value="" placeholder="{{ $gateway?->public_key ? 'Public key saved — leave blank to keep' : 'Public key / merchant account' }}" class="rounded-lg border-slate-200 text-sm">
|
||||
<input type="password" name="gateway_secret_key" value="" placeholder="{{ $gateway?->secret_key ? 'Secret saved — leave blank to keep' : 'Secret / API key' }}" autocomplete="new-password" class="rounded-lg border-slate-200 text-sm">
|
||||
</div>
|
||||
<input type="password" name="gateway_webhook_secret" value="" placeholder="Webhook secret (optional)" autocomplete="new-password" class="w-full rounded-lg border-slate-200 text-sm">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="hidden" name="gateway_is_active" value="0">
|
||||
<input type="checkbox" name="gateway_is_active" value="1" @checked(old('gateway_is_active', $gateway?->is_active ?? false)) class="rounded border-slate-300 text-indigo-600">
|
||||
Use my gateway instead of Ladill Pay
|
||||
</label>
|
||||
<p class="text-xs text-slate-500">Incomplete or invalid credentials automatically fall back to Ladill Pay.</p>
|
||||
@else
|
||||
<p class="rounded-lg bg-indigo-50 px-3 py-2 text-sm text-indigo-800">Free uses Ladill Pay only. Upgrade to Pro to optionally connect your own gateway.</p>
|
||||
@endif
|
||||
<p class="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">Ladill Pay is the only payment option. Standard platform fees apply on paid tickets and contributions and settle to your Ladill wallet.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
@@ -309,7 +287,7 @@
|
||||
<p class="mt-1 text-xs text-slate-500">
|
||||
Attendee and speaker email/SMS use suite messaging by default — no Bird or SMS API keys required.
|
||||
Set a Ladill mailbox (From address) and optional SMS sender ID under Account → Messaging.
|
||||
Plan allowances cover included sends; Pro overage bills your wallet.
|
||||
Included suite messaging allowances apply; overage bills your wallet.
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm font-semibold">
|
||||
<a href="{{ $messagingSettingsUrl ?? '#' }}" class="text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Events\ProSubscription;
|
||||
use App\Models\PaymentGatewaySetting;
|
||||
use App\Models\User;
|
||||
use App\Services\Events\SubscriptionService;
|
||||
use App\Services\Payments\MerchantGatewayService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EventsProTest extends TestCase
|
||||
@@ -21,14 +21,7 @@ class EventsProTest extends TestCase
|
||||
config([
|
||||
'billing.api_url' => 'https://ladill.com/api/billing',
|
||||
'billing.api_key' => 'events-billing-key',
|
||||
'events.pro.enabled' => true,
|
||||
'events.pro.price_minor' => 4900,
|
||||
'events.plans.pro.price_minor' => 4900,
|
||||
'events.plans.enterprise.price_minor' => 14900,
|
||||
'events.pro.period_days' => 30,
|
||||
'events.pro.grace_days' => 3,
|
||||
'events.free.max_live_events' => 2,
|
||||
'events.free.max_tickets_per_month' => 100,
|
||||
'events.pro.enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -37,92 +30,40 @@ class EventsProTest extends TestCase
|
||||
return User::factory()->create(['public_id' => 'usr_'.uniqid()]);
|
||||
}
|
||||
|
||||
public function test_subscribe_charges_wallet_and_activates(): void
|
||||
public function test_subscriptions_are_retired(): void
|
||||
{
|
||||
Http::fake(['*/debit' => Http::response(['id' => 1], 201)]);
|
||||
$user = $this->user();
|
||||
$svc = app(SubscriptionService::class);
|
||||
|
||||
[$ok] = $svc->subscribe($user);
|
||||
|
||||
$this->assertTrue($ok);
|
||||
$this->assertTrue($svc->isPro($user));
|
||||
Http::assertSent(fn ($r) => str_ends_with(parse_url($r->url(), PHP_URL_PATH) ?: '', '/debit')
|
||||
&& $r['amount_minor'] === 4900
|
||||
&& $r['service'] === 'events');
|
||||
}
|
||||
|
||||
public function test_subscribe_enterprise_charges_wallet_and_activates(): void
|
||||
{
|
||||
Http::fake(['*/debit' => Http::response(['id' => 1], 201)]);
|
||||
$user = $this->user();
|
||||
$svc = app(SubscriptionService::class);
|
||||
|
||||
[$ok] = $svc->subscribeEnterprise($user);
|
||||
|
||||
$this->assertTrue($ok);
|
||||
$this->assertTrue($svc->isEnterprise($user));
|
||||
$sub = ProSubscription::where('user_id', $user->id)->first();
|
||||
$this->assertSame('enterprise', $sub->plan);
|
||||
$this->assertSame(14900, $sub->price_minor);
|
||||
}
|
||||
|
||||
public function test_free_user_is_gated_to_event_limit(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
$svc = app(SubscriptionService::class);
|
||||
|
||||
$this->assertFalse($svc->gatingActive());
|
||||
$this->assertTrue($svc->hasPaidPlan($user));
|
||||
$this->assertTrue($svc->canCreateEvent($user));
|
||||
$this->assertSame(0, $svc->liveEventCount($user));
|
||||
}
|
||||
|
||||
public function test_free_user_cannot_use_payment_gateway(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
$svc = app(SubscriptionService::class);
|
||||
|
||||
$this->assertFalse($svc->canUsePaymentGateway($user));
|
||||
$this->assertFalse(app(\App\Services\Payments\MerchantGatewayService::class)->isConfigured($user));
|
||||
}
|
||||
|
||||
public function test_pro_user_can_use_payment_gateway_when_configured(): void
|
||||
public function test_pro_routes_redirect_to_dashboard(): void
|
||||
{
|
||||
Http::fake(['*/debit' => Http::response(['id' => 1], 201)]);
|
||||
$user = $this->user();
|
||||
$svc = app(SubscriptionService::class);
|
||||
$svc->subscribe($user);
|
||||
$this->actingAs($this->user())
|
||||
->get(route('events.pro.index'))
|
||||
->assertRedirect(route('events.dashboard'));
|
||||
|
||||
\App\Models\PaymentGatewaySetting::create([
|
||||
$this->actingAs($this->user())
|
||||
->post(route('events.pro.subscribe'))
|
||||
->assertRedirect(route('events.dashboard'));
|
||||
}
|
||||
|
||||
public function test_owner_gateway_is_never_used(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
PaymentGatewaySetting::create([
|
||||
'owner_ref' => $user->public_id,
|
||||
'provider' => \App\Models\PaymentGatewaySetting::PROVIDER_PAYSTACK,
|
||||
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
|
||||
'public_key' => 'pk_test',
|
||||
'secret_key' => 'sk_test',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->assertTrue($svc->canUsePaymentGateway($user));
|
||||
$this->assertTrue(app(\App\Services\Payments\MerchantGatewayService::class)->isConfigured($user));
|
||||
}
|
||||
|
||||
public function test_invalid_credentials_and_downgrade_disable_owner_gateway_without_deleting_keys(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
ProSubscription::create([
|
||||
'user_id' => $user->id, 'plan' => 'enterprise', 'status' => 'active',
|
||||
'price_minor' => 14900, 'current_period_end' => now()->addMonth(),
|
||||
]);
|
||||
$setting = \App\Models\PaymentGatewaySetting::create([
|
||||
'owner_ref' => $user->public_id, 'provider' => 'paystack',
|
||||
'public_key' => 'bad', 'secret_key' => 'bad', 'is_active' => true,
|
||||
]);
|
||||
$gateway = app(\App\Services\Payments\MerchantGatewayService::class);
|
||||
$this->assertFalse($gateway->shouldUseOwnerGateway($user));
|
||||
|
||||
$setting->update(['public_key' => 'pk_test_saved', 'secret_key' => 'sk_test_saved']);
|
||||
$this->assertTrue($gateway->shouldUseOwnerGateway($user));
|
||||
|
||||
ProSubscription::where('user_id', $user->id)->update(['status' => 'past_due']);
|
||||
$this->assertFalse($gateway->shouldUseOwnerGateway($user->fresh()));
|
||||
$this->assertDatabaseHas('payment_gateway_settings', ['id' => $setting->id, 'is_active' => true]);
|
||||
$this->assertFalse(app(MerchantGatewayService::class)->shouldUseOwnerGateway($user));
|
||||
$this->assertFalse(app(MerchantGatewayService::class)->isConfigured($user));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user