HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/app/Jobs/Ads/AbstractAdsJob.php
<?php

namespace App\Jobs\Ads;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * Base class for every queued Ads job.
 *
 * Enforces production-grade defaults:
 *   - Dedicated `ads` queue (email sends must not share — one Meta outage
 *     should NOT delay email deliveries)
 *   - Webhook callbacks go on separate `ads-webhooks` queue (subclasses
 *     override `$queue` to `'ads-webhooks'`)
 *   - Progressive backoff over 5 attempts up to 4 hours
 *   - Every subclass implements `failed()` — mandatory fatal log + AdActivityLog
 *   - Tags for filtering in Laravel Horizon (or any queue monitor)
 *
 * Acelle ships with `QUEUE_CONNECTION=database` by default. Commercial
 * customers can swap to Redis for scale without touching job code.
 *
 * Subclasses MUST:
 *   - Implement `handle()`
 *   - Implement `failed(Throwable $e)` — call `parent::logFailure($e)` as minimum
 *   - Set `$customerId` + `$platform` properties in constructor when known,
 *     so `tags()` labels are accurate for monitoring
 */
abstract class AbstractAdsJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    // $connection and $queue are declared by Queueable trait WITHOUT defaults.
    // Redeclaring them here would be "incompatible composition". Set via
    // onConnection() / onQueue() in constructor.

    // $tries, $backoff, $timeout, $maxExceptions are NOT declared by any
    // framework trait — they are convention properties Laravel reads via
    // reflection. Declare them here so subclasses (and PHPStan) can see them.

    /** 5 retries over 1m → 5m → 15m → 1h → 4h, covering platform outages. */
    public int $tries = 5;

    /** @var array<int,int> */
    public array $backoff = [60, 300, 900, 3600, 14400];

    /** 5 minutes — must be longer than any single adapter call's timeout. */
    public int $timeout = 300;

    /** Stop retrying after 3 distinct exceptions (separate from $tries). */
    public int $maxExceptions = 3;

    /**
     * Defaults bound in constructor. Subclasses MUST call parent::__construct()
     * when they define their own __construct so queue routing is applied.
     *
     * Queue name comes from config('ads.queues.default') which defaults to
     * 'default' — matches Acelle's out-of-the-box supervisor setup so existing
     * installs keep working after upgrade. Customers opting in to queue
     * isolation set ADS_QUEUE_NAME=ads in .env and reconfigure their worker.
     */
    public function __construct()
    {
        $this->onQueue(config('ads.queues.default', 'default'));
    }

    /**
     * Subclasses populate in constructor for Horizon tag filtering.
     * Nullable — some jobs (PullAdMetrics across all customers) have no single scope.
     */
    protected ?int $customerId = null;
    protected ?string $platform = null;

    /**
     * Tags used by Laravel Horizon (if installed) + custom monitors.
     *
     * @return array<int,string>
     */
    public function tags(): array
    {
        return array_values(array_filter([
            'ads',
            $this->platform ? "platform:{$this->platform}" : null,
            $this->customerId ? "customer:{$this->customerId}" : null,
            class_basename(static::class),
        ]));
    }

    /**
     * Every subclass MUST implement `failed()`. This base helper gives them a
     * standard structured log entry to call from their override:
     *
     *   public function failed(Throwable $e): void
     *   {
     *       $this->logFailure($e);
     *       // job-specific cleanup: mark DB row as 'error', notify customer, etc.
     *   }
     */
    protected function logFailure(Throwable $e): void
    {
        Log::channel(config('ads.observability.log_channel', 'stack'))->error(
            'ads.job.failed',
            [
                'job' => static::class,
                'customer_id' => $this->customerId,
                'platform' => $this->platform,
                'tries' => $this->attempts(),
                'error_class' => $e::class,
                'error_message' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]
        );
    }

    /**
     * Shared implementation marker so subclasses can't accidentally skip failed().
     */
    abstract public function failed(Throwable $exception): void;
}