feat(addons): finalize event addon catalog and ai styling upgrade flow
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Models\Tenant;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyCheckoutService;
|
||||
use App\Services\PayPal\Exceptions\PayPalException;
|
||||
use App\Services\PayPal\PayPalOrderService;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -31,8 +32,9 @@ class EventAddonCheckoutService
|
||||
$quantity = max(1, (int) ($payload['quantity'] ?? 1));
|
||||
$acceptedWaiver = (bool) ($payload['accepted_waiver'] ?? false);
|
||||
$acceptedTerms = (bool) ($payload['accepted_terms'] ?? false);
|
||||
$addon = is_string($addonKey) ? $this->catalog->find($addonKey) : null;
|
||||
|
||||
if (! $addonKey || ! $this->catalog->find($addonKey)) {
|
||||
if (! is_string($addonKey) || $addon === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'addon_key' => __('Unbekanntes Add-on.'),
|
||||
]);
|
||||
@@ -42,27 +44,29 @@ class EventAddonCheckoutService
|
||||
|
||||
if (! $event->eventPackage) {
|
||||
throw ValidationException::withMessages([
|
||||
'event' => __('Kein Paket für dieses Event hinterlegt.'),
|
||||
'event' => __('Kein Paket fuer dieses Event hinterlegt.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$provider = $this->resolveProvider();
|
||||
|
||||
if ($provider === CheckoutSession::PROVIDER_PAYPAL) {
|
||||
return $this->createPayPalCheckout($tenant, $event, $addonKey, $quantity, $acceptedTerms, $acceptedWaiver, $payload);
|
||||
return $this->createPayPalCheckout($tenant, $event, $addonKey, $addon, $quantity, $acceptedTerms, $acceptedWaiver, $payload);
|
||||
}
|
||||
|
||||
return $this->createLemonSqueezyCheckout($tenant, $event, $addonKey, $quantity, $acceptedTerms, $acceptedWaiver, $payload);
|
||||
return $this->createLemonSqueezyCheckout($tenant, $event, $addonKey, $addon, $quantity, $acceptedTerms, $acceptedWaiver, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{addon_key: string, quantity?: int, success_url?: string|null, cancel_url?: string|null} $payload
|
||||
* @param array<string, mixed> $addon
|
||||
* @return array{checkout_url: string|null, id: string|null, expires_at: string|null}
|
||||
*/
|
||||
protected function createLemonSqueezyCheckout(
|
||||
Tenant $tenant,
|
||||
Event $event,
|
||||
string $addonKey,
|
||||
array $addon,
|
||||
int $quantity,
|
||||
bool $acceptedTerms,
|
||||
bool $acceptedWaiver,
|
||||
@@ -72,14 +76,18 @@ class EventAddonCheckoutService
|
||||
|
||||
if (! $variantId) {
|
||||
throw ValidationException::withMessages([
|
||||
'addon_key' => __('Für dieses Add-on ist kein Lemon Squeezy Variant hinterlegt.'),
|
||||
'addon_key' => __('Fuer dieses Add-on ist keine Lemon Squeezy Variant hinterlegt.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$addonIntent = (string) Str::uuid();
|
||||
$increments = $this->catalog->resolveIncrements($addonKey);
|
||||
$addonLabel = $this->resolveAddonLabel($addonKey, $addon);
|
||||
$addonMetadata = $this->resolveAddonMetadata($addon);
|
||||
$entitlements = $this->resolveAddonEntitlements($addonKey, $addonMetadata);
|
||||
$price = $this->catalog->resolvePrice($addonKey);
|
||||
|
||||
$metadata = array_filter([
|
||||
$providerMetadata = array_filter([
|
||||
'tenant_id' => (string) $tenant->id,
|
||||
'event_id' => (string) $event->id,
|
||||
'event_package_id' => (string) $event->eventPackage->id,
|
||||
@@ -94,7 +102,7 @@ class EventAddonCheckoutService
|
||||
'cancel_url' => $payload['cancel_url'] ?? null,
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$response = $this->checkout->createVariantCheckout($variantId, $metadata, [
|
||||
$response = $this->checkout->createVariantCheckout($variantId, $providerMetadata, [
|
||||
'success_url' => $payload['success_url'] ?? null,
|
||||
'return_url' => $payload['cancel_url'] ?? null,
|
||||
'customer_email' => $tenant->contact_email ?? $tenant->user?->email,
|
||||
@@ -117,15 +125,19 @@ class EventAddonCheckoutService
|
||||
'checkout_id' => $checkoutId,
|
||||
'transaction_id' => null,
|
||||
'status' => 'pending',
|
||||
'metadata' => array_merge($metadata, [
|
||||
'metadata' => array_filter(array_merge($providerMetadata, [
|
||||
'label' => $addonLabel,
|
||||
'scope' => Arr::get($addonMetadata, 'scope'),
|
||||
'entitlements' => $entitlements === [] ? null : $entitlements,
|
||||
'price_eur' => $price,
|
||||
'increments' => $increments,
|
||||
'provider_payload' => $response,
|
||||
'consents' => [
|
||||
'legal_version' => $metadata['legal_version'],
|
||||
'legal_version' => $providerMetadata['legal_version'],
|
||||
'accepted_terms_at' => $acceptedTerms ? now()->toIso8601String() : null,
|
||||
'digital_content_waiver_at' => $acceptedWaiver ? now()->toIso8601String() : null,
|
||||
],
|
||||
]),
|
||||
]), static fn (mixed $value): bool => $value !== null),
|
||||
'extra_photos' => ($increments['extra_photos'] ?? 0) * $quantity,
|
||||
'extra_guests' => ($increments['extra_guests'] ?? 0) * $quantity,
|
||||
'extra_gallery_days' => ($increments['extra_gallery_days'] ?? 0) * $quantity,
|
||||
@@ -140,23 +152,27 @@ class EventAddonCheckoutService
|
||||
|
||||
/**
|
||||
* @param array{addon_key: string, quantity?: int, success_url?: string|null, cancel_url?: string|null} $payload
|
||||
* @param array<string, mixed> $addon
|
||||
* @return array{checkout_url: string|null, id: string|null, expires_at: string|null}
|
||||
*/
|
||||
protected function createPayPalCheckout(
|
||||
Tenant $tenant,
|
||||
Event $event,
|
||||
string $addonKey,
|
||||
array $addon,
|
||||
int $quantity,
|
||||
bool $acceptedTerms,
|
||||
bool $acceptedWaiver,
|
||||
array $payload,
|
||||
): array {
|
||||
$price = $this->catalog->resolvePrice($addonKey);
|
||||
$addonLabel = $this->catalog->find($addonKey)['label'] ?? $addonKey;
|
||||
$addonLabel = $this->resolveAddonLabel($addonKey, $addon);
|
||||
$addonMetadata = $this->resolveAddonMetadata($addon);
|
||||
$entitlements = $this->resolveAddonEntitlements($addonKey, $addonMetadata);
|
||||
|
||||
if (! $price) {
|
||||
throw ValidationException::withMessages([
|
||||
'addon_key' => __('Dieses Add-on ist aktuell nicht verfügbar.'),
|
||||
'addon_key' => __('Dieses Add-on ist aktuell nicht verfuegbar.'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -181,7 +197,7 @@ class EventAddonCheckoutService
|
||||
'cancel_url' => $payload['cancel_url'] ?? null,
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$addon = EventPackageAddon::create([
|
||||
$addonPurchase = EventPackageAddon::create([
|
||||
'event_package_id' => $event->eventPackage->id,
|
||||
'event_id' => $event->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
@@ -193,14 +209,18 @@ class EventAddonCheckoutService
|
||||
'status' => 'pending',
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'metadata' => array_merge($metadata, [
|
||||
'metadata' => array_filter(array_merge($metadata, [
|
||||
'label' => $addonLabel,
|
||||
'scope' => Arr::get($addonMetadata, 'scope'),
|
||||
'entitlements' => $entitlements === [] ? null : $entitlements,
|
||||
'price_eur' => $price,
|
||||
'increments' => $increments,
|
||||
'consents' => [
|
||||
'legal_version' => $metadata['legal_version'],
|
||||
'accepted_terms_at' => $acceptedTerms ? now()->toIso8601String() : null,
|
||||
'digital_content_waiver_at' => $acceptedWaiver ? now()->toIso8601String() : null,
|
||||
],
|
||||
]),
|
||||
]), static fn (mixed $value): bool => $value !== null),
|
||||
'extra_photos' => ($increments['extra_photos'] ?? 0) * $quantity,
|
||||
'extra_guests' => ($increments['extra_guests'] ?? 0) * $quantity,
|
||||
'extra_gallery_days' => ($increments['extra_gallery_days'] ?? 0) * $quantity,
|
||||
@@ -212,26 +232,26 @@ class EventAddonCheckoutService
|
||||
|
||||
try {
|
||||
$order = $this->paypalOrders->createSimpleOrder(
|
||||
referenceId: 'addon-'.$addon->id,
|
||||
referenceId: 'addon-'.$addonPurchase->id,
|
||||
description: $addonLabel,
|
||||
amount: $amount,
|
||||
currency: $currency,
|
||||
options: [
|
||||
'custom_id' => 'addon_'.$addon->id,
|
||||
'custom_id' => 'addon_'.$addonPurchase->id,
|
||||
'return_url' => $paypalReturnUrl,
|
||||
'cancel_url' => $paypalReturnUrl,
|
||||
'locale' => $tenant->user?->preferred_locale ?? app()->getLocale(),
|
||||
'request_id' => 'addon-'.$addon->id,
|
||||
'request_id' => 'addon-'.$addonPurchase->id,
|
||||
],
|
||||
);
|
||||
} catch (PayPalException $exception) {
|
||||
Log::warning('PayPal addon checkout creation failed', [
|
||||
'addon_id' => $addon->id,
|
||||
'addon_id' => $addonPurchase->id,
|
||||
'message' => $exception->getMessage(),
|
||||
'status' => $exception->status(),
|
||||
]);
|
||||
|
||||
$addon->forceFill([
|
||||
$addonPurchase->forceFill([
|
||||
'status' => 'failed',
|
||||
'error' => $exception->getMessage(),
|
||||
])->save();
|
||||
@@ -244,7 +264,7 @@ class EventAddonCheckoutService
|
||||
$orderId = $order['id'] ?? null;
|
||||
|
||||
if (! is_string($orderId) || $orderId === '') {
|
||||
$addon->forceFill([
|
||||
$addonPurchase->forceFill([
|
||||
'status' => 'failed',
|
||||
'error' => 'PayPal order ID missing.',
|
||||
])->save();
|
||||
@@ -256,9 +276,9 @@ class EventAddonCheckoutService
|
||||
|
||||
$approveUrl = $this->paypalOrders->resolveApproveUrl($order);
|
||||
|
||||
$addon->forceFill([
|
||||
$addonPurchase->forceFill([
|
||||
'checkout_id' => $orderId,
|
||||
'metadata' => array_merge($addon->metadata ?? [], array_filter([
|
||||
'metadata' => array_merge($addonPurchase->metadata ?? [], array_filter([
|
||||
'paypal_order_id' => $orderId,
|
||||
'paypal_approve_url' => $approveUrl,
|
||||
'paypal_success_url' => $successUrl,
|
||||
@@ -291,4 +311,61 @@ class EventAddonCheckoutService
|
||||
{
|
||||
return config('app.legal_version', now()->toDateString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $addon
|
||||
*/
|
||||
protected function resolveAddonLabel(string $addonKey, array $addon): string
|
||||
{
|
||||
$label = trim((string) ($addon['label'] ?? ''));
|
||||
|
||||
return $label !== '' ? $label : $addonKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $addon
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function resolveAddonMetadata(array $addon): array
|
||||
{
|
||||
$metadata = $addon['metadata'] ?? [];
|
||||
|
||||
return is_array($metadata) ? $metadata : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $addonMetadata
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function resolveAddonEntitlements(string $addonKey, array $addonMetadata): array
|
||||
{
|
||||
$entitlements = Arr::get($addonMetadata, 'entitlements', []);
|
||||
$entitlements = is_array($entitlements) ? $entitlements : [];
|
||||
|
||||
$features = Arr::get($entitlements, 'features', []);
|
||||
$features = is_array($features) ? $features : [];
|
||||
|
||||
if ($features === [] && in_array($addonKey, (array) config('ai-editing.entitlements.addon_keys', []), true)) {
|
||||
$requiredFeature = trim((string) config('ai-editing.entitlements.package_feature', 'ai_styling'));
|
||||
|
||||
if ($requiredFeature !== '') {
|
||||
$features[] = $requiredFeature;
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedFeatures = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $feature): string => trim((string) $feature),
|
||||
$features,
|
||||
))));
|
||||
|
||||
if ($normalizedFeatures !== []) {
|
||||
$entitlements['features'] = $normalizedFeatures;
|
||||
}
|
||||
|
||||
if (is_string(Arr::get($entitlements, 'expires_at'))) {
|
||||
$entitlements['expires_at'] = trim((string) $entitlements['expires_at']);
|
||||
}
|
||||
|
||||
return $entitlements;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user