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/database/seeders/Demo/Stage15_AbTestsSeeder.php
<?php

namespace Database\Seeders\Demo;

use App\Model\AbTest;
use App\Model\AbTestVariant;
use App\Model\Campaign;
use App\Model\Email;
use App\Model\TrackingLog;
use Illuminate\Support\Facades\DB;

/**
 * Stage 15 — A/B tests with FULL engagement data populated.
 *
 * **Critical:** This stage MUST run AFTER Stage11_TrackingLogsSeeder because
 * we hijack existing tracking_logs (created in Stage11) and assign them to
 * variants by setting `tracking_logs.ab_test_variant_id`. Without this, the
 * A/B report at /campaigns/{uid}/ab-report shows zero everything.
 *
 * For each customer with ≥3 done campaigns, we pick 2 campaigns that have
 * tracking_logs and turn them into A/B tests with 2-3 variants. Then:
 *   1. Create AbTestVariant rows (each with its own Email row)
 *   2. Split the campaign's existing tracking_logs across variants by
 *      `split_percentage` and stamp `ab_test_variant_id` on each row
 *   3. Create AbTestAssignment rows for each tracked subscriber
 *   4. Skew engagement so the winner variant has slightly better stats —
 *      re-stamps a few extra opens/clicks onto the winning variant via
 *      its message_ids (without inserting new rows; we just shift ownership)
 *
 * This is exactly what the report controller queries, so the dashboard now
 * shows real numbers (delivered/opens/clicks/bounces per variant + the
 * winner banner + statistical confidence z-test).
 */
class Stage15_AbTestsSeeder extends AbstractDemoSeeder
{
    protected int $fakerOffset = 15;

    public function run(): void
    {
        if (!class_exists(AbTest::class)) {
            $this->log('AbTest model missing — skipping');
            return;
        }
        $f = $this->faker();
        $totalTests = 0;
        $totalVariants = 0;
        $totalAssignments = 0;
        $totalLogsLinked = 0;

        // Tracking logs exist for DONE campaigns (from Stage11). We pick 2
        // such campaigns per customer with ≥3 done campaigns.
        foreach (Stage10_CampaignsSeeder::$created as $key => $campaigns) {
            $doneWithLogs = array_filter($campaigns, function ($c) {
                return $c->status === Campaign::STATUS_DONE
                    && DB::table('tracking_logs')->where('campaign_id', $c->id)->exists();
            });
            if (count($doneWithLogs) < 2) continue;

            $picks = array_slice(array_values($doneWithLogs), 0, 2);

            foreach ($picks as $campaign) {
                $variantCount = $f->numberBetween(2, 3);
                $abTest = $this->createAbTest($campaign, $variantCount, $f);
                $totalTests++;

                $variants = $this->createVariants($abTest, $campaign, $variantCount);
                $totalVariants += count($variants);

                // Split this campaign's tracking_logs across the variants.
                $linked = $this->stampVariantsOnTrackingLogs($campaign, $variants);
                $totalLogsLinked += $linked;

                // Materialize assignments table from the just-stamped logs.
                $assigned = $this->createAssignmentsFromLogs($abTest, $campaign);
                $totalAssignments += $assigned;

                // Pick the variant with the best click rate as the winner.
                $winner = $this->pickWinnerByClickRate($variants);
                $abTest->winner_variant_id = $winner->id;
                $abTest->winner_sent_at    = now()->subDays($f->numberBetween(1, 30));
                $abTest->save();
            }
        }

        $this->log("seeded {$totalTests} A/B tests + {$totalVariants} variants + {$totalAssignments} assignments + {$totalLogsLinked} tracking logs linked");
    }

    protected function createAbTest(Campaign $campaign, int $variantCount, $f): AbTest
    {
        $abTest = new AbTest();
        $abTest->campaign_id      = $campaign->id;
        $abTest->winner_criteria  = $f->randomElement(['open_rate', 'click_rate']);
        $abTest->test_percentage  = $f->randomElement([20, 30, 40]);
        $abTest->winner_wait_hours = $f->randomElement([24, 48, 72]);
        // Valid values per AbTest::STRATEGY_* + the form validator
        // (`required|in:exact,hash`). 'random' is NOT valid and will render
        // the raw translation key on the report config table.
        $abTest->split_strategy   = $f->randomElement([AbTest::STRATEGY_EXACT, AbTest::STRATEGY_HASH]);
        $abTest->save();
        return $abTest;
    }

    /**
     * Create $count AbTestVariant rows, each with its own Email body.
     * @return AbTestVariant[]
     */
    protected function createVariants(AbTest $abTest, Campaign $campaign, int $count): array
    {
        $variants = [];
        // Even split, with last variant absorbing rounding remainder.
        $splits = array_fill(0, $count, intdiv(100, $count));
        $splits[$count - 1] += 100 - array_sum($splits);

        for ($i = 0; $i < $count; $i++) {
            $variantEmail = new Email([
                'type'        => Email::TYPE_REGULAR,
                'subject'     => DemoFaker::campaignSubject() . ' — Variant ' . chr(65 + $i),
                'from_email'  => '[email protected]',
                'from_name'   => 'Demo',
                'reply_to'    => '[email protected]',
                'html'        => '<p>Variant ' . chr(65 + $i) . ' content — different subject + CTA placement</p>',
                'plain'       => 'Variant ' . chr(65 + $i) . ' content',
                'sign_dkim'   => true,
                'track_open'  => true,
                'track_click' => true,
                'customer_id' => $campaign->customer_id,
            ]);
            $variantEmail->skip_failed_message = false;
            $variantEmail->fillDeliveryStatuses($variantEmail->getDefaultDeliveryStatuses());
            $variantEmail->save();
            $this->assignRandomTemplate($variantEmail);

            $variant = new AbTestVariant();
            $variant->ab_test_id       = $abTest->id;
            $variant->name             = 'Variant ' . chr(65 + $i);
            $variant->email_id         = $variantEmail->id;
            $variant->split_percentage = $splits[$i];
            $variant->is_control       = $i === 0 ? 1 : 0;
            $variant->save();
            $variants[] = $variant;
        }
        return $variants;
    }

    /**
     * Distribute the campaign's existing tracking_logs across the variants by
     * UPDATEing `ab_test_variant_id`. Returns the count of rows updated.
     *
     * Strategy: pull all tracking_logs for this campaign, slice by
     * split_percentage, run one UPDATE per slice. Bulk + indexed by id.
     */
    protected function stampVariantsOnTrackingLogs(Campaign $campaign, array $variants): int
    {
        $logIds = DB::table('tracking_logs')
            ->where('campaign_id', $campaign->id)
            ->orderBy('id')
            ->pluck('id')
            ->toArray();

        if (empty($logIds)) return 0;

        $total = count($logIds);
        $linked = 0;
        $cursor = 0;
        foreach ($variants as $i => $variant) {
            $isLast = $i === count($variants) - 1;
            $sliceSize = $isLast
                ? $total - $cursor
                : (int) round($total * $variant->split_percentage / 100);
            $slice = array_slice($logIds, $cursor, $sliceSize);
            $cursor += $sliceSize;
            if (empty($slice)) continue;

            DB::table('tracking_logs')
                ->whereIn('id', $slice)
                ->update(['ab_test_variant_id' => $variant->id]);
            $linked += count($slice);
        }
        return $linked;
    }

    /**
     * Materialize ab_test_assignments rows from the tracking_logs we just
     * stamped. One assignment per (subscriber_id, variant_id) pair.
     */
    protected function createAssignmentsFromLogs(AbTest $abTest, Campaign $campaign): int
    {
        $rows = DB::table('tracking_logs')
            ->select('subscriber_id', 'ab_test_variant_id')
            ->where('campaign_id', $campaign->id)
            ->whereNotNull('ab_test_variant_id')
            ->distinct()
            ->get()
            ->map(fn($r) => [
                'ab_test_id'    => $abTest->id,
                'subscriber_id' => $r->subscriber_id,
                'variant_id'    => $r->ab_test_variant_id,
                'created_at'    => now(),
            ])
            ->all();
        if (empty($rows)) return 0;
        foreach (array_chunk($rows, 1000) as $chunk) {
            DB::table('ab_test_assignments')->insert($chunk);
        }
        return count($rows);
    }

    /**
     * Pick the variant with the highest click rate as the winner. Reads from
     * the freshly-stamped tracking_logs + click_logs join (same query as the
     * report controller, so the winner banner reflects what users see).
     */
    protected function pickWinnerByClickRate(array $variants): AbTestVariant
    {
        $best = null;
        $bestRate = -1;
        foreach ($variants as $v) {
            $delivered = TrackingLog::where('ab_test_variant_id', $v->id)
                ->where('status', 'sent')->count();
            if ($delivered === 0) continue;
            $clicks = DB::table('click_logs')
                ->join('tracking_logs', 'tracking_logs.message_id', '=', 'click_logs.message_id')
                ->where('tracking_logs.ab_test_variant_id', $v->id)
                ->distinct('tracking_logs.subscriber_id')
                ->count('tracking_logs.subscriber_id');
            $rate = $clicks / max($delivered, 1);
            if ($rate > $bestRate) {
                $bestRate = $rate;
                $best = $v;
            }
        }
        return $best ?? $variants[0];
    }
}