$data * @return array{registration: QrEventRegistration, paid: bool, checkout_url: ?string} */ public function register(QrCode $qrCode, array $data): array { $content = $qrCode->content(); $mode = ($content['mode'] ?? 'ticketing') === 'contributions' ? 'contributions' : 'ticketing'; if (empty($content['registration_open'])) { throw new RuntimeException($mode === 'contributions' ? 'Contributions for this event are closed.' : 'Registration for this event is closed.'); } $tierName = trim((string) ($data['tier'] ?? '')); $tier = null; if ($mode === 'contributions') { if (! in_array($tierName, $content['contribution_categories'] ?? [], true)) { throw new RuntimeException('Select a valid contribution category.'); } } else { $tier = collect($content['tiers'] ?? [])->firstWhere('name', $tierName); if (! $tier) { throw new RuntimeException('Select a valid ticket type.'); } } $name = trim((string) ($data['attendee_name'] ?? '')); if ($name === '') { throw new RuntimeException('Enter your full name.'); } $email = trim((string) ($data['attendee_email'] ?? '')); if ($email === '' || ! str_contains($email, '@')) { throw new RuntimeException('Enter a valid email address.'); } // Capacity guard (0 = unlimited) — ticketing only. if ($mode === 'ticketing') { $capacity = (int) ($tier['capacity'] ?? 0); if ($capacity > 0) { $taken = QrEventRegistration::query() ->where('qr_code_id', $qrCode->id) ->where('tier_name', $tierName) ->where('status', QrEventRegistration::STATUS_CONFIRMED) ->count(); if ($taken >= $capacity) { throw new RuntimeException('Sorry, "' . $tierName . '" is sold out.'); } } } $badgeFields = []; foreach ($content['badge_fields'] ?? [] as $label) { $value = trim((string) (($data['badge_fields'] ?? [])[$label] ?? '')); if ($value !== '') { $badgeFields[$label] = mb_substr($value, 0, 120); } } if ($mode === 'contributions') { $priceGhs = round((float) ($data['amount'] ?? 0), 2); if ($priceGhs <= 0) { throw new RuntimeException('Enter a contribution amount.'); } } else { $priceGhs = round((float) ($tier['price'] ?? 0), 2); } $amountMinor = (int) round($priceGhs * 100); $registration = QrEventRegistration::create([ 'qr_code_id' => $qrCode->id, 'user_id' => $qrCode->user_id, 'reference' => 'QRE-' . strtoupper(Str::random(16)), 'badge_code' => $this->uniqueBadgeCode(), 'tier_name' => $tierName, 'amount_minor' => $amountMinor, 'currency' => $content['currency'] ?? 'GHS', 'attendee_name' => mb_substr($name, 0, 120), 'attendee_email' => $email, 'attendee_phone' => trim((string) ($data['attendee_phone'] ?? '')) ?: null, 'badge_fields' => $badgeFields ?: null, 'status' => $amountMinor > 0 ? QrEventRegistration::STATUS_PENDING : QrEventRegistration::STATUS_CONFIRMED, ]); // Free ticket — done. if ($amountMinor === 0) { $this->notifyConfirmed($registration->fresh('qrCode')); return ['registration' => $registration, 'paid' => false, 'checkout_url' => null]; } // Paid ticket — start Paystack. $reference = 'QREP-' . strtoupper(Str::random(16)); $registration->update(['payment_reference' => $reference]); $paystackData = $this->paystack->initializeTransaction([ 'email' => $email, 'amount' => $amountMinor, 'currency' => $registration->currency, 'reference' => $reference, 'callback_url' => route('qr.public.event.callback', ['shortCode' => $qrCode->short_code]), 'metadata' => [ 'registration_id' => $registration->id, 'qr_code_id' => $qrCode->id, 'merchant_id' => $qrCode->user_id, 'attendee_name' => $registration->attendee_name, 'attendee_email' => $email, ], ]); return [ 'registration' => $registration, 'paid' => true, 'checkout_url' => 'https://checkout.paystack.com/' . $paystackData['access_code'], ]; } public function complete(string $reference): QrEventRegistration { $registration = QrEventRegistration::where('payment_reference', $reference) ->where('status', QrEventRegistration::STATUS_PENDING) ->firstOrFail(); $data = $this->paystack->verifyTransaction($reference); if (($data['status'] ?? '') !== 'success') { $registration->update(['status' => QrEventRegistration::STATUS_FAILED]); throw new RuntimeException('Payment was not successful.'); } $feeRate = ($registration->qrCode?->content()['mode'] ?? 'ticketing') === 'contributions' ? self::PLATFORM_FEE_RATE_CONTRIBUTIONS : self::PLATFORM_FEE_RATE_TICKETING; $paidAmount = round(($data['amount'] ?? 0) / 100, 2); $platformFee = round($paidAmount * $feeRate, 2); $organizerAmount = round($paidAmount - $platformFee, 2); $registration->update([ 'status' => QrEventRegistration::STATUS_CONFIRMED, 'paid_at' => now(), 'metadata' => array_merge((array) $registration->metadata, ['paystack' => $data, 'platform_fee_ghs' => $platformFee]), ]); $this->wallet->credit( $registration->organizer, $organizerAmount, UserWalletTransaction::SOURCE_SERVICE_TRANSFER, $reference, sprintf( '%s — %s (%s)', ($registration->qrCode->content()['mode'] ?? 'ticketing') === 'contributions' ? 'Event contribution' : 'Event ticket', $registration->qrCode->label, $registration->tier_name ), [ 'source' => 'qr_event', 'registration_id' => $registration->id, 'qr_code_id' => $registration->qr_code_id, 'platform_fee_ghs' => $platformFee, ] ); $this->notifyConfirmed($registration->fresh('qrCode')); return $registration; } private function notifyConfirmed(QrEventRegistration $registration): void { if (! $registration->attendee_phone) { return; } $content = $registration->qrCode?->content() ?? []; $eventName = $content['name'] ?? $registration->qrCode?->label ?? config('app.name'); $firstName = explode(' ', $registration->attendee_name)[0]; if (($content['mode'] ?? 'ticketing') === 'contributions') { $message = sprintf( 'Hi %s, thank you for your %s of %s %s to %s. Ref: %s', $firstName, $registration->tier_name, $registration->currency, number_format($registration->amountCedis(), 2), $eventName, $registration->badge_code ); } else { $message = sprintf( 'Hi %s, you are registered for %s (%s). Badge code: %s', $firstName, $eventName, $registration->tier_name, $registration->badge_code ); } $this->sms->send($registration->attendee_phone, $message); } private function uniqueBadgeCode(): string { do { $code = strtoupper(Str::random(8)); } while (QrEventRegistration::where('badge_code', $code)->exists()); return $code; } }