Send Events mail from platform SMTP like Invoice, not owner Bird domain.
Deploy Ladill Events / deploy (push) Successful in 31s
Deploy Ladill Events / deploy (push) Successful in 31s
Use EventMailer with MAIL_FROM and organiser Reply-To instead of the Bird API, which rewrote the From address onto the account's verified domain (e.g. climp.me). Redesign the speaker invitation to use the shared Events email layout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,6 +50,16 @@ SMS_PLATFORM_API_URL=https://ladill.com/api
|
|||||||
SMS_API_KEY_EVENTS=
|
SMS_API_KEY_EVENTS=
|
||||||
SMS_DEFAULT_SENDER_ID=Ladill
|
SMS_DEFAULT_SENDER_ID=Ladill
|
||||||
|
|
||||||
|
# Transactional email (platform mailer — same pattern as Invoice)
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_FROM_ADDRESS=events@ladill.com
|
||||||
|
MAIL_FROM_NAME="Ladill Events"
|
||||||
|
|
||||||
|
# Legacy Bird SMTP API (unused for attendee/speaker mail; kept for future Bird integration)
|
||||||
SMTP_PLATFORM_API_URL=https://ladill.com/api/smtp
|
SMTP_PLATFORM_API_URL=https://ladill.com/api/smtp
|
||||||
SMTP_API_KEY_EVENTS=
|
SMTP_API_KEY_EVENTS=
|
||||||
EVENTS_SMTP_FROM=events@ladill.com
|
EVENTS_SMTP_FROM=events@ladill.com
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class EventCommsService
|
|||||||
$programmeUrl = $programme?->publicUrl();
|
$programmeUrl = $programme?->publicUrl();
|
||||||
$joinUrl = $this->primaryJoinUrl($event);
|
$joinUrl = $this->primaryJoinUrl($event);
|
||||||
$ownerRef = (string) $event->user->public_id;
|
$ownerRef = (string) $event->user->public_id;
|
||||||
|
$organizerEmail = $event->user->email;
|
||||||
|
$organizerName = $event->user->name;
|
||||||
|
|
||||||
$emailed = 0;
|
$emailed = 0;
|
||||||
$texted = 0;
|
$texted = 0;
|
||||||
@@ -68,15 +70,15 @@ class EventCommsService
|
|||||||
|
|
||||||
if ($reg->attendee_email && in_array($mode, [self::MODE_PROGRAMME, self::MODE_BOTH], true) && $programmeUrl) {
|
if ($reg->attendee_email && in_array($mode, [self::MODE_PROGRAMME, self::MODE_BOTH], true) && $programmeUrl) {
|
||||||
if ($mode === self::MODE_BOTH) {
|
if ($mode === self::MODE_BOTH) {
|
||||||
$sent = $this->email->sendJoinAndProgramme($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $programmeUrl, $reg->attendee_name);
|
$sent = $this->email->sendJoinAndProgramme($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $programmeUrl, $reg->attendee_name, $organizerEmail, $organizerName);
|
||||||
} else {
|
} else {
|
||||||
$sent = $this->email->sendProgrammeShare($ownerRef, $reg->attendee_email, $eventName, $programmeUrl, $reg->attendee_name);
|
$sent = $this->email->sendProgrammeShare($ownerRef, $reg->attendee_email, $eventName, $programmeUrl, $reg->attendee_name, $organizerEmail, $organizerName);
|
||||||
}
|
}
|
||||||
if ($sent) {
|
if ($sent) {
|
||||||
$emailed++;
|
$emailed++;
|
||||||
}
|
}
|
||||||
} elseif ($reg->attendee_email && $mode === self::MODE_JOIN && $joinUrl !== '') {
|
} elseif ($reg->attendee_email && $mode === self::MODE_JOIN && $joinUrl !== '') {
|
||||||
$sent = $this->email->sendJoinLink($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $reg->attendee_name);
|
$sent = $this->email->sendJoinLink($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $reg->attendee_name, $organizerEmail, $organizerName);
|
||||||
if ($sent) {
|
if ($sent) {
|
||||||
$emailed++;
|
$emailed++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Services\Events;
|
namespace App\Services\Events;
|
||||||
|
|
||||||
use App\Services\Billing\PlatformEmailClient;
|
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
|
|
||||||
class EventEmailService
|
class EventEmailService
|
||||||
{
|
{
|
||||||
public function __construct(private readonly PlatformEmailClient $platform) {}
|
public function __construct(private readonly EventMailer $mailer) {}
|
||||||
|
|
||||||
public function lastError(): ?string
|
public function lastError(): ?string
|
||||||
{
|
{
|
||||||
return $this->platform->lastError();
|
return $this->mailer->lastError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendProgrammeShare(
|
public function sendProgrammeShare(
|
||||||
@@ -20,6 +19,8 @@ class EventEmailService
|
|||||||
string $eventName,
|
string $eventName,
|
||||||
string $programmeUrl,
|
string $programmeUrl,
|
||||||
?string $attendeeName = null,
|
?string $attendeeName = null,
|
||||||
|
?string $organizerEmail = null,
|
||||||
|
?string $organizerName = null,
|
||||||
): bool {
|
): bool {
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$ownerPublicId,
|
$ownerPublicId,
|
||||||
@@ -27,6 +28,9 @@ class EventEmailService
|
|||||||
'Programme for '.$eventName,
|
'Programme for '.$eventName,
|
||||||
'mail.notifications.event-programme',
|
'mail.notifications.event-programme',
|
||||||
compact('eventName', 'programmeUrl', 'attendeeName'),
|
compact('eventName', 'programmeUrl', 'attendeeName'),
|
||||||
|
$eventName,
|
||||||
|
$organizerEmail,
|
||||||
|
$organizerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +40,8 @@ class EventEmailService
|
|||||||
string $eventName,
|
string $eventName,
|
||||||
string $joinUrl,
|
string $joinUrl,
|
||||||
?string $attendeeName = null,
|
?string $attendeeName = null,
|
||||||
|
?string $organizerEmail = null,
|
||||||
|
?string $organizerName = null,
|
||||||
): bool {
|
): bool {
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$ownerPublicId,
|
$ownerPublicId,
|
||||||
@@ -43,6 +49,9 @@ class EventEmailService
|
|||||||
'Join '.$eventName,
|
'Join '.$eventName,
|
||||||
'mail.notifications.event-join',
|
'mail.notifications.event-join',
|
||||||
compact('eventName', 'joinUrl', 'attendeeName'),
|
compact('eventName', 'joinUrl', 'attendeeName'),
|
||||||
|
$eventName,
|
||||||
|
$organizerEmail,
|
||||||
|
$organizerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +62,8 @@ class EventEmailService
|
|||||||
string $joinUrl,
|
string $joinUrl,
|
||||||
string $programmeUrl,
|
string $programmeUrl,
|
||||||
?string $attendeeName = null,
|
?string $attendeeName = null,
|
||||||
|
?string $organizerEmail = null,
|
||||||
|
?string $organizerName = null,
|
||||||
): bool {
|
): bool {
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$ownerPublicId,
|
$ownerPublicId,
|
||||||
@@ -60,6 +71,9 @@ class EventEmailService
|
|||||||
$eventName.' — programme & join link',
|
$eventName.' — programme & join link',
|
||||||
'mail.notifications.event-join-programme',
|
'mail.notifications.event-join-programme',
|
||||||
compact('eventName', 'joinUrl', 'programmeUrl', 'attendeeName'),
|
compact('eventName', 'joinUrl', 'programmeUrl', 'attendeeName'),
|
||||||
|
$eventName,
|
||||||
|
$organizerEmail,
|
||||||
|
$organizerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +84,8 @@ class EventEmailService
|
|||||||
string $badgeCode,
|
string $badgeCode,
|
||||||
?string $joinUrl = null,
|
?string $joinUrl = null,
|
||||||
?string $attendeeName = null,
|
?string $attendeeName = null,
|
||||||
|
?string $organizerEmail = null,
|
||||||
|
?string $organizerName = null,
|
||||||
): bool {
|
): bool {
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$ownerPublicId,
|
$ownerPublicId,
|
||||||
@@ -77,6 +93,9 @@ class EventEmailService
|
|||||||
'Registration confirmed — '.$eventName,
|
'Registration confirmed — '.$eventName,
|
||||||
'mail.notifications.event-registration-confirmed',
|
'mail.notifications.event-registration-confirmed',
|
||||||
compact('eventName', 'badgeCode', 'joinUrl', 'attendeeName'),
|
compact('eventName', 'badgeCode', 'joinUrl', 'attendeeName'),
|
||||||
|
$eventName,
|
||||||
|
$organizerEmail,
|
||||||
|
$organizerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +105,8 @@ class EventEmailService
|
|||||||
string $eventName,
|
string $eventName,
|
||||||
string $portalUrl,
|
string $portalUrl,
|
||||||
?string $speakerName = null,
|
?string $speakerName = null,
|
||||||
|
?string $organizerEmail = null,
|
||||||
|
?string $organizerName = null,
|
||||||
): bool {
|
): bool {
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$ownerPublicId,
|
$ownerPublicId,
|
||||||
@@ -97,20 +118,35 @@ class EventEmailService
|
|||||||
'portalUrl' => $portalUrl,
|
'portalUrl' => $portalUrl,
|
||||||
'speakerName' => $speakerName,
|
'speakerName' => $speakerName,
|
||||||
],
|
],
|
||||||
|
$eventName,
|
||||||
|
$organizerEmail,
|
||||||
|
$organizerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $viewData */
|
/** @param array<string, mixed> $viewData */
|
||||||
private function send(string $ownerPublicId, string $to, string $subject, string $view, array $viewData): bool
|
private function send(
|
||||||
{
|
string $ownerPublicId,
|
||||||
|
string $to,
|
||||||
|
string $subject,
|
||||||
|
string $view,
|
||||||
|
array $viewData,
|
||||||
|
?string $fromDisplayName = null,
|
||||||
|
?string $replyToEmail = null,
|
||||||
|
?string $replyToName = null,
|
||||||
|
): bool {
|
||||||
$html = View::make($view, $viewData)->render();
|
$html = View::make($view, $viewData)->render();
|
||||||
|
$text = strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html));
|
||||||
|
|
||||||
return $this->platform->send(
|
return $this->mailer->send(
|
||||||
$ownerPublicId,
|
$ownerPublicId,
|
||||||
$to,
|
$to,
|
||||||
$subject,
|
$subject,
|
||||||
$html,
|
$html,
|
||||||
strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html)),
|
$text,
|
||||||
|
$fromDisplayName,
|
||||||
|
$replyToEmail,
|
||||||
|
$replyToName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Events;
|
||||||
|
|
||||||
|
use App\Services\Billing\BillingClient;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends Ladill Events transactional email from the platform mailer (like Invoice),
|
||||||
|
* not the event owner's Bird SMTP domain. Reply-To is set to the organiser.
|
||||||
|
*/
|
||||||
|
class EventMailer
|
||||||
|
{
|
||||||
|
public const EMAIL_PRICE_MINOR = 1;
|
||||||
|
|
||||||
|
private ?string $lastError = null;
|
||||||
|
|
||||||
|
public function __construct(private readonly BillingClient $billing) {}
|
||||||
|
|
||||||
|
public function lastError(): ?string
|
||||||
|
{
|
||||||
|
return $this->lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConfigured(): bool
|
||||||
|
{
|
||||||
|
return filter_var((string) config('mail.from.address'), FILTER_VALIDATE_EMAIL) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(
|
||||||
|
string $ownerPublicId,
|
||||||
|
string $to,
|
||||||
|
string $subject,
|
||||||
|
string $html,
|
||||||
|
?string $text = null,
|
||||||
|
?string $fromDisplayName = null,
|
||||||
|
?string $replyToEmail = null,
|
||||||
|
?string $replyToName = null,
|
||||||
|
): bool {
|
||||||
|
$this->lastError = null;
|
||||||
|
|
||||||
|
if (! $this->isConfigured()) {
|
||||||
|
$this->lastError = 'Outbound email is not configured on this Events instance.';
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->billing->canAfford($ownerPublicId, self::EMAIL_PRICE_MINOR)) {
|
||||||
|
$this->lastError = 'Insufficient Ladill wallet balance. Add funds in Billing and try again.';
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reference = 'events_email:'.Str::uuid();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::html($html, function ($message) use ($to, $subject, $text, $fromDisplayName, $replyToEmail, $replyToName): void {
|
||||||
|
$message->to($to)
|
||||||
|
->subject($subject)
|
||||||
|
->from(
|
||||||
|
(string) config('mail.from.address'),
|
||||||
|
$fromDisplayName ?: (string) config('mail.from.name'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($replyToEmail) {
|
||||||
|
$message->replyTo($replyToEmail, $replyToName ?: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($text) {
|
||||||
|
$message->text($text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('Events mail send failed', ['error' => $e->getMessage(), 'to' => $to]);
|
||||||
|
$this->lastError = 'The email could not be sent. Please try again.';
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->billing->debit(
|
||||||
|
$ownerPublicId,
|
||||||
|
self::EMAIL_PRICE_MINOR,
|
||||||
|
'smtp',
|
||||||
|
'events_email',
|
||||||
|
$reference,
|
||||||
|
null,
|
||||||
|
'Events email to '.$to,
|
||||||
|
)) {
|
||||||
|
$this->lastError = 'Insufficient Ladill wallet balance. Add funds in Billing and try again.';
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -276,6 +276,8 @@ class EventRegistrationService
|
|||||||
$registration->badge_code,
|
$registration->badge_code,
|
||||||
$joinUrl !== '' ? $joinUrl : null,
|
$joinUrl !== '' ? $joinUrl : null,
|
||||||
$registration->attendee_name,
|
$registration->attendee_name,
|
||||||
|
$registration->qrCode?->user?->email,
|
||||||
|
$registration->qrCode?->user?->name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class EventSpeakerInviteService
|
|||||||
return ['ok' => false, 'error' => 'Could not resolve the event owner account for sending email.'];
|
return ['ok' => false, 'error' => 'Could not resolve the event owner account for sending email.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! app(\App\Services\Billing\PlatformEmailClient::class)->isConfigured()) {
|
if (! app(EventMailer::class)->isConfigured()) {
|
||||||
return ['ok' => false, 'error' => 'Outbound email is not configured on this Events instance.'];
|
return ['ok' => false, 'error' => 'Outbound email is not configured on this Events instance.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +244,8 @@ class EventSpeakerInviteService
|
|||||||
$eventName,
|
$eventName,
|
||||||
$portalUrl,
|
$portalUrl,
|
||||||
$speaker['name'] ?? null,
|
$speaker['name'] ?? null,
|
||||||
|
$owner->email,
|
||||||
|
$owner->name,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $sent) {
|
if (! $sent) {
|
||||||
|
|||||||
@@ -1,12 +1,61 @@
|
|||||||
<!DOCTYPE html>
|
@extends('mail.notifications.layout')
|
||||||
<html>
|
|
||||||
<body style="font-family: sans-serif; color: #1e293b; line-height: 1.5;">
|
@section('email-header')
|
||||||
@if($speakerName)
|
@include('mail.partials.brand-header', ['brand' => 'events'])
|
||||||
<p>Hi {{ $speakerName }},</p>
|
@endsection
|
||||||
@endif
|
|
||||||
<p>You have been invited to speak at <strong>{{ $eventName }}</strong>.</p>
|
@section('email-footer')
|
||||||
<p>Your speaker page has the programme, your session times, and links to join the virtual stage when it is time:</p>
|
@include('mail.partials.brand-footer', ['brand' => 'events'])
|
||||||
<p><a href="{{ $portalUrl }}">Open your speaker page</a></p>
|
@endsection
|
||||||
<p style="color: #64748b; font-size: 13px;">No attendee registration is required — use the join links on that page to enter the speaker waiting area.</p>
|
|
||||||
</body>
|
@section('content')
|
||||||
</html>
|
<div class="highlight-box">
|
||||||
|
<p class="highlight-box-text">Speaker invitation</p>
|
||||||
|
<p class="highlight-box-subtext">{{ $eventName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="email-title">
|
||||||
|
You're invited to speak{{ $speakerName ? ', '.explode(' ', $speakerName)[0] : '' }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="email-text">
|
||||||
|
The organiser of <strong>{{ $eventName }}</strong> has added you to the speaker roster.
|
||||||
|
Your speaker page includes the programme, your session times, and links to join the virtual stage.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin:24px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="{{ $portalUrl }}" class="email-button" style="display:inline-block;background:#4f46e5;color:#ffffff;text-decoration:none;padding:12px 28px;border-radius:10px;font-size:14px;font-weight:600;">
|
||||||
|
Open your speaker page
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="email-details">
|
||||||
|
<p class="email-details-title">What to expect</p>
|
||||||
|
<div class="checklist-item">
|
||||||
|
<div class="checklist-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="checklist-content">
|
||||||
|
<p class="checklist-title">No attendee registration</p>
|
||||||
|
<p class="checklist-desc">Speakers skip the public registration flow.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="checklist-item">
|
||||||
|
<div class="checklist-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="checklist-content">
|
||||||
|
<p class="checklist-title">Join from your speaker page</p>
|
||||||
|
<p class="checklist-desc">Use the join links when your session is scheduled to open the speaker waiting area.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="email-text-sm">
|
||||||
|
Or open this link: <a href="{{ $portalUrl }}" style="color:#4f46e5;">{{ $portalUrl }}</a>
|
||||||
|
</p>
|
||||||
|
@endsection
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Models\User;
|
|||||||
use App\Services\Events\EventSpeakerInviteService;
|
use App\Services\Events\EventSpeakerInviteService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class EventSpeakerInviteTest extends TestCase
|
class EventSpeakerInviteTest extends TestCase
|
||||||
@@ -19,14 +20,17 @@ class EventSpeakerInviteTest extends TestCase
|
|||||||
$this->withoutMiddleware(\App\Http\Middleware\EnsurePlatformSession::class);
|
$this->withoutMiddleware(\App\Http\Middleware\EnsurePlatformSession::class);
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'smtp.platform_api_key' => 'test-smtp-key',
|
'mail.from.address' => 'events@ladill.com',
|
||||||
'smtp.platform_api_url' => 'https://ladill.test/api/smtp',
|
'mail.from.name' => 'Ladill Events',
|
||||||
'events.service_api_keys.meet' => 'test-meet-key',
|
'events.service_api_keys.meet' => 'test-meet-key',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
Http::fake([
|
Http::fake([
|
||||||
config('billing.api_url').'/balance*' => Http::response(['balance_minor' => 50000]),
|
config('billing.api_url').'/balance*' => Http::response(['balance_minor' => 50000]),
|
||||||
'https://ladill.test/api/smtp/messages/send' => Http::response(['success' => true]),
|
config('billing.api_url').'/can-afford*' => Http::response(['affordable' => true]),
|
||||||
|
config('billing.api_url').'/debit' => Http::response(['success' => true]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +103,7 @@ class EventSpeakerInviteTest extends TestCase
|
|||||||
->assertRedirect(route('speakers.show', $event))
|
->assertRedirect(route('speakers.show', $event))
|
||||||
->assertSessionHas('success');
|
->assertSessionHas('success');
|
||||||
|
|
||||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/messages/send')
|
Http::assertSent(fn ($request) => str_contains($request->url(), '/debit'));
|
||||||
&& $request['to'] === 'alex@example.com');
|
|
||||||
|
|
||||||
$event->refresh();
|
$event->refresh();
|
||||||
$speaker = $event->content()['speakers'][0];
|
$speaker = $event->content()['speakers'][0];
|
||||||
@@ -163,7 +166,7 @@ class EventSpeakerInviteTest extends TestCase
|
|||||||
|
|
||||||
app(EventSpeakerInviteService::class)->inviteProgrammeHosts($programme, $owner);
|
app(EventSpeakerInviteService::class)->inviteProgrammeHosts($programme, $owner);
|
||||||
|
|
||||||
Http::assertSent(fn ($request) => $request['to'] === 'pat@example.com');
|
Http::assertSent(fn ($request) => str_contains($request->url(), '/debit'));
|
||||||
|
|
||||||
$event->refresh();
|
$event->refresh();
|
||||||
$speaker = $event->content()['speakers'][0];
|
$speaker = $event->content()['speakers'][0];
|
||||||
@@ -229,7 +232,7 @@ class EventSpeakerInviteTest extends TestCase
|
|||||||
|
|
||||||
public function test_manual_speaker_invite_reports_when_email_not_configured(): void
|
public function test_manual_speaker_invite_reports_when_email_not_configured(): void
|
||||||
{
|
{
|
||||||
config(['smtp.platform_api_key' => '']);
|
config(['mail.from.address' => '']);
|
||||||
|
|
||||||
$owner = User::factory()->create(['public_id' => 'usr_sp_no_smtp']);
|
$owner = User::factory()->create(['public_id' => 'usr_sp_no_smtp']);
|
||||||
|
|
||||||
@@ -293,6 +296,6 @@ class EventSpeakerInviteTest extends TestCase
|
|||||||
->assertRedirect(route('speakers.show', $event))
|
->assertRedirect(route('speakers.show', $event))
|
||||||
->assertSessionHas('success');
|
->assertSessionHas('success');
|
||||||
|
|
||||||
Http::assertSent(fn ($request) => $request['to'] === 'pat@example.com');
|
Http::assertSent(fn ($request) => str_contains($request->url(), '/debit'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user