verschieben des sofortigen verzichts auf das Widerrrufsrecht zum Anlegen des Events

This commit is contained in:
Codex Agent
2025-12-22 13:11:16 +01:00
parent 84234bfb8e
commit c947e638eb
29 changed files with 877 additions and 374 deletions

View File

@@ -8,10 +8,12 @@ use App\Http\Requests\Tenant\EventStoreRequest;
use App\Http\Resources\Tenant\EventJoinTokenResource;
use App\Http\Resources\Tenant\EventResource;
use App\Http\Resources\Tenant\PhotoResource;
use App\Models\CheckoutSession;
use App\Models\Event;
use App\Models\EventPackage;
use App\Models\GuestNotification;
use App\Models\Package;
use App\Models\PackagePurchase;
use App\Models\Photo;
use App\Models\Tenant;
use App\Services\EventJoinTokenService;
@@ -116,6 +118,17 @@ class EventController extends Controller
]);
}
$requiresWaiver = $package->isEndcustomer();
$latestPurchase = $requiresWaiver ? $this->resolveLatestPackagePurchase($tenant, $package) : null;
$existingWaiver = $latestPurchase ? data_get($latestPurchase->metadata, 'consents.digital_content_waiver_at') : null;
$needsWaiver = $requiresWaiver && ! $existingWaiver;
if ($needsWaiver && ! $request->boolean('accepted_waiver')) {
throw ValidationException::withMessages([
'accepted_waiver' => 'Ein sofortiger Beginn der digitalen Dienstleistung erfordert Ihre ausdrückliche Zustimmung.',
]);
}
$eventData = array_merge($validated, [
'tenant_id' => $tenantId,
'status' => $validated['status'] ?? 'draft',
@@ -180,15 +193,21 @@ class EventController extends Controller
'gallery_expires_at' => $package->gallery_days ? now()->addDays($package->gallery_days) : null,
]);
$note = sprintf('Event #%d created (%s)', $event->id, $event->name);
if ($package->isReseller()) {
$note = sprintf('Event #%d created (%s)', $event->id, $event->name);
if (! $tenant->consumeEventAllowance(1, 'event.create', $note)) {
throw new HttpException(402, 'Insufficient package allowance.');
if (! $tenant->consumeEventAllowance(1, 'event.create', $note)) {
throw new HttpException(402, 'Insufficient package allowance.');
}
}
return $event;
});
if ($needsWaiver) {
$this->recordEventStartWaiver($tenant, $package, $latestPurchase);
}
$tenant->refresh();
$event->load(['eventType', 'tenant', 'eventPackages.package', 'eventPackages.addons']);
@@ -200,6 +219,45 @@ class EventController extends Controller
], 201);
}
private function resolveLatestPackagePurchase(Tenant $tenant, Package $package): ?PackagePurchase
{
return PackagePurchase::query()
->where('tenant_id', $tenant->id)
->where('package_id', $package->id)
->orderByDesc('purchased_at')
->orderByDesc('id')
->first();
}
private function recordEventStartWaiver(Tenant $tenant, Package $package, ?PackagePurchase $purchase): void
{
$timestamp = now();
$legalVersion = config('app.legal_version', $timestamp->toDateString());
if ($purchase) {
$metadata = $purchase->metadata ?? [];
$consents = is_array($metadata['consents'] ?? null) ? $metadata['consents'] : [];
$consents['digital_content_waiver_at'] = $timestamp->toIso8601String();
$consents['legal_version'] = $consents['legal_version'] ?? $legalVersion;
$metadata['consents'] = $consents;
$purchase->metadata = $metadata;
$purchase->save();
}
$session = CheckoutSession::query()
->where('tenant_id', $tenant->id)
->where('package_id', $package->id)
->where('status', CheckoutSession::STATUS_COMPLETED)
->orderByDesc('completed_at')
->first();
if ($session && ! $session->digital_content_waiver_at) {
$session->digital_content_waiver_at = $timestamp;
$session->legal_version = $session->legal_version ?? $legalVersion;
$session->save();
}
}
public function show(Request $request, Event $event): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EventPackageAddon;
use App\Services\Paddle\PaddleCustomerPortalService;
use App\Services\Paddle\PaddleCustomerService;
use App\Services\Paddle\PaddleTransactionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -12,7 +14,11 @@ use Illuminate\Support\Facades\Log;
class TenantBillingController extends Controller
{
public function __construct(private readonly PaddleTransactionService $paddleTransactions) {}
public function __construct(
private readonly PaddleTransactionService $paddleTransactions,
private readonly PaddleCustomerService $paddleCustomers,
private readonly PaddleCustomerPortalService $portalSessions,
) {}
public function transactions(Request $request): JsonResponse
{
@@ -116,4 +122,44 @@ class TenantBillingController extends Controller
],
]);
}
public function portal(Request $request): JsonResponse
{
$tenant = $request->attributes->get('tenant');
if (! $tenant) {
return response()->json([
'message' => 'Tenant not found.',
], 404);
}
try {
$customerId = $this->paddleCustomers->ensureCustomerId($tenant);
$session = $this->portalSessions->createSession($customerId);
} catch (\Throwable $exception) {
Log::warning('Failed to create Paddle customer portal session', [
'tenant_id' => $tenant->id,
'error' => $exception->getMessage(),
]);
return response()->json([
'message' => 'Failed to create Paddle customer portal session.',
], 502);
}
$url = Arr::get($session, 'data.urls.general.overview')
?? Arr::get($session, 'data.urls.general')
?? Arr::get($session, 'urls.general.overview')
?? Arr::get($session, 'urls.general');
if (! $url) {
return response()->json([
'message' => 'Paddle customer portal session missing URL.',
], 502);
}
return response()->json([
'url' => $url,
]);
}
}

View File

@@ -219,16 +219,6 @@ class CheckoutController extends Controller
], 422);
}
$requiresWaiver = (bool) ($package->activates_immediately ?? true);
if ($requiresWaiver && ! $request->boolean('accepted_waiver')) {
return response()->json([
'errors' => [
'accepted_waiver' => ['Ein sofortiger Beginn der digitalen Dienstleistung erfordert Ihre ausdrückliche Zustimmung.'],
],
], 422);
}
$session = $sessions->createOrResume($user, $package, [
'tenant' => $user->tenant,
'locale' => $validated['locale'] ?? null,
@@ -241,7 +231,7 @@ class CheckoutController extends Controller
'accepted_terms_at' => $now,
'accepted_privacy_at' => $now,
'accepted_withdrawal_notice_at' => $now,
'digital_content_waiver_at' => $requiresWaiver && $request->boolean('accepted_waiver') ? $now : null,
'digital_content_waiver_at' => null,
'legal_version' => config('app.legal_version', $now->toDateString()),
])->save();

View File

@@ -38,14 +38,6 @@ class PaddleCheckoutController extends Controller
throw ValidationException::withMessages(['package_id' => 'Package is not linked to a Paddle price.']);
}
$requiresWaiver = (bool) ($package->activates_immediately ?? true);
if ($requiresWaiver && ! $request->boolean('accepted_waiver')) {
throw ValidationException::withMessages([
'accepted_waiver' => 'Ein sofortiger Beginn der digitalen Dienstleistung erfordert Ihre ausdrückliche Zustimmung.',
]);
}
$session = $this->sessions->createOrResume($user, $package, [
'tenant' => $tenant,
]);
@@ -58,7 +50,7 @@ class PaddleCheckoutController extends Controller
'accepted_terms_at' => $now,
'accepted_privacy_at' => $now,
'accepted_withdrawal_notice_at' => $now,
'digital_content_waiver_at' => $requiresWaiver ? $now : null,
'digital_content_waiver_at' => null,
'legal_version' => $this->resolveLegalVersion(),
])->save();
@@ -95,7 +87,6 @@ class PaddleCheckoutController extends Controller
'checkout_session_id' => (string) $session->id,
'legal_version' => $session->legal_version,
'accepted_terms' => '1',
'accepted_waiver' => $requiresWaiver && $request->boolean('accepted_waiver') ? '1' : '0',
],
'customer' => array_filter([
'email' => $user->email,
@@ -112,7 +103,6 @@ class PaddleCheckoutController extends Controller
'coupon_code' => $couponCode ?: null,
'legal_version' => $session->legal_version,
'accepted_terms' => true,
'accepted_waiver' => $requiresWaiver && $request->boolean('accepted_waiver'),
],
'discount_id' => $discountId,
]);

View File

@@ -24,7 +24,6 @@ class CheckoutFreeActivationRequest extends FormRequest
return [
'package_id' => ['required', 'exists:packages,id'],
'accepted_terms' => ['required', 'boolean', 'accepted'],
'accepted_waiver' => ['nullable', 'boolean'],
'locale' => ['nullable', 'string', 'max:10'],
];
}

View File

@@ -28,7 +28,6 @@ class PaddleCheckoutRequest extends FormRequest
'inline' => ['sometimes', 'boolean'],
'coupon_code' => ['nullable', 'string', 'max:64'],
'accepted_terms' => ['required', 'boolean', 'accepted'],
'accepted_waiver' => ['sometimes', 'boolean'],
];
}

View File

@@ -67,6 +67,7 @@ class EventStoreRequest extends FormRequest
'settings.watermark.offset_x' => ['nullable', 'integer', 'min:-500', 'max:500'],
'settings.watermark.offset_y' => ['nullable', 'integer', 'min:-500', 'max:500'],
'settings.watermark_serve_originals' => ['nullable', 'boolean'],
'accepted_waiver' => ['nullable', 'boolean'],
];
}

View File

@@ -10,6 +10,15 @@ class PackageLimitEvaluator
{
public function assessEventCreation(Tenant $tenant): ?array
{
$hasEndcustomerPackage = $tenant->tenantPackages()
->where('active', true)
->whereHas('package', fn ($query) => $query->withTrashed()->where('type', 'endcustomer'))
->exists();
if ($hasEndcustomerPackage) {
return null;
}
if ($tenant->hasEventAllowance()) {
return null;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Services\Paddle;
class PaddleCustomerPortalService
{
public function __construct(private readonly PaddleClient $client) {}
/**
* @param array{subscription_ids?: array<int, string>} $options
* @return array<string, mixed>
*/
public function createSession(string $customerId, array $options = []): array
{
$payload = [
'customer_id' => $customerId,
];
if (! empty($options['subscription_ids'])) {
$payload['subscription_ids'] = array_values(
array_filter($options['subscription_ids'], 'is_string')
);
}
return $this->client->post('/customer-portal-sessions', $payload);
}
}