Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -174,12 +174,17 @@ class AccountController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
$provider = (string) ($data['gateway_provider'] ?? '');
|
$provider = (string) ($data['gateway_provider'] ?? '');
|
||||||
if ($provider !== '' && $this->subscriptions->canUsePaymentGateway($account)) {
|
if ($this->subscriptions->canUsePaymentGateway($account)) {
|
||||||
$existing = PaymentGatewaySetting::query()->firstOrNew([
|
$existing = PaymentGatewaySetting::query()->firstOrNew([
|
||||||
'owner_ref' => $account->public_id,
|
'owner_ref' => $account->public_id,
|
||||||
]);
|
]);
|
||||||
|
if ($provider === '' && ! $existing->exists) {
|
||||||
|
return redirect()->route('account.settings')->with('success', 'Settings saved.');
|
||||||
|
}
|
||||||
|
if ($provider !== '') {
|
||||||
$existing->provider = $provider;
|
$existing->provider = $provider;
|
||||||
$existing->is_active = $request->boolean('gateway_is_active', true);
|
}
|
||||||
|
$existing->is_active = $provider !== '' && $request->boolean('gateway_is_active');
|
||||||
if (filled($data['gateway_public_key'] ?? null)) {
|
if (filled($data['gateway_public_key'] ?? null)) {
|
||||||
$existing->public_key = $data['gateway_public_key'];
|
$existing->public_key = $data['gateway_public_key'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,13 @@ class PaymentGatewaySetting extends Model
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$public = trim((string) $this->public_key);
|
||||||
$secret = trim((string) $this->secret_key);
|
$secret = trim((string) $this->secret_key);
|
||||||
|
|
||||||
return $secret !== '' && in_array($this->provider, [
|
return match ($this->provider) {
|
||||||
self::PROVIDER_PAYSTACK,
|
self::PROVIDER_PAYSTACK => str_starts_with($public, 'pk_') && str_starts_with($secret, 'sk_'),
|
||||||
self::PROVIDER_FLUTTERWAVE,
|
self::PROVIDER_FLUTTERWAVE, self::PROVIDER_HUBTEL => $public !== '' && $secret !== '',
|
||||||
self::PROVIDER_HUBTEL,
|
default => false,
|
||||||
], true);
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,31 @@ class EventRegistrationService
|
|||||||
$feeTier = $mode === 'contributions' ? 'donations' : 'sales';
|
$feeTier = $mode === 'contributions' ? 'donations' : 'sales';
|
||||||
$callbackUrl = LadillLink::path($qrCode->short_code, 'register/callback');
|
$callbackUrl = LadillLink::path($qrCode->short_code, 'register/callback');
|
||||||
|
|
||||||
|
$usesOwnerGateway = $this->gateway->shouldUseOwnerGateway($qrCode->user);
|
||||||
|
$payOrder = null;
|
||||||
|
if ($usesOwnerGateway) {
|
||||||
|
try {
|
||||||
|
$payOrder = $this->gateway->initializeCheckout(
|
||||||
|
$qrCode->user,
|
||||||
|
$amountMinor,
|
||||||
|
(string) ($content['currency'] ?? 'GHS'),
|
||||||
|
(string) config('pay.mini_checkout_email', 'pay@ladill.com'),
|
||||||
|
$callbackUrl,
|
||||||
|
'EVTP-'.strtoupper(Str::random(16)),
|
||||||
|
[
|
||||||
|
'title' => $lineName,
|
||||||
|
'registration_id' => $registration->id,
|
||||||
|
'registration_reference' => $registration->reference,
|
||||||
|
'qr_code_id' => $qrCode->id,
|
||||||
|
'mode' => $mode,
|
||||||
|
'attendee_email' => $email,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$usesOwnerGateway = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (! $usesOwnerGateway) {
|
||||||
$payOrder = $this->pay->createCheckout([
|
$payOrder = $this->pay->createCheckout([
|
||||||
'merchant' => $qrCode->user->public_id,
|
'merchant' => $qrCode->user->public_id,
|
||||||
'fee_tier' => $feeTier,
|
'fee_tier' => $feeTier,
|
||||||
@@ -142,13 +167,11 @@ class EventRegistrationService
|
|||||||
'customer_name' => $name,
|
'customer_name' => $name,
|
||||||
'customer_email' => $email,
|
'customer_email' => $email,
|
||||||
'customer_phone' => $registration->attendee_phone,
|
'customer_phone' => $registration->attendee_phone,
|
||||||
'line_items' => [
|
'line_items' => [[
|
||||||
[
|
|
||||||
'name' => $lineName,
|
'name' => $lineName,
|
||||||
'unit_price_minor' => $amountMinor,
|
'unit_price_minor' => $amountMinor,
|
||||||
'quantity' => 1,
|
'quantity' => 1,
|
||||||
],
|
]],
|
||||||
],
|
|
||||||
'metadata' => [
|
'metadata' => [
|
||||||
'registration_id' => $registration->id,
|
'registration_id' => $registration->id,
|
||||||
'registration_reference' => $registration->reference,
|
'registration_reference' => $registration->reference,
|
||||||
@@ -157,6 +180,7 @@ class EventRegistrationService
|
|||||||
'attendee_email' => $email,
|
'attendee_email' => $email,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$checkoutUrl = (string) ($payOrder['checkout_url'] ?? '');
|
$checkoutUrl = (string) ($payOrder['checkout_url'] ?? '');
|
||||||
if ($checkoutUrl === '') {
|
if ($checkoutUrl === '') {
|
||||||
@@ -164,10 +188,11 @@ class EventRegistrationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$registration->update([
|
$registration->update([
|
||||||
'pay_order_id' => $payOrder['id'] ?? null,
|
'pay_order_id' => $usesOwnerGateway ? null : ($payOrder['id'] ?? null),
|
||||||
'payment_reference' => $payOrder['reference'],
|
'payment_reference' => $payOrder['reference'],
|
||||||
'metadata' => array_merge((array) $registration->metadata, [
|
'metadata' => array_merge((array) $registration->metadata, [
|
||||||
'ladill_pay_init' => [
|
($usesOwnerGateway ? 'merchant_gateway_init' : 'ladill_pay_init') => [
|
||||||
|
'provider' => $payOrder['provider'] ?? 'ladill_pay',
|
||||||
'platform_fee_minor' => $payOrder['platform_fee_minor'] ?? null,
|
'platform_fee_minor' => $payOrder['platform_fee_minor'] ?? null,
|
||||||
'merchant_amount_minor' => $payOrder['merchant_amount_minor'] ?? null,
|
'merchant_amount_minor' => $payOrder['merchant_amount_minor'] ?? null,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ class MerchantGatewayService
|
|||||||
|
|
||||||
public function isConfigured(User|string $owner): bool
|
public function isConfigured(User|string $owner): bool
|
||||||
{
|
{
|
||||||
if (! $this->ownerMayUseGateway($owner)) {
|
return $this->shouldUseOwnerGateway($owner);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool) $this->settingFor($owner)?->isConfigured();
|
public function shouldUseOwnerGateway(User|string $owner): bool
|
||||||
|
{
|
||||||
|
return $this->ownerMayUseGateway($owner)
|
||||||
|
&& (bool) $this->settingFor($owner)?->isConfigured();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +57,12 @@ class MerchantGatewayService
|
|||||||
*/
|
*/
|
||||||
public function verify(User|string $owner, string $reference): array
|
public function verify(User|string $owner, string $reference): array
|
||||||
{
|
{
|
||||||
$setting = $this->requireConfigured($owner);
|
// Do not re-check the current plan here: an owner-gateway payment
|
||||||
|
// initiated before a downgrade must remain verifiable.
|
||||||
|
$setting = $this->settingFor($owner);
|
||||||
|
if (! $setting?->isConfigured()) {
|
||||||
|
throw new RuntimeException('The payment gateway used for this transaction is no longer configured.');
|
||||||
|
}
|
||||||
|
|
||||||
return match ($setting->provider) {
|
return match ($setting->provider) {
|
||||||
PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackVerify($setting, $reference),
|
PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackVerify($setting, $reference),
|
||||||
|
|||||||
@@ -272,9 +272,31 @@
|
|||||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 space-y-4">
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-base font-semibold text-slate-900">Online payments</h2>
|
<h2 class="text-base font-semibold text-slate-900">Online payments</h2>
|
||||||
<p class="mt-1 text-sm text-slate-500">Ticket and contribution payments are processed by Ladill Pay (Paystack). Funds settle to your Ladill wallet — no API keys required.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<p class="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">Ladill Pay is active for paid tickets and contributions on all plans.</p>
|
<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
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary">
|
<button type="submit" class="btn-primary">
|
||||||
|
|||||||
@@ -103,4 +103,26 @@ class EventsProTest extends TestCase
|
|||||||
$this->assertTrue($svc->canUsePaymentGateway($user));
|
$this->assertTrue($svc->canUsePaymentGateway($user));
|
||||||
$this->assertTrue(app(\App\Services\Payments\MerchantGatewayService::class)->isConfigured($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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user