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/be.naniguide.com/tests/Unit/FunnelFlowTest.php
<?php

/**
 * FunnelFlowTest — unit test for funnel lifecycle:
 *   - Create funnel from template (clone steps, clone templates, verify content)
 *   - Add step to funnel
 *   - Delete step from funnel (cleanup template)
 *   - Duplicate funnel (deep copy isolation)
 *
 * Uses real DB with manual cleanup (same pattern as CampaignFlowTest).
 */

use App\Model\Contact;
use App\Model\Customer;
use App\Model\Funnel;
use App\Model\FunnelStep;
use App\Model\Template;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

uses(TestCase::class);

// ─── Cleanup ─────────────────────────────────────────────────────────────────

afterEach(function () {
    // Clean up test funnels, steps, templates created during tests
    $customerIds = DB::table('customers')->where('uid', 'like', 'funnel_test_%')->pluck('id');

    if ($customerIds->isEmpty()) {
        return;
    }

    // Delete funnel steps + their templates
    $funnelIds = DB::table('funnels')->whereIn('customer_id', $customerIds)->pluck('id');
    $stepTemplateIds = DB::table('funnel_steps')->whereIn('funnel_id', $funnelIds)->pluck('template_id')->filter();
    DB::table('funnel_steps')->whereIn('funnel_id', $funnelIds)->delete();
    if ($stepTemplateIds->isNotEmpty()) {
        DB::table('templates')->whereIn('id', $stepTemplateIds)->delete();
    }

    // Delete funnel product links
    DB::table('funnel_product_links')->whereIn('funnel_id', $funnelIds)->delete();

    // Delete funnel domains
    DB::table('funnel_domains')->whereIn('funnel_id', $funnelIds)->delete();

    // Delete funnels (including soft-deleted)
    DB::table('funnels')->whereIn('customer_id', $customerIds)->delete();

    // Delete customer + contact
    $contactIds = DB::table('customers')->whereIn('id', $customerIds)->pluck('contact_id');
    DB::table('customers')->whereIn('id', $customerIds)->delete();
    DB::table('contacts')->whereIn('id', $contactIds)->delete();
});

// ─── Helpers ─────────────────────────────────────────────────────────────────

function funnelTestCustomer(): Customer
{
    $contact = Contact::forceCreate([
        'uid'        => uniqid('funnel_test_'),
        'first_name' => 'Funnel',
        'last_name'  => 'Tester',
        'email'      => uniqid() . '@funnel-test.invalid',
    ]);

    return Customer::forceCreate([
        'uid'        => uniqid('funnel_test_'),
        'contact_id' => $contact->id,
        'status'     => 'active',
        'timezone'   => 'UTC',
    ]);
}

/**
 * Create a template funnel with N steps, each having its own Template with content.
 */
function createTemplateFunnel(array $steps = []): Funnel
{
    $funnel = new Funnel();
    $funnel->name = 'Test Template Funnel';
    $funnel->slug = 'test-template-funnel-' . uniqid();
    $funnel->is_template = true;
    $funnel->status = Funnel::STATUS_ACTIVE;
    $funnel->save();

    foreach ($steps as $i => $stepData) {
        // Create template with content
        $template = new Template();
        $template->name = $stepData['name'] . ' Template';
        $template->content = $stepData['content'];
        $template->json = $stepData['json'] ?? Template::DEFAULT_BUILDER_JSON;
        $template->save();

        // Create step
        $step = new FunnelStep();
        $step->funnel_id = $funnel->id;
        $step->name = $stepData['name'];
        $step->type = $stepData['type'];
        $step->slug = $stepData['slug'] ?? \Illuminate\Support\Str::slug($stepData['name']);
        $step->sort_order = $i;
        $step->is_published = true;
        $step->template_id = $template->id;
        $step->save();
    }

    return $funnel->fresh();
}

// ─── Tests ───────────────────────────────────────────────────────────────────

test('clone from template copies all steps with independent templates', function () {
    $customer = funnelTestCustomer();

    $templateFunnel = createTemplateFunnel([
        [
            'name'    => 'Opt-in Page',
            'type'    => FunnelStep::TYPE_OPTIN,
            'content' => '<h1>Subscribe Now</h1><p>Get our free guide.</p>',
            'json'    => '{"blocks":[]}',
        ],
        [
            'name'    => 'Thank You',
            'type'    => FunnelStep::TYPE_THANK_YOU,
            'content' => '<h1>Thanks!</h1><p>Check your inbox.</p>',
        ],
        [
            'name'    => 'Sales Page',
            'type'    => FunnelStep::TYPE_SALES,
            'content' => '<h1>Buy Our Product</h1><p>Only $49.</p>',
        ],
    ]);

    // Verify template funnel has 3 steps
    expect($templateFunnel->steps)->toHaveCount(3);

    // Clone
    $funnel = Funnel::cloneFrom($templateFunnel, $customer);

    // ── Funnel properties ──
    expect($funnel->id)->not->toBe($templateFunnel->id);
    expect($funnel->uid)->not->toBe($templateFunnel->uid);
    expect($funnel->customer_id)->toBe($customer->id);
    expect($funnel->is_template)->toBeFalse();
    expect($funnel->published_at)->toBeNull();

    // ── Steps cloned ──
    $clonedSteps = $funnel->steps()->orderBy('sort_order')->get();
    expect($clonedSteps)->toHaveCount(3);

    $originalSteps = $templateFunnel->steps()->orderBy('sort_order')->get();

    foreach ($clonedSteps as $i => $clonedStep) {
        $originalStep = $originalSteps[$i];

        // Same name and type
        expect($clonedStep->name)->toBe($originalStep->name);
        expect($clonedStep->type)->toBe($originalStep->type);
        expect($clonedStep->sort_order)->toBe($originalStep->sort_order);

        // Different IDs — not the same record
        expect($clonedStep->id)->not->toBe($originalStep->id);
        expect($clonedStep->uid)->not->toBe($originalStep->uid);

        // Belongs to the new funnel
        expect($clonedStep->funnel_id)->toBe($funnel->id);

        // ── Template is cloned, not shared ──
        expect($clonedStep->template_id)->not->toBeNull();
        expect($clonedStep->template_id)->not->toBe($originalStep->template_id);

        // Content is identical
        expect($clonedStep->template->content)->toBe($originalStep->template->content);
        expect($clonedStep->template->json)->toBe($originalStep->template->json);
    }
});

test('clone from template with no steps produces empty funnel', function () {
    $customer = funnelTestCustomer();
    $templateFunnel = createTemplateFunnel([]);

    $funnel = Funnel::cloneFrom($templateFunnel, $customer);

    expect($funnel->steps)->toHaveCount(0);
    expect($funnel->customer_id)->toBe($customer->id);
    expect($funnel->is_template)->toBeFalse();
});

test('cloned funnel name and slug can be overridden', function () {
    $customer = funnelTestCustomer();
    $templateFunnel = createTemplateFunnel([
        ['name' => 'Step 1', 'type' => FunnelStep::TYPE_OPTIN, 'content' => '<p>Hi</p>'],
    ]);

    $funnel = Funnel::cloneFrom($templateFunnel, $customer);
    $funnel->name = 'My Custom Funnel';
    $funnel->slug = $funnel->generateUniqueSlug('My Custom Funnel');
    $funnel->save();

    expect($funnel->name)->toBe('My Custom Funnel');
    expect($funnel->slug)->toBe('my-custom-funnel');
});

test('add step to funnel creates step with template', function () {
    $customer = funnelTestCustomer();
    $funnel = Funnel::newDefault($customer);
    $funnel->name = 'Add Step Test';
    $funnel->save();

    // Create a template for the step
    $template = new Template();
    $template->name = 'Checkout Template';
    $template->content = '<h1>Checkout</h1>';
    $template->json = Template::DEFAULT_BUILDER_JSON;
    $template->save();

    // Create step
    $step = new FunnelStep();
    $step->funnel_id = $funnel->id;
    $step->template_id = $template->id;
    $validator = $step->saveFromParams([
        'name' => 'Checkout Step',
        'type' => FunnelStep::TYPE_CHECKOUT,
    ]);

    expect($validator->fails())->toBeFalse();
    expect($step->exists)->toBeTrue();
    expect($step->funnel_id)->toBe($funnel->id);
    expect($step->type)->toBe(FunnelStep::TYPE_CHECKOUT);
    expect($step->template_id)->toBe($template->id);
    expect($step->hasTemplate())->toBeTrue();
    expect($step->getTemplateContent())->toBe('<h1>Checkout</h1>');

    // Funnel now has 1 step
    expect($funnel->steps()->count())->toBe(1);
});

test('add step without template and set content later', function () {
    $customer = funnelTestCustomer();
    $funnel = Funnel::newDefault($customer);
    $funnel->name = 'No Template Test';
    $funnel->save();

    // Create step without template
    $step = new FunnelStep();
    $step->funnel_id = $funnel->id;
    $step->saveFromParams([
        'name' => 'Custom Page',
        'type' => FunnelStep::TYPE_CUSTOM,
    ]);

    expect($step->hasTemplate())->toBeFalse();

    // Later: create template and set content (mimics FunnelStepController::updateContent)
    $template = new Template();
    $template->name = $step->name;
    $template->content = '';
    $template->json = Template::DEFAULT_BUILDER_JSON;
    $template->save();

    $step->template_id = $template->id;
    $step->save();
    $step->refresh();

    expect($step->hasTemplate())->toBeTrue();

    // Set content
    $json = Template::DEFAULT_BUILDER_JSON;
    $content = '<div class="landing">Hello World</div>';
    $step->setTemplateContent($content, $json);

    $step->refresh();
    expect($step->getTemplateContent())->toBe($content);
});

test('delete step cleans up its template', function () {
    $customer = funnelTestCustomer();
    $funnel = Funnel::newDefault($customer);
    $funnel->name = 'Delete Step Test';
    $funnel->save();

    // Create template
    $template = new Template();
    $template->name = 'To Be Deleted';
    $template->content = '<p>Temporary</p>';
    $template->json = Template::DEFAULT_BUILDER_JSON;
    $template->save();
    $templateId = $template->id;

    // Create step with template
    $step = new FunnelStep();
    $step->funnel_id = $funnel->id;
    $step->template_id = $templateId;
    $step->saveFromParams([
        'name' => 'Temp Step',
        'type' => FunnelStep::TYPE_CUSTOM,
    ]);
    $stepId = $step->id;

    // Verify exists
    expect(FunnelStep::find($stepId))->not->toBeNull();
    expect(Template::find($templateId))->not->toBeNull();

    // Delete
    $step->deleteAndCleanup();

    // Step is gone
    expect(FunnelStep::find($stepId))->toBeNull();

    // Template is also gone
    expect(Template::find($templateId))->toBeNull();
});

test('delete step without template does not error', function () {
    $customer = funnelTestCustomer();
    $funnel = Funnel::newDefault($customer);
    $funnel->name = 'Delete No Template Test';
    $funnel->save();

    $step = new FunnelStep();
    $step->funnel_id = $funnel->id;
    $step->saveFromParams([
        'name' => 'No Template Step',
        'type' => FunnelStep::TYPE_OPTIN,
    ]);
    $stepId = $step->id;

    // Should not throw
    $step->deleteAndCleanup();

    expect(FunnelStep::find($stepId))->toBeNull();
});

test('duplicate funnel creates independent deep copy', function () {
    $customer = funnelTestCustomer();

    // Create funnel with 2 steps
    $funnel = Funnel::newDefault($customer);
    $funnel->name = 'Original Funnel';
    $funnel->save();

    $htmlContents = [
        '<h1>Opt-in</h1><form>...</form>',
        '<h1>Thanks!</h1><p>Done.</p>',
    ];

    foreach ($htmlContents as $i => $html) {
        $tpl = new Template();
        $tpl->name = "Step $i Template";
        $tpl->content = $html;
        $tpl->json = Template::DEFAULT_BUILDER_JSON;
        $tpl->save();

        $step = new FunnelStep();
        $step->funnel_id = $funnel->id;
        $step->template_id = $tpl->id;
        $step->saveFromParams([
            'name' => $i === 0 ? 'Opt-in' : 'Thank You',
            'type' => $i === 0 ? FunnelStep::TYPE_OPTIN : FunnelStep::TYPE_THANK_YOU,
        ]);
    }

    $funnel->refresh();
    expect($funnel->steps)->toHaveCount(2);

    // Duplicate
    $copy = $funnel->duplicate();

    expect($copy->id)->not->toBe($funnel->id);
    expect($copy->uid)->not->toBe($funnel->uid);
    expect($copy->name)->toBe('Original Funnel (Copy)');
    expect($copy->is_template)->toBeFalse();
    expect($copy->published_at)->toBeNull();

    $copySteps = $copy->steps()->orderBy('sort_order')->get();
    $origSteps = $funnel->steps()->orderBy('sort_order')->get();
    expect($copySteps)->toHaveCount(2);

    foreach ($copySteps as $i => $cs) {
        // Independent records
        expect($cs->id)->not->toBe($origSteps[$i]->id);
        expect($cs->funnel_id)->toBe($copy->id);

        // Independent template with same content
        expect($cs->template_id)->not->toBe($origSteps[$i]->template_id);
        expect($cs->template->content)->toBe($origSteps[$i]->template->content);
    }

    // ── Mutation isolation: changing copy's content doesn't affect original ──
    $copySteps[0]->setTemplateContent('<h1>Modified</h1>', Template::DEFAULT_BUILDER_JSON);
    $origSteps[0]->refresh();
    expect($origSteps[0]->template->content)->toBe('<h1>Opt-in</h1><form>...</form>');
    expect($copySteps[0]->fresh()->template->content)->toBe('<h1>Modified</h1>');
});

test('deleteAndCleanup removes funnel with all steps and templates', function () {
    $customer = funnelTestCustomer();

    $funnel = Funnel::newDefault($customer);
    $funnel->name = 'Full Cleanup Test';
    $funnel->save();
    $funnelId = $funnel->id;

    $templateIds = [];
    $stepIds = [];

    for ($i = 0; $i < 3; $i++) {
        $tpl = new Template();
        $tpl->name = "Cleanup tpl $i";
        $tpl->content = "<p>Step $i</p>";
        $tpl->json = Template::DEFAULT_BUILDER_JSON;
        $tpl->save();
        $templateIds[] = $tpl->id;

        $step = new FunnelStep();
        $step->funnel_id = $funnel->id;
        $step->template_id = $tpl->id;
        $step->saveFromParams([
            'name' => "Step $i",
            'type' => FunnelStep::TYPE_CUSTOM,
        ]);
        $stepIds[] = $step->id;
    }

    // Verify all exist
    expect(FunnelStep::whereIn('id', $stepIds)->count())->toBe(3);
    expect(Template::whereIn('id', $templateIds)->count())->toBe(3);

    // Delete funnel
    $funnel->deleteAndCleanup();

    // Funnel soft-deleted
    expect(Funnel::find($funnelId))->toBeNull();

    // All steps gone
    expect(FunnelStep::whereIn('id', $stepIds)->count())->toBe(0);

    // All templates gone
    expect(Template::whereIn('id', $templateIds)->count())->toBe(0);
});