implemented event package addons with filament resource, event-admin purchase path and notifications, showing up in purchase history

This commit is contained in:
Codex Agent
2025-11-21 11:25:45 +01:00
parent 07fe049b8a
commit 7a8d22a238
58 changed files with 3339 additions and 60 deletions

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Services\Addons\EventAddonCatalog;
use Illuminate\Http\JsonResponse;
class EventAddonCatalogController extends Controller
{
public function __construct(private readonly EventAddonCatalog $catalog) {}
public function index(): JsonResponse
{
$addons = collect($this->catalog->all())
->filter(fn (array $addon) => ! empty($addon['price_id']))
->map(fn (array $addon, string $key) => array_merge($addon, ['key' => $key]))
->values()
->all();
return response()->json(['data' => $addons]);
}
}

View File

@@ -0,0 +1,114 @@
<?php
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
{
public function __construct(private readonly EventAddonCheckoutService $checkoutService) {}
public function checkout(EventAddonCheckoutRequest $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 ?: $event->eventPackages()
->orderByDesc('purchased_at')
->orderByDesc('created_at')
->first();
if ($eventPackage) {
$event->setRelation('eventPackage', $eventPackage);
}
$checkout = $this->checkoutService->createCheckout(
$request->attributes->get('tenant'),
$event,
$request->validated(),
);
return response()->json([
'checkout_url' => $checkout['checkout_url'] ?? null,
'checkout_id' => $checkout['id'] ?? null,
'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

@@ -192,7 +192,7 @@ class EventController extends Controller
});
$tenant->refresh();
$event->load(['eventType', 'tenant', 'eventPackages.package']);
$event->load(['eventType', 'tenant', 'eventPackages.package', 'eventPackages.addons']);
return response()->json([
'message' => 'Event created successfully',
@@ -222,7 +222,7 @@ class EventController extends Controller
'tasks',
'tenant' => fn ($query) => $query->select('id', 'name', 'event_credits_balance'),
'eventPackages' => fn ($query) => $query
->with('package')
->with(['package', 'addons'])
->orderByDesc('purchased_at')
->orderByDesc('created_at'),
]);

View File

@@ -3,9 +3,11 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EventPackageAddon;
use App\Services\Paddle\PaddleTransactionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
class TenantBillingController extends Controller
@@ -60,4 +62,58 @@ class TenantBillingController extends Controller
'meta' => $result['meta'],
]);
}
public function addons(Request $request): JsonResponse
{
$tenant = $request->attributes->get('tenant');
if (! $tenant) {
return response()->json([
'data' => [],
'message' => 'Tenant not found.',
], 404);
}
$perPage = max(1, min((int) $request->query('per_page', 25), 100));
$page = max(1, (int) $request->query('page', 1));
$paginator = EventPackageAddon::query()
->where('tenant_id', $tenant->id)
->with(['event:id,name,slug'])
->orderByDesc('purchased_at')
->orderByDesc('created_at')
->paginate($perPage, ['*'], 'page', $page);
$data = $paginator->getCollection()->map(function (EventPackageAddon $addon) {
return [
'id' => $addon->id,
'addon_key' => $addon->addon_key,
'label' => $addon->metadata['label'] ?? null,
'quantity' => (int) ($addon->quantity ?? 1),
'status' => $addon->status,
'amount' => $addon->amount !== null ? (float) $addon->amount : null,
'currency' => $addon->currency,
'extra_photos' => (int) $addon->extra_photos,
'extra_guests' => (int) $addon->extra_guests,
'extra_gallery_days' => (int) $addon->extra_gallery_days,
'purchased_at' => $addon->purchased_at?->toIso8601String(),
'receipt_url' => Arr::get($addon->receipt_payload, 'receipt_url'),
'event' => $addon->event ? [
'id' => $addon->event->id,
'slug' => $addon->event->slug,
'name' => $addon->event->name,
] : null,
];
})->values();
return response()->json([
'data' => $data,
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
],
]);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Services\Addons\EventAddonWebhookService;
use App\Services\Checkout\CheckoutWebhookService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -10,7 +11,10 @@ use Symfony\Component\HttpFoundation\Response;
class PaddleWebhookController extends Controller
{
public function __construct(private readonly CheckoutWebhookService $webhooks) {}
public function __construct(
private readonly CheckoutWebhookService $webhooks,
private readonly EventAddonWebhookService $addonWebhooks,
) {}
public function handle(Request $request): JsonResponse
{
@@ -31,6 +35,7 @@ class PaddleWebhookController extends Controller
if ($eventType) {
$handled = $this->webhooks->handlePaddleEvent($payload);
$handled = $this->addonWebhooks->handle($payload) || $handled;
}
Log::info('Paddle webhook processed', [