Add account company logo for customer event emails.
Deploy Ladill Events / deploy (push) Successful in 1m39s
Deploy Ladill Events / deploy (push) Successful in 1m39s
Upload logo in account settings and prefer it over Ladill product marks in event notification headers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Qr;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\QrSetting;
|
use App\Models\QrSetting;
|
||||||
use App\Services\Billing\BillingClient;
|
use App\Services\Billing\BillingClient;
|
||||||
|
use App\Support\AccountBranding;
|
||||||
use App\Support\Qr\QrCornerStyleCatalog;
|
use App\Support\Qr\QrCornerStyleCatalog;
|
||||||
use App\Support\Qr\QrFrameStyleCatalog;
|
use App\Support\Qr\QrFrameStyleCatalog;
|
||||||
use App\Support\Qr\QrModuleStyleCatalog;
|
use App\Support\Qr\QrModuleStyleCatalog;
|
||||||
@@ -112,11 +113,25 @@ class AccountController extends Controller
|
|||||||
'default_style.gradient_color1' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
'default_style.gradient_color1' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||||
'default_style.gradient_color2' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
'default_style.gradient_color2' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||||
'default_style.gradient_rotation' => ['nullable', 'integer', 'min:0', 'max:360'],
|
'default_style.gradient_rotation' => ['nullable', 'integer', 'min:0', 'max:360'],
|
||||||
|
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
|
||||||
|
'remove_logo' => ['nullable', 'boolean'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$eventDefaults = $data['event_defaults'] ?? [];
|
$eventDefaults = $data['event_defaults'] ?? [];
|
||||||
$eventDefaults['registration_open'] = $request->boolean('event_defaults.registration_open');
|
$eventDefaults['registration_open'] = $request->boolean('event_defaults.registration_open');
|
||||||
|
|
||||||
|
$settings = $account->getOrCreateQrSetting();
|
||||||
|
$logoPath = $settings->logo_path;
|
||||||
|
|
||||||
|
if ($request->boolean('remove_logo')) {
|
||||||
|
AccountBranding::deleteStoredLogo($settings);
|
||||||
|
$logoPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('logo')) {
|
||||||
|
$logoPath = AccountBranding::storeLogo($settings, $request->file('logo'));
|
||||||
|
}
|
||||||
|
|
||||||
QrSetting::updateOrCreate(
|
QrSetting::updateOrCreate(
|
||||||
['user_id' => $account->id],
|
['user_id' => $account->id],
|
||||||
[
|
[
|
||||||
@@ -127,6 +142,7 @@ class AccountController extends Controller
|
|||||||
'notify_payouts' => $request->boolean('notify_payouts'),
|
'notify_payouts' => $request->boolean('notify_payouts'),
|
||||||
'event_defaults' => QrSetting::sanitizeEventDefaults($eventDefaults),
|
'event_defaults' => QrSetting::sanitizeEventDefaults($eventDefaults),
|
||||||
'default_style' => QrSetting::sanitizeDefaultStyle($data['default_style'] ?? null),
|
'default_style' => QrSetting::sanitizeDefaultStyle($data['default_style'] ?? null),
|
||||||
|
'logo_path' => $logoPath,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class QrSetting extends Model
|
|||||||
'default_type',
|
'default_type',
|
||||||
'default_style',
|
'default_style',
|
||||||
'event_defaults',
|
'event_defaults',
|
||||||
|
'logo_path',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Events;
|
namespace App\Services\Events;
|
||||||
|
|
||||||
|
use App\Support\AccountBranding;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
|
|
||||||
class EventEmailService
|
class EventEmailService
|
||||||
@@ -129,6 +130,10 @@ class EventEmailService
|
|||||||
?string $replyToEmail = null,
|
?string $replyToEmail = null,
|
||||||
?string $replyToName = null,
|
?string $replyToName = null,
|
||||||
): bool {
|
): bool {
|
||||||
|
$brandingSettings = AccountBranding::forOwnerRef($ownerPublicId);
|
||||||
|
$viewData['logoUrl'] = AccountBranding::emailLogoUrl($brandingSettings);
|
||||||
|
$viewData['companyName'] = AccountBranding::companyName($brandingSettings);
|
||||||
|
|
||||||
$html = View::make($view, $viewData)->render();
|
$html = View::make($view, $viewData)->render();
|
||||||
$text = strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html));
|
$text = strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\QrSetting;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class AccountBranding
|
||||||
|
{
|
||||||
|
public static function forUser(User $user): QrSetting
|
||||||
|
{
|
||||||
|
return $user->getOrCreateQrSetting();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function forOwnerRef(string $ownerPublicId): ?QrSetting
|
||||||
|
{
|
||||||
|
$user = User::query()->where('public_id', $ownerPublicId)->first();
|
||||||
|
|
||||||
|
return $user ? $user->getOrCreateQrSetting() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function companyName(?QrSetting $settings, ?User $user = null): string
|
||||||
|
{
|
||||||
|
if ($user) {
|
||||||
|
return (string) ($user->name ?: 'Company');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($settings?->user) {
|
||||||
|
return (string) ($settings->user->name ?: 'Company');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($settings?->user_id) {
|
||||||
|
$owner = User::query()->find($settings->user_id);
|
||||||
|
|
||||||
|
return (string) ($owner?->name ?: 'Company');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Company';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hasCustomLogo(?QrSetting $settings): bool
|
||||||
|
{
|
||||||
|
return $settings !== null
|
||||||
|
&& $settings->logo_path !== null
|
||||||
|
&& Storage::disk('public')->exists($settings->logo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function emailLogoUrl(?QrSetting $settings): ?string
|
||||||
|
{
|
||||||
|
if (! self::hasCustomLogo($settings)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo((string) $settings->logo_path, PATHINFO_EXTENSION));
|
||||||
|
if ($ext === 'svg') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = $settings->updated_at?->getTimestamp() ?? time();
|
||||||
|
|
||||||
|
return Storage::disk('public')->url($settings->logo_path).'?v='.$version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function logoUrl(?QrSetting $settings): ?string
|
||||||
|
{
|
||||||
|
if (! self::hasCustomLogo($settings)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = $settings->updated_at?->getTimestamp() ?? time();
|
||||||
|
|
||||||
|
return Storage::disk('public')->url($settings->logo_path).'?v='.$version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function storeLogo(QrSetting $settings, UploadedFile $file): string
|
||||||
|
{
|
||||||
|
self::deleteStoredLogo($settings);
|
||||||
|
|
||||||
|
return $file->store('events/accounts/'.$settings->user_id, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteStoredLogo(QrSetting $settings): void
|
||||||
|
{
|
||||||
|
if ($settings->logo_path) {
|
||||||
|
Storage::disk('public')->delete($settings->logo_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('qr_settings', function (Blueprint $table) {
|
||||||
|
$table->string('logo_path')->nullable()->after('event_defaults');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('qr_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('logo_path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;background:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#0f172a;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;padding:48px 16px;">
|
||||||
|
<div style="text-align:center;padding:0 0 24px;">
|
||||||
|
@if (! empty($logoUrl))
|
||||||
|
<img src="{{ $logoUrl }}" alt="{{ $companyName }}" style="height:42px;max-height:42px;width:auto;">
|
||||||
|
@else
|
||||||
|
<div style="font-size:22px;font-weight:700;letter-spacing:-0.02em;color:#0f172a;">{{ $companyName }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div style="background:#ffffff;border-radius:16px;padding:40px;border:1px solid #e2e8f0;box-shadow:0 4px 6px -1px rgba(0,0,0,0.1);">
|
||||||
|
{!! $bodyHtml !!}
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:20px;text-align:center;font-size:13px;color:#94a3b8;">{{ $companyName }}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
@extends('mail.notifications.layout')
|
@extends('mail.notifications.layout')
|
||||||
|
|
||||||
@section('email-header')
|
@section('email-header')
|
||||||
@include('mail.partials.brand-header', ['brand' => 'events'])
|
@include('mail.partials.brand-header', ['brand' => 'events', 'logoUrl' => $logoUrl ?? null, 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('email-footer')
|
@section('email-footer')
|
||||||
@include('mail.partials.brand-footer', ['brand' => 'events'])
|
@include('mail.partials.brand-footer', ['brand' => 'events', 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@extends('mail.notifications.layout')
|
@extends('mail.notifications.layout')
|
||||||
|
|
||||||
@section('email-header')
|
@section('email-header')
|
||||||
@include('mail.partials.brand-header', ['brand' => 'events'])
|
@include('mail.partials.brand-header', ['brand' => 'events', 'logoUrl' => $logoUrl ?? null, 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('email-footer')
|
@section('email-footer')
|
||||||
@include('mail.partials.brand-footer', ['brand' => 'events'])
|
@include('mail.partials.brand-footer', ['brand' => 'events', 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@extends('mail.notifications.layout')
|
@extends('mail.notifications.layout')
|
||||||
|
|
||||||
@section('email-header')
|
@section('email-header')
|
||||||
@include('mail.partials.brand-header', ['brand' => 'events'])
|
@include('mail.partials.brand-header', ['brand' => 'events', 'logoUrl' => $logoUrl ?? null, 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('email-footer')
|
@section('email-footer')
|
||||||
@include('mail.partials.brand-footer', ['brand' => 'events'])
|
@include('mail.partials.brand-footer', ['brand' => 'events', 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@extends('mail.notifications.layout')
|
@extends('mail.notifications.layout')
|
||||||
|
|
||||||
@section('email-header')
|
@section('email-header')
|
||||||
@include('mail.partials.brand-header', ['brand' => 'events'])
|
@include('mail.partials.brand-header', ['brand' => 'events', 'logoUrl' => $logoUrl ?? null, 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('email-footer')
|
@section('email-footer')
|
||||||
@include('mail.partials.brand-footer', ['brand' => 'events'])
|
@include('mail.partials.brand-footer', ['brand' => 'events', 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@extends('mail.notifications.layout')
|
@extends('mail.notifications.layout')
|
||||||
|
|
||||||
@section('email-header')
|
@section('email-header')
|
||||||
@include('mail.partials.brand-header', ['brand' => 'events'])
|
@include('mail.partials.brand-header', ['brand' => 'events', 'logoUrl' => $logoUrl ?? null, 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('email-footer')
|
@section('email-footer')
|
||||||
@include('mail.partials.brand-footer', ['brand' => 'events'])
|
@include('mail.partials.brand-footer', ['brand' => 'events', 'companyName' => $companyName ?? null])
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
@php
|
@if (! empty($companyName))
|
||||||
|
<p class="email-footer-text" style="margin-top: 20px; color: #64748b;">{{ $companyName }}</p>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
$brandKey = $brand ?? 'ladill';
|
$brandKey = $brand ?? 'ladill';
|
||||||
$brandConfig = config("mail_brands.brands.{$brandKey}", config('mail_brands.brands.ladill'));
|
$brandConfig = config("mail_brands.brands.{$brandKey}", config('mail_brands.brands.ladill'));
|
||||||
$appBase = rtrim((string) ($brandConfig['app_url'] ?? config('app.url')), '/');
|
$appBase = rtrim((string) ($brandConfig['app_url'] ?? config('app.url')), '/');
|
||||||
@@ -6,15 +9,16 @@
|
|||||||
$accountBase = rtrim((string) ($brandConfig['account_url'] ?? config('mail_brands.account_url', 'https://account.ladill.com')), '/');
|
$accountBase = rtrim((string) ($brandConfig['account_url'] ?? config('mail_brands.account_url', 'https://account.ladill.com')), '/');
|
||||||
$supportUrl = $brandConfig['support_url'] ?? $accountBase.'/support-tickets';
|
$supportUrl = $brandConfig['support_url'] ?? $accountBase.'/support-tickets';
|
||||||
$homeLabel = $brandConfig['home_label'] ?? parse_url($appBase, PHP_URL_HOST) ?: 'ladill.com';
|
$homeLabel = $brandConfig['home_label'] ?? parse_url($appBase, PHP_URL_HOST) ?: 'ladill.com';
|
||||||
@endphp
|
@endphp
|
||||||
<p class="email-footer-text">
|
<p class="email-footer-text">
|
||||||
You're receiving this email because you have {{ $brandConfig['footer_account'] ?? 'an account with Ladill' }}.
|
You're receiving this email because you have {{ $brandConfig['footer_account'] ?? 'an account with Ladill' }}.
|
||||||
</p>
|
</p>
|
||||||
<div class="email-footer-links">
|
<div class="email-footer-links">
|
||||||
<a href="{{ $appBase }}{{ $dashboardPath }}">Dashboard</a>
|
<a href="{{ $appBase }}{{ $dashboardPath }}">Dashboard</a>
|
||||||
<a href="{{ $supportUrl }}">Support</a>
|
<a href="{{ $supportUrl }}">Support</a>
|
||||||
<a href="{{ $appBase }}">{{ $homeLabel }}</a>
|
<a href="{{ $appBase }}">{{ $homeLabel }}</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="email-footer-text" style="margin-top: 20px; color: #64748b;">
|
<p class="email-footer-text" style="margin-top: 20px; color: #64748b;">
|
||||||
© {{ date('Y') }} Ladill Technologies. All rights reserved.
|
© {{ date('Y') }} Ladill Technologies. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
|
@endif
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
@php
|
@if (! empty($logoUrl))
|
||||||
|
<img src="{{ $logoUrl }}" alt="{{ $companyName ?? '' }}" style="height: 40px; max-height: 42px; width: auto;">
|
||||||
|
@elseif (! empty($companyName))
|
||||||
|
<div style="font-size: 22px; font-weight: 700; letter-spacing: -0.02em; color: #0f172a;">{{ $companyName }}</div>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
$brandKey = $brand ?? 'ladill';
|
$brandKey = $brand ?? 'ladill';
|
||||||
$brandConfig = config("mail_brands.brands.{$brandKey}", config('mail_brands.brands.ladill'));
|
$brandConfig = config("mail_brands.brands.{$brandKey}", config('mail_brands.brands.ladill'));
|
||||||
@endphp
|
@endphp
|
||||||
<img src="{{ asset('images/logo/'.$brandConfig['logo']) }}" alt="{{ $brandConfig['name'] }}" style="height: 38px; max-height: 38px; width: auto;">
|
<img src="{{ asset('images/logo/'.$brandConfig['logo']) }}" alt="{{ $brandConfig['name'] }}" style="height: 38px; max-height: 38px; width: auto;">
|
||||||
|
@endif
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
|
<div class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form method="POST" action="{{ route('account.settings.update') }}" class="mt-6 space-y-4">
|
<form method="POST" action="{{ route('account.settings.update') }}" enctype="multipart/form-data" class="mt-6 space-y-4">
|
||||||
@csrf
|
@csrf
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
|
||||||
@@ -71,6 +71,28 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Company branding</h2>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">Your logo appears in customer emails (registration confirmations, join links, and programme shares).</p>
|
||||||
|
|
||||||
|
@if (\App\Support\AccountBranding::hasCustomLogo($settings))
|
||||||
|
<div class="mt-4">
|
||||||
|
<img src="{{ \App\Support\AccountBranding::logoUrl($settings) }}" alt="Company logo" class="h-12 w-auto max-w-[200px] rounded-lg border border-slate-100 bg-slate-50 object-contain p-2">
|
||||||
|
</div>
|
||||||
|
<label class="mt-3 flex items-center gap-2 text-sm text-slate-600">
|
||||||
|
<input type="checkbox" name="remove_logo" value="1" class="rounded border-slate-300 text-rose-600 focus:ring-rose-500">
|
||||||
|
Remove current logo
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="logo" class="block text-[11px] font-medium text-slate-500">{{ \App\Support\AccountBranding::hasCustomLogo($settings) ? 'Replace logo' : 'Upload logo' }}</label>
|
||||||
|
<input type="file" id="logo" name="logo" accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||||
|
class="mt-1 block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-3 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100">
|
||||||
|
@error('logo') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
<h2 class="text-sm font-semibold text-slate-900">New event defaults</h2>
|
<h2 class="text-sm font-semibold text-slate-900">New event defaults</h2>
|
||||||
<p class="mt-1 text-xs text-slate-500">Pre-fill the create flow so every new event starts with your usual setup.</p>
|
<p class="mt-1 text-xs text-slate-500">Pre-fill the create flow so every new event starts with your usual setup.</p>
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Http\Middleware\EnsurePlatformSession;
|
||||||
|
use App\Models\MessagingCredential;
|
||||||
|
use App\Models\QrSetting;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Events\EventEmailService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class EventEmailBrandingTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
Storage::fake('public');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_upload_logo_on_settings_update(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => (string) Str::uuid(),
|
||||||
|
'name' => 'Branded Events Co',
|
||||||
|
'email' => 'brand+'.uniqid().'@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->put(route('account.settings.update'), [
|
||||||
|
'notify_registrations' => '1',
|
||||||
|
'notify_payouts' => '1',
|
||||||
|
'product_updates' => '1',
|
||||||
|
'low_balance_alerts' => '1',
|
||||||
|
'event_defaults' => [
|
||||||
|
'registration_open' => '1',
|
||||||
|
],
|
||||||
|
'logo' => UploadedFile::fake()->image('company-logo.png', 200, 80),
|
||||||
|
])
|
||||||
|
->assertRedirect(route('account.settings'));
|
||||||
|
|
||||||
|
$settings = QrSetting::where('user_id', $user->id)->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($settings);
|
||||||
|
$this->assertNotNull($settings->logo_path);
|
||||||
|
Storage::disk('public')->assertExists($settings->logo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_registration_email_contains_logo_url_when_set(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'usr-brand-logo-001',
|
||||||
|
'name' => 'Branded Events Co',
|
||||||
|
'email' => 'brand-logo@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$logoPath = 'events/accounts/'.$user->id.'/company-logo.png';
|
||||||
|
Storage::disk('public')->put($logoPath, 'fake-logo');
|
||||||
|
|
||||||
|
QrSetting::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'logo_path' => $logoPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->seedBirdCredentials($user);
|
||||||
|
|
||||||
|
$capturedHtml = null;
|
||||||
|
Http::fake([
|
||||||
|
'*/api/smtp/send' => function ($request) use (&$capturedHtml) {
|
||||||
|
$capturedHtml = $request['html'];
|
||||||
|
|
||||||
|
return Http::response(['success' => true, 'message_id' => 'x']);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sent = app(EventEmailService::class)->sendRegistrationConfirmation(
|
||||||
|
$user->public_id,
|
||||||
|
'ada@example.com',
|
||||||
|
'Demo event',
|
||||||
|
'BADGE123',
|
||||||
|
'https://meet.test/join',
|
||||||
|
'Ada Lovelace',
|
||||||
|
$user->email,
|
||||||
|
$user->name,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($sent);
|
||||||
|
$this->assertNotNull($capturedHtml);
|
||||||
|
$this->assertStringContainsString('/storage/'.$logoPath, $capturedHtml);
|
||||||
|
$this->assertStringNotContainsString('ladillevents-logo-email.png', $capturedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_registration_email_uses_company_name_without_logo(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'usr-brand-text-001',
|
||||||
|
'name' => 'CAPBuSS Events',
|
||||||
|
'email' => 'brand-text@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->seedBirdCredentials($user);
|
||||||
|
|
||||||
|
$capturedHtml = null;
|
||||||
|
Http::fake([
|
||||||
|
'*/api/smtp/send' => function ($request) use (&$capturedHtml) {
|
||||||
|
$capturedHtml = $request['html'];
|
||||||
|
|
||||||
|
return Http::response(['success' => true, 'message_id' => 'x']);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sent = app(EventEmailService::class)->sendRegistrationConfirmation(
|
||||||
|
$user->public_id,
|
||||||
|
'ada@example.com',
|
||||||
|
'Demo event',
|
||||||
|
'BADGE123',
|
||||||
|
null,
|
||||||
|
'Ada Lovelace',
|
||||||
|
$user->email,
|
||||||
|
$user->name,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($sent);
|
||||||
|
$this->assertNotNull($capturedHtml);
|
||||||
|
$this->assertStringContainsString('CAPBuSS Events', $capturedHtml);
|
||||||
|
$this->assertStringNotContainsString('ladillevents-logo-email.png', $capturedHtml);
|
||||||
|
$this->assertStringNotContainsString('Ladill Technologies', $capturedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedBirdCredentials(User $user): void
|
||||||
|
{
|
||||||
|
$plain = 'lsk_live_'.str_repeat('f', 32);
|
||||||
|
|
||||||
|
MessagingCredential::create([
|
||||||
|
'owner_ref' => $user->public_id,
|
||||||
|
'bird_api_key_encrypted' => Crypt::encryptString($plain),
|
||||||
|
'bird_api_key_prefix' => substr($plain, 0, 16),
|
||||||
|
'bird_from_email' => 'events@example.test',
|
||||||
|
'bird_from_name' => 'Events',
|
||||||
|
'bird_status' => MessagingCredential::STATUS_VALID,
|
||||||
|
'bird_validated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user