Add Events Pro/Business and BYO ticket gateways.
Deploy Ladill Events / deploy (push) Successful in 42s
Deploy Ladill Events / deploy (push) Successful in 42s
Cut ticket checkouts off Ladill Pay, settle to merchant gateways at 0% platform fee, and mirror Invoice freemium pricing (GHS 49 / 149). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,12 +4,10 @@ namespace App\Services\Events;
|
||||
|
||||
use App\Models\QrCode;
|
||||
use App\Models\QrEventRegistration;
|
||||
use App\Services\Billing\BillingClient;
|
||||
use App\Services\Billing\PaystackService;
|
||||
use App\Services\Billing\SmsService;
|
||||
use App\Services\Events\EventEmailService;
|
||||
use App\Services\Meet\EventMeetAccessService;
|
||||
use App\Services\Pay\PayClient;
|
||||
use App\Services\Payments\MerchantGatewayService;
|
||||
use App\Support\LadillLink;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
@@ -17,9 +15,8 @@ use RuntimeException;
|
||||
class EventRegistrationService
|
||||
{
|
||||
public function __construct(
|
||||
private PayClient $pay,
|
||||
private PaystackService $paystack,
|
||||
private BillingClient $billing,
|
||||
private MerchantGatewayService $gateway,
|
||||
private SubscriptionService $subscriptions,
|
||||
private SmsService $sms,
|
||||
private EventEmailService $email,
|
||||
private EventMeetAccessService $meetAccess,
|
||||
@@ -27,7 +24,7 @@ class EventRegistrationService
|
||||
|
||||
/**
|
||||
* Register an attendee. Free tiers confirm instantly; paid tiers return a
|
||||
* Paystack checkout URL and stay pending until payment completes.
|
||||
* merchant-gateway checkout URL and stay pending until payment completes.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array{registration: QrEventRegistration, paid: bool, checkout_url: ?string}
|
||||
@@ -98,7 +95,12 @@ class EventRegistrationService
|
||||
}
|
||||
$amountMinor = (int) round($priceGhs * 100);
|
||||
|
||||
$meetReturn = trim((string) ($data['meet_return'] ?? ''));
|
||||
$qrCode->loadMissing('user');
|
||||
if ($qrCode->user && ! $this->subscriptions->canAcceptTicket($qrCode->user)) {
|
||||
throw new RuntimeException('This organizer has reached the free monthly ticket limit. Ask them to upgrade Events Pro.');
|
||||
}
|
||||
|
||||
$meetReturn = trim((string) ($data['meet_return'] ?? ''));
|
||||
$metadata = $meetReturn !== '' ? ['meet_return' => $meetReturn] : null;
|
||||
|
||||
$registration = QrEventRegistration::create([
|
||||
@@ -124,131 +126,65 @@ class EventRegistrationService
|
||||
}
|
||||
|
||||
$qrCode->loadMissing('user');
|
||||
$feeTier = $mode === 'contributions' ? 'donations' : 'sales';
|
||||
$lineName = $mode === 'contributions'
|
||||
? sprintf('%s — %s', $content['name'] ?? $qrCode->label, $tierName)
|
||||
: sprintf('%s ticket — %s', $content['name'] ?? $qrCode->label, $tierName);
|
||||
|
||||
$payOrder = $this->pay->createCheckout([
|
||||
'merchant' => $qrCode->user->public_id,
|
||||
'fee_tier' => $feeTier,
|
||||
'source_service' => 'events',
|
||||
'source_ref' => (string) $qrCode->id,
|
||||
'callback_url' => LadillLink::path($qrCode->short_code, 'register/callback'),
|
||||
'customer_name' => $registration->attendee_name,
|
||||
'customer_email' => $email,
|
||||
'customer_phone' => $registration->attendee_phone,
|
||||
'line_items' => [
|
||||
[
|
||||
'name' => $lineName,
|
||||
'unit_price_minor' => $amountMinor,
|
||||
'quantity' => 1,
|
||||
],
|
||||
],
|
||||
'metadata' => [
|
||||
$checkout = $this->gateway->initializeCheckout(
|
||||
$qrCode->user,
|
||||
$amountMinor,
|
||||
(string) ($registration->currency ?? 'GHS'),
|
||||
$email,
|
||||
LadillLink::path($qrCode->short_code, 'register/callback'),
|
||||
$registration->reference,
|
||||
[
|
||||
'title' => $lineName,
|
||||
'registration_id' => $registration->id,
|
||||
'registration_reference' => $registration->reference,
|
||||
'qr_code_id' => $qrCode->id,
|
||||
'mode' => $mode,
|
||||
],
|
||||
]);
|
||||
);
|
||||
|
||||
$registration->update([
|
||||
'pay_order_id' => $payOrder['id'] ?? null,
|
||||
'payment_reference' => $payOrder['reference'],
|
||||
'payment_reference' => $checkout['reference'],
|
||||
]);
|
||||
|
||||
$checkoutUrl = (string) ($payOrder['checkout_url'] ?? '');
|
||||
if ($checkoutUrl === '') {
|
||||
throw new RuntimeException('Could not start checkout. Please try again.');
|
||||
}
|
||||
|
||||
return [
|
||||
'registration' => $registration->fresh(),
|
||||
'paid' => true,
|
||||
'checkout_url' => $checkoutUrl,
|
||||
'checkout_url' => $checkout['checkout_url'],
|
||||
];
|
||||
}
|
||||
|
||||
public function complete(string $reference): QrEventRegistration
|
||||
{
|
||||
if (str_starts_with($reference, 'LP-')) {
|
||||
return $this->completeLadillPay($reference);
|
||||
}
|
||||
|
||||
return $this->completeLegacy($reference);
|
||||
}
|
||||
|
||||
private function completeLadillPay(string $reference): QrEventRegistration
|
||||
{
|
||||
$registration = QrEventRegistration::where('payment_reference', $reference)
|
||||
->where('status', QrEventRegistration::STATUS_PENDING)
|
||||
->with('qrCode.user')
|
||||
->firstOrFail();
|
||||
|
||||
$payOrder = $this->pay->verify($reference);
|
||||
$platformFeeGhs = ((int) ($payOrder['platform_fee_minor'] ?? 0)) / 100;
|
||||
|
||||
$registration->update([
|
||||
'status' => QrEventRegistration::STATUS_CONFIRMED,
|
||||
'paid_at' => now(),
|
||||
'pay_order_id' => $payOrder['id'] ?? $registration->pay_order_id,
|
||||
'metadata' => array_merge((array) $registration->metadata, [
|
||||
'ladill_pay' => $payOrder,
|
||||
'platform_fee_ghs' => $platformFeeGhs,
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->notifyConfirmed($registration->fresh('qrCode'));
|
||||
|
||||
return $registration;
|
||||
}
|
||||
|
||||
/** Legacy QREP-* references before Ladill Pay migration. */
|
||||
private function completeLegacy(string $reference): QrEventRegistration
|
||||
{
|
||||
$registration = QrEventRegistration::where('payment_reference', $reference)
|
||||
->where('status', QrEventRegistration::STATUS_PENDING)
|
||||
->firstOrFail();
|
||||
|
||||
$data = $this->paystack->verifyTransaction($reference);
|
||||
|
||||
if (($data['status'] ?? '') !== 'success') {
|
||||
$owner = $registration->qrCode?->user ?? $registration->organizer;
|
||||
$result = $this->gateway->verify($owner, $reference);
|
||||
if (! $result['paid']) {
|
||||
$registration->update(['status' => QrEventRegistration::STATUS_FAILED]);
|
||||
throw new RuntimeException('Payment was not successful.');
|
||||
}
|
||||
|
||||
$feeRate = ($registration->qrCode?->content()['mode'] ?? 'ticketing') === 'contributions' ? 0.035 : 0.055;
|
||||
$paidAmount = round(($data['amount'] ?? 0) / 100, 2);
|
||||
$platformFee = round($paidAmount * $feeRate, 2);
|
||||
|
||||
$organizerAmountMinor = (int) round(($paidAmount - $platformFee) * 100);
|
||||
|
||||
$registration->update([
|
||||
'status' => QrEventRegistration::STATUS_CONFIRMED,
|
||||
'paid_at' => now(),
|
||||
'amount_minor' => (int) ($result['amount_minor'] ?: $registration->amount_minor),
|
||||
'metadata' => array_merge((array) $registration->metadata, [
|
||||
'paystack' => $data,
|
||||
'platform_fee_ghs' => $platformFee,
|
||||
'legacy' => true,
|
||||
'merchant_gateway' => [
|
||||
'provider' => $result['provider'],
|
||||
'reference' => $result['reference'],
|
||||
'raw' => $result['raw'],
|
||||
],
|
||||
'platform_fee_ghs' => 0,
|
||||
]),
|
||||
]);
|
||||
|
||||
$mode = ($registration->qrCode->content()['mode'] ?? 'ticketing') === 'contributions' ? 'contributions' : 'ticketing';
|
||||
$this->billing->credit(
|
||||
$registration->organizer->public_id,
|
||||
$organizerAmountMinor,
|
||||
'events',
|
||||
'pay',
|
||||
$reference,
|
||||
$registration->id,
|
||||
sprintf(
|
||||
'%s — %s (%s)',
|
||||
$mode === 'contributions' ? 'Event contribution' : 'Event ticket',
|
||||
$registration->qrCode->label,
|
||||
$registration->tier_name
|
||||
),
|
||||
);
|
||||
|
||||
$this->notifyConfirmed($registration->fresh('qrCode'));
|
||||
|
||||
return $registration;
|
||||
|
||||
Reference in New Issue
Block a user