From 207725d4600a4f3c2a7402eddbc280525e08dc95 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Tue, 23 Dec 2025 14:03:42 +0100 Subject: [PATCH] =?UTF-8?q?Converted=20all=20notification=20emails=20to=20?= =?UTF-8?q?the=20branded=20layout=20by=20routing=20them=20through=20a=20sh?= =?UTF-8?q?ared=20Blade=20template=20and=20swapping=20=20=20the=20MailMess?= =?UTF-8?q?age=20builders=20to=20use=20view().=20This=20keeps=20the=20exis?= =?UTF-8?q?ting=20copy/labels=20but=20aligns=20the=20look=20with=20resourc?= =?UTF-8?q?es/views/=20=20=20emails/partials/layout.blade.php.=20I=20also?= =?UTF-8?q?=20switched=20the=20customer=20add=E2=80=91on=20receipt=20notif?= =?UTF-8?q?ication=20to=20reuse=20the=20existing=20=20=20branded=20view=20?= =?UTF-8?q?and=20added=20missing=20translations=20for=20the=20upload=20pip?= =?UTF-8?q?eline=20alert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/Tenant/EventMemberController.php | 4 + app/Models/User.php | 6 + .../Addons/AddonPurchaseReceipt.php | 16 +- app/Notifications/Customer/RefundReceipt.php | 25 ++- .../InactiveTenantDeletionWarning.php | 24 ++- app/Notifications/Ops/AddonPurchased.php | 36 ++-- app/Notifications/Ops/PurchaseCreated.php | 33 ++-- app/Notifications/Ops/RefundProcessed.php | 33 ++-- ...EventPackageGalleryExpiredNotification.php | 40 ++-- ...ventPackageGalleryExpiringNotification.php | 44 +++-- .../EventPackageGuestLimitNotification.php | 47 +++-- ...EventPackageGuestThresholdNotification.php | 48 +++-- .../EventPackagePhotoLimitNotification.php | 47 +++-- ...EventPackagePhotoThresholdNotification.php | 48 +++-- .../TenantPackageEventLimitNotification.php | 38 ++-- ...enantPackageEventThresholdNotification.php | 46 +++-- .../TenantPackageExpiredNotification.php | 38 ++-- .../TenantPackageExpiringNotification.php | 42 ++-- app/Notifications/TenantFeedbackSubmitted.php | 39 ++-- app/Notifications/UploadPipelineFailed.php | 34 ++-- app/Notifications/VerifyEmailNotification.php | 22 +++ resources/lang/de/emails.php | 103 +++++++--- resources/lang/en/emails.php | 102 +++++++--- .../views/emails/abandoned-checkout.blade.php | 85 ++++---- .../views/emails/addons/receipt.blade.php | 62 +++--- .../emails/contact-confirmation.blade.php | 32 ++- resources/views/emails/gift-voucher.blade.php | 75 +++---- .../emails/notifications/basic.blade.php | 40 ++++ .../views/emails/partials/layout.blade.php | 55 ++++++ resources/views/emails/purchase.blade.php | 185 +++++++----------- resources/views/emails/verify-email.blade.php | 29 +++ resources/views/emails/welcome.blade.php | 63 ++++-- .../Feature/BrandedNotificationEmailsTest.php | 71 +++++++ tests/Feature/CustomerEmailRenderTest.php | 119 +++++++++++ tests/Feature/VerifyEmailNotificationTest.php | 44 +++++ 35 files changed, 1247 insertions(+), 528 deletions(-) create mode 100644 app/Notifications/VerifyEmailNotification.php create mode 100644 resources/views/emails/notifications/basic.blade.php create mode 100644 resources/views/emails/partials/layout.blade.php create mode 100644 resources/views/emails/verify-email.blade.php create mode 100644 tests/Feature/BrandedNotificationEmailsTest.php create mode 100644 tests/Feature/CustomerEmailRenderTest.php create mode 100644 tests/Feature/VerifyEmailNotificationTest.php diff --git a/app/Http/Controllers/Api/Tenant/EventMemberController.php b/app/Http/Controllers/Api/Tenant/EventMemberController.php index 1848074..da05f3c 100644 --- a/app/Http/Controllers/Api/Tenant/EventMemberController.php +++ b/app/Http/Controllers/Api/Tenant/EventMemberController.php @@ -82,6 +82,10 @@ class EventMemberController extends Controller $member->refresh(); + if (! $user->hasVerifiedEmail()) { + $user->sendEmailVerificationNotification(); + } + return (new EventMemberResource($member))->response()->setStatusCode(201); } diff --git a/app/Models/User.php b/app/Models/User.php index 30e58b4..0ff026f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Notifications\VerifyEmailNotification; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasName; use Filament\Models\Contracts\HasTenants as FilamentHasTenants; @@ -92,6 +93,11 @@ class User extends Authenticatable implements FilamentHasTenants, FilamentUser, return null; } + public function sendEmailVerificationNotification(): void + { + $this->notify(new VerifyEmailNotification); + } + protected function fullName(): Attribute { return Attribute::make( diff --git a/app/Notifications/Addons/AddonPurchaseReceipt.php b/app/Notifications/Addons/AddonPurchaseReceipt.php index 4ef32fe..e24dfe2 100644 --- a/app/Notifications/Addons/AddonPurchaseReceipt.php +++ b/app/Notifications/Addons/AddonPurchaseReceipt.php @@ -30,18 +30,8 @@ class AddonPurchaseReceipt extends Notification implements ShouldQueue return (new MailMessage) ->subject(__('emails.addons.receipt.subject', ['addon' => $label])) - ->greeting(__('emails.addons.receipt.greeting', ['name' => $tenant?->name ?? __('emails.package_limits.team_fallback')])) - ->line(__('emails.addons.receipt.body', [ - 'addon' => $label, - 'event' => $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'), - 'amount' => $amount ? $amount.' '.$currency : __('emails.addons.receipt.unknown_amount'), - ])) - ->line(__('emails.addons.receipt.summary', [ - 'photos' => $this->addon->extra_photos, - 'guests' => $this->addon->extra_guests, - 'days' => $this->addon->extra_gallery_days, - ])) - ->action(__('emails.addons.receipt.action'), $url) - ->line(__('emails.package_limits.footer')); + ->view('emails.addons.receipt', [ + 'addon' => $this->addon, + ]); } } diff --git a/app/Notifications/Customer/RefundReceipt.php b/app/Notifications/Customer/RefundReceipt.php index d7d62df..e35f97e 100644 --- a/app/Notifications/Customer/RefundReceipt.php +++ b/app/Notifications/Customer/RefundReceipt.php @@ -27,20 +27,29 @@ class RefundReceipt extends Notification implements ShouldQueue $tenant = $this->purchase->tenant; $package = $this->purchase->package; $amount = number_format((float) $this->purchase->price, 2); - - $mail = (new MailMessage) - ->subject(__('emails.refund.subject', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')])) - ->greeting(__('emails.refund.greeting', ['name' => $tenant?->name ?? __('emails.package_limits.team_fallback')])) - ->line(__('emails.refund.body', [ + $subject = __('emails.refund.subject', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')]); + $greeting = __('emails.refund.greeting', ['name' => $tenant?->name ?? __('emails.package_limits.team_fallback')]); + $lines = [ + __('emails.refund.body', [ 'amount' => $amount, 'currency' => '€', 'provider_id' => $this->purchase->provider_id ?? '—', - ])); + ]), + ]; if ($this->reason) { - $mail->line(__('emails.refund.reason', ['reason' => $this->reason])); + $lines[] = __('emails.refund.reason', ['reason' => $this->reason]); } - return $mail->line(__('emails.refund.footer')); + return (new MailMessage) + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => $lines, + 'footer' => __('emails.refund.footer'), + ]); } } diff --git a/app/Notifications/InactiveTenantDeletionWarning.php b/app/Notifications/InactiveTenantDeletionWarning.php index a8c4c06..e874c82 100644 --- a/app/Notifications/InactiveTenantDeletionWarning.php +++ b/app/Notifications/InactiveTenantDeletionWarning.php @@ -27,13 +27,25 @@ class InactiveTenantDeletionWarning extends Notification implements ShouldQueue { $locale = $this->tenant->user?->preferred_locale ?? app()->getLocale(); $formattedDate = $this->plannedDeletion->copy()->locale($locale)->translatedFormat('d. F Y'); + $subject = __('profile.retention.warning_subject', [], $locale); return (new MailMessage) - ->locale($locale) - ->subject(__('profile.retention.warning_subject', [], $locale)) - ->line(__('profile.retention.line1', ['name' => $this->tenant->name], $locale)) - ->line(__('profile.retention.line2', ['date' => $formattedDate], $locale)) - ->line(__('profile.retention.line3', [], $locale)) - ->action(__('profile.retention.action', [], $locale), url('/login')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => __('profile.retention.line1', ['name' => $this->tenant->name], $locale), + 'heroTitle' => $subject, + 'lines' => [ + __('profile.retention.line1', ['name' => $this->tenant->name], $locale), + __('profile.retention.line2', ['date' => $formattedDate], $locale), + __('profile.retention.line3', [], $locale), + ], + 'cta' => [ + [ + 'label' => __('profile.retention.action', [], $locale), + 'url' => url('/login'), + ], + ], + ]); } } diff --git a/app/Notifications/Ops/AddonPurchased.php b/app/Notifications/Ops/AddonPurchased.php index 6cd2e25..a50a9cd 100644 --- a/app/Notifications/Ops/AddonPurchased.php +++ b/app/Notifications/Ops/AddonPurchased.php @@ -26,18 +26,32 @@ class AddonPurchased extends Notification implements ShouldQueue $label = $this->addon->metadata['label'] ?? $this->addon->addon_key; $amount = $this->addon->amount ? number_format((float) $this->addon->amount, 2) : null; $currency = $this->addon->currency ?? 'EUR'; + $subject = __('emails.ops.addon.subject', ['addon' => $label]); + $greeting = __('emails.ops.addon.greeting'); + $lines = [ + __('emails.ops.addon.tenant', ['tenant' => $tenant?->name ?? __('emails.tenant_feedback.unknown_tenant')]), + __('emails.ops.addon.event', ['event' => $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback')]), + __('emails.ops.addon.addon', ['addon' => $label, 'quantity' => $this->addon->quantity]), + ]; + + if ($amount) { + $lines[] = __('emails.ops.addon.amount', ['amount' => $amount, 'currency' => $currency]); + } + + $lines[] = __('emails.ops.addon.provider', [ + 'checkout' => $this->addon->checkout_id ?? '—', + 'transaction' => $this->addon->transaction_id ?? '—', + ]); return (new MailMessage) - ->subject(__('emails.ops.addon.subject', ['addon' => $label])) - ->greeting(__('emails.ops.addon.greeting')) - ->line(__('emails.ops.addon.tenant', ['tenant' => $tenant?->name ?? __('emails.tenant_feedback.unknown_tenant')])) - ->line(__('emails.ops.addon.event', ['event' => $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback')])) - ->line(__('emails.ops.addon.addon', ['addon' => $label, 'quantity' => $this->addon->quantity])) - ->when($amount, fn ($mail) => $mail->line(__('emails.ops.addon.amount', ['amount' => $amount, 'currency' => $currency]))) - ->line(__('emails.ops.addon.provider', [ - 'checkout' => $this->addon->checkout_id ?? '—', - 'transaction' => $this->addon->transaction_id ?? '—', - ])) - ->line(__('emails.ops.addon.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => $lines, + 'footer' => __('emails.ops.addon.footer'), + ]); } } diff --git a/app/Notifications/Ops/PurchaseCreated.php b/app/Notifications/Ops/PurchaseCreated.php index e296aeb..d463b6e 100644 --- a/app/Notifications/Ops/PurchaseCreated.php +++ b/app/Notifications/Ops/PurchaseCreated.php @@ -25,19 +25,28 @@ class PurchaseCreated extends Notification implements ShouldQueue $package = $this->purchase->package; $amount = number_format((float) $this->purchase->price, 2); $consents = $this->purchase->metadata['consents'] ?? []; + $subject = __('emails.ops.purchase.subject', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')]); + $greeting = __('emails.ops.purchase.greeting'); return (new MailMessage) - ->subject(__('emails.ops.purchase.subject', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')])) - ->greeting(__('emails.ops.purchase.greeting')) - ->line(__('emails.ops.purchase.tenant', ['tenant' => $tenant?->name ?? __('emails.tenant_feedback.unknown_tenant')])) - ->line(__('emails.ops.purchase.package', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')])) - ->line(__('emails.ops.purchase.amount', ['amount' => $amount, 'currency' => '€'])) - ->line(__('emails.ops.purchase.provider', ['provider' => $this->purchase->provider, 'id' => $this->purchase->provider_id ?? '—'])) - ->line(__('emails.ops.purchase.consents', [ - 'legal' => $consents['legal_version'] ?? 'n/a', - 'terms' => $consents['accepted_terms_at'] ?? 'n/a', - 'waiver' => $consents['digital_content_waiver_at'] ?? 'n/a', - ])) - ->line(__('emails.ops.purchase.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.ops.purchase.tenant', ['tenant' => $tenant?->name ?? __('emails.tenant_feedback.unknown_tenant')]), + __('emails.ops.purchase.package', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')]), + __('emails.ops.purchase.amount', ['amount' => $amount, 'currency' => '€']), + __('emails.ops.purchase.provider', ['provider' => $this->purchase->provider, 'id' => $this->purchase->provider_id ?? '—']), + __('emails.ops.purchase.consents', [ + 'legal' => $consents['legal_version'] ?? 'n/a', + 'terms' => $consents['accepted_terms_at'] ?? 'n/a', + 'waiver' => $consents['digital_content_waiver_at'] ?? 'n/a', + ]), + ], + 'footer' => __('emails.ops.purchase.footer'), + ]); } } diff --git a/app/Notifications/Ops/RefundProcessed.php b/app/Notifications/Ops/RefundProcessed.php index 1d5061b..85c7ed1 100644 --- a/app/Notifications/Ops/RefundProcessed.php +++ b/app/Notifications/Ops/RefundProcessed.php @@ -29,24 +29,33 @@ class RefundProcessed extends Notification implements ShouldQueue $tenant = $this->purchase->tenant; $package = $this->purchase->package; $amount = number_format((float) $this->purchase->price, 2); - - $mail = (new MailMessage) - ->subject(__('emails.ops.refund.subject', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')])) - ->greeting(__('emails.ops.refund.greeting')) - ->line(__('emails.ops.refund.tenant', ['tenant' => $tenant?->name ?? __('emails.tenant_feedback.unknown_tenant')])) - ->line(__('emails.ops.refund.package', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')])) - ->line(__('emails.ops.refund.amount', ['amount' => $amount, 'currency' => '€'])) - ->line(__('emails.ops.refund.provider', ['provider' => $this->purchase->provider, 'id' => $this->purchase->provider_id ?? '—'])) - ->line($this->success ? __('emails.ops.refund.status_success') : __('emails.ops.refund.status_failed')); + $subject = __('emails.ops.refund.subject', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')]); + $greeting = __('emails.ops.refund.greeting'); + $lines = [ + __('emails.ops.refund.tenant', ['tenant' => $tenant?->name ?? __('emails.tenant_feedback.unknown_tenant')]), + __('emails.ops.refund.package', ['package' => $package?->name ?? __('emails.package_limits.package_fallback')]), + __('emails.ops.refund.amount', ['amount' => $amount, 'currency' => '€']), + __('emails.ops.refund.provider', ['provider' => $this->purchase->provider, 'id' => $this->purchase->provider_id ?? '—']), + $this->success ? __('emails.ops.refund.status_success') : __('emails.ops.refund.status_failed'), + ]; if ($this->reason) { - $mail->line(__('emails.ops.refund.reason', ['reason' => $this->reason])); + $lines[] = __('emails.ops.refund.reason', ['reason' => $this->reason]); } if ($this->error) { - $mail->line(__('emails.ops.refund.error', ['error' => $this->error])); + $lines[] = __('emails.ops.refund.error', ['error' => $this->error]); } - return $mail->line(__('emails.ops.refund.footer')); + return (new MailMessage) + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => $lines, + 'footer' => __('emails.ops.refund.footer'), + ]); } } diff --git a/app/Notifications/Packages/EventPackageGalleryExpiredNotification.php b/app/Notifications/Packages/EventPackageGalleryExpiredNotification.php index 58f6655..b17f130 100644 --- a/app/Notifications/Packages/EventPackageGalleryExpiredNotification.php +++ b/app/Notifications/Packages/EventPackageGalleryExpiredNotification.php @@ -27,20 +27,34 @@ class EventPackageGalleryExpiredNotification extends Notification implements Sho $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); $url = url('/tenant/events/'.($event?->slug ?? '')); + $subject = __('emails.package_limits.gallery_expired.subject', [ + 'event' => $eventName, + ]); + $greeting = __('emails.package_limits.gallery_expired.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.gallery_expired.subject', [ - 'event' => $eventName, - ])) - ->greeting(__('emails.package_limits.gallery_expired.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.gallery_expired.body', [ - 'event' => $eventName, - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'date' => optional($this->eventPackage->gallery_expires_at)->toFormattedDateString(), - ])) - ->action(__('emails.package_limits.gallery_expired.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.gallery_expired.body', [ + 'event' => $eventName, + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'date' => optional($this->eventPackage->gallery_expires_at)->toFormattedDateString(), + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.gallery_expired.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/EventPackageGalleryExpiringNotification.php b/app/Notifications/Packages/EventPackageGalleryExpiringNotification.php index 43f1fef..2053bd5 100644 --- a/app/Notifications/Packages/EventPackageGalleryExpiringNotification.php +++ b/app/Notifications/Packages/EventPackageGalleryExpiringNotification.php @@ -30,22 +30,36 @@ class EventPackageGalleryExpiringNotification extends Notification implements Sh $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); $url = url('/tenant/events/'.($event?->slug ?? '')); + $subject = trans_choice('emails.package_limits.gallery_warning.subject', $this->daysRemaining, [ + 'event' => $eventName, + 'days' => $this->daysRemaining, + ]); + $greeting = __('emails.package_limits.gallery_warning.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(trans_choice('emails.package_limits.gallery_warning.subject', $this->daysRemaining, [ - 'event' => $eventName, - 'days' => $this->daysRemaining, - ])) - ->greeting(__('emails.package_limits.gallery_warning.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(trans_choice('emails.package_limits.gallery_warning.body', $this->daysRemaining, [ - 'event' => $eventName, - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'days' => $this->daysRemaining, - 'date' => optional($this->eventPackage->gallery_expires_at)->toFormattedDateString(), - ])) - ->action(__('emails.package_limits.gallery_warning.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + trans_choice('emails.package_limits.gallery_warning.body', $this->daysRemaining, [ + 'event' => $eventName, + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'days' => $this->daysRemaining, + 'date' => optional($this->eventPackage->gallery_expires_at)->toFormattedDateString(), + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.gallery_warning.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/EventPackageGuestLimitNotification.php b/app/Notifications/Packages/EventPackageGuestLimitNotification.php index 0d6bafb..99c59e0 100644 --- a/app/Notifications/Packages/EventPackageGuestLimitNotification.php +++ b/app/Notifications/Packages/EventPackageGuestLimitNotification.php @@ -30,22 +30,39 @@ class EventPackageGuestLimitNotification extends Notification implements ShouldQ $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); $url = url('/tenant/events/'.($event?->slug ?? '')); + $subject = __('emails.package_limits.guest_limit.subject', [ + 'event' => $eventName, + ]); + $greeting = __('emails.package_limits.guest_limit.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.guest_limit.subject', [ - 'event' => $eventName, - ])) - ->greeting(__('emails.package_limits.guest_limit.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.guest_limit.body', [ - 'event' => $eventName, - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'limit' => $this->limit, - ])) - ->line(__('emails.package_limits.guest_limit.cta_addon')) - ->action(__('emails.package_limits.guest_limit.addon_action'), $url) - ->action(__('emails.package_limits.guest_limit.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.guest_limit.body', [ + 'event' => $eventName, + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'limit' => $this->limit, + ]), + __('emails.package_limits.guest_limit.cta_addon'), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.guest_limit.addon_action'), + 'url' => $url, + ], + [ + 'label' => __('emails.package_limits.guest_limit.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/EventPackageGuestThresholdNotification.php b/app/Notifications/Packages/EventPackageGuestThresholdNotification.php index f787260..c0daf3d 100644 --- a/app/Notifications/Packages/EventPackageGuestThresholdNotification.php +++ b/app/Notifications/Packages/EventPackageGuestThresholdNotification.php @@ -34,24 +34,38 @@ class EventPackageGuestThresholdNotification extends Notification implements Sho $remaining = max(0, $this->limit - $this->used); $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); $url = url('/tenant/events/'.($event?->slug ?? '')); + $subject = __('emails.package_limits.guest_threshold.subject', [ + 'event' => $eventName, + 'percentage' => $percentage, + ]); + $greeting = __('emails.package_limits.guest_threshold.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.guest_threshold.subject', [ - 'event' => $eventName, - 'percentage' => $percentage, - ])) - ->greeting(__('emails.package_limits.guest_threshold.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.guest_threshold.body', [ - 'event' => $eventName, - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'percentage' => $percentage, - 'used' => $this->used, - 'limit' => $this->limit, - 'remaining' => $remaining, - ])) - ->action(__('emails.package_limits.guest_threshold.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.guest_threshold.body', [ + 'event' => $eventName, + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'percentage' => $percentage, + 'used' => $this->used, + 'limit' => $this->limit, + 'remaining' => $remaining, + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.guest_threshold.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/EventPackagePhotoLimitNotification.php b/app/Notifications/Packages/EventPackagePhotoLimitNotification.php index e76a492..e34756a 100644 --- a/app/Notifications/Packages/EventPackagePhotoLimitNotification.php +++ b/app/Notifications/Packages/EventPackagePhotoLimitNotification.php @@ -30,22 +30,39 @@ class EventPackagePhotoLimitNotification extends Notification implements ShouldQ $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); $url = url('/tenant/events/'.($event?->slug ?? '')); + $subject = __('emails.package_limits.photo_limit.subject', [ + 'event' => $eventName, + ]); + $greeting = __('emails.package_limits.photo_limit.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.photo_limit.subject', [ - 'event' => $eventName, - ])) - ->greeting(__('emails.package_limits.photo_limit.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.photo_limit.body', [ - 'event' => $eventName, - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'limit' => $this->limit, - ])) - ->line(__('emails.package_limits.photo_limit.cta_addon')) - ->action(__('emails.package_limits.photo_limit.addon_action'), $url) - ->action(__('emails.package_limits.photo_limit.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.photo_limit.body', [ + 'event' => $eventName, + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'limit' => $this->limit, + ]), + __('emails.package_limits.photo_limit.cta_addon'), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.photo_limit.addon_action'), + 'url' => $url, + ], + [ + 'label' => __('emails.package_limits.photo_limit.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/EventPackagePhotoThresholdNotification.php b/app/Notifications/Packages/EventPackagePhotoThresholdNotification.php index f15facd..83d9654 100644 --- a/app/Notifications/Packages/EventPackagePhotoThresholdNotification.php +++ b/app/Notifications/Packages/EventPackagePhotoThresholdNotification.php @@ -35,24 +35,38 @@ class EventPackagePhotoThresholdNotification extends Notification implements Sho $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); $url = url('/tenant/events/'.($event?->slug ?? '')); + $subject = __('emails.package_limits.photo_threshold.subject', [ + 'event' => $eventName, + 'percentage' => $percentage, + ]); + $greeting = __('emails.package_limits.photo_threshold.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.photo_threshold.subject', [ - 'event' => $eventName, - 'percentage' => $percentage, - ])) - ->greeting(__('emails.package_limits.photo_threshold.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.photo_threshold.body', [ - 'event' => $eventName, - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'percentage' => $percentage, - 'used' => $this->used, - 'limit' => $this->limit, - 'remaining' => $remaining, - ])) - ->action(__('emails.package_limits.photo_threshold.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.photo_threshold.body', [ + 'event' => $eventName, + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'percentage' => $percentage, + 'used' => $this->used, + 'limit' => $this->limit, + 'remaining' => $remaining, + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.photo_threshold.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/TenantPackageEventLimitNotification.php b/app/Notifications/Packages/TenantPackageEventLimitNotification.php index ec23740..787c2c0 100644 --- a/app/Notifications/Packages/TenantPackageEventLimitNotification.php +++ b/app/Notifications/Packages/TenantPackageEventLimitNotification.php @@ -28,19 +28,33 @@ class TenantPackageEventLimitNotification extends Notification implements Should $package = $this->tenantPackage->package; $url = url('/tenant/billing'); + $subject = __('emails.package_limits.event_limit.subject', [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + ]); + $greeting = __('emails.package_limits.event_limit.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.event_limit.subject', [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - ])) - ->greeting(__('emails.package_limits.event_limit.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.event_limit.body', [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'limit' => $this->limit, - ])) - ->action(__('emails.package_limits.event_limit.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.event_limit.body', [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'limit' => $this->limit, + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.event_limit.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/TenantPackageEventThresholdNotification.php b/app/Notifications/Packages/TenantPackageEventThresholdNotification.php index de71ae0..1c07f4e 100644 --- a/app/Notifications/Packages/TenantPackageEventThresholdNotification.php +++ b/app/Notifications/Packages/TenantPackageEventThresholdNotification.php @@ -33,23 +33,37 @@ class TenantPackageEventThresholdNotification extends Notification implements Sh $remaining = max(0, $this->limit - $this->used); $url = url('/tenant/billing'); + $subject = __('emails.package_limits.event_threshold.subject', [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'percentage' => $percentage, + ]); + $greeting = __('emails.package_limits.event_threshold.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.event_threshold.subject', [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'percentage' => $percentage, - ])) - ->greeting(__('emails.package_limits.event_threshold.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.event_threshold.body', [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'percentage' => $percentage, - 'used' => $this->used, - 'limit' => $this->limit, - 'remaining' => $remaining, - ])) - ->action(__('emails.package_limits.event_threshold.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.event_threshold.body', [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'percentage' => $percentage, + 'used' => $this->used, + 'limit' => $this->limit, + 'remaining' => $remaining, + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.event_threshold.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/TenantPackageExpiredNotification.php b/app/Notifications/Packages/TenantPackageExpiredNotification.php index 2e53d9c..e0e47b3 100644 --- a/app/Notifications/Packages/TenantPackageExpiredNotification.php +++ b/app/Notifications/Packages/TenantPackageExpiredNotification.php @@ -25,19 +25,33 @@ class TenantPackageExpiredNotification extends Notification implements ShouldQue $package = $this->tenantPackage->package; $url = url('/tenant/billing'); + $subject = __('emails.package_limits.package_expired.subject', [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + ]); + $greeting = __('emails.package_limits.package_expired.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(__('emails.package_limits.package_expired.subject', [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - ])) - ->greeting(__('emails.package_limits.package_expired.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(__('emails.package_limits.package_expired.body', [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'date' => optional($this->tenantPackage->expires_at)->toFormattedDateString(), - ])) - ->action(__('emails.package_limits.package_expired.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + __('emails.package_limits.package_expired.body', [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'date' => optional($this->tenantPackage->expires_at)->toFormattedDateString(), + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.package_expired.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/Packages/TenantPackageExpiringNotification.php b/app/Notifications/Packages/TenantPackageExpiringNotification.php index ae9f14b..4de6bfb 100644 --- a/app/Notifications/Packages/TenantPackageExpiringNotification.php +++ b/app/Notifications/Packages/TenantPackageExpiringNotification.php @@ -28,21 +28,35 @@ class TenantPackageExpiringNotification extends Notification implements ShouldQu $package = $this->tenantPackage->package; $url = url('/tenant/billing'); + $subject = trans_choice('emails.package_limits.package_expiring.subject', $this->daysRemaining, [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'days' => $this->daysRemaining, + ]); + $greeting = __('emails.package_limits.package_expiring.greeting', [ + 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), + ]); return (new MailMessage) - ->subject(trans_choice('emails.package_limits.package_expiring.subject', $this->daysRemaining, [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'days' => $this->daysRemaining, - ])) - ->greeting(__('emails.package_limits.package_expiring.greeting', [ - 'name' => $tenant?->name ?? __('emails.package_limits.team_fallback'), - ])) - ->line(trans_choice('emails.package_limits.package_expiring.body', $this->daysRemaining, [ - 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), - 'days' => $this->daysRemaining, - 'date' => optional($this->tenantPackage->expires_at)->toFormattedDateString(), - ])) - ->action(__('emails.package_limits.package_expiring.action'), $url) - ->line(__('emails.package_limits.footer')); + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $greeting, + 'heroTitle' => $greeting, + 'heroSubtitle' => $subject, + 'lines' => [ + trans_choice('emails.package_limits.package_expiring.body', $this->daysRemaining, [ + 'package' => $package?->getNameForLocale() ?? $package?->name ?? __('emails.package_limits.package_fallback'), + 'days' => $this->daysRemaining, + 'date' => optional($this->tenantPackage->expires_at)->toFormattedDateString(), + ]), + ], + 'cta' => [ + [ + 'label' => __('emails.package_limits.package_expiring.action'), + 'url' => $url, + ], + ], + 'footer' => __('emails.package_limits.footer'), + ]); } } diff --git a/app/Notifications/TenantFeedbackSubmitted.php b/app/Notifications/TenantFeedbackSubmitted.php index 8d3a67c..f575b69 100644 --- a/app/Notifications/TenantFeedbackSubmitted.php +++ b/app/Notifications/TenantFeedbackSubmitted.php @@ -35,38 +35,45 @@ class TenantFeedbackSubmitted extends Notification implements ShouldQueue 'tenant' => $tenantName, 'sentiment' => $sentiment, ]); - - $mail = (new MailMessage()) - ->subject($subject) - ->line(__('emails.tenant_feedback.tenant', ['tenant' => $tenantName])) - ->line(__('emails.tenant_feedback.category', ['category' => $this->feedback->category ? Str::headline($this->feedback->category) : '—'])) - ->line(__('emails.tenant_feedback.sentiment', ['sentiment' => $sentiment])); + $lines = [ + __('emails.tenant_feedback.tenant', ['tenant' => $tenantName]), + __('emails.tenant_feedback.category', ['category' => $this->feedback->category ? Str::headline($this->feedback->category) : '—']), + __('emails.tenant_feedback.sentiment', ['sentiment' => $sentiment]), + ]; if ($eventName) { - $mail->line(__('emails.tenant_feedback.event', ['event' => $eventName])); + $lines[] = __('emails.tenant_feedback.event', ['event' => $eventName]); } if ($rating) { - $mail->line(__('emails.tenant_feedback.rating', ['rating' => $rating])); + $lines[] = __('emails.tenant_feedback.rating', ['rating' => $rating]); } if ($this->feedback->title) { - $mail->line(__('emails.tenant_feedback.title', ['subject' => $this->feedback->title])); + $lines[] = __('emails.tenant_feedback.title', ['subject' => $this->feedback->title]); } if ($this->feedback->message) { - $mail->line(__('emails.tenant_feedback.message'))->line($this->feedback->message); + $lines[] = __('emails.tenant_feedback.message'); + $lines[] = $this->feedback->message; } $url = TenantFeedbackResource::getUrl('view', ['record' => $this->feedback], panel: 'superadmin'); - if ($url) { - $mail->action(__('emails.tenant_feedback.open'), $url); - } + $lines[] = __('emails.tenant_feedback.received_at', ['date' => $this->feedback->created_at?->toDayDateTimeString()]); - $mail->line(__('emails.tenant_feedback.received_at', ['date' => $this->feedback->created_at?->toDayDateTimeString()])); - - return $mail; + return (new MailMessage) + ->subject($subject) + ->view('emails.notifications.basic', [ + 'title' => $subject, + 'preheader' => $subject, + 'heroTitle' => $subject, + 'lines' => $lines, + 'cta' => $url ? [[ + 'label' => __('emails.tenant_feedback.open'), + 'url' => $url, + ]] : [], + ]); } protected function resolveName(mixed $name): ?string diff --git a/app/Notifications/UploadPipelineFailed.php b/app/Notifications/UploadPipelineFailed.php index 5edb712..04182a4 100644 --- a/app/Notifications/UploadPipelineFailed.php +++ b/app/Notifications/UploadPipelineFailed.php @@ -11,9 +11,7 @@ class UploadPipelineFailed extends Notification implements ShouldQueue { use Queueable; - public function __construct(private readonly array $context) - { - } + public function __construct(private readonly array $context) {} public function via(object $notifiable): array { @@ -29,15 +27,29 @@ class UploadPipelineFailed extends Notification implements ShouldQueue public function toMail(object $notifiable): MailMessage { $context = $this->context; + $job = $context['job'] ?? 'n/a'; + $queue = $context['queue'] ?? 'n/a'; + $eventId = $context['event_id'] ?? 'n/a'; + $photoId = $context['photo_id'] ?? 'n/a'; + $exception = $context['exception'] ?? 'n/a'; + $timestamp = now()->toDateTimeString(); return (new MailMessage) - ->subject('Upload-Pipeline Fehler: '.($context['job'] ?? 'Unbekannter Job')) - ->line('In der Upload-Pipeline ist ein Fehler aufgetreten.') - ->line('Job: '.($context['job'] ?? 'n/a')) - ->line('Queue: '.($context['queue'] ?? 'n/a')) - ->line('Event ID: '.($context['event_id'] ?? 'n/a')) - ->line('Foto ID: '.($context['photo_id'] ?? 'n/a')) - ->line('Exception: '.($context['exception'] ?? 'n/a')) - ->line('Zeitpunkt: '.now()->toDateTimeString()); + ->subject(__('emails.upload_pipeline_failed.subject', ['job' => $job])) + ->view('emails.notifications.basic', [ + 'title' => __('emails.upload_pipeline_failed.subject', ['job' => $job]), + 'preheader' => __('emails.upload_pipeline_failed.preheader'), + 'heroTitle' => __('emails.upload_pipeline_failed.hero_title'), + 'heroSubtitle' => __('emails.upload_pipeline_failed.hero_subtitle'), + 'lines' => [ + __('emails.upload_pipeline_failed.line_job', ['job' => $job]), + __('emails.upload_pipeline_failed.line_queue', ['queue' => $queue]), + __('emails.upload_pipeline_failed.line_event', ['event' => $eventId]), + __('emails.upload_pipeline_failed.line_photo', ['photo' => $photoId]), + __('emails.upload_pipeline_failed.line_exception', ['exception' => $exception]), + __('emails.upload_pipeline_failed.line_time', ['time' => $timestamp]), + ], + 'footer' => __('emails.upload_pipeline_failed.footer'), + ]); } } diff --git a/app/Notifications/VerifyEmailNotification.php b/app/Notifications/VerifyEmailNotification.php new file mode 100644 index 0000000..afc0203 --- /dev/null +++ b/app/Notifications/VerifyEmailNotification.php @@ -0,0 +1,22 @@ +verificationUrl($notifiable); + + return (new MailMessage) + ->subject(__('emails.verification.subject')) + ->view('emails.verify-email', [ + 'user' => $notifiable, + 'verificationUrl' => $verificationUrl, + 'expiresIn' => (int) config('auth.verification.expire', 60), + ]); + } +} diff --git a/resources/lang/de/emails.php b/resources/lang/de/emails.php index 9f0c085..ecdfbe2 100644 --- a/resources/lang/de/emails.php +++ b/resources/lang/de/emails.php @@ -1,14 +1,51 @@ [ + 'label' => 'Die Fotospiel.App', + 'footer' => 'Mit freundlichen Grüßen,
Das Team von Die Fotospiel.App', + 'tagline' => 'Die Fotospiel.App · Event-Pakete mit Wow-Effekt', + ], + 'welcome' => [ - 'subject' => 'Willkommen bei Fotospiel, :name!', - 'greeting' => 'Willkommen bei Fotospiel, :name!', - 'body' => 'Vielen Dank für Ihre Registrierung. Ihr Account ist nun aktiv.', + 'subject' => 'Willkommen bei Die Fotospiel.App, :name!', + 'greeting' => 'Willkommen bei Die Fotospiel.App, :name!', + 'subtitle' => 'Schön, dass Sie da sind. Ihr Event-Erlebnis kann sofort starten.', + 'body' => 'Vielen Dank für Ihre Registrierung. Wir haben Ihren Account vorbereitet – jetzt fehlt nur noch Ihr erstes Event.', + 'account_label' => 'Ihre Zugangsdaten', 'username' => 'Benutzername: :username', 'email' => 'E-Mail: :email', - 'verification' => 'Bitte verifizieren Sie Ihre E-Mail-Adresse, um auf das Admin-Panel zuzugreifen.', - 'footer' => 'Mit freundlichen Grüßen,
Das Fotospiel-Team', + 'verification' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse, um vollen Zugriff zu erhalten.', + 'next_steps_title' => 'So geht es weiter', + 'step_one' => 'Event anlegen und das passende Paket auswählen', + 'step_two' => 'Gäste einladen und den Upload-Link teilen', + 'step_three' => 'Fotos sammeln, kuratieren und genießen', + 'cta' => 'Event-Admin öffnen', + 'footer' => 'Sie brauchen Unterstützung? Wir sind jederzeit für Sie da.', + ], + 'verification' => [ + 'subject' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse', + 'preheader' => 'Bestätigen Sie Ihre E-Mail, um den vollen Zugriff zu erhalten.', + 'hero_title' => 'E-Mail bestätigen, :name', + 'hero_subtitle' => 'Ein Klick genügt, um Ihr Konto zu aktivieren.', + 'body' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den Button klicken.', + 'expires' => 'Dieser Bestätigungslink ist :minutes Minuten gültig.', + 'link_fallback' => 'Falls der Button nicht funktioniert, kopieren Sie diesen Link in Ihren Browser:', + 'cta' => 'E-Mail bestätigen', + 'footer' => 'Falls Sie kein Konto erstellt haben, können Sie diese E-Mail ignorieren.', + ], + 'upload_pipeline_failed' => [ + 'subject' => 'Upload-Pipeline Fehler: :job', + 'preheader' => 'In der Upload-Pipeline ist ein Fehler aufgetreten.', + 'hero_title' => 'Upload-Pipeline Alarm', + 'hero_subtitle' => 'Beim Verarbeiten der Uploads ist ein Fehler aufgetreten.', + 'line_job' => 'Job: :job', + 'line_queue' => 'Queue: :queue', + 'line_event' => 'Event ID: :event', + 'line_photo' => 'Foto ID: :photo', + 'line_exception' => 'Exception: :exception', + 'line_time' => 'Zeitpunkt: :time', + 'footer' => 'Bitte prüfen Sie die Queue-Logs für weitere Details.', ], 'purchase' => [ @@ -17,9 +54,7 @@ return [ 'package' => 'Package: :package', 'price' => 'Preis: :price', 'activation' => 'Ihr Event-Paket ist jetzt im Tenant-Account aktiviert.', - 'footer' => 'Mit freundlichen Grüßen,
Das Team von Die Fotospiel.App', - 'brand_label' => 'Die Fotospiel.App', - 'brand_footer' => 'Die Fotospiel.App · Event-Pakete mit Wow-Effekt', + 'footer' => 'Sie brauchen Unterstützung? Wir sind jederzeit für Sie da.', 'subtitle' => 'Ihre Bestellung wurde erfolgreich bestätigt. Hier finden Sie alle Details auf einen Blick.', 'summary_title' => 'Bestellübersicht', 'package_label' => 'Event-Paket', @@ -57,23 +92,20 @@ return [ 'subject_1w' => 'Letzte Chance: Ihr gespeichertes Paket', 'greeting' => 'Hallo :name,', - - 'body_1h' => 'Sie haben vor kurzem begonnen, unser :package Paket zu kaufen, aber noch nicht abgeschlossen. Ihr Warenkorb ist für Sie reserviert.', - - 'body_24h' => 'Erinnerung: Ihr :package Paket wartet seit 24 Stunden auf Sie. Schließen Sie jetzt Ihren Kauf ab und sichern Sie sich alle Vorteile.', - - 'body_1w' => 'Letzte Erinnerung: Ihr :package Paket wartet seit einer Woche auf Sie. Dies ist Ihre letzte Chance, den Kauf abzuschließen.', - - 'cta_button' => 'Jetzt fortfahren', + 'subtitle' => ':package ist nur einen Schritt entfernt.', + 'body_1h' => 'Sie haben vor kurzem unser :package Event-Paket ausgewählt. Ihr Checkout ist weiterhin für Sie reserviert.', + 'body_24h' => 'Ihr :package Event-Paket wartet seit 24 Stunden auf Sie. Schließen Sie den Kauf jetzt ab und starten Sie direkt.', + 'body_1w' => 'Letzte Erinnerung: Ihr :package Event-Paket ist noch reserviert. Wenn Sie möchten, können Sie den Checkout jetzt abschließen.', + 'cta_button' => 'Checkout fortsetzen', 'cta_link' => 'Oder kopieren Sie diesen Link: :url', - - 'benefits_title' => 'Warum jetzt kaufen?', - 'benefit1' => 'Schneller Checkout in 2 Minuten', + 'cta_hint_title' => 'Reserviert für Sie', + 'cta_hint_body' => 'Ihr Angebot bleibt bestehen – Sie können jederzeit nahtlos fortfahren.', + 'benefits_title' => 'Was Sie erwartet', + 'benefit1' => 'Premium Checkout in wenigen Minuten', 'benefit2' => 'Sichere Zahlung mit Paddle', - 'benefit3' => 'Sofortiger Zugriff nach Zahlung', - 'benefit4' => '10% Rabatt sichern', - - 'footer' => 'Mit freundlichen Grüßen,
Das Fotospiel-Team', + 'benefit3' => 'Sofortige Aktivierung nach Zahlung', + 'benefit4' => 'Support durch das Die Fotospiel.App Team', + 'footer' => 'Wir helfen Ihnen gern weiter, falls Fragen offen sind.', ], 'contact' => [ @@ -82,10 +114,13 @@ return [ ], 'contact_confirmation' => [ - 'subject' => 'Vielen Dank für Ihre Nachricht, :name!', + 'subject' => 'Danke für Ihre Nachricht, :name!', 'greeting' => 'Hallo :name,', - 'body' => 'Vielen Dank für Ihre Nachricht an das Fotospiel-Team. Wir melden uns so schnell wie möglich zurück.', - 'footer' => 'Viele Grüße
Ihr Fotospiel-Team', + 'subtitle' => 'Wir kümmern uns persönlich um Ihr Anliegen.', + 'body' => 'Vielen Dank für Ihre Nachricht. Unser Team meldet sich so schnell wie möglich mit einer passenden Lösung.', + 'response_time' => 'In der Regel erhalten Sie innerhalb eines Werktags eine Antwort.', + 'cta' => 'Support kontaktieren', + 'footer' => 'Viele Grüße
Ihr Die Fotospiel.App Team', ], 'package_limits' => [ @@ -163,8 +198,14 @@ return [ 'receipt' => [ 'subject' => 'Add-on gekauft: :addon', 'greeting' => 'Hallo :name,', - 'body' => 'Sie haben „ :addon “ für das Event „ :event “ gebucht. Betrag: :amount.', - 'summary' => 'Enthalten: +:photos Fotos, +:guests Gäste, +:days Tage Galerie.', + 'subtitle' => 'Ihr Add-on ist aktiv und sofort verfügbar.', + 'body' => 'Sie haben „:addon“ für das Event „:event“ gebucht. Gesamtbetrag: :amount.', + 'summary_title' => 'Enthaltene Upgrades', + 'summary' => [ + 'photos' => '+:count Fotos', + 'guests' => '+:count Gäste', + 'gallery' => '+:count Tage Galerie', + ], 'unknown_amount' => 'k.A.', 'action' => 'Event-Dashboard öffnen', ], @@ -174,12 +215,14 @@ return [ 'purchaser' => [ 'subject' => 'Dein Geschenkgutschein (:amount :currency)', 'greeting' => 'Danke für deinen Kauf!', - 'body' => 'Hier ist dein Fotospiel-Geschenkgutschein im Wert von :amount :currency. Teile den Code mit deiner beschenkten Person: :recipient.', + 'subtitle' => 'Dein Gutschein ist bereit, Freude zu schenken.', + 'body' => 'Hier ist dein Fotospiel-Geschenkgutschein im Wert von :amount :currency. Teile den Code mit :recipient und schenke ein unvergessliches Event.', 'recipient_fallback' => 'dein:e Beschenkte:r', ], 'recipient' => [ 'subject' => 'Du hast einen Fotospiel-Geschenkgutschein erhalten (:amount :currency)', 'greeting' => 'Du hast ein Geschenk bekommen!', + 'subtitle' => 'Zeit für ein Event mit Wow-Effekt.', 'body' => ':purchaser hat dir einen Fotospiel-Geschenkgutschein im Wert von :amount :currency gesendet. Löse ihn mit dem untenstehenden Code ein.', ], 'code_label' => 'Gutscheincode', @@ -187,7 +230,7 @@ return [ 'expiry' => 'Gültig bis :date.', 'message_title' => 'Persönliche Nachricht', 'withdrawal' => 'Widerrufsbelehrung: Details ansehen (14 Tage; erlischt mit Einlösung).', - 'footer' => 'Viele Grüße,
dein Fotospiel Team', + 'footer' => 'Viele Grüße,
dein Die Fotospiel.App Team', 'printable' => 'Druckversion (mit QR)', 'reminder' => 'Erinnerung: Dein Gutschein ist noch nicht eingelöst.', 'expiry_soon' => 'Hinweis: Dein Gutschein läuft bald ab.', diff --git a/resources/lang/en/emails.php b/resources/lang/en/emails.php index e5ed1c0..82b292a 100644 --- a/resources/lang/en/emails.php +++ b/resources/lang/en/emails.php @@ -1,14 +1,50 @@ [ + 'label' => 'Die Fotospiel.App', + 'footer' => 'Best regards,
The team at Die Fotospiel.App', + 'tagline' => 'Die Fotospiel.App · Event packages with wow-factor', + ], 'welcome' => [ - 'subject' => 'Welcome to Fotospiel, :name!', - 'greeting' => 'Welcome to Fotospiel, :name!', - 'body' => 'Thank you for your registration. Your account is now active.', + 'subject' => 'Welcome to Die Fotospiel.App, :name!', + 'greeting' => 'Welcome to Die Fotospiel.App, :name!', + 'subtitle' => 'We are glad you are here. Your event experience can start immediately.', + 'body' => 'Thank you for signing up. Your account is ready—now let’s launch your first event.', + 'account_label' => 'Your account details', 'username' => 'Username: :username', 'email' => 'Email: :email', - 'verification' => 'Please verify your email address to access the admin panel.', - 'footer' => 'Best regards,
The Fotospiel Team', + 'verification' => 'Please verify your email address to unlock full access.', + 'next_steps_title' => 'Next steps', + 'step_one' => 'Create your event and pick the right package', + 'step_two' => 'Invite guests and share the upload link', + 'step_three' => 'Collect, curate, and celebrate every photo', + 'cta' => 'Open Event Admin', + 'footer' => 'Need help? We are here whenever you need us.', + ], + 'verification' => [ + 'subject' => 'Verify your email address', + 'preheader' => 'Confirm your email to unlock full access.', + 'hero_title' => 'Confirm your email, :name', + 'hero_subtitle' => 'One click to activate your account.', + 'body' => 'Please confirm your email address by clicking the button below.', + 'expires' => 'This verification link expires in :minutes minutes.', + 'link_fallback' => 'If the button does not work, copy and paste this link into your browser:', + 'cta' => 'Verify email', + 'footer' => 'If you did not create an account, you can ignore this email.', + ], + 'upload_pipeline_failed' => [ + 'subject' => 'Upload pipeline error: :job', + 'preheader' => 'An error occurred in the upload pipeline.', + 'hero_title' => 'Upload pipeline alert', + 'hero_subtitle' => 'We hit an error while processing uploads.', + 'line_job' => 'Job: :job', + 'line_queue' => 'Queue: :queue', + 'line_event' => 'Event ID: :event', + 'line_photo' => 'Photo ID: :photo', + 'line_exception' => 'Exception: :exception', + 'line_time' => 'Time: :time', + 'footer' => 'Please investigate the failure in the queue logs.', ], 'purchase' => [ @@ -17,9 +53,7 @@ return [ 'package' => 'Package: :package', 'price' => 'Price: :price', 'activation' => 'Your event package is now activated in your tenant account.', - 'footer' => 'Best regards,
The team at Die Fotospiel.App', - 'brand_label' => 'Die Fotospiel.App', - 'brand_footer' => 'Die Fotospiel.App · Event packages with wow-factor', + 'footer' => 'Need assistance? We are always happy to help.', 'subtitle' => 'Your order has been confirmed successfully. Here are the details at a glance.', 'summary_title' => 'Order summary', 'package_label' => 'Event package', @@ -57,23 +91,20 @@ return [ 'subject_1w' => 'Last Chance: Your Saved Package', 'greeting' => 'Hello :name,', - - 'body_1h' => 'You recently started purchasing our :package package but haven\'t completed it yet. Your cart is reserved for you.', - - 'body_24h' => 'Reminder: Your :package package has been waiting for 24 hours. Complete your purchase now and secure all the benefits.', - - 'body_1w' => 'Final reminder: Your :package package has been waiting for a week. This is your last chance to complete the purchase.', - - 'cta_button' => 'Continue Now', + 'subtitle' => ':package is just one step away.', + 'body_1h' => 'You selected the :package event package but haven\'t completed checkout yet. Your selection is still reserved.', + 'body_24h' => 'Your :package event package has been waiting for 24 hours. Finish checkout now and get started instantly.', + 'body_1w' => 'Final reminder: Your :package event package is still reserved. You can complete your purchase at any time.', + 'cta_button' => 'Resume checkout', 'cta_link' => 'Or copy this link: :url', - - 'benefits_title' => 'Why buy now?', - 'benefit1' => 'Quick checkout in 2 minutes', + 'cta_hint_title' => 'Reserved for you', + 'cta_hint_body' => 'Your selection stays locked—continue whenever you are ready.', + 'benefits_title' => 'What you get', + 'benefit1' => 'Premium checkout in minutes', 'benefit2' => 'Secure payment with Paddle', - 'benefit3' => 'Instant access after payment', - 'benefit4' => 'Secure 10% discount', - - 'footer' => 'Best regards,
The Fotospiel Team', + 'benefit3' => 'Instant activation after payment', + 'benefit4' => 'Support from the Die Fotospiel.App team', + 'footer' => 'Let us know if you need anything.', ], 'contact' => [ @@ -82,10 +113,13 @@ return [ ], 'contact_confirmation' => [ - 'subject' => 'Thank you for reaching out, :name!', + 'subject' => 'Thanks for reaching out, :name!', 'greeting' => 'Hi :name,', - 'body' => 'Thank you for your message to the Fotospiel team. We will get back to you as soon as possible.', - 'footer' => 'Best regards,
The Fotospiel Team', + 'subtitle' => 'Your message is in good hands.', + 'body' => 'Thank you for contacting us. Our team will reply with a tailored answer as quickly as possible.', + 'response_time' => 'We usually respond within one business day.', + 'cta' => 'Contact support', + 'footer' => 'Best regards,
The Die Fotospiel.App team', ], 'package_limits' => [ @@ -163,8 +197,14 @@ return [ 'receipt' => [ 'subject' => 'Add-on purchase: :addon', 'greeting' => 'Hello :name,', - 'body' => 'You purchased " :addon " for the event " :event ". Amount: :amount.', - 'summary' => 'Included: +:photos photos, +:guests guests, +:days gallery days.', + 'subtitle' => 'Your add-on is active and ready to use.', + 'body' => 'You booked “:addon” for the event “:event”. Total: :amount.', + 'summary_title' => 'Included upgrades', + 'summary' => [ + 'photos' => '+:count photos', + 'guests' => '+:count guests', + 'gallery' => '+:count gallery days', + ], 'unknown_amount' => 'n/a', 'action' => 'Open event dashboard', ], @@ -233,12 +273,14 @@ return [ 'purchaser' => [ 'subject' => 'Your gift voucher (:amount :currency)', 'greeting' => 'Thank you for your purchase!', - 'body' => 'Here is your Fotospiel gift voucher worth :amount :currency. You can share the code with your recipient: :recipient.', + 'subtitle' => 'Your voucher is ready to make someone smile.', + 'body' => 'Here is your Fotospiel gift voucher worth :amount :currency. Share the code with :recipient and gift an unforgettable event.', 'recipient_fallback' => 'your recipient', ], 'recipient' => [ 'subject' => 'You received a Fotospiel gift voucher (:amount :currency)', 'greeting' => 'You have a gift!', + 'subtitle' => 'Time for an event with wow-factor.', 'body' => ':purchaser sent you a Fotospiel gift voucher worth :amount :currency. Redeem it with the code below.', ], 'code_label' => 'Voucher code', @@ -246,7 +288,7 @@ return [ 'expiry' => 'Valid until :date.', 'message_title' => 'Personal message', 'withdrawal' => 'Withdrawal policy: View details (14 days; expires upon redemption).', - 'footer' => 'Best regards,
The Fotospiel Team', + 'footer' => 'Best regards,
The Die Fotospiel.App team', 'printable' => 'Printable version (with QR)', 'reminder' => 'Reminder: You still have an unused voucher.', 'expiry_soon' => 'Heads up: Your voucher will expire soon.', diff --git a/resources/views/emails/abandoned-checkout.blade.php b/resources/views/emails/abandoned-checkout.blade.php index 57fa8b8..887f328 100644 --- a/resources/views/emails/abandoned-checkout.blade.php +++ b/resources/views/emails/abandoned-checkout.blade.php @@ -1,47 +1,48 @@ - - - - {{ __('emails.abandoned_checkout.subject_' . $timing, ['package' => $packageName]) }} - - - -

{{ __('emails.abandoned_checkout.greeting', ['name' => $user->fullName]) }}

+@extends('emails.partials.layout') -

{{ __('emails.abandoned_checkout.body_' . $timing, ['package' => $packageName]) }}

+@section('title', __('emails.abandoned_checkout.subject_' . $timing, ['package' => $packageName])) +@section('preheader', __('emails.abandoned_checkout.subtitle', ['package' => $packageName])) +@section('hero_title', __('emails.abandoned_checkout.greeting', ['name' => $user->fullName])) +@section('hero_subtitle', __('emails.abandoned_checkout.subtitle', ['package' => $packageName])) - +@section('content') +

+ {{ __('emails.abandoned_checkout.body_' . $timing, ['package' => $packageName]) }} +

+
+

+ {{ __('emails.abandoned_checkout.cta_hint_title') }} +

+

+ {{ __('emails.abandoned_checkout.cta_hint_body') }} +

+
+

+ {{ __('emails.abandoned_checkout.cta_link', ['url' => $resumeUrl]) }} +

+

{{ __('emails.abandoned_checkout.benefits_title') }}

+ + + + + + + + + + + + + +
✓ {{ __('emails.abandoned_checkout.benefit1') }}
✓ {{ __('emails.abandoned_checkout.benefit2') }}
✓ {{ __('emails.abandoned_checkout.benefit3') }}
✓ {{ __('emails.abandoned_checkout.benefit4') }}
+@endsection + +@section('cta') +
{{ __('emails.abandoned_checkout.cta_button') }} +@endsection -

{{ __('emails.abandoned_checkout.cta_link', ['url' => $resumeUrl]) }}

- -
-

{{ __('emails.abandoned_checkout.benefits_title') }}

-
✓ {{ __('emails.abandoned_checkout.benefit1') }}
-
✓ {{ __('emails.abandoned_checkout.benefit2') }}
-
✓ {{ __('emails.abandoned_checkout.benefit3') }}
-
✓ {{ __('emails.abandoned_checkout.benefit4') }}
-
- -

{!! __('emails.abandoned_checkout.footer') !!}

- - +@section('footer') + {!! __('emails.abandoned_checkout.footer') !!} +@endsection diff --git a/resources/views/emails/addons/receipt.blade.php b/resources/views/emails/addons/receipt.blade.php index 783f217..3a88582 100644 --- a/resources/views/emails/addons/receipt.blade.php +++ b/resources/views/emails/addons/receipt.blade.php @@ -1,3 +1,5 @@ +@extends('emails.partials.layout') + @php /** @var \App\Models\EventPackageAddon $addon */ $event = $addon->event; @@ -14,31 +16,45 @@ if ($addon->extra_gallery_days > 0) { $summary[] = __('emails.addons.receipt.summary.gallery', ['count' => $addon->extra_gallery_days]); } + $eventName = $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'); + $ctaUrl = url('/tenant/events/'.($event?->slug ?? '')); @endphp -@component('mail::message') -# {{ __('emails.addons.receipt.subject', ['addon' => $label]) }} +@section('title', __('emails.addons.receipt.subject', ['addon' => $label])) +@section('preheader', __('emails.addons.receipt.subtitle', ['addon' => $label])) +@section('hero_title', __('emails.addons.receipt.greeting', ['name' => $tenant?->name ?? __('emails.package_limits.team_fallback')])) +@section('hero_subtitle', __('emails.addons.receipt.subtitle', ['addon' => $label])) -{{ __('emails.addons.receipt.greeting', ['name' => $tenant?->name ?? __('emails.package_limits.team_fallback')]) }} +@section('content') +

+ {{ __('emails.addons.receipt.body', [ + 'addon' => $label, + 'event' => $eventName, + 'amount' => $amount, + ]) }} +

+ @if (! empty($summary)) +
+

+ {{ __('emails.addons.receipt.summary_title') }} +

+ + @foreach ($summary as $line) + + + + @endforeach +
{{ $line }}
+
+ @endif +@endsection -{{ __('emails.addons.receipt.body', [ - 'addon' => $label, - 'event' => $event?->name['de'] ?? $event?->name['en'] ?? $event?->name ?? __('emails.package_limits.event_fallback'), - 'amount' => $amount, -]) }} +@section('cta') + + {{ __('emails.addons.receipt.action') }} + +@endsection -@if(!empty($summary)) -**{{ __('emails.addons.receipt.summary_title', 'Included:') }}** - -@foreach($summary as $line) -- {{ $line }} -@endforeach -@endif - -@component('mail::button', ['url' => url('/tenant/events/'.($event?->slug ?? ''))]) -{{ __('emails.addons.receipt.action') }} -@endcomponent - -{{ __('emails.package_limits.footer') }} - -@endcomponent +@section('footer') + {!! __('emails.brand.footer') !!} +@endsection diff --git a/resources/views/emails/contact-confirmation.blade.php b/resources/views/emails/contact-confirmation.blade.php index 8b5a8ed..9a88cda 100644 --- a/resources/views/emails/contact-confirmation.blade.php +++ b/resources/views/emails/contact-confirmation.blade.php @@ -1,7 +1,29 @@ -@component('mail::message') -# {{ __('emails.contact_confirmation.greeting', ['name' => $name]) }} +@extends('emails.partials.layout') -{{ __('emails.contact_confirmation.body') }} +@php + $contactEmail = config('mail.contact_address', config('mail.from.address')); +@endphp -{{ __('emails.contact_confirmation.footer') }} -@endcomponent +@section('title', __('emails.contact_confirmation.subject', ['name' => $name])) +@section('preheader', __('emails.contact_confirmation.subtitle')) +@section('hero_title', __('emails.contact_confirmation.greeting', ['name' => $name])) +@section('hero_subtitle', __('emails.contact_confirmation.subtitle')) + +@section('content') +

+ {{ __('emails.contact_confirmation.body') }} +

+

+ {{ __('emails.contact_confirmation.response_time') }} +

+@endsection + +@section('cta') + + {{ __('emails.contact_confirmation.cta') }} + +@endsection + +@section('footer') + {!! __('emails.contact_confirmation.footer') !!} +@endsection diff --git a/resources/views/emails/gift-voucher.blade.php b/resources/views/emails/gift-voucher.blade.php index 66dd9d4..33465c5 100644 --- a/resources/views/emails/gift-voucher.blade.php +++ b/resources/views/emails/gift-voucher.blade.php @@ -1,41 +1,48 @@ +@extends('emails.partials.layout') + @php $withdrawalUrl = app()->getLocale() === 'de' ? url('/de/widerrufsbelehrung') : url('/en/withdrawal'); + $subject = $forRecipient + ? __('emails.gift_voucher.recipient.subject', ['amount' => $amount, 'currency' => $currency]) + : __('emails.gift_voucher.purchaser.subject', ['amount' => $amount, 'currency' => $currency]); + $greeting = $forRecipient + ? __('emails.gift_voucher.recipient.greeting') + : __('emails.gift_voucher.purchaser.greeting'); + $subtitle = $forRecipient + ? __('emails.gift_voucher.recipient.subtitle') + : __('emails.gift_voucher.purchaser.subtitle'); + $body = $forRecipient + ? __('emails.gift_voucher.recipient.body', [ + 'amount' => $amount, + 'currency' => $currency, + 'purchaser' => $voucher->purchaser_email, + ]) + : __('emails.gift_voucher.purchaser.body', [ + 'amount' => $amount, + 'currency' => $currency, + 'recipient' => $voucher->recipient_email ?: __('emails.gift_voucher.purchaser.recipient_fallback'), + ]); @endphp - - - - - {{ $forRecipient ? __('emails.gift_voucher.recipient.subject', ['amount' => $amount, 'currency' => $currency]) : __('emails.gift_voucher.purchaser.subject', ['amount' => $amount, 'currency' => $currency]) }} - - -
-

- {{ $forRecipient ? __('emails.gift_voucher.recipient.greeting') : __('emails.gift_voucher.purchaser.greeting') }} -

-

- {!! $forRecipient - ? __('emails.gift_voucher.recipient.body', [ - 'amount' => $amount, - 'currency' => $currency, - 'purchaser' => $voucher->purchaser_email, - ]) - : __('emails.gift_voucher.purchaser.body', [ - 'amount' => $amount, - 'currency' => $currency, - 'recipient' => $voucher->recipient_email ?: __('emails.gift_voucher.purchaser.recipient_fallback'), - ]) - !!} -

+@section('title', $subject) +@section('preheader', $subtitle) +@section('hero_title', $greeting) +@section('hero_subtitle', $subtitle) + +@section('content') +

+ {!! $body !!} +

@if ($voucher->message)
{{ __('emails.gift_voucher.message_title') }}

{{ $voucher->message }}

@endif -
-

{{ __('emails.gift_voucher.code_label') }}

+

+ {{ __('emails.gift_voucher.code_label') }} +

{{ $voucher->code }}
@@ -44,22 +51,18 @@

@isset($printUrl)

- {{ __('emails.gift_voucher.printable') }} + {{ __('emails.gift_voucher.printable') }}

@endisset
-

{{ __('emails.gift_voucher.expiry', ['date' => optional($voucher->expires_at)->toFormattedDateString()]) }}

-

{!! __('emails.gift_voucher.withdrawal', ['url' => $withdrawalUrl]) !!}

+@endsection -

- {!! __('emails.gift_voucher.footer') !!} -

-
- - +@section('footer') + {!! __('emails.gift_voucher.footer') !!} +@endsection diff --git a/resources/views/emails/notifications/basic.blade.php b/resources/views/emails/notifications/basic.blade.php new file mode 100644 index 0000000..7b873e4 --- /dev/null +++ b/resources/views/emails/notifications/basic.blade.php @@ -0,0 +1,40 @@ +@extends('emails.partials.layout') + +@section('title', $title) +@section('preheader', $preheader ?? '') +@section('hero_title', $heroTitle) + +@isset($heroSubtitle) + @section('hero_subtitle', $heroSubtitle) +@endisset + +@section('content') + @isset($intro) +

+ {{ $intro }} +

+ @endisset + @if (! empty($lines)) + + @foreach ($lines as $line) + + + + @endforeach +
{{ $line }}
+ @endif +@endsection + +@section('cta') + @if (! empty($cta)) + @foreach ($cta as $action) + + {{ $action['label'] }} + + @endforeach + @endif +@endsection + +@section('footer') + {!! $footer ?? __('emails.brand.footer') !!} +@endsection diff --git a/resources/views/emails/partials/layout.blade.php b/resources/views/emails/partials/layout.blade.php new file mode 100644 index 0000000..f9514d0 --- /dev/null +++ b/resources/views/emails/partials/layout.blade.php @@ -0,0 +1,55 @@ + + + + + @yield('title') + + + + @yield('preheader', '') + + + + + +
+ + + + + + + + @hasSection('cta') + + + + @endif + + + +
+

+ @yield('brand_label', __('emails.brand.label')) +

+

+ @yield('hero_title') +

+ @hasSection('hero_subtitle') +

+ @yield('hero_subtitle') +

+ @endif +
+ @yield('content') +
+ @yield('cta') +
+ @yield('footer', __('emails.brand.footer')) +
+

+ @yield('brand_footer', __('emails.brand.tagline')) +

+
+ + diff --git a/resources/views/emails/purchase.blade.php b/resources/views/emails/purchase.blade.php index 4ce9fa2..68f1a8d 100644 --- a/resources/views/emails/purchase.blade.php +++ b/resources/views/emails/purchase.blade.php @@ -1,116 +1,75 @@ - - - - {{ __('emails.purchase.subject', ['package' => $packageName]) }} - - - +@extends('emails.partials.layout') + +@section('title', __('emails.purchase.subject', ['package' => $packageName])) +@section('preheader', __('emails.purchase.subtitle')) +@section('hero_title', __('emails.purchase.greeting', ['name' => $user->fullName])) +@section('hero_subtitle', __('emails.purchase.subtitle')) + +@section('content') +

+ {{ __('emails.purchase.summary_title') }} +

+
- + + + + + + + + + + + + + + + + + + + + + +
- - - - - - - - - - - @if (! empty($limits)) - - - - @endif - @if ($invoiceUrl) - - - - @endif - - - - - - -
-

- {{ __('emails.purchase.brand_label') }} -

-

- {{ __('emails.purchase.greeting', ['name' => $user->fullName]) }} -

-

- {{ __('emails.purchase.subtitle') }} -

-
-

- {{ __('emails.purchase.summary_title') }} -

- - - - - - - - - - - - - - - - - - - - - - - - - -
{{ __('emails.purchase.package_label') }}{{ $packageName }}
{{ __('emails.purchase.type_label') }}{{ $packageTypeLabel }}
{{ __('emails.purchase.date_label') }}{{ $purchaseDate }}
{{ __('emails.purchase.provider_label') }}{{ $providerLabel }}
{{ __('emails.purchase.order_label') }}{{ $orderId }}
{{ __('emails.purchase.price_label') }}{{ $priceFormatted }}
-
-
-

- {{ __('emails.purchase.activation_label') }} -

-

- {{ __('emails.purchase.activation') }} -

-
-
-

{{ __('emails.purchase.limits_title') }}

- - @foreach ($limits as $limit) - - - - - @endforeach -
{{ $limit['label'] }}{{ $limit['value'] }}
-
-

{{ __('emails.purchase.invoice_title') }}

-

- - {{ __('emails.purchase.invoice_link') }} - -

-
- - {{ __('emails.purchase.cta') }} - -
- {!! __('emails.purchase.footer') !!} -
-

- {{ __('emails.purchase.brand_footer') }} -

-
{{ __('emails.purchase.package_label') }}{{ $packageName }}
{{ __('emails.purchase.type_label') }}{{ $packageTypeLabel }}
{{ __('emails.purchase.date_label') }}{{ $purchaseDate }}
{{ __('emails.purchase.provider_label') }}{{ $providerLabel }}
{{ __('emails.purchase.order_label') }}{{ $orderId }}
{{ __('emails.purchase.price_label') }}{{ $priceFormatted }}
- - +
+

+ {{ __('emails.purchase.activation_label') }} +

+

+ {{ __('emails.purchase.activation') }} +

+
+ @if (! empty($limits)) +

{{ __('emails.purchase.limits_title') }}

+ + @foreach ($limits as $limit) + + + + + @endforeach +
{{ $limit['label'] }}{{ $limit['value'] }}
+ @endif + @if ($invoiceUrl) +

{{ __('emails.purchase.invoice_title') }}

+

+ + {{ __('emails.purchase.invoice_link') }} + +

+ @endif +@endsection + +@section('cta') + + {{ __('emails.purchase.cta') }} + +@endsection + +@section('footer') + {!! __('emails.purchase.footer') !!} +@endsection diff --git a/resources/views/emails/verify-email.blade.php b/resources/views/emails/verify-email.blade.php new file mode 100644 index 0000000..0c93f94 --- /dev/null +++ b/resources/views/emails/verify-email.blade.php @@ -0,0 +1,29 @@ +@extends('emails.partials.layout') + +@section('title', __('emails.verification.subject')) +@section('preheader', __('emails.verification.preheader')) +@section('hero_title', __('emails.verification.hero_title', ['name' => $user->fullName ?? $user->name ?? $user->email])) +@section('hero_subtitle', __('emails.verification.hero_subtitle')) + +@section('content') +

+ {{ __('emails.verification.body') }} +

+

+ {{ __('emails.verification.expires', ['minutes' => $expiresIn]) }} +

+

+ {{ __('emails.verification.link_fallback') }}
+ {{ $verificationUrl }} +

+@endsection + +@section('cta') + + {{ __('emails.verification.cta') }} + +@endsection + +@section('footer') + {!! __('emails.verification.footer') !!} +@endsection diff --git a/resources/views/emails/welcome.blade.php b/resources/views/emails/welcome.blade.php index 101613e..1f67f57 100644 --- a/resources/views/emails/welcome.blade.php +++ b/resources/views/emails/welcome.blade.php @@ -1,14 +1,49 @@ - - - - {{ __('emails.welcome.subject', ['name' => $user->fullName]) }} - - -

{{ __('emails.welcome.greeting', ['name' => $user->fullName]) }}

-

{{ __('emails.welcome.body') }}

-

{{ __('emails.welcome.username', ['username' => $user->username]) }}

-

{{ __('emails.welcome.email', ['email' => $user->email]) }}

-

{{ __('emails.welcome.verification') }}

-

{!! __('emails.welcome.footer') !!}

- - \ No newline at end of file +@extends('emails.partials.layout') + +@section('title', __('emails.welcome.subject', ['name' => $user->fullName])) +@section('preheader', __('emails.welcome.subtitle')) +@section('hero_title', __('emails.welcome.greeting', ['name' => $user->fullName])) +@section('hero_subtitle', __('emails.welcome.subtitle')) + +@section('content') +

+ {{ __('emails.welcome.body') }} +

+
+

+ {{ __('emails.welcome.account_label') }} +

+

+ {{ __('emails.welcome.username', ['username' => $user->username]) }}
+ {{ __('emails.welcome.email', ['email' => $user->email]) }} +

+
+

+ {{ __('emails.welcome.verification') }} +

+

{{ __('emails.welcome.next_steps_title') }}

+ + + + + + + + + + + + + +
01{{ __('emails.welcome.step_one') }}
02{{ __('emails.welcome.step_two') }}
03{{ __('emails.welcome.step_three') }}
+@endsection + +@section('cta') + + {{ __('emails.welcome.cta') }} + +@endsection + +@section('footer') + {!! __('emails.welcome.footer') !!} +@endsection diff --git a/tests/Feature/BrandedNotificationEmailsTest.php b/tests/Feature/BrandedNotificationEmailsTest.php new file mode 100644 index 0000000..96b6bf0 --- /dev/null +++ b/tests/Feature/BrandedNotificationEmailsTest.php @@ -0,0 +1,71 @@ + 'UploadJob', + 'queue' => 'uploads', + 'event_id' => 123, + 'photo_id' => 456, + 'exception' => 'ExampleException', + ]); + + $mailMessage = $notification->toMail((object) []); + + $this->assertInstanceOf(MailMessage::class, $mailMessage); + $this->assertSame('emails.notifications.basic', $mailMessage->view); + $this->assertArrayHasKey('lines', $mailMessage->viewData); + } + + public function test_inactive_tenant_deletion_warning_uses_branded_view(): void + { + $tenant = Tenant::factory()->create([ + 'name' => 'Demo Tenant', + ]); + + $notification = new InactiveTenantDeletionWarning($tenant, Carbon::now()->addDays(10)); + $mailMessage = $notification->toMail((object) []); + + $this->assertInstanceOf(MailMessage::class, $mailMessage); + $this->assertSame('emails.notifications.basic', $mailMessage->view); + $this->assertArrayHasKey('cta', $mailMessage->viewData); + } + + public function test_refund_receipt_uses_branded_view(): void + { + $purchase = PackagePurchase::factory()->create(); + $notification = new RefundReceipt($purchase); + $mailMessage = $notification->toMail((object) []); + + $this->assertInstanceOf(MailMessage::class, $mailMessage); + $this->assertSame('emails.notifications.basic', $mailMessage->view); + $this->assertArrayHasKey('footer', $mailMessage->viewData); + } + + public function test_ops_purchase_created_uses_branded_view(): void + { + $purchase = PackagePurchase::factory()->create(); + $notification = new PurchaseCreated($purchase); + $mailMessage = $notification->toMail((object) []); + + $this->assertInstanceOf(MailMessage::class, $mailMessage); + $this->assertSame('emails.notifications.basic', $mailMessage->view); + } +} diff --git a/tests/Feature/CustomerEmailRenderTest.php b/tests/Feature/CustomerEmailRenderTest.php new file mode 100644 index 0000000..35c6639 --- /dev/null +++ b/tests/Feature/CustomerEmailRenderTest.php @@ -0,0 +1,119 @@ +setLocale('de'); + + [$user, $tenant, $package] = $this->makeCustomerContext(); + + $welcome = view('emails.welcome', ['user' => $user])->render(); + $this->assertStringContainsString('Die Fotospiel.App', $welcome); + + $abandoned = view('emails.abandoned-checkout', [ + 'user' => $user, + 'package' => $package, + 'packageName' => $package->name, + 'timing' => '1h', + 'resumeUrl' => 'https://example.test/checkout', + ])->render(); + $this->assertStringContainsString($package->name, $abandoned); + + $contact = view('emails.contact-confirmation', ['name' => 'Soren'])->render(); + $this->assertStringContainsString('Soren', $contact); + + $event = Event::factory()->create(['tenant_id' => $tenant->id]); + $eventPackage = EventPackage::create([ + 'event_id' => $event->id, + 'package_id' => $package->id, + 'purchased_price' => 59, + 'purchased_at' => now(), + ]); + $addon = EventPackageAddon::create([ + 'event_package_id' => $eventPackage->id, + 'event_id' => $event->id, + 'tenant_id' => $tenant->id, + 'addon_key' => 'extra_photos', + 'extra_photos' => 250, + 'amount' => 9.0, + 'currency' => 'EUR', + 'status' => 'completed', + 'metadata' => ['label' => 'Extra Fotos'], + 'purchased_at' => now(), + ]); + $receipt = view('emails.addons.receipt', ['addon' => $addon])->render(); + $this->assertStringContainsString('Extra Fotos', $receipt); + + $voucher = GiftVoucher::factory()->create([ + 'message' => 'Herzlichen Glückwunsch!', + ]); + $gift = view('emails.gift-voucher', [ + 'voucher' => $voucher, + 'amount' => number_format((float) $voucher->amount, 2), + 'currency' => $voucher->currency, + 'forRecipient' => false, + 'printUrl' => 'https://example.test/print', + ])->render(); + $this->assertStringContainsString($voucher->code, $gift); + } + + public function test_customer_emails_render_in_en(): void + { + app()->setLocale('en'); + + [$user, $tenant, $package] = $this->makeCustomerContext(); + + $welcome = view('emails.welcome', ['user' => $user])->render(); + $this->assertStringContainsString('Die Fotospiel.App', $welcome); + + $abandoned = view('emails.abandoned-checkout', [ + 'user' => $user, + 'package' => $package, + 'packageName' => $package->name, + 'timing' => '24h', + 'resumeUrl' => 'https://example.test/checkout', + ])->render(); + $this->assertStringContainsString('Resume checkout', $abandoned); + } + + /** + * @return array{0: User, 1: Tenant, 2: Package} + */ + private function makeCustomerContext(): array + { + $user = User::factory()->create([ + 'first_name' => 'Soren', + 'last_name' => 'Eberhardt', + 'username' => 'soren', + ]); + $tenant = Tenant::factory()->create([ + 'user_id' => $user->id, + ]); + $user->forceFill(['tenant_id' => $tenant->id])->save(); + + $package = Package::factory()->create([ + 'name' => 'Standard', + 'type' => 'endcustomer', + 'max_photos' => 500, + 'max_guests' => 200, + 'gallery_days' => 30, + ]); + + return [$user, $tenant, $package]; + } +} diff --git a/tests/Feature/VerifyEmailNotificationTest.php b/tests/Feature/VerifyEmailNotificationTest.php new file mode 100644 index 0000000..f7e22de --- /dev/null +++ b/tests/Feature/VerifyEmailNotificationTest.php @@ -0,0 +1,44 @@ +create([ + 'email_verified_at' => null, + ]); + + $user->sendEmailVerificationNotification(); + + Notification::assertSentTo($user, VerifyEmailNotification::class); + } + + public function test_verify_email_notification_uses_custom_view(): void + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + $notification = new VerifyEmailNotification; + $mailMessage = $notification->toMail($user); + + $this->assertInstanceOf(MailMessage::class, $mailMessage); + $this->assertSame('emails.verify-email', $mailMessage->view); + $this->assertArrayHasKey('verificationUrl', $mailMessage->viewData); + $this->assertArrayHasKey('user', $mailMessage->viewData); + $this->assertSame($user->getKey(), $mailMessage->viewData['user']->getKey()); + } +}