Files
fotospiel-app/app/Services/Checkout/CheckoutAssignmentService.php
Codex Agent 2e4226a838 Checkout‑Registrierung validiert jetzt die E‑Mail‑Länge, und die Checkout‑Flows sind Paddle‑only: Stripe‑Endpoints/
Services/Helpers sind entfernt, API/Frontend angepasst, Tests auf Paddle umgestellt. Außerdem wurde die CSP gestrafft
  und Stripe‑Texte in den Abandoned‑Checkout‑Mails ersetzt.
2025-12-18 11:14:42 +01:00

200 lines
6.9 KiB
PHP

<?php
namespace App\Services\Checkout;
use App\Mail\PurchaseConfirmation;
use App\Mail\Welcome;
use App\Models\AbandonedCheckout;
use App\Models\CheckoutSession;
use App\Models\Package;
use App\Models\PackagePurchase;
use App\Models\Tenant;
use App\Models\TenantPackage;
use App\Models\User;
use App\Notifications\Ops\PurchaseCreated;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
class CheckoutAssignmentService
{
/**
* Persist the purchase artefacts for a completed checkout session.
*
* @param array{provider_reference?: string, payload?: array} $options
*/
public function finalise(CheckoutSession $session, array $options = []): void
{
DB::transaction(function () use ($session, $options) {
$tenant = $session->tenant;
$user = $session->user;
if (! $tenant && $user) {
$tenant = $this->ensureTenant($user, $session);
}
if (! $tenant) {
Log::warning('Checkout assignment skipped: missing tenant', ['session' => $session->id]);
return;
}
$package = $session->package;
if (! $package) {
Log::warning('Checkout assignment skipped: missing package', ['session' => $session->id]);
return;
}
$metadata = $session->provider_metadata ?? [];
$consents = [
'accepted_terms_at' => optional($session->accepted_terms_at)->toIso8601String(),
'accepted_privacy_at' => optional($session->accepted_privacy_at)->toIso8601String(),
'accepted_withdrawal_notice_at' => optional($session->accepted_withdrawal_notice_at)->toIso8601String(),
'digital_content_waiver_at' => optional($session->digital_content_waiver_at)->toIso8601String(),
'legal_version' => $session->legal_version,
];
$consents = array_filter($consents);
$providerReference = $options['provider_reference']
?? $metadata['paddle_transaction_id'] ?? null
?? $metadata['paddle_checkout_id'] ?? null
?? CheckoutSession::PROVIDER_FREE;
$providerName = $options['provider']
?? $session->provider
?? ($metadata['paddle_transaction_id'] ?? $metadata['paddle_checkout_id'] ? CheckoutSession::PROVIDER_PADDLE : null)
?? CheckoutSession::PROVIDER_FREE;
$purchase = PackagePurchase::updateOrCreate(
[
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'provider_id' => $providerReference,
],
[
'provider' => $providerName,
'price' => $session->amount_total,
'type' => $package->type === 'reseller' ? 'reseller_subscription' : 'endcustomer_event',
'purchased_at' => now(),
'metadata' => array_filter([
'payload' => $options['payload'] ?? null,
'checkout_session_id' => $session->id,
'consents' => $consents ?: null,
]),
]
);
TenantPackage::updateOrCreate(
[
'tenant_id' => $tenant->id,
'package_id' => $package->id,
],
[
'price' => $session->amount_total,
'active' => true,
'purchased_at' => now(),
'expires_at' => $this->resolveExpiry($package, $tenant),
]
);
if ($user && $user->pending_purchase) {
$this->activateUser($user);
}
if ($user) {
$mailLocale = $user->preferred_locale ?? app()->getLocale();
Mail::to($user)
->locale($mailLocale)
->queue(new Welcome($user));
if ($purchase->wasRecentlyCreated) {
Mail::to($user)
->locale($mailLocale)
->queue(new PurchaseConfirmation($purchase));
$opsEmail = config('mail.ops_address');
if ($opsEmail) {
Notification::route('mail', $opsEmail)->notify(new PurchaseCreated($purchase));
}
}
AbandonedCheckout::query()
->where('user_id', $user->id)
->where('package_id', $package->id)
->where('converted', false)
->update([
'converted' => true,
'reminder_stage' => 'converted',
]);
}
Log::info('Checkout session assigned', [
'session' => $session->id,
'tenant' => $tenant->id,
'package' => $package->id,
'purchase' => $purchase->id,
]);
});
}
protected function ensureTenant(User $user, CheckoutSession $session): ?Tenant
{
if ($user->tenant) {
if (! $user->tenant_id) {
$user->forceFill(['tenant_id' => $user->tenant->getKey()])->save();
}
return $user->tenant;
}
$tenant = Tenant::create([
'user_id' => $user->id,
'name' => $session->package_snapshot['name'] ?? $user->name,
'slug' => Str::slug(($user->name ?: $user->email).' '.now()->timestamp),
'email' => $user->email,
'is_active' => true,
'is_suspended' => false,
'subscription_tier' => 'free',
'subscription_status' => 'active',
'settings' => [
'contact_email' => $user->email,
],
]);
if ($user->tenant_id !== $tenant->id) {
$user->forceFill(['tenant_id' => $tenant->id])->save();
}
event(new Registered($user));
return $tenant;
}
protected function resolveExpiry(Package $package, Tenant $tenant)
{
if ($package->type === 'reseller') {
$hasActive = TenantPackage::where('tenant_id', $tenant->id)
->where('active', true)
->exists();
return $hasActive ? now()->addYear() : now()->addDays(14);
}
return now()->addYear();
}
protected function activateUser(User $user): void
{
$user->forceFill([
'email_verified_at' => $user->email_verified_at ?? now(),
'role' => $user->role === 'user' ? 'tenant_admin' : $user->role,
'pending_purchase' => false,
])->save();
}
}