'boolean', 'starts_at' => 'datetime', 'ends_at' => 'datetime', ]; /** Aktif + zamanlaması uygun reklamlar */ public function scopeLive(Builder $q): Builder { return $q->where('is_active', true) ->where(fn($s) => $s->whereNull('starts_at')->orWhere('starts_at', '<=', now())) ->where(fn($s) => $s->whereNull('ends_at')->orWhere('ends_at', '>=', now())); } /** * Medya URL'si — yüklenen dosya veya dış URL. * Yüklenen dosyalar /media/{path} route'undan servis edilir (MediaController); * public/storage symlink'ine bağımlı değil — kapaklar/avatarlarla aynı yol. */ public function getMediaUrlAttribute(): ?string { if ($this->file_path) return MediaUrl::fromStoragePath($this->file_path); if ($this->external_url) return $this->external_url; return null; } /** CTR yüzdesi */ public function getCtrAttribute(): float { return $this->impressions > 0 ? round($this->clicks / $this->impressions * 100, 2) : 0.0; } /** Ağırlıklı rastgele seçim — pre-roll video reklam */ public static function pickVideo(): ?self { return self::weightedPick( self::live()->where('type', 'video')->where('placement', 'preroll')->get() ); } /** Ağırlıklı rastgele seçim — banner (placement bazlı) */ public static function pickBanner(string $placement): ?self { return self::weightedPick( self::live()->where('type', 'banner')->where('placement', $placement)->get() ); } private static function weightedPick($ads): ?self { if ($ads->isEmpty()) return null; $total = max(1, $ads->sum('weight')); $roll = random_int(1, $total); foreach ($ads as $ad) { $roll -= max(1, $ad->weight); if ($roll <= 0) return $ad; } return $ads->first(); } }