File: /home/xedaptot/ai.naniguide.com/app/Jobs/ContinueCampaign.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class ContinueCampaign implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $tries = 1;
public $maxExceptions = 1;
public $failOnTimeout = true;
public $timeout = 60;
protected $campaignId;
protected $campaignClass;
protected $campaignQueue;
protected $customerId;
public function __construct(int $campaignId, string $campaignClass, string $campaignQueue, int $customerId)
{
$this->campaignId = $campaignId;
$this->campaignClass = $campaignClass;
$this->campaignQueue = $campaignQueue;
$this->customerId = $customerId;
}
public function handle(): void
{
$customer = \App\Model\Customer::find($this->customerId);
$campaign = $this->campaignClass::on($customer->getDbConnection())->find($this->campaignId);
if ($campaign->isPaused()) {
$campaign->logger()->warning('ContinueCampaign: campaign is paused, stopping.');
return;
}
$count = $campaign->countSubscribersToSend();
if ($count === 0) {
$campaign->logger()->warning('ContinueCampaign: no contacts left, campaign finishes successfully!');
$this->transitionStatus($campaign);
$campaign->debug(function ($info) {
$finishAt = Carbon::now();
$info['finish_at'] = $finishAt->toString();
$info['total_time'] = $finishAt->diffInSeconds(Carbon::parse($info['start_at']), $abs = true);
return $info;
});
return;
}
$campaign->logger()->warning("ContinueCampaign: {$count} contacts remaining, dispatching next batch.");
// run() with $check = false — campaign is already in 'sending' status
$campaign->run($check = false, $this->campaignQueue);
}
/**
* Single source of truth for campaign status transitions after all
* subscribers have been sent. Handles both A/B test and normal campaigns.
*/
public function failed(\Throwable $exception): void
{
Log::error('[JOB_FAILED] ' . static::class, [
'exception' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'campaign_id' => $this->campaignId,
'campaign_class' => $this->campaignClass,
]);
}
private function transitionStatus($campaign): void
{
if ($campaign->hasAbTest()) {
$abTest = $campaign->abTest;
if ($campaign->isSendingWinner()) {
// Winner rollout finished → mark done
$campaign->setDone();
} elseif ($campaign->isSending() && !$campaign->isError()) {
// Test phase finished → awaiting winner determination
$campaign->is_error = false;
$campaign->is_paused = false;
$campaign->status = \App\Model\Campaign::STATUS_AWAITING_WINNER;
$campaign->save();
// Auto-evaluate winner when:
// 1. Criteria is NOT manual (manual → user picks via UI)
// 2. winner_wait_hours is explicitly set (not null)
// - > 0: dispatch with delay (collect engagement data first)
// - = 0: dispatch immediately (pick winner right away)
// - null: not configured → stay in AWAITING_WINNER until manual action
$isAutoEvaluate = $abTest->winner_criteria !== \App\Model\AbTest::CRITERIA_MANUAL
&& !is_null($abTest->winner_wait_hours);
if ($isAutoEvaluate) {
$waitHours = max(0, (int) $abTest->winner_wait_hours);
$job = \App\Jobs\EvaluateAbTestWinner::dispatch($abTest->id)
->onQueue(ACM_QUEUE_TYPE_BATCH);
if ($waitHours > 0) {
$job->delay(now()->addHours($waitHours));
}
}
}
} elseif (!$campaign->isError()) {
$campaign->setDone();
}
}
}