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/Model/AbTest.php
<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use App\Library\Traits\HasUid;
use App\Services\AbTest\AssignmentStrategy;
use App\Services\AbTest\ExactSplitStrategy;
use App\Services\AbTest\HashSplitStrategy;

class AbTest extends Model
{
    use HasUid;

    protected $table = 'ab_tests';

    public const CRITERIA_OPEN_RATE  = 'open_rate';
    public const CRITERIA_CLICK_RATE = 'click_rate';
    public const CRITERIA_MANUAL     = 'manual';

    public const STRATEGY_EXACT = 'exact';
    public const STRATEGY_HASH  = 'hash';

    protected $fillable = [
        'uid', 'campaign_id', 'winner_criteria',
        'winner_variant_id', 'test_percentage', 'winner_wait_hours', 'winner_sent_at',
        'split_strategy',
    ];

    protected $casts = [
        'winner_sent_at' => 'datetime',
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Campaign, $this>
     */
    public function campaign(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(Campaign::class);
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany<AbTestVariant, $this>
     */
    public function variants(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(AbTestVariant::class)->orderBy('id');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<AbTestVariant, $this>
     */
    public function winnerVariant(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(AbTestVariant::class, 'winner_variant_id');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany<AbTestAssignment, $this>
     */
    public function assignments(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(AbTestAssignment::class);
    }

    public function isDraft(): bool
    {
        return $this->campaign->status === Campaign::STATUS_NEW;
    }

    public function isRunning(): bool
    {
        return $this->campaign->status === Campaign::STATUS_SENDING;
    }

    public function isAwaitingWinner(): bool
    {
        return $this->campaign->status === Campaign::STATUS_AWAITING_WINNER;
    }

    public function isSendingWinner(): bool
    {
        return $this->campaign->status === Campaign::STATUS_SENDING_WINNER;
    }

    public function isCompleted(): bool
    {
        return $this->campaign->status === Campaign::STATUS_DONE;
    }

    public function isPaused(): bool
    {
        return $this->campaign->isPaused();
    }

    public function isError(): bool
    {
        return $this->campaign->isError();
    }

    /** Test is active (testing, awaiting winner, or sending winner phase). */
    public function isActive(): bool
    {
        return in_array($this->campaign->status, [
            Campaign::STATUS_SENDING,
            Campaign::STATUS_AWAITING_WINNER,
            Campaign::STATUS_SENDING_WINNER,
        ]);
    }

    /** Campaign is in error state AND the error occurred during the test-send phase. */
    public function isErrorDuringTestPhase(): bool
    {
        return $this->campaign->isError()
            && $this->campaign->status === Campaign::STATUS_SENDING;
    }

    /** Campaign is in error state AND the error occurred during winner rollout. */
    public function isErrorDuringWinnerPhase(): bool
    {
        return $this->campaign->isError()
            && $this->campaign->status === Campaign::STATUS_SENDING_WINNER;
    }

    /** Test covers a subset of the list; remaining gets winner after evaluation */
    public function hasRemainingPhase(): bool
    {
        return $this->test_percentage < 100;
    }

    public function resolveStrategy(): AssignmentStrategy
    {
        return match ($this->split_strategy) {
            self::STRATEGY_HASH => new HashSplitStrategy(),
            default             => new ExactSplitStrategy(),
        };
    }

    /**
     * Declare winner and optionally send to remaining subscribers.
     * Shared by EvaluateAbTestWinner job and manual choose-winner controller.
     */
    public function declareWinner(AbTestVariant $winner): void
    {
        $this->winner_variant_id = $winner->id;
        $this->save();

        $campaign = $this->campaign;
        if ($this->hasRemainingPhase()) {
            $campaign->status = Campaign::STATUS_SENDING_WINNER;
            $campaign->save();
            $this->sendWinnerToRemaining($winner);
        } else {
            $campaign->setDone();
        }
    }

    /**
     * Assign remaining subscribers to the winner variant, then re-trigger campaign sending.
     */
    private function sendWinnerToRemaining(AbTestVariant $winner): void
    {
        $campaign = $this->campaign;
        $listIds  = $campaign->listsSegments->pluck('mail_list_id')->unique()->toArray();

        if (empty($listIds)) {
            return;
        }

        $listPlaceholders = implode(',', array_fill(0, count($listIds), '?'));

        \Illuminate\Support\Facades\DB::statement("
            INSERT IGNORE INTO ab_test_assignments (ab_test_id, subscriber_id, variant_id, created_at)
            SELECT ?, s.id, ?, NOW()
            FROM subscribers s
            WHERE s.mail_list_id IN ({$listPlaceholders})
              AND s.status = 'subscribed'
              AND s.email NOT IN (
                  SELECT sub2.email FROM tracking_logs tl
                  JOIN subscribers sub2 ON tl.subscriber_id = sub2.id
                  WHERE tl.campaign_id = ? AND tl.status != 'duplicate'
              )
              AND s.id NOT IN (
                  SELECT subscriber_id FROM ab_test_assignments WHERE ab_test_id = ?
              )
        ", array_merge([$this->id, $winner->id], $listIds, [$campaign->id, $this->id]));

        $this->winner_sent_at = now();
        $this->save();

        $campaign->run($check = false, ACM_QUEUE_TYPE_BATCH);
    }
}