Deploy Ladill Events / deploy (push) Successful in 48s
Mirror Care: suite platform keys count as ready for attendee email/SMS; Account Messaging is primary, product Bird/SMS keys are advanced. SmsService now prefers channel=suite before customer API keys.
209 lines
8.0 KiB
PHP
209 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Events;
|
|
|
|
use App\Models\QrCode;
|
|
use App\Models\QrEventRegistration;
|
|
use App\Services\Billing\PlatformEmailClient;
|
|
use App\Services\Billing\PlatformSmsClient;
|
|
use App\Services\Billing\SmsService;
|
|
use App\Services\Messaging\MessagingCredentialsService;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class EventCommsService
|
|
{
|
|
public const MODE_PROGRAMME = 'programme';
|
|
public const MODE_JOIN = 'join';
|
|
public const MODE_BOTH = 'both';
|
|
|
|
private const SMS_SEGMENT_CHARS = 160;
|
|
private const SMS_PRICE_GHS = 0.03;
|
|
private const EMAIL_PRICE_GHS = 0.0135;
|
|
|
|
public function __construct(
|
|
private readonly EventEmailService $email,
|
|
private readonly SmsService $sms,
|
|
private readonly MessagingCredentialsService $credentials,
|
|
private readonly EventProgrammeService $programmes,
|
|
private readonly PlatformEmailClient $platformEmail,
|
|
private readonly PlatformSmsClient $platformSms,
|
|
) {}
|
|
|
|
/** @return array{recipients: int, emails: int, sms: int, estimated_ghs: float, affordable: bool, integrations_error: ?string} */
|
|
public function preview(QrCode $event, string $mode): array
|
|
{
|
|
$regs = $this->confirmedRegistrations($event);
|
|
$emails = $regs->filter(fn ($r) => (bool) $r->attendee_email)->count();
|
|
$phones = $regs->filter(fn ($r) => (bool) $r->attendee_phone)->count();
|
|
$smsSegments = $phones * $this->estimateSmsSegments($event, $mode);
|
|
$estimated = round(($emails * self::EMAIL_PRICE_GHS) + ($smsSegments * self::SMS_PRICE_GHS), 2);
|
|
$ownerRef = (string) $event->user->public_id;
|
|
$integrationsError = $this->integrationsError($event, $mode, $emails, $phones);
|
|
|
|
return [
|
|
'recipients' => $regs->count(),
|
|
'emails' => $emails,
|
|
'sms' => $phones,
|
|
'estimated_ghs' => $estimated,
|
|
'affordable' => $integrationsError === null,
|
|
'integrations_error' => $integrationsError,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{emailed: int, texted: int, skipped: int}
|
|
*/
|
|
public function send(QrCode $event, string $mode): array
|
|
{
|
|
$preview = $this->preview($event, $mode);
|
|
abort_if($preview['integrations_error'] !== null, 422, $preview['integrations_error']);
|
|
|
|
$programme = $this->programmeFor($event);
|
|
$eventName = $event->content()['name'] ?? $event->label;
|
|
$programmeUrl = $programme?->publicUrl();
|
|
$joinUrl = $this->primaryJoinUrl($event);
|
|
$ownerRef = (string) $event->user->public_id;
|
|
$organizerEmail = $event->user->email;
|
|
$organizerName = $event->user->name;
|
|
|
|
$emailed = 0;
|
|
$texted = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($this->confirmedRegistrations($event) as $reg) {
|
|
$sent = false;
|
|
|
|
if ($reg->attendee_email && in_array($mode, [self::MODE_PROGRAMME, self::MODE_BOTH], true) && $programmeUrl) {
|
|
if ($mode === self::MODE_BOTH) {
|
|
$sent = $this->email->sendJoinAndProgramme($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $programmeUrl, $reg->attendee_name, $organizerEmail, $organizerName);
|
|
} else {
|
|
$sent = $this->email->sendProgrammeShare($ownerRef, $reg->attendee_email, $eventName, $programmeUrl, $reg->attendee_name, $organizerEmail, $organizerName);
|
|
}
|
|
if ($sent) {
|
|
$emailed++;
|
|
}
|
|
} elseif ($reg->attendee_email && $mode === self::MODE_JOIN && $joinUrl !== '') {
|
|
$sent = $this->email->sendJoinLink($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $reg->attendee_name, $organizerEmail, $organizerName);
|
|
if ($sent) {
|
|
$emailed++;
|
|
}
|
|
}
|
|
|
|
if ($reg->attendee_phone) {
|
|
$message = $this->smsBody($reg, $eventName, $mode, $programmeUrl, $joinUrl);
|
|
if ($message !== '' && $this->sms->send($ownerRef, $reg->attendee_phone, $message)) {
|
|
$texted++;
|
|
$sent = true;
|
|
}
|
|
}
|
|
|
|
if (! $sent) {
|
|
$skipped++;
|
|
}
|
|
}
|
|
|
|
return compact('emailed', 'texted', 'skipped');
|
|
}
|
|
|
|
/** @return Collection<int, QrEventRegistration> */
|
|
private function confirmedRegistrations(QrCode $event): Collection
|
|
{
|
|
return $event->eventRegistrations()
|
|
->where('status', QrEventRegistration::STATUS_CONFIRMED)
|
|
->get();
|
|
}
|
|
|
|
private function programmeFor(QrCode $event): ?QrCode
|
|
{
|
|
$programmeId = (int) ($event->content()['programme_qr_id'] ?? 0);
|
|
if ($programmeId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return QrCode::where('id', $programmeId)
|
|
->where('user_id', $event->user_id)
|
|
->where('type', QrCode::TYPE_ITINERARY)
|
|
->first();
|
|
}
|
|
|
|
public function primaryJoinUrl(QrCode $event): string
|
|
{
|
|
foreach ((array) ($event->content()['virtual_sessions'] ?? []) as $session) {
|
|
if (! is_array($session)) {
|
|
continue;
|
|
}
|
|
$url = trim((string) ($session['join_url'] ?? ''));
|
|
if ($url !== '') {
|
|
return $url;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function smsBody(QrEventRegistration $reg, string $eventName, string $mode, ?string $programmeUrl, string $joinUrl): string
|
|
{
|
|
$first = explode(' ', $reg->attendee_name ?? 'there')[0];
|
|
|
|
return match ($mode) {
|
|
self::MODE_PROGRAMME => $programmeUrl
|
|
? sprintf('Hi %s, the programme for %s: %s', $first, $eventName, $programmeUrl)
|
|
: '',
|
|
self::MODE_JOIN => $joinUrl !== ''
|
|
? sprintf('Hi %s, join %s here: %s', $first, $eventName, $joinUrl)
|
|
: '',
|
|
self::MODE_BOTH => ($programmeUrl && $joinUrl !== '')
|
|
? sprintf('Hi %s, %s — programme: %s | join: %s', $first, $eventName, $programmeUrl, $joinUrl)
|
|
: ($joinUrl !== '' ? sprintf('Hi %s, join %s: %s', $first, $eventName, $joinUrl) : ''),
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
private function estimateSmsSegments(QrCode $event, string $mode): int
|
|
{
|
|
$sample = $this->smsBody(
|
|
new QrEventRegistration(['attendee_name' => 'Guest']),
|
|
$event->content()['name'] ?? $event->label,
|
|
$mode,
|
|
$this->programmeFor($event)?->publicUrl(),
|
|
$this->primaryJoinUrl($event),
|
|
);
|
|
|
|
return max(1, (int) ceil(strlen($sample) / self::SMS_SEGMENT_CHARS));
|
|
}
|
|
|
|
private function integrationsError(QrCode $event, string $mode, int $emails, int $phones): ?string
|
|
{
|
|
$ownerRef = (string) $event->user->public_id;
|
|
$credential = $this->credentials->forOwner($ownerRef);
|
|
$programmeUrl = $this->programmeFor($event)?->publicUrl();
|
|
$joinUrl = $this->primaryJoinUrl($event);
|
|
|
|
$needsEmail = $emails > 0 && (
|
|
(in_array($mode, [self::MODE_PROGRAMME, self::MODE_BOTH], true) && $programmeUrl)
|
|
|| ($mode === self::MODE_JOIN && $joinUrl !== '')
|
|
);
|
|
|
|
$needsSms = $phones > 0 && $this->smsBody(
|
|
new QrEventRegistration(['attendee_name' => 'Guest']),
|
|
$event->content()['name'] ?? $event->label,
|
|
$mode,
|
|
$programmeUrl,
|
|
$joinUrl,
|
|
) !== '';
|
|
|
|
$emailReady = $this->platformEmail->isConfigured() || $credential->hasValidBird();
|
|
$smsReady = $this->platformSms->isConfigured() || $credential->hasValidSms();
|
|
|
|
if ($needsEmail && ! $emailReady) {
|
|
return 'Email is not available yet. Bind a mailbox under Account → Messaging once platform keys are configured, or connect Bird under Advanced keys.';
|
|
}
|
|
|
|
if ($needsSms && ! $smsReady) {
|
|
return 'SMS is not available yet. Ensure platform SMS is configured on this Events instance, or connect a product SMS key under Advanced keys.';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|