$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.'); } 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); $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([ '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, 'metadata' => $metadata, ]); if ($amountMinor === 0) { $this->notifyConfirmed($registration->fresh('qrCode')); return ['registration' => $registration, 'paid' => false, 'checkout_url' => null]; } $qrCode->loadMissing('user'); $lineName = $mode === 'contributions' ? sprintf('%s — %s', $content['name'] ?? $qrCode->label, $tierName) : sprintf('%s ticket — %s', $content['name'] ?? $qrCode->label, $tierName); $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([ 'payment_reference' => $checkout['reference'], ]); return [ 'registration' => $registration->fresh(), 'paid' => true, 'checkout_url' => $checkout['checkout_url'], ]; } public function complete(string $reference): QrEventRegistration { $registration = QrEventRegistration::where('payment_reference', $reference) ->where('status', QrEventRegistration::STATUS_PENDING) ->with('qrCode.user') ->firstOrFail(); $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.'); } $registration->update([ 'status' => QrEventRegistration::STATUS_CONFIRMED, 'paid_at' => now(), 'amount_minor' => (int) ($result['amount_minor'] ?: $registration->amount_minor), 'metadata' => array_merge((array) $registration->metadata, [ 'merchant_gateway' => [ 'provider' => $result['provider'], 'reference' => $result['reference'], 'raw' => $result['raw'], ], 'platform_fee_ghs' => 0, ]), ]); $this->notifyConfirmed($registration->fresh('qrCode')); return $registration; } private function notifyConfirmed(QrEventRegistration $registration): void { $registration->loadMissing('qrCode.user'); $ownerPublicId = (string) $registration->qrCode?->user?->public_id; if ($ownerPublicId === '') { return; } $content = $registration->qrCode?->content() ?? []; $eventName = $content['name'] ?? $registration->qrCode?->label ?? config('app.name'); $firstName = explode(' ', $registration->attendee_name)[0]; $joinUrl = $this->nextVirtualJoinUrl($content); if ($registration->attendee_email) { $this->email->sendRegistrationConfirmation( $ownerPublicId, $registration->attendee_email, $eventName, $registration->badge_code, $joinUrl !== '' ? $joinUrl : null, $registration->attendee_name, $registration->qrCode?->user?->email, $registration->qrCode?->user?->name, ); } if ($registration->attendee_phone) { 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 ); } if ($joinUrl !== '') { $message .= ' Join: '.$joinUrl; } $this->sms->send( $ownerPublicId, $registration->attendee_phone, $message, ); } $this->meetAccess->syncRegistration($registration->fresh()); } /** @param array $content */ private function nextVirtualJoinUrl(array $content): string { $format = (string) ($content['format'] ?? 'in_person'); if (! in_array($format, ['virtual', 'hybrid'], true)) { return ''; } foreach ((array) ($content['virtual_sessions'] ?? []) as $session) { if (! is_array($session)) { continue; } $joinUrl = trim((string) ($session['join_url'] ?? '')); if ($joinUrl !== '') { return $joinUrl; } } return ''; } private function uniqueBadgeCode(): string { do { $code = strtoupper(Str::random(8)); } while (QrEventRegistration::where('badge_code', $code)->exists()); return $code; } }