From 886b24b06bd46824c323421cde4003e8fc74180f Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Tue, 23 Dec 2025 10:33:06 +0100 Subject: [PATCH] =?UTF-8?q?schickere=20bestellbest=C3=A4tigung=20und=20use?= =?UTF-8?q?r=20role=20detaults=20auf=20"user"=20gesetzt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/CheckoutController.php | 1 + app/Mail/PurchaseConfirmation.php | 96 +++++++++++++++ ...2001_update_users_role_default_to_user.php | 32 +++++ resources/lang/de/emails.php | 37 +++++- resources/lang/en/emails.php | 37 +++++- resources/views/emails/purchase.blade.php | 115 +++++++++++++++++- tests/Feature/CheckoutRegisterRoleTest.php | 45 +++++++ .../Feature/PurchaseConfirmationMailTest.php | 58 +++++++++ 8 files changed, 409 insertions(+), 12 deletions(-) create mode 100644 database/migrations/2025_12_23_102001_update_users_role_default_to_user.php create mode 100644 tests/Feature/CheckoutRegisterRoleTest.php create mode 100644 tests/Feature/PurchaseConfirmationMailTest.php diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php index bdb8c61..86ac630 100644 --- a/app/Http/Controllers/CheckoutController.php +++ b/app/Http/Controllers/CheckoutController.php @@ -88,6 +88,7 @@ class CheckoutController extends Controller 'address' => $validated['address'], 'phone' => $validated['phone'], 'preferred_locale' => $validated['locale'] ?? null, + 'role' => 'user', 'password' => Hash::make($validated['password']), 'pending_purchase' => true, ]); diff --git a/app/Mail/PurchaseConfirmation.php b/app/Mail/PurchaseConfirmation.php index 55ce775..cbe3d72 100644 --- a/app/Mail/PurchaseConfirmation.php +++ b/app/Mail/PurchaseConfirmation.php @@ -35,6 +35,13 @@ class PurchaseConfirmation extends Mailable 'package' => $this->purchase->package, 'packageName' => $this->localizedPackageName(), 'priceFormatted' => $this->formattedTotal(), + 'purchaseDate' => $this->formattedPurchaseDate(), + 'providerLabel' => $this->providerLabel(), + 'orderId' => $this->purchase->provider_id, + 'packageTypeLabel' => $this->packageTypeLabel(), + 'limits' => $this->packageLimits(), + 'invoiceUrl' => $this->resolveInvoiceUrl(), + 'ctaUrl' => route('tenant.admin.dashboard'), ], ); } @@ -81,6 +88,18 @@ class PurchaseConfirmation extends Mailable return number_format($amount, 2, ',', '.').' '.$symbol; } + private function formattedPurchaseDate(): string + { + $locale = $this->locale ?? app()->getLocale(); + $date = $this->purchase->purchased_at ?? now(); + + if (str_starts_with($locale, 'en')) { + return $date->translatedFormat('F j, Y'); + } + + return $date->translatedFormat('d. F Y'); + } + private function mapLocale(string $locale): string { $normalized = strtolower(str_replace('_', '-', $locale)); @@ -91,4 +110,81 @@ class PurchaseConfirmation extends Mailable default => 'de_DE', }; } + + private function providerLabel(): string + { + $provider = $this->purchase->provider ?? 'paddle'; + $labelKey = 'emails.purchase.provider.'.$provider; + $label = __($labelKey); + + if ($label === $labelKey) { + return ucfirst((string) $provider); + } + + return $label; + } + + private function packageTypeLabel(): string + { + $type = $this->purchase->package?->type ?? 'endcustomer'; + $labelKey = 'emails.purchase.package_type.'.$type; + $label = __($labelKey); + + if ($label === $labelKey) { + return ucfirst((string) $type); + } + + return $label; + } + + /** + * @return array + */ + private function packageLimits(): array + { + $package = $this->purchase->package; + if (! $package) { + return []; + } + + $limits = [ + 'max_photos' => $package->max_photos, + 'max_guests' => $package->max_guests, + 'gallery_days' => $package->gallery_days, + 'max_tasks' => $package->max_tasks, + 'max_events_per_year' => $package->max_events_per_year, + ]; + + $result = []; + + foreach ($limits as $key => $value) { + if (! is_numeric($value) || (int) $value <= 0) { + continue; + } + + $result[] = [ + 'label' => __('emails.purchase.limits.'.$key), + 'value' => number_format((int) $value, 0, ',', '.'), + ]; + } + + return $result; + } + + private function resolveInvoiceUrl(): ?string + { + $payload = $this->purchase->metadata['payload'] ?? []; + + $invoiceUrl = $payload['invoice_url'] ?? null; + if (is_string($invoiceUrl) && $invoiceUrl !== '') { + return $invoiceUrl; + } + + $receiptUrl = $payload['receipt_url'] ?? null; + if (is_string($receiptUrl) && $receiptUrl !== '') { + return $receiptUrl; + } + + return null; + } } diff --git a/database/migrations/2025_12_23_102001_update_users_role_default_to_user.php b/database/migrations/2025_12_23_102001_update_users_role_default_to_user.php new file mode 100644 index 0000000..c68f513 --- /dev/null +++ b/database/migrations/2025_12_23_102001_update_users_role_default_to_user.php @@ -0,0 +1,32 @@ +getDriverName() === 'sqlite') { + return; + } + + DB::statement("ALTER TABLE users ALTER COLUMN role SET DEFAULT 'user'"); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::getConnection()->getDriverName() === 'sqlite') { + return; + } + + DB::statement("ALTER TABLE users ALTER COLUMN role SET DEFAULT 'super_admin'"); + } +}; diff --git a/resources/lang/de/emails.php b/resources/lang/de/emails.php index 9544642..9f0c085 100644 --- a/resources/lang/de/emails.php +++ b/resources/lang/de/emails.php @@ -15,9 +15,40 @@ return [ 'subject' => 'Kauf-Bestätigung - :package', 'greeting' => 'Vielen Dank für Ihren Kauf, :name!', 'package' => 'Package: :package', - 'price' => 'Preis: :price €', - 'activation' => 'Das Package ist nun in Ihrem Tenant-Account aktiviert.', - 'footer' => 'Mit freundlichen Grüßen,
Das Fotospiel-Team', + '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', + 'subtitle' => 'Ihre Bestellung wurde erfolgreich bestätigt. Hier finden Sie alle Details auf einen Blick.', + 'summary_title' => 'Bestellübersicht', + 'package_label' => 'Event-Paket', + 'type_label' => 'Paketart', + 'date_label' => 'Kaufdatum', + 'provider_label' => 'Zahlungsanbieter', + 'order_label' => 'Bestellnummer', + 'price_label' => 'Gesamtbetrag', + 'activation_label' => 'Aktivierung', + 'limits_title' => 'Ihre Paketdetails', + 'invoice_title' => 'Rechnung', + 'invoice_link' => 'Rechnung öffnen', + 'cta' => 'Zum Event-Admin', + 'provider' => [ + 'paddle' => 'Paddle', + 'manual' => 'Manuell', + 'free' => 'Kostenfrei', + ], + 'package_type' => [ + 'endcustomer' => 'Einmalkauf pro Event', + 'reseller' => 'Jahresabo für Reseller', + ], + 'limits' => [ + 'max_photos' => 'Max. Fotos', + 'max_guests' => 'Max. Gäste', + 'gallery_days' => 'Galerie-Tage', + 'max_tasks' => 'Max. Aufgaben', + 'max_events_per_year' => 'Events pro Jahr', + ], ], 'abandoned_checkout' => [ diff --git a/resources/lang/en/emails.php b/resources/lang/en/emails.php index a9f8387..e5ed1c0 100644 --- a/resources/lang/en/emails.php +++ b/resources/lang/en/emails.php @@ -15,9 +15,40 @@ return [ 'subject' => 'Purchase Confirmation - :package', 'greeting' => 'Thank you for your purchase, :name!', 'package' => 'Package: :package', - 'price' => 'Price: :price €', - 'activation' => 'The package is now activated in your tenant account.', - 'footer' => 'Best regards,
The Fotospiel Team', + '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', + 'subtitle' => 'Your order has been confirmed successfully. Here are the details at a glance.', + 'summary_title' => 'Order summary', + 'package_label' => 'Event package', + 'type_label' => 'Package type', + 'date_label' => 'Purchase date', + 'provider_label' => 'Payment provider', + 'order_label' => 'Order ID', + 'price_label' => 'Total amount', + 'activation_label' => 'Activation', + 'limits_title' => 'Your package details', + 'invoice_title' => 'Invoice', + 'invoice_link' => 'Open invoice', + 'cta' => 'Open Event Admin', + 'provider' => [ + 'paddle' => 'Paddle', + 'manual' => 'Manual', + 'free' => 'Free', + ], + 'package_type' => [ + 'endcustomer' => 'One-time purchase per event', + 'reseller' => 'Annual subscription for resellers', + ], + 'limits' => [ + 'max_photos' => 'Max. photos', + 'max_guests' => 'Max. guests', + 'gallery_days' => 'Gallery days', + 'max_tasks' => 'Max. tasks', + 'max_events_per_year' => 'Events per year', + ], ], 'abandoned_checkout' => [ diff --git a/resources/views/emails/purchase.blade.php b/resources/views/emails/purchase.blade.php index 59b73cf..4ce9fa2 100644 --- a/resources/views/emails/purchase.blade.php +++ b/resources/views/emails/purchase.blade.php @@ -3,11 +3,114 @@ {{ __('emails.purchase.subject', ['package' => $packageName]) }} - -

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

-

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

-

{{ __('emails.purchase.price', ['price' => $priceFormatted]) }}

-

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

-

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

+ + + + + +
+ + + + + + + + + + + @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') }} +

+
diff --git a/tests/Feature/CheckoutRegisterRoleTest.php b/tests/Feature/CheckoutRegisterRoleTest.php new file mode 100644 index 0000000..a03226f --- /dev/null +++ b/tests/Feature/CheckoutRegisterRoleTest.php @@ -0,0 +1,45 @@ +create(); + + $payload = [ + 'email' => 'buyer@example.test', + 'username' => 'buyer', + 'password' => 'Password!123', + 'password_confirmation' => 'Password!123', + 'first_name' => 'Soren', + 'last_name' => 'Eberhardt', + 'address' => 'Example Street 1', + 'phone' => '123456789', + 'package_id' => $package->id, + 'terms' => true, + 'privacy_consent' => true, + 'locale' => 'de', + ]; + + $response = $this->postJson('/checkout/register', $payload); + + $response->assertOk(); + + $user = User::where('email', 'buyer@example.test')->first(); + + $this->assertNotNull($user); + $this->assertSame('user', $user->role); + } +} diff --git a/tests/Feature/PurchaseConfirmationMailTest.php b/tests/Feature/PurchaseConfirmationMailTest.php new file mode 100644 index 0000000..700f5ba --- /dev/null +++ b/tests/Feature/PurchaseConfirmationMailTest.php @@ -0,0 +1,58 @@ +create([ + 'first_name' => 'Soren', + 'last_name' => 'Eberhardt', + ]); + $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, + ]); + + $purchase = PackagePurchase::factory()->create([ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + 'provider' => 'paddle', + 'provider_id' => 'txn_123', + 'price' => 59, + 'metadata' => [ + 'payload' => [ + 'invoice_url' => 'https://paddle.test/invoice/123', + ], + 'currency' => 'EUR', + ], + ]); + + $mailable = (new PurchaseConfirmation($purchase))->locale('de'); + $html = $mailable->render(); + + $this->assertStringContainsString('Die Fotospiel.App', $html); + $this->assertStringContainsString('Standard', $html); + $this->assertStringContainsString('txn_123', $html); + $this->assertStringContainsString('https://paddle.test/invoice/123', $html); + } +}