File: /home/xedaptot/ai.naniguide.com/app/Model/AdCampaign.php
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Library\Traits\HasUid;
/**
* @property int $id
* @property string $uid
* @property int $customer_id
* @property string $name
* @property string|null $objective
* @property int|null $ad_creative_id
* @property int|null $mail_list_id
* @property string $status
*/
class AdCampaign extends Model
{
use HasFactory;
use HasUid;
use SoftDeletes;
// Objectives
const OBJECTIVE_AWARENESS = 'awareness';
const OBJECTIVE_TRAFFIC = 'traffic';
const OBJECTIVE_ENGAGEMENT = 'engagement';
const OBJECTIVE_LEADS = 'leads';
const OBJECTIVE_SALES = 'sales';
const OBJECTIVE_APP_INSTALLS = 'app_installs';
const OBJECTIVES = [
self::OBJECTIVE_AWARENESS,
self::OBJECTIVE_TRAFFIC,
self::OBJECTIVE_ENGAGEMENT,
self::OBJECTIVE_LEADS,
self::OBJECTIVE_SALES,
self::OBJECTIVE_APP_INSTALLS,
];
const OBJECTIVE_LABELS = [
self::OBJECTIVE_AWARENESS => 'Brand Awareness',
self::OBJECTIVE_TRAFFIC => 'Website Traffic',
self::OBJECTIVE_ENGAGEMENT => 'Engagement',
self::OBJECTIVE_LEADS => 'Lead Generation',
self::OBJECTIVE_SALES => 'Sales / Conversions',
self::OBJECTIVE_APP_INSTALLS => 'App Installs',
];
// Statuses
const STATUS_DRAFT = 'draft';
const STATUS_REVIEW = 'review';
const STATUS_ACTIVE = 'active';
const STATUS_PAUSED = 'paused';
const STATUS_COMPLETED = 'completed';
const STATUS_ERROR = 'error';
const STATUS_ARCHIVED = 'archived';
const STATUSES = [
self::STATUS_DRAFT,
self::STATUS_REVIEW,
self::STATUS_ACTIVE,
self::STATUS_PAUSED,
self::STATUS_COMPLETED,
self::STATUS_ERROR,
self::STATUS_ARCHIVED,
];
const STATUS_BADGE_MAP = [
self::STATUS_DRAFT => 'orange',
self::STATUS_REVIEW => 'blue',
self::STATUS_ACTIVE => 'green',
self::STATUS_PAUSED => 'orange',
self::STATUS_COMPLETED => 'green',
self::STATUS_ERROR => 'red',
self::STATUS_ARCHIVED => 'blue',
];
// Bid strategies
const BID_AUTO = 'auto';
const BID_LOWEST_COST = 'lowest_cost';
const BID_COST_CAP = 'cost_cap';
const BID_BID_CAP = 'bid_cap';
const BID_STRATEGIES = [
self::BID_AUTO,
self::BID_LOWEST_COST,
self::BID_COST_CAP,
self::BID_BID_CAP,
];
protected $table = 'ad_campaigns';
protected $fillable = [
'uid',
'customer_id',
'name',
'objective',
'ad_creative_id',
'ad_audience_id',
'mail_list_id',
'schedule_start',
'schedule_end',
'daily_budget',
'lifetime_budget',
'budget_currency',
'bid_strategy',
'bid_amount',
'utm_source',
'utm_medium',
'utm_campaign',
'settings',
'published_at',
];
protected $casts = [
'settings' => 'json',
'schedule_start' => 'datetime',
'schedule_end' => 'datetime',
'published_at' => 'datetime',
'daily_budget' => 'decimal:2',
'lifetime_budget' => 'decimal:2',
'bid_amount' => 'decimal:2',
];
// ── Relations ──────────────────────────────────────────────────────────
public function customer()
{
return $this->belongsTo(Customer::class, 'customer_id');
}
public function creative()
{
return $this->belongsTo(AdCreative::class, 'ad_creative_id');
}
/**
* R5.1 — direct audience binding (separate from mail_list which targets
* an Acelle list). Nullable: legacy campaigns leave it null.
*/
public function audience()
{
return $this->belongsTo(AdAudience::class, 'ad_audience_id');
}
public function mailList()
{
return $this->belongsTo(MailList::class, 'mail_list_id');
}
public function campaignPlatforms()
{
return $this->hasMany(AdCampaignPlatform::class, 'ad_campaign_id');
}
public function platforms()
{
return $this->belongsToMany(AdPlatform::class, 'ad_campaign_platforms', 'ad_campaign_id', 'ad_platform_id')
->withPivot('status', 'remote_campaign_id', 'remote_adset_id', 'remote_ad_id')
->withTimestamps();
}
public function activityLogs()
{
return $this->morphMany(AdActivityLog::class, 'loggable');
}
// ── Scopes ─────────────────────────────────────────────────────────────
public function scopeOfCustomer($query, $customerId)
{
return $query->where('customer_id', $customerId);
}
public function scopeActive($query)
{
return $query->where('status', self::STATUS_ACTIVE);
}
public function scopeDraft($query)
{
return $query->where('status', self::STATUS_DRAFT);
}
public function scopeSearch($query, $keyword)
{
if (!$keyword) {
return $query;
}
return $query->where('name', 'like', '%' . $keyword . '%');
}
public function scopeOfStatus($query, $status)
{
if (!$status) {
return $query;
}
return $query->where('status', $status);
}
public function scopeOfObjective($query, $objective)
{
if (!$objective) {
return $query;
}
return $query->where('objective', $objective);
}
// ── Accessors ──────────────────────────────────────────────────────────
public function getObjectiveLabelAttribute(): string
{
return self::OBJECTIVE_LABELS[$this->objective] ?? ucfirst($this->objective);
}
public function getStatusBadgeAttribute(): string
{
return self::STATUS_BADGE_MAP[$this->status] ?? 'blue';
}
public function getBudgetDisplayAttribute(): string
{
if ($this->daily_budget) {
return $this->budget_currency . ' ' . number_format($this->daily_budget, 2) . '/day';
}
if ($this->lifetime_budget) {
return $this->budget_currency . ' ' . number_format($this->lifetime_budget, 2) . ' total';
}
return '-';
}
// ── Helpers ─────────────────────────────────────────────────────────────
public function isDraft(): bool
{
return $this->status === self::STATUS_DRAFT;
}
public function isActive(): bool
{
return $this->status === self::STATUS_ACTIVE;
}
public function isPaused(): bool
{
return $this->status === self::STATUS_PAUSED;
}
public function isArchived(): bool
{
return $this->status === self::STATUS_ARCHIVED;
}
public function getPlatformCount(): int
{
return $this->campaignPlatforms()->count();
}
// ── Factory ─────────────────────────────────────────────────────────────
protected static function newFactory()
{
return \Database\Factories\AdCampaignFactory::new();
}
}