fixed event join token handling in the event admin. created new seeders with new tenants and package purchases. added new playwright test scenarios.

This commit is contained in:
Codex Agent
2025-10-26 14:44:47 +01:00
parent 6290a3a448
commit ecf5a23b28
59 changed files with 3900 additions and 691 deletions

View File

@@ -7,18 +7,16 @@ use App\Models\Package;
use App\Models\PackagePurchase;
use App\Models\Tenant;
use App\Models\TenantPackage;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Stripe\Stripe;
use Stripe\PaymentIntent;
use PayPal\PayPalClient;
use PayPal\Environment\SandboxEnvironment;
use PayPal\Environment\LiveEnvironment;
use PayPal\Checkout\Orders\OrdersCreateRequest;
use PayPal\Checkout\Orders\OrdersCaptureRequest;
use PayPal\Checkout\Orders\OrdersCreateRequest;
use PayPal\Environment\LiveEnvironment;
use PayPal\Environment\SandboxEnvironment;
use PayPal\PayPalClient;
class PackageController extends Controller
{
@@ -30,7 +28,16 @@ class PackageController extends Controller
->get();
$packages->each(function ($package) {
$package->features = json_decode($package->features ?? '[]', true);
if (is_string($package->features)) {
$decoded = json_decode($package->features, true);
$package->features = is_array($decoded) ? $decoded : [];
return;
}
if (! is_array($package->features)) {
$package->features = [];
}
});
return response()->json([
@@ -51,7 +58,7 @@ class PackageController extends Controller
$package = Package::findOrFail($request->package_id);
$tenant = $request->attributes->get('tenant');
if (!$tenant) {
if (! $tenant) {
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
}
@@ -73,7 +80,7 @@ class PackageController extends Controller
$package = Package::findOrFail($request->package_id);
$tenant = $request->attributes->get('tenant');
if (!$tenant) {
if (! $tenant) {
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
}
@@ -105,7 +112,7 @@ class PackageController extends Controller
$package = Package::findOrFail($request->package_id);
$tenant = $request->attributes->get('tenant');
if (!$tenant) {
if (! $tenant) {
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
}
@@ -146,7 +153,7 @@ class PackageController extends Controller
$package = Package::findOrFail($request->package_id);
$tenant = $request->attributes->get('tenant');
if (!$tenant) {
if (! $tenant) {
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
}
@@ -154,7 +161,7 @@ class PackageController extends Controller
throw ValidationException::withMessages(['package' => 'Not a free package.']);
}
DB::transaction(function () use ($request, $package, $tenant) {
DB::transaction(function () use ($package, $tenant) {
PackagePurchase::create([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
@@ -188,7 +195,7 @@ class PackageController extends Controller
$package = Package::findOrFail($request->package_id);
$tenant = $request->attributes->get('tenant');
if (!$tenant) {
if (! $tenant) {
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
}
@@ -202,25 +209,25 @@ class PackageController extends Controller
$client = PayPalClient::client($environment);
$request = new OrdersCreateRequest();
$request = new OrdersCreateRequest;
$request->prefer('return=representation');
$request->body = [
"intent" => "CAPTURE",
"purchase_units" => [[
"amount" => [
"currency_code" => "EUR",
"value" => number_format($package->price, 2, '.', ''),
'intent' => 'CAPTURE',
'purchase_units' => [[
'amount' => [
'currency_code' => 'EUR',
'value' => number_format($package->price, 2, '.', ''),
],
"description" => 'Fotospiel Package: ' . $package->name,
"custom_id" => json_encode([
'description' => 'Fotospiel Package: '.$package->name,
'custom_id' => json_encode([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'user_id' => $tenant->user_id ?? null,
]),
]],
"application_context" => [
"shipping_preference" => "NO_SHIPPING",
"user_action" => "PAY_NOW",
'application_context' => [
'shipping_preference' => 'NO_SHIPPING',
'user_action' => 'PAY_NOW',
],
];
@@ -232,7 +239,7 @@ class PackageController extends Controller
'orderID' => $order->id,
]);
} catch (\Exception $e) {
Log::error('PayPal order creation error: ' . $e->getMessage());
Log::error('PayPal order creation error: '.$e->getMessage());
throw ValidationException::withMessages(['payment' => 'PayPal-Bestellung fehlgeschlagen.']);
}
}
@@ -263,11 +270,11 @@ class PackageController extends Controller
$capture = $response->result;
if ($capture->status !== 'COMPLETED') {
throw new \Exception('PayPal capture not completed: ' . $capture->status);
throw new \Exception('PayPal capture not completed: '.$capture->status);
}
$customId = $capture->purchaseUnits[0]->customId ?? null;
if (!$customId) {
if (! $customId) {
throw new \Exception('No metadata in PayPal order');
}
@@ -275,7 +282,7 @@ class PackageController extends Controller
$tenant = Tenant::find($metadata['tenant_id']);
$package = Package::find($metadata['package_id']);
if (!$tenant || !$package) {
if (! $tenant || ! $package) {
throw new \Exception('Tenant or package not found');
}
@@ -325,8 +332,9 @@ class PackageController extends Controller
return response()->json(['success' => true, 'message' => 'Payment successful']);
} catch (\Exception $e) {
Log::error('PayPal capture error: ' . $e->getMessage(), ['order_id' => $orderId]);
return response()->json(['success' => false, 'message' => 'Capture failed: ' . $e->getMessage()], 422);
Log::error('PayPal capture error: '.$e->getMessage(), ['order_id' => $orderId]);
return response()->json(['success' => false, 'message' => 'Capture failed: '.$e->getMessage()], 422);
}
}
@@ -380,14 +388,16 @@ class PackageController extends Controller
$type = $request->type;
if ($type === 'reseller_subscription') {
$response = (new StripeController())->createSubscription($request);
$response = (new StripeController)->createSubscription($request);
return $response;
} else {
$response = (new StripeController())->createPaymentIntent($request);
$response = (new StripeController)->createPaymentIntent($request);
return $response;
}
}
// Helper for PayPal client - add this if not exists, or use global
// But since SDK has PayPalClient, assume it's used
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Models\Event;
use App\Models\Photo;
use App\Models\Tenant;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
class DashboardController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$tenant = $request->attributes->get('tenant');
if (! $tenant instanceof Tenant) {
$decoded = $request->attributes->get('decoded_token', []);
$tenantId = Arr::get($decoded, 'tenant_id');
if ($tenantId) {
$tenant = Tenant::query()->find($tenantId);
}
}
if (! $tenant instanceof Tenant) {
return response()->json([
'message' => 'Tenant context missing.',
], 401);
}
$eventsQuery = Event::query()
->where('tenant_id', $tenant->getKey());
$activeEvents = (clone $eventsQuery)
->where(fn ($query) => $query
->where('is_active', true)
->orWhere('status', 'published'))
->count();
$upcomingEvents = (clone $eventsQuery)
->whereDate('date', '>=', Carbon::now()->startOfDay())
->count();
$eventsWithTasks = (clone $eventsQuery)
->whereHas('tasks')
->count();
$totalEvents = (clone $eventsQuery)->count();
$taskProgress = $totalEvents > 0
? (int) round(($eventsWithTasks / $totalEvents) * 100)
: 0;
$newPhotos = Photo::query()
->whereHas('event', fn ($query) => $query->where('tenant_id', $tenant->getKey()))
->where('created_at', '>=', Carbon::now()->subDays(7))
->count();
$activePackage = $tenant->tenantPackages()
->with('package')
->where('active', true)
->orderByDesc('expires_at')
->orderByDesc('purchased_at')
->first();
return response()->json([
'active_events' => $activeEvents,
'new_photos' => $newPhotos,
'task_progress' => $taskProgress,
'credit_balance' => $tenant->event_credits_balance ?? null,
'upcoming_events' => $upcomingEvents,
'active_package' => $activePackage ? [
'name' => $activePackage->package?->getNameForLocale(app()->getLocale()) ?? $activePackage->package?->name ?? '',
'expires_at' => optional($activePackage->expires_at)->toIso8601String(),
'remaining_events' => $activePackage->remaining_events ?? null,
] : null,
]);
}
}

View File

@@ -4,11 +4,12 @@ namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\EventStoreRequest;
use App\Http\Resources\Tenant\EventJoinTokenResource;
use App\Http\Resources\Tenant\EventResource;
use App\Models\Event;
use App\Models\Tenant;
use App\Models\EventPackage;
use App\Models\Package;
use App\Models\Photo;
use App\Models\Tenant;
use App\Services\EventJoinTokenService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -21,9 +22,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
class EventController extends Controller
{
public function __construct(private readonly EventJoinTokenService $joinTokenService)
{
}
public function __construct(private readonly EventJoinTokenService $joinTokenService) {}
public function index(Request $request): AnonymousResourceCollection
{
@@ -36,7 +35,12 @@ class EventController extends Controller
}
$query = Event::where('tenant_id', $tenantId)
->with(['eventType', 'photos'])
->with([
'eventType',
'photos',
'eventPackages.package',
'eventPackage.package',
])
->orderBy('created_at', 'desc');
if ($request->has('status')) {
@@ -65,9 +69,31 @@ class EventController extends Controller
$validated = $request->validated();
$tenantId = $tenant->id;
$packageId = $validated['package_id'] ?? 1; // Default to Free package ID 1
$requestedPackageId = $validated['package_id'] ?? null;
unset($validated['package_id']);
$tenantPackage = $tenant->tenantPackages()
->with('package')
->where('active', true)
->orderByDesc('purchased_at')
->first();
$package = null;
if ($requestedPackageId) {
$package = Package::query()->find($requestedPackageId);
}
if (! $package && $tenantPackage) {
$package = $tenantPackage->package ?? Package::query()->find($tenantPackage->package_id);
}
if (! $package) {
throw ValidationException::withMessages([
'package_id' => __('Aktuell ist kein aktives Paket verfügbar. Bitte buche zunächst ein Paket.'),
]);
}
$eventData = array_merge($validated, [
'tenant_id' => $tenantId,
'status' => $validated['status'] ?? 'draft',
@@ -122,37 +148,30 @@ class EventController extends Controller
$eventData = Arr::only($eventData, $allowed);
$event = DB::transaction(function () use ($tenant, $eventData, $packageId) {
$event = DB::transaction(function () use ($tenant, $eventData, $package) {
$event = Event::create($eventData);
$package = \App\Models\Package::findOrFail($packageId);
\App\Models\EventPackage::create([
EventPackage::create([
'event_id' => $event->id,
'package_id' => $packageId,
'price' => $package->price,
'package_id' => $package->id,
'purchased_price' => $package->price,
'purchased_at' => now(),
'gallery_expires_at' => $package->gallery_days ? now()->addDays($package->gallery_days) : null,
]);
\App\Models\PackagePurchase::create([
'tenant_id' => $tenant->id,
'event_id' => $event->id,
'package_id' => $packageId,
'provider_id' => 'free',
'price' => $package->price,
'type' => 'endcustomer_event',
'metadata' => json_encode(['note' => 'Free package assigned on event creation']),
]);
if ($package->isReseller()) {
$note = sprintf('Event #%d created (%s)', $event->id, $event->name);
$note = sprintf('Event #%d created (%s)', $event->id, $event->name);
if (! $tenant->consumeEventAllowance(1, 'event.create', $note)) {
throw new HttpException(402, 'Insufficient credits or package allowance.');
if (! $tenant->consumeEventAllowance(1, 'event.create', $note)) {
throw new HttpException(402, 'Insufficient credits or package allowance.');
}
}
return $event;
});
$tenant->refresh();
$event->load(['eventType', 'tenant', 'eventPackage.package']);
$event->load(['eventType', 'tenant', 'eventPackages.package']);
return response()->json([
'message' => 'Event created successfully',
@@ -175,6 +194,10 @@ class EventController extends Controller
'photos' => fn ($query) => $query->with('likes')->latest(),
'tasks',
'tenant' => fn ($query) => $query->select('id', 'name', 'event_credits_balance'),
'eventPackages' => fn ($query) => $query
->with('package')
->orderByDesc('purchased_at')
->orderByDesc('created_at'),
]);
return response()->json([
@@ -229,7 +252,6 @@ class EventController extends Controller
]);
}
public function stats(Request $request, Event $event): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
@@ -312,6 +334,7 @@ class EventController extends Controller
'join_token' => new EventJoinTokenResource($joinToken),
]);
}
public function bulkUpdateStatus(Request $request): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
@@ -341,7 +364,7 @@ class EventController extends Controller
->where('tenant_id', $tenantId)
->when($excludeId, fn ($query) => $query->where('id', '!=', $excludeId))
->exists()) {
$slug = $originalSlug . '-' . $counter;
$slug = $originalSlug.'-'.$counter;
$counter++;
}

View File

@@ -7,17 +7,15 @@ use App\Http\Resources\Tenant\EventJoinTokenResource;
use App\Models\Event;
use App\Models\EventJoinToken;
use App\Services\EventJoinTokenService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Auth;
class EventJoinTokenController extends Controller
{
public function __construct(private readonly EventJoinTokenService $joinTokenService)
{
}
public function __construct(private readonly EventJoinTokenService $joinTokenService) {}
public function index(Request $request, Event $event): JsonResponse
public function index(Request $request, Event $event): AnonymousResourceCollection
{
$this->authorizeEvent($request, $event);
@@ -48,7 +46,7 @@ class EventJoinTokenController extends Controller
->setStatusCode(201);
}
public function destroy(Request $request, Event $event, EventJoinToken $joinToken): JsonResponse
public function destroy(Request $request, Event $event, EventJoinToken $joinToken): EventJoinTokenResource
{
$this->authorizeEvent($request, $event);

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Resources\Tenant\EventTypeResource;
use App\Models\EventType;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class EventTypeController extends Controller
{
public function __invoke(): AnonymousResourceCollection
{
$eventTypes = EventType::query()
->orderBy('slug')
->get();
return EventTypeResource::collection($eventTypes);
}
}