Files
fotospiel-app/app/Http/Controllers/Api/TenantPackageController.php
Codex Agent 0291d537fb
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Link tenant packages to events and show usage in billing
2026-02-06 12:54:33 +01:00

217 lines
7.5 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EventPackage;
use App\Models\TenantPackage;
use App\Support\ApiError;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class TenantPackageController extends Controller
{
public function index(Request $request): JsonResponse
{
$tenant = $request->attributes->get('tenant');
if (! $tenant) {
return ApiError::response(
'tenant_not_found',
'Tenant Not Found',
'The authenticated tenant context could not be resolved.',
Response::HTTP_NOT_FOUND
);
}
$packages = TenantPackage::where('tenant_id', $tenant->id)
->with('package')
->orderBy('created_at', 'desc')
->get();
$usageEventPackage = $this->resolveUsageEventPackage($tenant->id);
$linkedEventPackages = $this->resolveLinkedEventPackages($tenant->id, $packages->pluck('id')->all());
$packages->each(function (TenantPackage $package) use ($usageEventPackage, $linkedEventPackages): void {
$eventPackage = $package->active ? $usageEventPackage : null;
$this->hydratePackageSnapshot($package, $eventPackage);
$this->attachUsageEvents($package, $linkedEventPackages);
});
$activePackage = $tenant->getActiveResellerPackage();
if (! ($activePackage instanceof TenantPackage)) {
$activePackage = $packages->firstWhere('active', true);
} else {
$this->hydratePackageSnapshot($activePackage, $usageEventPackage);
$this->attachUsageEvents($activePackage, $linkedEventPackages);
}
return response()->json([
'data' => $packages,
'active_package' => $activePackage,
'message' => 'Tenant packages loaded successfully.',
]);
}
/**
* @param array<int, int> $tenantPackageIds
* @return array<int, array{current: ?EventPackage, last: ?EventPackage, count: int}>
*/
private function resolveLinkedEventPackages(int $tenantId, array $tenantPackageIds): array
{
if ($tenantPackageIds === []) {
return [];
}
$eventPackages = EventPackage::query()
->whereIn('tenant_package_id', $tenantPackageIds)
->whereHas('event', fn ($query) => $query->where('tenant_id', $tenantId))
->with(['event:id,slug,name,date,status'])
->orderByDesc('purchased_at')
->orderByDesc('created_at')
->get()
->groupBy('tenant_package_id');
$result = [];
foreach ($eventPackages as $tenantPackageId => $groupedPackages) {
$current = $groupedPackages
->first(function (EventPackage $eventPackage) {
return $eventPackage->gallery_expires_at && $eventPackage->gallery_expires_at->isFuture();
});
$result[(int) $tenantPackageId] = [
'current' => $current,
'last' => $groupedPackages->first(),
'count' => $groupedPackages->count(),
];
}
return $result;
}
/**
* @param array<int, array{current: ?EventPackage, last: ?EventPackage, count: int}> $linkedEventPackages
*/
private function attachUsageEvents(TenantPackage $package, array $linkedEventPackages): void
{
$usage = $linkedEventPackages[$package->id] ?? null;
if (! $usage) {
$package->linked_events_count = 0;
$package->current_event = null;
$package->last_event = null;
return;
}
$package->linked_events_count = $usage['count'];
$package->current_event = $this->formatLinkedEvent($usage['current']);
$package->last_event = $this->formatLinkedEvent($usage['last']);
}
private function formatLinkedEvent(?EventPackage $eventPackage): ?array
{
if (! $eventPackage || ! $eventPackage->event) {
return null;
}
return [
'id' => $eventPackage->event->id,
'slug' => $eventPackage->event->slug,
'name' => $eventPackage->event->name,
'status' => $eventPackage->event->status,
'event_date' => $eventPackage->event->date?->toIso8601String(),
'linked_at' => $eventPackage->purchased_at?->toIso8601String(),
];
}
private function hydratePackageSnapshot(TenantPackage $package, ?EventPackage $eventPackage = null): void
{
$pkg = $package->package;
$maxEvents = $pkg?->max_events_per_year;
$package->remaining_events = $maxEvents === null ? null : max($maxEvents - $package->used_events, 0);
$package->package_limits = array_merge(
$pkg?->limits ?? [],
$this->buildUsageSnapshot($eventPackage),
[
'included_package_slug' => $pkg?->included_package_slug,
'branding_allowed' => $pkg?->branding_allowed,
'watermark_allowed' => $pkg?->watermark_allowed,
'features' => $pkg?->features ?? [],
]
);
}
/**
* @return Collection<int, EventPackage>
*/
private function resolveUsageEventPackage(int $tenantId): ?EventPackage
{
$baseQuery = EventPackage::query()
->whereHas('event', fn ($query) => $query->where('tenant_id', $tenantId))
->with('package')
->orderByDesc('purchased_at')
->orderByDesc('created_at');
$activeEventPackage = (clone $baseQuery)
->whereNotNull('gallery_expires_at')
->where('gallery_expires_at', '>=', now())
->first();
return $activeEventPackage ?? $baseQuery->first();
}
private function buildUsageSnapshot(?EventPackage $eventPackage): array
{
if (! $eventPackage) {
return [];
}
$limits = $eventPackage->effectiveLimits();
$maxPhotos = $this->normalizeLimit($limits['max_photos'] ?? null);
$maxGuests = $this->normalizeLimit($limits['max_guests'] ?? null);
$galleryDays = $this->normalizeLimit($limits['gallery_days'] ?? null);
$usedPhotos = (int) $eventPackage->used_photos;
$usedGuests = (int) $eventPackage->used_guests;
$remainingPhotos = $maxPhotos === null ? null : max(0, $maxPhotos - $usedPhotos);
$remainingGuests = $maxGuests === null ? null : max(0, $maxGuests - $usedGuests);
$remainingGalleryDays = null;
$usedGalleryDays = null;
if ($galleryDays !== null && $eventPackage->gallery_expires_at) {
$remainingGalleryDays = max(0, now()->diffInDays($eventPackage->gallery_expires_at, false));
$usedGalleryDays = max(0, $galleryDays - $remainingGalleryDays);
}
return array_filter([
'used_photos' => $maxPhotos === null ? null : $usedPhotos,
'remaining_photos' => $remainingPhotos,
'used_guests' => $maxGuests === null ? null : $usedGuests,
'remaining_guests' => $remainingGuests,
'used_gallery_days' => $usedGalleryDays,
'remaining_gallery_days' => $remainingGalleryDays,
], static fn ($value) => $value !== null);
}
private function normalizeLimit(?int $value): ?int
{
if ($value === null) {
return null;
}
$value = (int) $value;
if ($value <= 0) {
return null;
}
return $value;
}
}