diff --git a/app/Http/Controllers/Api/TenantBillingController.php b/app/Http/Controllers/Api/TenantBillingController.php index d8ade7f..91e1583 100644 --- a/app/Http/Controllers/Api/TenantBillingController.php +++ b/app/Http/Controllers/Api/TenantBillingController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\EventPackageAddon; +use App\Services\Paddle\Exceptions\PaddleException; use App\Services\Paddle\PaddleCustomerPortalService; use App\Services\Paddle\PaddleCustomerService; use App\Services\Paddle\PaddleTransactionService; @@ -142,13 +143,36 @@ class TenantBillingController extends Controller ], 404); } + $customerId = null; + try { $customerId = $this->paddleCustomers->ensureCustomerId($tenant); + + Log::debug('Creating Paddle customer portal session', [ + 'tenant_id' => $tenant->id, + 'paddle_customer_id' => $customerId, + 'paddle_environment' => config('paddle.environment'), + 'paddle_base_url' => config('paddle.base_url'), + ]); + $session = $this->portalSessions->createSession($customerId); } catch (\Throwable $exception) { - Log::warning('Failed to create Paddle customer portal session', [ + $context = [ 'tenant_id' => $tenant->id, + 'paddle_customer_id' => $customerId ?? $tenant->paddle_customer_id, 'error' => $exception->getMessage(), + 'paddle_environment' => config('paddle.environment'), + 'paddle_base_url' => config('paddle.base_url'), + ]; + + if ($exception instanceof PaddleException) { + $context['paddle_status'] = $exception->status(); + $context['paddle_error_code'] = Arr::get($exception->context(), 'error.code'); + $context['paddle_error_message'] = Arr::get($exception->context(), 'error.message'); + } + + Log::warning('Failed to create Paddle customer portal session', [ + ...$context, ]); return response()->json([ @@ -162,6 +186,19 @@ class TenantBillingController extends Controller ?? Arr::get($session, 'urls.general'); if (! $url) { + $sessionData = Arr::get($session, 'data'); + $sessionUrls = Arr::get($session, 'data.urls') ?? Arr::get($session, 'urls'); + + Log::warning('Paddle customer portal session missing URL', [ + 'tenant_id' => $tenant->id, + 'paddle_customer_id' => $customerId ?? $tenant->paddle_customer_id, + 'paddle_environment' => config('paddle.environment'), + 'paddle_base_url' => config('paddle.base_url'), + 'session_keys' => array_keys($session), + 'session_data_keys' => is_array($sessionData) ? array_keys($sessionData) : null, + 'session_url_keys' => is_array($sessionUrls) ? array_keys($sessionUrls) : null, + ]); + return response()->json([ 'message' => 'Paddle customer portal session missing URL.', ], 502); diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index 48d5add..59e3211 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Mail\ContactConfirmation; +use App\Mail\ContactRequest; use App\Models\BlogPost; use App\Models\CheckoutSession; use App\Models\Event; @@ -74,17 +75,13 @@ class MarketingController extends Controller } try { - Mail::raw( - __('emails.contact.body', [ - 'name' => $request->name, - 'email' => $request->email, - 'message' => $request->message, - ], $locale), - function ($message) use ($contactAddress, $locale) { - $message->to($contactAddress) - ->subject(__('emails.contact.subject', [], $locale)); - } - ); + Mail::to($contactAddress) + ->locale($locale) + ->send(new ContactRequest( + name: $request->name, + email: $request->email, + messageBody: $request->message, + )); Mail::to($request->email) ->locale($locale) diff --git a/app/Mail/ContactRequest.php b/app/Mail/ContactRequest.php new file mode 100644 index 0000000..1c05a04 --- /dev/null +++ b/app/Mail/ContactRequest.php @@ -0,0 +1,46 @@ +email, $this->name)], + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.contact-request', + with: [ + 'name' => $this->name, + 'email' => $this->email, + 'messageBody' => $this->messageBody, + ], + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 0ff026f..b53683c 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\ResetPasswordNotification; use App\Notifications\VerifyEmailNotification; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasName; @@ -98,6 +99,11 @@ class User extends Authenticatable implements FilamentHasTenants, FilamentUser, $this->notify(new VerifyEmailNotification); } + public function sendPasswordResetNotification($token): void + { + $this->notify(new ResetPasswordNotification($token)); + } + protected function fullName(): Attribute { return Attribute::make( diff --git a/app/Notifications/ResetPasswordNotification.php b/app/Notifications/ResetPasswordNotification.php new file mode 100644 index 0000000..d67bf00 --- /dev/null +++ b/app/Notifications/ResetPasswordNotification.php @@ -0,0 +1,23 @@ +resetUrl($notifiable); + $expire = (int) config('auth.passwords.'.config('auth.defaults.passwords').'.expire', 60); + + return (new MailMessage) + ->subject(__('emails.reset_password.subject')) + ->view('emails.reset-password', [ + 'user' => $notifiable, + 'resetUrl' => $resetUrl, + 'expiresIn' => $expire, + ]); + } +} diff --git a/resources/lang/de/emails.php b/resources/lang/de/emails.php index ecdfbe2..f24af6c 100644 --- a/resources/lang/de/emails.php +++ b/resources/lang/de/emails.php @@ -47,6 +47,27 @@ return [ 'line_time' => 'Zeitpunkt: :time', 'footer' => 'Bitte prüfen Sie die Queue-Logs für weitere Details.', ], + 'reset_password' => [ + 'subject' => 'Passwort zurücksetzen', + 'preheader' => 'Verwenden Sie diesen Link, um Ihr Passwort zurückzusetzen.', + 'hero_title' => 'Passwort zurücksetzen, :name', + 'hero_subtitle' => 'Sichern Sie Ihr Konto in einem Schritt.', + 'body' => 'Klicken Sie auf den Button, um Ihr Passwort zurückzusetzen.', + 'expires' => 'Dieser Link ist :minutes Minuten gültig.', + 'link_fallback' => 'Falls der Button nicht funktioniert, kopieren Sie diesen Link in Ihren Browser:', + 'cta' => 'Passwort zurücksetzen', + 'footer' => 'Wenn Sie kein neues Passwort angefordert haben, können Sie diese E-Mail ignorieren.', + ], + 'contact_request' => [ + 'subject' => 'Neue Kontaktanfrage', + 'preheader' => 'Eine neue Nachricht ist über das Kontaktformular eingegangen.', + 'hero_title' => 'Neue Kontaktanfrage', + 'hero_subtitle' => 'Ein Besucher hat eine neue Nachricht gesendet.', + 'line_name' => 'Name: :name', + 'line_email' => 'E-Mail: :email', + 'line_message' => 'Nachricht:', + 'footer' => 'Antworte direkt auf die Nachricht, um nachzufassen.', + ], 'purchase' => [ 'subject' => 'Kauf-Bestätigung - :package', diff --git a/resources/lang/en/emails.php b/resources/lang/en/emails.php index 82b292a..03c99a7 100644 --- a/resources/lang/en/emails.php +++ b/resources/lang/en/emails.php @@ -46,6 +46,27 @@ return [ 'line_time' => 'Time: :time', 'footer' => 'Please investigate the failure in the queue logs.', ], + 'reset_password' => [ + 'subject' => 'Reset your password', + 'preheader' => 'Use this link to reset your password.', + 'hero_title' => 'Reset your password, :name', + 'hero_subtitle' => 'Secure your account in one step.', + 'body' => 'Click the button below to reset your password.', + 'expires' => 'This password reset link expires in :minutes minutes.', + 'link_fallback' => 'If the button does not work, copy and paste this link into your browser:', + 'cta' => 'Reset password', + 'footer' => 'If you did not request a password reset, you can ignore this email.', + ], + 'contact_request' => [ + 'subject' => 'New contact request', + 'preheader' => 'A new message arrived via the contact form.', + 'hero_title' => 'New contact request', + 'hero_subtitle' => 'A visitor sent a new message.', + 'line_name' => 'Name: :name', + 'line_email' => 'Email: :email', + 'line_message' => 'Message:', + 'footer' => 'Reply directly to the sender to follow up.', + ], 'purchase' => [ 'subject' => 'Purchase Confirmation - :package', diff --git a/resources/views/emails/contact-request.blade.php b/resources/views/emails/contact-request.blade.php new file mode 100644 index 0000000..f8fdda7 --- /dev/null +++ b/resources/views/emails/contact-request.blade.php @@ -0,0 +1,31 @@ +@extends('emails.partials.layout') + +@section('title', __('emails.contact_request.subject')) +@section('preheader', __('emails.contact_request.preheader')) +@section('hero_title', __('emails.contact_request.hero_title')) +@section('hero_subtitle', __('emails.contact_request.hero_subtitle')) + +@section('content') +
| + {{ __('emails.contact_request.line_name', ['name' => $name]) }} + | +
| + {{ __('emails.contact_request.line_email', ['email' => $email]) }} + | +
+ {{ __('emails.contact_request.line_message') }} +
+{{ $messageBody }}
+@endsection + +@section('footer') + {!! __('emails.contact_request.footer') !!} +@endsection diff --git a/resources/views/emails/reset-password.blade.php b/resources/views/emails/reset-password.blade.php new file mode 100644 index 0000000..739ab7a --- /dev/null +++ b/resources/views/emails/reset-password.blade.php @@ -0,0 +1,29 @@ +@extends('emails.partials.layout') + +@section('title', __('emails.reset_password.subject')) +@section('preheader', __('emails.reset_password.preheader')) +@section('hero_title', __('emails.reset_password.hero_title', ['name' => $user->fullName ?? $user->name ?? $user->email])) +@section('hero_subtitle', __('emails.reset_password.hero_subtitle')) + +@section('content') ++ {{ __('emails.reset_password.body') }} +
++ {{ __('emails.reset_password.expires', ['minutes' => $expiresIn]) }} +
+
+ {{ __('emails.reset_password.link_fallback') }}
+ {{ $resetUrl }}
+