schickere bestellbestätigung und user role detaults auf "user" gesetzt.
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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<int, array{label: string, value: string}>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::getConnection()->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'");
|
||||
}
|
||||
};
|
||||
@@ -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,<br>Das Fotospiel-Team',
|
||||
'price' => 'Preis: :price',
|
||||
'activation' => 'Ihr Event-Paket ist jetzt im Tenant-Account aktiviert.',
|
||||
'footer' => 'Mit freundlichen Grüßen,<br>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' => [
|
||||
|
||||
@@ -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,<br>The Fotospiel Team',
|
||||
'price' => 'Price: :price',
|
||||
'activation' => 'Your event package is now activated in your tenant account.',
|
||||
'footer' => 'Best regards,<br>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' => [
|
||||
|
||||
@@ -3,11 +3,114 @@
|
||||
<head>
|
||||
<title>{{ __('emails.purchase.subject', ['package' => $packageName]) }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ __('emails.purchase.greeting', ['name' => $user->fullName]) }}</h1>
|
||||
<p>{{ __('emails.purchase.package', ['package' => $packageName]) }}</p>
|
||||
<p>{{ __('emails.purchase.price', ['price' => $priceFormatted]) }}</p>
|
||||
<p>{{ __('emails.purchase.activation') }}</p>
|
||||
<p>{!! __('emails.purchase.footer') !!}</p>
|
||||
<body style="margin:0; padding:0; background-color:#f4f5f7; font-family:Arial, Helvetica, sans-serif; color:#1a1a1a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color:#f4f5f7; padding:32px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color:#ffffff; border-radius:16px; overflow:hidden; box-shadow:0 12px 30px rgba(15, 23, 42, 0.08);">
|
||||
<tr>
|
||||
<td style="background:linear-gradient(135deg,#0f172a,#334155); color:#ffffff; padding:32px;">
|
||||
<p style="margin:0 0 10px; font-size:12px; letter-spacing:0.12em; text-transform:uppercase; opacity:0.7;">
|
||||
{{ __('emails.purchase.brand_label') }}
|
||||
</p>
|
||||
<h1 style="margin:0; font-size:24px; line-height:1.35;">
|
||||
{{ __('emails.purchase.greeting', ['name' => $user->fullName]) }}
|
||||
</h1>
|
||||
<p style="margin:12px 0 0; font-size:15px; opacity:0.9;">
|
||||
{{ __('emails.purchase.subtitle') }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:28px 32px 12px;">
|
||||
<h2 style="margin:0 0 12px; font-size:18px;">
|
||||
{{ __('emails.purchase.summary_title') }}
|
||||
</h2>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:10px 0; font-size:14px; color:#6b7280;">{{ __('emails.purchase.package_label') }}</td>
|
||||
<td style="padding:10px 0; font-size:14px; text-align:right; font-weight:600;">{{ $packageName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 0; font-size:14px; color:#6b7280;">{{ __('emails.purchase.type_label') }}</td>
|
||||
<td style="padding:10px 0; font-size:14px; text-align:right;">{{ $packageTypeLabel }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 0; font-size:14px; color:#6b7280;">{{ __('emails.purchase.date_label') }}</td>
|
||||
<td style="padding:10px 0; font-size:14px; text-align:right;">{{ $purchaseDate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 0; font-size:14px; color:#6b7280;">{{ __('emails.purchase.provider_label') }}</td>
|
||||
<td style="padding:10px 0; font-size:14px; text-align:right;">{{ $providerLabel }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 0; font-size:14px; color:#6b7280;">{{ __('emails.purchase.order_label') }}</td>
|
||||
<td style="padding:10px 0; font-size:14px; text-align:right;">{{ $orderId }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 0; font-size:14px; color:#6b7280;">{{ __('emails.purchase.price_label') }}</td>
|
||||
<td style="padding:10px 0; font-size:16px; text-align:right; font-weight:700;">{{ $priceFormatted }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 32px 18px;">
|
||||
<div style="background-color:#f8fafc; border:1px solid #e2e8f0; border-radius:12px; padding:16px;">
|
||||
<p style="margin:0 0 6px; font-size:13px; text-transform:uppercase; letter-spacing:0.08em; color:#64748b;">
|
||||
{{ __('emails.purchase.activation_label') }}
|
||||
</p>
|
||||
<p style="margin:0; font-size:14px; color:#0f172a;">
|
||||
{{ __('emails.purchase.activation') }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@if (! empty($limits))
|
||||
<tr>
|
||||
<td style="padding:0 32px 18px;">
|
||||
<h3 style="margin:0 0 12px; font-size:16px;">{{ __('emails.purchase.limits_title') }}</h3>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse;">
|
||||
@foreach ($limits as $limit)
|
||||
<tr>
|
||||
<td style="padding:6px 0; font-size:14px; color:#6b7280;">{{ $limit['label'] }}</td>
|
||||
<td style="padding:6px 0; font-size:14px; text-align:right; font-weight:600;">{{ $limit['value'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if ($invoiceUrl)
|
||||
<tr>
|
||||
<td style="padding:0 32px 18px;">
|
||||
<h3 style="margin:0 0 8px; font-size:16px;">{{ __('emails.purchase.invoice_title') }}</h3>
|
||||
<p style="margin:0; font-size:14px; color:#6b7280;">
|
||||
<a href="{{ $invoiceUrl }}" style="color:#1d4ed8; text-decoration:none;">
|
||||
{{ __('emails.purchase.invoice_link') }}
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr>
|
||||
<td style="padding:0 32px 32px;">
|
||||
<a href="{{ $ctaUrl }}" style="display:inline-block; background-color:#111827; color:#ffffff; text-decoration:none; padding:12px 20px; border-radius:999px; font-weight:600; font-size:14px;">
|
||||
{{ __('emails.purchase.cta') }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 32px 32px; font-size:12px; color:#6b7280;">
|
||||
{!! __('emails.purchase.footer') !!}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:16px 0 0; font-size:12px; color:#94a3b8;">
|
||||
{{ __('emails.purchase.brand_footer') }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
45
tests/Feature/CheckoutRegisterRoleTest.php
Normal file
45
tests/Feature/CheckoutRegisterRoleTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Package;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CheckoutRegisterRoleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_checkout_register_sets_user_role_until_purchase(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$package = Package::factory()->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);
|
||||
}
|
||||
}
|
||||
58
tests/Feature/PurchaseConfirmationMailTest.php
Normal file
58
tests/Feature/PurchaseConfirmationMailTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Mail\PurchaseConfirmation;
|
||||
use App\Models\Package;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PurchaseConfirmationMailTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_purchase_confirmation_mail_renders_expected_details(): void
|
||||
{
|
||||
$user = User::factory()->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user