File: /home/xedaptot/ai.naniguide.com/tests/Feature/AdsR5_1WizardUiTest.php
<?php
/**
* Ads Phase R5.1 — 6-step campaign wizard UI.
*
* R5 added the tenant-scoped FormRequest fields (ad_platform_uids[],
* ad_creative_uid, ad_audience_uid). R5.1 surfaces them as wizard steps
* and wires the service to persist them.
*
* Verifies:
* 1. Migration applied — ad_audience_id column on ad_campaigns.
* 2. AdCampaign casts + relations include ad_audience_id + audience().
* 3. AdCampaignController::create passes platforms + creatives +
* audiences (tenant-scoped, active platforms only).
* 4. AdCampaignService::create resolves ad_creative_uid → ad_creative_id.
* 5. AdCampaignService::create resolves ad_audience_uid → ad_audience_id.
* 6. AdCampaignService::create attaches ad_platform_uids[] via the
* belongsToMany pivot, scoped to current customer + active status.
* 7. AdCampaignService::create with empty/missing wizard fields keeps
* the legacy create flow working (backward compat).
* 8. Wizard view markup carries all 6 steps + R5.1 lang keys.
*/
use App\Model\AdAudience;
use App\Model\AdCampaign;
use App\Model\AdCreative;
use App\Model\AdPlatform;
use App\Model\Customer;
use App\Services\Ads\AdCampaignService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Schema;
uses(Tests\TestCase::class);
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->customer = Customer::whereHas('user')->orderBy('id')->first();
if (! $this->customer) {
$this->markTestSkipped('No seeded customer with user — run DemoSeeder first');
}
$this->user = $this->customer->user;
$this->customerId = $this->customer->local()->id;
});
function r51Platform(int $customerId, string $platformKey = 'meta', string $name = 'R5.1 Platform', string $status = AdPlatform::STATUS_ACTIVE): AdPlatform
{
$platform = new AdPlatform([
'uid' => 'r51plat_' . uniqid(),
'customer_id' => $customerId,
'platform' => $platformKey,
'name' => $name,
'access_token' => 'r51tok_' . uniqid(),
'platform_user_id' => 'p_' . uniqid(),
'platform_user_name' => 'r51user',
]);
$platform->status = $status;
$platform->health_status = AdPlatform::HEALTH_HEALTHY;
$platform->save();
return $platform;
}
// ---------------------------------------------------------------------------
// 1-2. Migration + model
// ---------------------------------------------------------------------------
test('R5.1 migration adds ad_audience_id column to ad_campaigns', function () {
expect(Schema::hasColumn('ad_campaigns', 'ad_audience_id'))->toBeTrue();
});
test('AdCampaign fillable carries ad_audience_id + audience() relation', function () {
$campaign = new AdCampaign();
expect($campaign->getFillable())->toContain('ad_audience_id');
expect(method_exists($campaign, 'audience'))->toBeTrue();
});
// ---------------------------------------------------------------------------
// 3. Controller passes wizard data
// ---------------------------------------------------------------------------
test('create() passes platforms (active) + creatives + audiences to view', function () {
r51Platform($this->customerId, 'meta', 'R5.1 Active Meta', AdPlatform::STATUS_ACTIVE);
r51Platform($this->customerId, 'google', 'R5.1 Stale Google', AdPlatform::STATUS_REVOKED);
AdCreative::create([
'uid' => 'r51cr_' . uniqid(),
'customer_id' => $this->customerId,
'name' => 'R5.1 Test Creative',
'type' => 'image',
'status' => 'active',
]);
AdAudience::create([
'uid' => 'r51aud_' . uniqid(),
'customer_id' => $this->customerId,
'name' => 'R5.1 Test Audience',
'type' => 'custom',
'status' => 'active',
]);
$response = $this->actingAs($this->user)->get(route('refactor.ads.campaigns.create'));
$response->assertOk();
$response->assertSee('R5.1 Active Meta');
$response->assertDontSee('R5.1 Stale Google');
$response->assertSee('R5.1 Test Creative');
$response->assertSee('R5.1 Test Audience');
});
// ---------------------------------------------------------------------------
// 4-6. Service::create wires wizard fields to the model
// ---------------------------------------------------------------------------
test('Service::create resolves ad_creative_uid to ad_creative_id', function () {
$creative = AdCreative::create([
'uid' => 'r51cr_' . uniqid(),
'customer_id' => $this->customerId,
'name' => 'Wizard Creative',
'type' => 'image',
'status' => 'active',
]);
$service = app(AdCampaignService::class);
$campaign = $service->create($this->customer->local(), [
'name' => 'R5.1 Test Campaign',
'objective' => 'traffic',
'ad_creative_uid' => $creative->uid,
]);
expect($campaign->ad_creative_id)->toBe($creative->id);
});
test('Service::create resolves ad_audience_uid to ad_audience_id', function () {
$audience = AdAudience::create([
'uid' => 'r51aud_' . uniqid(),
'customer_id' => $this->customerId,
'name' => 'Wizard Audience',
'type' => 'custom',
'status' => 'active',
]);
$service = app(AdCampaignService::class);
$campaign = $service->create($this->customer->local(), [
'name' => 'R5.1 Audience Campaign',
'objective' => 'leads',
'ad_audience_uid' => $audience->uid,
]);
expect($campaign->ad_audience_id)->toBe($audience->id);
});
test('Service::create attaches ad_platform_uids via belongsToMany pivot', function () {
$p1 = r51Platform($this->customerId, 'meta', 'R5.1 Pivot Meta');
$p2 = r51Platform($this->customerId, 'google', 'R5.1 Pivot Google');
$service = app(AdCampaignService::class);
$campaign = $service->create($this->customer->local(), [
'name' => 'R5.1 Multi-Platform',
'objective' => 'sales',
'ad_platform_uids' => [$p1->uid, $p2->uid],
]);
$platformIds = $campaign->platforms()->pluck('ad_platforms.id')->all();
expect($platformIds)->toContain($p1->id);
expect($platformIds)->toContain($p2->id);
});
test('Service::create skips inactive platforms even if uid is in the array', function () {
$active = r51Platform($this->customerId, 'meta', 'Active', AdPlatform::STATUS_ACTIVE);
$inactive = r51Platform($this->customerId, 'google', 'Revoked', AdPlatform::STATUS_REVOKED);
$service = app(AdCampaignService::class);
$campaign = $service->create($this->customer->local(), [
'name' => 'R5.1 Skip Inactive',
'objective' => 'awareness',
'ad_platform_uids' => [$active->uid, $inactive->uid],
]);
$platformIds = $campaign->platforms()->pluck('ad_platforms.id')->all();
expect($platformIds)->toContain($active->id);
expect($platformIds)->not->toContain($inactive->id);
});
// ---------------------------------------------------------------------------
// 7. Backward compat — legacy create flow still works
// ---------------------------------------------------------------------------
test('Service::create with no wizard fields keeps the legacy 4-step flow working', function () {
$service = app(AdCampaignService::class);
$campaign = $service->create($this->customer->local(), [
'name' => 'Legacy Campaign',
'objective' => 'traffic',
'daily_budget' => '100',
]);
expect($campaign)->toBeInstanceOf(AdCampaign::class);
expect($campaign->ad_creative_id)->toBeNull();
expect($campaign->ad_audience_id)->toBeNull();
expect($campaign->platforms()->count())->toBe(0);
});
// ---------------------------------------------------------------------------
// 8. Wizard view markup
// ---------------------------------------------------------------------------
test('Wizard view markup carries all 6 steps + new wizard step labels', function () {
$response = $this->actingAs($this->user)->get(route('refactor.ads.campaigns.create'));
$response->assertOk();
// 6 step indicators
$response->assertSee('data-step="1"', false);
$response->assertSee('data-step="2"', false);
$response->assertSee('data-step="3"', false);
$response->assertSee('data-step="4"', false);
$response->assertSee('data-step="5"', false);
$response->assertSee('data-step="6"', false);
// 6 panels
$response->assertSee('data-panel="6"', false);
// R5.1 lang keys rendered
$response->assertSee('Platforms');
$response->assertSee('Audience');
$response->assertSee('Creative');
$response->assertSee('Review');
});