feat(addons): finalize event addon catalog and ai styling upgrade flow
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-07 12:35:07 +01:00
parent 8cc0918881
commit d2808ffa4f
36 changed files with 1372 additions and 457 deletions

View File

@@ -27,21 +27,22 @@ class PackageController extends Controller
public function index(Request $request): JsonResponse
{
$type = $request->query('type', 'endcustomer');
$provider = strtolower((string) config('checkout.default_provider', CheckoutSession::PROVIDER_PAYPAL));
$packages = Package::where('type', $type)
->orderBy('price')
->get();
$packages->each(function ($package) {
$packages->each(function ($package) use ($provider) {
if (is_string($package->features)) {
$decoded = json_decode($package->features, true);
$package->features = is_array($decoded) ? $decoded : [];
return;
}
if (! is_array($package->features)) {
} elseif (! is_array($package->features)) {
$package->features = [];
}
$package->setAttribute('checkout_provider', $provider);
$package->setAttribute('can_checkout', $this->canCheckoutPackage($package, $provider));
});
return response()->json([
@@ -365,4 +366,17 @@ class PackageController extends Controller
'cancel_url' => $cancelUrl,
]);
}
private function canCheckoutPackage(Package $package, string $provider): bool
{
if ((float) $package->price <= 0) {
return true;
}
if ($provider === CheckoutSession::PROVIDER_LEMONSQUEEZY) {
return filled($package->lemonsqueezy_variant_id);
}
return true;
}
}

View File

@@ -4,13 +4,10 @@ namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\EventAddonCheckoutRequest;
use App\Http\Requests\Tenant\EventAddonRequest;
use App\Http\Resources\Tenant\EventResource;
use App\Models\Event;
use App\Services\Addons\EventAddonCheckoutService;
use App\Support\ApiError;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;
class EventAddonController extends Controller
{
@@ -51,64 +48,4 @@ class EventAddonController extends Controller
'expires_at' => $checkout['expires_at'] ?? null,
]);
}
public function apply(EventAddonRequest $request, Event $event): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
if ($event->tenant_id !== $tenantId) {
return ApiError::response(
'event_not_found',
'Event not accessible',
__('Das Event konnte nicht gefunden werden.'),
404,
['event_slug' => $event->slug ?? null]
);
}
$eventPackage = $event->eventPackage;
if (! $eventPackage && method_exists($event, 'eventPackages')) {
$eventPackage = $event->eventPackages()
->with('package')
->orderByDesc('purchased_at')
->orderByDesc('created_at')
->first();
}
if (! $eventPackage) {
return ApiError::response(
'event_package_missing',
'Event package missing',
__('Kein Paket ist diesem Event zugeordnet.'),
409,
['event_slug' => $event->slug ?? null]
);
}
$data = $request->validated();
$eventPackage->fill([
'extra_photos' => ($eventPackage->extra_photos ?? 0) + (int) ($data['extra_photos'] ?? 0),
'extra_guests' => ($eventPackage->extra_guests ?? 0) + (int) ($data['extra_guests'] ?? 0),
'extra_gallery_days' => ($eventPackage->extra_gallery_days ?? 0) + (int) ($data['extend_gallery_days'] ?? 0),
]);
if (isset($data['extend_gallery_days'])) {
$base = $eventPackage->gallery_expires_at ?? Carbon::now();
$eventPackage->gallery_expires_at = $base->copy()->addDays((int) $data['extend_gallery_days']);
}
$eventPackage->save();
$event->load([
'eventPackage.package',
'eventPackages.package',
]);
return response()->json([
'message' => __('Add-ons applied successfully.'),
'data' => new EventResource($event),
]);
}
}

View File

@@ -7,6 +7,7 @@ use App\Http\Requests\Tenant\BillingAddonHistoryRequest;
use App\Models\Event;
use App\Models\EventPackageAddon;
use App\Models\PackagePurchase;
use App\Services\Addons\EventAddonCatalog;
use App\Services\LemonSqueezy\Exceptions\LemonSqueezyException;
use App\Services\LemonSqueezy\LemonSqueezySubscriptionService;
use Dompdf\Dompdf;
@@ -22,6 +23,7 @@ class TenantBillingController extends Controller
{
public function __construct(
private readonly LemonSqueezySubscriptionService $subscriptions,
private readonly EventAddonCatalog $addonCatalog,
) {}
public function transactions(Request $request): JsonResponse
@@ -132,11 +134,19 @@ class TenantBillingController extends Controller
->orderByDesc('created_at')
->paginate($perPage, ['*'], 'page', $page);
$data = $paginator->getCollection()->map(function (EventPackageAddon $addon) {
$addonLabels = collect($this->addonCatalog->all())
->mapWithKeys(fn (array $addon, string $key): array => [$key => $addon['label'] ?? null])
->all();
$data = $paginator->getCollection()->map(function (EventPackageAddon $addon) use ($addonLabels) {
$label = $addon->metadata['label']
?? ($addonLabels[$addon->addon_key] ?? null)
?? $addon->addon_key;
return [
'id' => $addon->id,
'addon_key' => $addon->addon_key,
'label' => $addon->metadata['label'] ?? null,
'label' => $label,
'quantity' => (int) ($addon->quantity ?? 1),
'status' => $addon->status,
'amount' => $addon->amount !== null ? (float) $addon->amount : null,

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Http\Requests\Tenant;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\ValidationException;
class EventAddonRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'extra_photos' => ['nullable', 'integer', 'min:1'],
'extra_guests' => ['nullable', 'integer', 'min:1'],
'extend_gallery_days' => ['nullable', 'integer', 'min:1'],
'reason' => ['nullable', 'string', 'max:255'],
];
}
protected function passedValidation(): void
{
if (
$this->input('extra_photos') === null
&& $this->input('extra_guests') === null
&& $this->input('extend_gallery_days') === null
) {
throw ValidationException::withMessages([
'addons' => __('Please provide at least one add-on to apply.'),
]);
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Resources\Tenant;
use App\Models\WatermarkSetting;
use App\Services\Addons\EventAddonCatalog;
use App\Services\AiEditing\AiStylingEntitlementService;
use App\Services\AiEditing\EventAiEditingPolicyService;
use App\Services\Packages\PackageLimitEvaluator;
@@ -222,11 +223,17 @@ class EventResource extends JsonResource
? $eventPackage->addons
: $eventPackage->addons()->latest()->take(10)->get();
return $addons->map(function ($addon) {
$addonLabels = collect(app(EventAddonCatalog::class)->all())
->mapWithKeys(fn (array $addon, string $key): array => [$key => $addon['label'] ?? null])
->all();
return $addons->map(function ($addon) use ($addonLabels) {
return [
'id' => $addon->id,
'key' => $addon->addon_key,
'label' => $addon->metadata['label'] ?? null,
'label' => $addon->metadata['label']
?? ($addonLabels[$addon->addon_key] ?? null)
?? $addon->addon_key,
'status' => $addon->status,
'variant_id' => $addon->variant_id,
'transaction_id' => $addon->transaction_id,