'decimal:2', 'purchased_at' => 'datetime', 'gallery_expires_at' => 'datetime', 'gallery_warning_sent_at' => 'datetime', 'gallery_expired_notified_at' => 'datetime', 'used_photos' => 'integer', 'used_guests' => 'integer', 'extra_photos' => 'integer', 'extra_guests' => 'integer', 'extra_gallery_days' => 'integer', 'limits_snapshot' => 'array', ]; public function event(): BelongsTo { return $this->belongsTo(Event::class); } public function package(): BelongsTo { return $this->belongsTo(Package::class)->withTrashed(); } public function addons(): HasMany { return $this->hasMany(EventPackageAddon::class); } public function isActive(): bool { return $this->gallery_expires_at && $this->gallery_expires_at->isFuture(); } public function canUploadPhoto(): bool { if (! $this->isActive()) { return false; } $maxPhotos = $this->effectiveLimits()['max_photos']; if ($maxPhotos === null) { return true; } return $this->used_photos < $maxPhotos; } public function canAddGuest(): bool { if (! $this->isActive()) { return false; } $maxGuests = $this->effectiveLimits()['max_guests']; if ($maxGuests === null) { return true; } return $this->used_guests < $maxGuests; } public function getRemainingPhotosAttribute(): int { $limit = $this->effectiveLimits()['max_photos'] ?? 0; return max(0, (int) $limit - $this->used_photos); } public function getRemainingGuestsAttribute(): int { $limit = $this->effectiveLimits()['max_guests'] ?? 0; return max(0, (int) $limit - $this->used_guests); } /** * @return array{max_photos: ?int, max_guests: ?int, gallery_days: ?int, max_tasks: ?int, max_events_per_year: ?int} */ public function effectiveLimits(): array { $snapshot = is_array($this->limits_snapshot) ? $this->limits_snapshot : []; $base = [ 'max_photos' => array_key_exists('max_photos', $snapshot) ? $snapshot['max_photos'] : ($this->package->max_photos ?? null), 'max_guests' => array_key_exists('max_guests', $snapshot) ? $snapshot['max_guests'] : ($this->package->max_guests ?? null), 'gallery_days' => array_key_exists('gallery_days', $snapshot) ? $snapshot['gallery_days'] : ($this->package->gallery_days ?? null), 'max_tasks' => array_key_exists('max_tasks', $snapshot) ? $snapshot['max_tasks'] : ($this->package->max_tasks ?? null), 'max_events_per_year' => array_key_exists('max_events_per_year', $snapshot) ? $snapshot['max_events_per_year'] : ($this->package->max_events_per_year ?? null), ]; $applyExtra = static function (?int $limit, int $extra): ?int { if ($limit === null) { return null; } $safeExtra = max(0, $extra); return max(0, $limit + $safeExtra); }; $maxPhotos = $applyExtra($this->normalizeLimit($base['max_photos']), (int) ($this->extra_photos ?? 0)); $maxGuests = $applyExtra($this->normalizeLimit($base['max_guests']), (int) ($this->extra_guests ?? 0)); return [ 'max_photos' => $maxPhotos, 'max_guests' => $maxGuests, 'gallery_days' => $this->normalizeLimit($base['gallery_days']), 'max_tasks' => $this->normalizeLimit($base['max_tasks']), 'max_events_per_year' => $this->normalizeLimit($base['max_events_per_year']), ]; } public function effectivePhotoLimit(): ?int { return $this->effectiveLimits()['max_photos']; } public function effectiveGuestLimit(): ?int { return $this->effectiveLimits()['max_guests']; } protected static function boot() { parent::boot(); static::creating(function ($eventPackage) { if (! $eventPackage->purchased_at) { $eventPackage->purchased_at = now(); } if (! $eventPackage->gallery_expires_at && $eventPackage->package) { $days = $eventPackage->package->gallery_days ?? 30; $eventPackage->gallery_expires_at = now()->addDays($days); } if (! $eventPackage->limits_snapshot) { $package = $eventPackage->relationLoaded('package') ? $eventPackage->package : Package::query()->find($eventPackage->package_id); if ($package) { $eventPackage->limits_snapshot = array_filter([ 'max_photos' => $package->max_photos, 'max_guests' => $package->max_guests, 'gallery_days' => $package->gallery_days, 'max_tasks' => $package->max_tasks, 'max_events_per_year' => $package->max_events_per_year, ], static fn ($value) => $value !== null); } } }); } private function normalizeLimit($value): ?int { if ($value === null) { return null; } return is_numeric($value) ? (int) $value : null; } }