Add virtual event sessions with Meet sync and platform-billed messaging.
Deploy Ladill Events / deploy (push) Successful in 41s
Deploy Ladill Events / deploy (push) Successful in 41s
Events can define hybrid/virtual sessions synced to Meet rooms; SMS and email use wallet-billed platform APIs instead of Termii and Laravel mail. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,18 +5,20 @@ namespace App\Http\Controllers\Events;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QrCode;
|
||||
use App\Models\QrEventRegistration;
|
||||
use App\Notifications\EventProgrammeSharedNotification;
|
||||
use App\Services\Billing\SmsService;
|
||||
use App\Services\Events\EventEmailService;
|
||||
use App\Support\Events\EventBadgeZpl;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AttendeeController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SmsService $sms) {}
|
||||
public function __construct(
|
||||
private readonly SmsService $sms,
|
||||
private readonly EventEmailService $email,
|
||||
) {}
|
||||
|
||||
public function hub(Request $request): View
|
||||
{
|
||||
@@ -163,13 +165,19 @@ class AttendeeController extends Controller
|
||||
|
||||
foreach ($recipients as $reg) {
|
||||
if ($reg->attendee_email) {
|
||||
Notification::route('mail', $reg->attendee_email)
|
||||
->notify(new EventProgrammeSharedNotification($event, $programme, $reg->attendee_name));
|
||||
$this->email->sendProgrammeShare(
|
||||
(string) $event->user->public_id,
|
||||
$reg->attendee_email,
|
||||
$eventName,
|
||||
$programmeUrl,
|
||||
$reg->attendee_name,
|
||||
);
|
||||
$emailed++;
|
||||
}
|
||||
|
||||
if ($reg->attendee_phone) {
|
||||
$this->sms->send(
|
||||
(string) $event->user->public_id,
|
||||
$reg->attendee_phone,
|
||||
sprintf(
|
||||
'Hi %s, the programme for %s is here: %s',
|
||||
|
||||
@@ -42,6 +42,7 @@ class QrSetting extends Model
|
||||
'organizer',
|
||||
'registration_open',
|
||||
'badge_fields',
|
||||
'format',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -142,6 +143,9 @@ class QrSetting extends Model
|
||||
'organizer' => mb_substr(trim((string) ($input['organizer'] ?? '')), 0, 120),
|
||||
'registration_open' => filter_var($input['registration_open'] ?? true, FILTER_VALIDATE_BOOL),
|
||||
'badge_fields' => $badgeFields !== [] ? $badgeFields : ['Company', 'Role'],
|
||||
'format' => in_array($input['format'] ?? 'in_person', ['in_person', 'virtual', 'hybrid'], true)
|
||||
? ($input['format'] ?? 'in_person')
|
||||
: 'in_person',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PlatformEmailClient
|
||||
{
|
||||
private function base(): string
|
||||
{
|
||||
return rtrim((string) config('smtp.platform_api_url', 'https://ladill.com/api/smtp'), '/');
|
||||
}
|
||||
|
||||
private function token(): string
|
||||
{
|
||||
return (string) config('smtp.platform_api_key', '');
|
||||
}
|
||||
|
||||
public function send(string $ownerPublicId, string $to, string $subject, string $html, ?string $text = null): bool
|
||||
{
|
||||
if ($this->token() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::withToken($this->token())->acceptJson()->timeout(30)
|
||||
->post($this->base().'/messages/send', array_filter([
|
||||
'user' => $ownerPublicId,
|
||||
'from' => config('smtp.from'),
|
||||
'from_name' => config('smtp.from_name'),
|
||||
'to' => $to,
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
'text' => $text,
|
||||
]));
|
||||
|
||||
if ($res->status() === 402) {
|
||||
Log::warning('Platform email send: insufficient balance', ['user' => $ownerPublicId]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($res->failed()) {
|
||||
Log::warning('Platform email send failed', ['status' => $res->status(), 'body' => $res->body()]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) ($res->json('success') ?? true);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Platform email send error', ['error' => $e->getMessage()]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PlatformSmsClient
|
||||
{
|
||||
private function base(): string
|
||||
{
|
||||
return rtrim((string) config('sms.platform_api_url', 'https://ladill.com/api'), '/');
|
||||
}
|
||||
|
||||
private function token(): string
|
||||
{
|
||||
return (string) config('sms.platform_api_key', '');
|
||||
}
|
||||
|
||||
/** @return list<array<string, mixed>> */
|
||||
public function services(string $ownerPublicId): array
|
||||
{
|
||||
if ($this->token() === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::withToken($this->token())->acceptJson()->timeout(15)
|
||||
->get($this->base().'/sms/services', ['user' => $ownerPublicId]);
|
||||
|
||||
if ($res->failed()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (array) ($res->json('data') ?? []);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Platform SMS services lookup failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function ensureServiceId(string $ownerPublicId): ?int
|
||||
{
|
||||
$services = $this->services($ownerPublicId);
|
||||
if ($services !== []) {
|
||||
return (int) ($services[0]['id'] ?? 0) ?: null;
|
||||
}
|
||||
|
||||
if ($this->token() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::withToken($this->token())->acceptJson()->timeout(15)
|
||||
->post($this->base().'/sms/services', [
|
||||
'user' => $ownerPublicId,
|
||||
'name' => 'Ladill Events',
|
||||
'brand_name' => 'Events',
|
||||
]);
|
||||
|
||||
if ($res->failed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) ($res->json('data.id') ?? 0) ?: null;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Platform SMS service create failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function send(string $ownerPublicId, string $to, string $message, ?string $senderId = null): bool
|
||||
{
|
||||
if ($this->token() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$serviceId = $this->ensureServiceId($ownerPublicId);
|
||||
if (! $serviceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::withToken($this->token())->acceptJson()->timeout(30)
|
||||
->post($this->base().'/sms/messages/send', array_filter([
|
||||
'user' => $ownerPublicId,
|
||||
'sms_service_id' => $serviceId,
|
||||
'to' => $to,
|
||||
'text' => $message,
|
||||
'sender_id' => $senderId ?? config('sms.default_sender_id'),
|
||||
]));
|
||||
|
||||
if ($res->status() === 402) {
|
||||
Log::warning('Platform SMS send: insufficient balance', ['user' => $ownerPublicId]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($res->failed()) {
|
||||
Log::warning('Platform SMS send failed', ['status' => $res->status(), 'body' => $res->body()]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) ($res->json('success') ?? true);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Platform SMS send error', ['error' => $e->getMessage()]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,12 @@
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SmsService
|
||||
{
|
||||
public function send(string $to, string $message): void
|
||||
public function __construct(private readonly PlatformSmsClient $platform) {}
|
||||
|
||||
public function send(string $ownerPublicId, string $to, string $message): void
|
||||
{
|
||||
$apiKey = config('services.termii.api_key', '');
|
||||
$senderId = config('services.termii.sender_id', config('app.name', 'LaDill'));
|
||||
|
||||
if ($apiKey === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$phone = preg_replace('/\D+/', '', $to);
|
||||
if (strlen($phone) < 9) {
|
||||
return;
|
||||
@@ -27,17 +19,6 @@ class SmsService
|
||||
$phone = '233'.substr($phone, 1);
|
||||
}
|
||||
|
||||
try {
|
||||
Http::timeout(10)->post('https://v3.api.termii.com/api/sms/send', [
|
||||
'api_key' => $apiKey,
|
||||
'to' => $phone,
|
||||
'from' => $senderId,
|
||||
'sms' => $message,
|
||||
'type' => 'plain',
|
||||
'channel' => 'dnd',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('SMS send failed', ['to' => $phone, 'error' => $e->getMessage()]);
|
||||
}
|
||||
$this->platform->send($ownerPublicId, $phone, $message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Events;
|
||||
|
||||
use App\Services\Billing\PlatformEmailClient;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class EventEmailService
|
||||
{
|
||||
public function __construct(private readonly PlatformEmailClient $platform) {}
|
||||
|
||||
public function sendProgrammeShare(
|
||||
string $ownerPublicId,
|
||||
string $to,
|
||||
string $eventName,
|
||||
string $programmeUrl,
|
||||
?string $attendeeName = null,
|
||||
): bool {
|
||||
$html = View::make('mail.notifications.event-programme', [
|
||||
'eventName' => $eventName,
|
||||
'programmeUrl' => $programmeUrl,
|
||||
'attendeeName' => $attendeeName,
|
||||
])->render();
|
||||
|
||||
return $this->platform->send(
|
||||
$ownerPublicId,
|
||||
$to,
|
||||
'Programme for '.$eventName,
|
||||
$html,
|
||||
strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -252,6 +252,11 @@ class EventRegistrationService
|
||||
return;
|
||||
}
|
||||
|
||||
$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];
|
||||
@@ -276,7 +281,38 @@ class EventRegistrationService
|
||||
);
|
||||
}
|
||||
|
||||
$this->sms->send($registration->attendee_phone, $message);
|
||||
$joinUrl = $this->nextVirtualJoinUrl($content);
|
||||
if ($joinUrl !== '') {
|
||||
$message .= ' Join: '.$joinUrl;
|
||||
}
|
||||
|
||||
$this->sms->send(
|
||||
$ownerPublicId,
|
||||
$registration->attendee_phone,
|
||||
$message,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $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
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Meet;
|
||||
|
||||
use App\Models\QrCode;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EventMeetSyncService
|
||||
{
|
||||
public function sync(QrCode $event, User $owner): QrCode
|
||||
{
|
||||
if ($event->type !== QrCode::TYPE_EVENT) {
|
||||
return $event;
|
||||
}
|
||||
|
||||
$content = $event->content();
|
||||
$format = (string) ($content['format'] ?? 'in_person');
|
||||
|
||||
if (! in_array($format, ['virtual', 'hybrid'], true)) {
|
||||
$this->cancelRemovedSessions($event, $owner, []);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
if ((string) config('meet.key', '') === '') {
|
||||
return $event;
|
||||
}
|
||||
|
||||
$sessions = (array) ($content['virtual_sessions'] ?? []);
|
||||
$client = MeetClient::for($owner->public_id);
|
||||
$synced = [];
|
||||
$activeIds = [];
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (! is_array($session)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sessionId = trim((string) ($session['id'] ?? ''));
|
||||
if ($sessionId === '') {
|
||||
$sessionId = 'vs-'.Str::lower(Str::random(12));
|
||||
}
|
||||
|
||||
$activeIds[] = $sessionId;
|
||||
$title = trim((string) ($session['title'] ?? ''));
|
||||
if ($title === '') {
|
||||
$synced[] = array_merge($session, ['id' => $sessionId]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$roomType = in_array($session['meet_room_type'] ?? '', ['town_hall', 'webinar'], true)
|
||||
? (string) $session['meet_room_type']
|
||||
: 'town_hall';
|
||||
|
||||
$scheduledAt = $session['scheduled_at'] ?? $content['starts_at'] ?? null;
|
||||
|
||||
try {
|
||||
$result = $client->upsertRoom([
|
||||
'title' => $title,
|
||||
'description' => trim((string) ($content['description'] ?? '')) ?: null,
|
||||
'type' => $roomType,
|
||||
'scheduled_at' => $scheduledAt,
|
||||
'duration_minutes' => max(15, (int) ($session['duration_minutes'] ?? 60)),
|
||||
'timezone' => config('app.timezone', 'UTC'),
|
||||
'source' => [
|
||||
'app' => 'events',
|
||||
'entity_type' => 'virtual_session',
|
||||
'entity_id' => $sessionId,
|
||||
'event_id' => (string) $event->id,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Events Meet sync failed', [
|
||||
'event_id' => $event->id,
|
||||
'session_id' => $sessionId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$synced[] = array_merge($session, ['id' => $sessionId]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$room = (array) ($result['room'] ?? []);
|
||||
$synced[] = array_merge($session, [
|
||||
'id' => $sessionId,
|
||||
'meet_room_uuid' => $room['uuid'] ?? $session['meet_room_uuid'] ?? null,
|
||||
'join_url' => $result['join_url'] ?? $session['join_url'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->cancelRemovedSessions($event, $owner, $activeIds);
|
||||
|
||||
$payload = (array) ($event->payload ?? []);
|
||||
$payload['content'] = array_merge($content, ['virtual_sessions' => array_values($synced)]);
|
||||
$event->payload = $payload;
|
||||
$event->save();
|
||||
|
||||
return $event->fresh();
|
||||
}
|
||||
|
||||
/** @param list<string> $keepIds */
|
||||
private function cancelRemovedSessions(QrCode $event, User $owner, array $keepIds): void
|
||||
{
|
||||
if ((string) config('meet.key', '') === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$previous = (array) ($event->content()['virtual_sessions'] ?? []);
|
||||
$client = MeetClient::for($owner->public_id);
|
||||
|
||||
foreach ($previous as $session) {
|
||||
if (! is_array($session)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sessionId = (string) ($session['id'] ?? '');
|
||||
$roomUuid = (string) ($session['meet_room_uuid'] ?? '');
|
||||
|
||||
if ($sessionId === '' || in_array($sessionId, $keepIds, true) || $roomUuid === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$client->cancelRoom($roomUuid);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Events Meet cancel failed', [
|
||||
'event_id' => $event->id,
|
||||
'session_id' => $sessionId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Meet;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Client for the Ladill Meet service API — virtual sessions linked to Events.
|
||||
*/
|
||||
class MeetClient
|
||||
{
|
||||
public function __construct(private readonly string $ownerRef) {}
|
||||
|
||||
public static function for(string $ownerRef): self
|
||||
{
|
||||
return new self($ownerRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findBySource(string $app, string $entityId): ?array
|
||||
{
|
||||
try {
|
||||
$response = $this->client()->get('rooms/by-source', [
|
||||
'source_app' => $app,
|
||||
'entity_id' => $entityId,
|
||||
'owner_ref' => $this->ownerRef,
|
||||
]);
|
||||
} catch (ConnectionException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($response->status() === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (array) $response->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function upsertRoom(array $data): array
|
||||
{
|
||||
return $this->post('rooms', array_merge($data, [
|
||||
'owner_ref' => $this->ownerRef,
|
||||
'host_user_ref' => $data['host_user_ref'] ?? $this->ownerRef,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateRoom(string $roomUuid, array $data): array
|
||||
{
|
||||
return $this->patch("rooms/{$roomUuid}", array_merge($data, [
|
||||
'owner_ref' => $this->ownerRef,
|
||||
'host_user_ref' => $data['host_user_ref'] ?? $this->ownerRef,
|
||||
]));
|
||||
}
|
||||
|
||||
public function cancelRoom(string $roomUuid, ?string $hostUserRef = null): array
|
||||
{
|
||||
return $this->post("rooms/{$roomUuid}/cancel", [
|
||||
'owner_ref' => $this->ownerRef,
|
||||
'host_user_ref' => $hostUserRef ?? $this->ownerRef,
|
||||
]);
|
||||
}
|
||||
|
||||
private function client(): PendingRequest
|
||||
{
|
||||
return Http::baseUrl((string) config('meet.url'))
|
||||
->withToken((string) config('meet.key'))
|
||||
->acceptJson()
|
||||
->asJson()
|
||||
->connectTimeout(10)
|
||||
->timeout(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function post(string $path, array $data): array
|
||||
{
|
||||
return $this->request('post', $path, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function patch(string $path, array $data): array
|
||||
{
|
||||
return $this->request('patch', $path, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function request(string $method, string $path, array $data): array
|
||||
{
|
||||
try {
|
||||
$response = $this->client()->{$method}($path, $data);
|
||||
} catch (ConnectionException) {
|
||||
throw new \RuntimeException('Could not reach the Meet service.');
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new \RuntimeException('Meet service error: '.($response->json('message') ?? $response->body()));
|
||||
}
|
||||
|
||||
return (array) $response->json();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class QrCodeManagerService
|
||||
private QrWalletBillingService $billing,
|
||||
private QrImageGeneratorService $imageGenerator,
|
||||
private QrPayloadValidator $payloadValidator,
|
||||
private \App\Services\Meet\EventMeetSyncService $meetSync,
|
||||
) {}
|
||||
|
||||
public function walletFor(User $user): QrWallet
|
||||
@@ -158,6 +159,10 @@ class QrCodeManagerService
|
||||
$this->billing->debitForQrCreation($wallet, $qrCode);
|
||||
$this->imageGenerator->generateAndStore($qrCode);
|
||||
|
||||
if ($type === QrCode::TYPE_EVENT) {
|
||||
$qrCode = $this->meetSync->sync($qrCode->fresh(), $user);
|
||||
}
|
||||
|
||||
return $qrCode->fresh(['document']);
|
||||
});
|
||||
}
|
||||
@@ -294,6 +299,10 @@ class QrCodeManagerService
|
||||
$this->imageGenerator->generateAndStore($qrCode);
|
||||
}
|
||||
|
||||
if ($qrCode->type === QrCode::TYPE_EVENT) {
|
||||
$qrCode = $this->meetSync->sync($qrCode->fresh(), $qrCode->user);
|
||||
}
|
||||
|
||||
return $qrCode->fresh(['document']);
|
||||
}
|
||||
|
||||
@@ -347,7 +356,7 @@ class QrCodeManagerService
|
||||
QrCode::TYPE_VCARD => ['first_name', 'last_name', 'phone', 'email', 'company', 'website', 'address', 'note', 'social'],
|
||||
QrCode::TYPE_BUSINESS => ['name', 'tagline', 'phone', 'email', 'website', 'address', 'hours'],
|
||||
QrCode::TYPE_CHURCH => ['name', 'denomination', 'description', 'phone', 'email', 'website', 'address', 'service_times', 'org_type', 'accepts_payment', 'collection_types', 'brand_color'],
|
||||
QrCode::TYPE_EVENT => ['name', 'tagline', 'description', 'location', 'starts_at', 'ends_at', 'organizer', 'website', 'brand_color', 'tiers', 'badge_fields', 'badge_size', 'registration_open'],
|
||||
QrCode::TYPE_EVENT => ['name', 'tagline', 'description', 'location', 'starts_at', 'ends_at', 'organizer', 'website', 'brand_color', 'tiers', 'badge_fields', 'badge_size', 'registration_open', 'format', 'virtual_sessions'],
|
||||
QrCode::TYPE_ITINERARY => ['title', 'subtitle', 'description', 'event_date', 'location', 'brand_color', 'days'],
|
||||
QrCode::TYPE_DOCUMENT => ['allow_download'],
|
||||
QrCode::TYPE_MENU => ['menu_title', 'sections', 'accepts_payment', 'brand_color', 'shipping_type', 'shipping_fee', 'free_shipping_above'],
|
||||
|
||||
@@ -260,6 +260,12 @@ class QrPayloadValidator
|
||||
$categories = [];
|
||||
}
|
||||
|
||||
$format = in_array($input['format'] ?? 'in_person', ['in_person', 'virtual', 'hybrid'], true)
|
||||
? ($input['format'] ?? 'in_person')
|
||||
: 'in_person';
|
||||
|
||||
$virtualSessions = $this->normalizeVirtualSessions($input['virtual_sessions'] ?? [], $format);
|
||||
|
||||
// Contributions always take payment; ticketing only for paid tiers; free never.
|
||||
$acceptsPayment = match ($mode) {
|
||||
'contributions' => true,
|
||||
@@ -289,11 +295,63 @@ class QrPayloadValidator
|
||||
'accepts_payment' => $acceptsPayment,
|
||||
'registration_open' => filter_var($input['registration_open'] ?? true, FILTER_VALIDATE_BOOL),
|
||||
'programme_qr_id' => ($pid = (int) ($input['programme_qr_id'] ?? 0)) > 0 ? $pid : null,
|
||||
'format' => $format,
|
||||
'virtual_sessions' => $virtualSessions,
|
||||
],
|
||||
'destination_url' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/** @param mixed $sessions */
|
||||
private function normalizeVirtualSessions(mixed $sessions, string $format): array
|
||||
{
|
||||
if ($format === 'in_person') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! is_array($sessions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($sessions as $session) {
|
||||
if (! is_array($session)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = trim((string) ($session['title'] ?? ''));
|
||||
if ($title === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = trim((string) ($session['id'] ?? ''));
|
||||
$roomType = in_array($session['meet_room_type'] ?? '', ['town_hall', 'webinar'], true)
|
||||
? (string) $session['meet_room_type']
|
||||
: 'town_hall';
|
||||
|
||||
$itemRefs = [];
|
||||
foreach ((array) ($session['programme_item_refs'] ?? []) as $ref) {
|
||||
$ref = trim((string) $ref);
|
||||
if ($ref !== '') {
|
||||
$itemRefs[] = mb_substr($ref, 0, 80);
|
||||
}
|
||||
}
|
||||
|
||||
$normalized[] = [
|
||||
'id' => $id !== '' ? mb_substr($id, 0, 64) : null,
|
||||
'title' => mb_substr($title, 0, 120),
|
||||
'meet_room_type' => $roomType,
|
||||
'scheduled_at' => QrDateFormatter::normalize((string) ($session['scheduled_at'] ?? '')),
|
||||
'duration_minutes' => max(15, min(480, (int) ($session['duration_minutes'] ?? 60))),
|
||||
'meet_room_uuid' => ($uuid = trim((string) ($session['meet_room_uuid'] ?? ''))) !== '' ? $uuid : null,
|
||||
'join_url' => ($url = trim((string) ($session['join_url'] ?? ''))) !== '' ? $url : null,
|
||||
'programme_item_refs' => array_values($itemRefs),
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($normalized);
|
||||
}
|
||||
|
||||
/** @param array<int, array{price: float}> $tiers */
|
||||
private function eventHasPaidTier(array $tiers): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user