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 $tenantPackageIds * @return array */ 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 $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 */ 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; } }