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/tests/Feature/AdminCustomerDashboardServiceTest.php
<?php

/**
 * Feature test for AdminCustomerDashboardService.
 *
 * Verifies the service returns well-shaped arrays for any customer state:
 * - rich customer (with subscription, invoices, activity)
 * - empty customer (no subscription, no data)
 * - expired customer (cancelled subscription)
 *
 * Source of truth: docs/automated/features/admin-customer-dashboard.md
 */

use App\Model\Customer;
use App\Services\Customer\AdminCustomerDashboardService;

uses(\Tests\TestCase::class);

// ============================================================
// Service shape — keys and types
// ============================================================

test('identity returns expected keys', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB — run DemoSeeder first');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $id = $svc->identity();

    expect($id)->toHaveKeys(['uid', 'name', 'email', 'company', 'avatar_url', 'initials', 'avatar_color', 'status', 'plan_name', 'plan_status', 'since', 'since_human']);
    expect($id['uid'])->toBe($customer->uid);
    expect($id['avatar_color'])->toBeInt()->toBeGreaterThanOrEqual(1)->toBeLessThanOrEqual(8);
    // avatar_url must be null when no real upload — gravatar/placeholder
    // fallbacks would defeat the colored-initials path in the rui system.
    if ($id['avatar_url'] !== null) {
        expect($id['avatar_url'])->toBeString();
        expect($id['avatar_url'])->not->toContain('user-placeholder.svg');
    }
});

test('ribbon returns 4 numeric counts plus currency', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $r = $svc->ribbon();

    expect($r)->toHaveKeys(['subscribers', 'campaigns', 'lists', 'lifetime_paid', 'currency']);
    expect($r['subscribers'])->toBeInt();
    expect($r['campaigns'])->toBeInt();
    expect($r['lists'])->toBeInt();
});

test('users returns total + first array + overflow', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $u = $svc->users(5);

    expect($u)->toHaveKeys(['total', 'first', 'overflow']);
    expect($u['total'])->toBeInt();
    expect($u['first'])->toBeArray();
    expect(count($u['first']))->toBeLessThanOrEqual(5);
    foreach ($u['first'] as $entry) {
        expect($entry)->toHaveKeys(['uid', 'name', 'email', 'avatar_url', 'initials', 'avatar_color']);
        expect($entry['avatar_color'])->toBeInt()->toBeGreaterThanOrEqual(1)->toBeLessThanOrEqual(8);
        if ($entry['avatar_url'] !== null) {
            expect($entry['avatar_url'])->not->toContain('user-placeholder.svg');
        }
    }
});

test('subscription gracefully handles missing subscription', function () {
    // Find a customer with no subscription if possible, otherwise use any
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $sub = $svc->subscription();

    expect($sub)->toHaveKeys(['has_subscription', 'plan_name', 'status', 'is_recurring', 'next_renewal', 'next_renewal_human', 'days_until_renewal', 'price', 'currency', 'started_at']);
    expect($sub['has_subscription'])->toBeBool();
});

test('billing returns numeric totals and counts', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $b = $svc->billing();

    expect($b)->toHaveKeys(['lifetime_paid', 'currency', 'paid_count', 'pending_count', 'total_count', 'last_invoice_amount', 'last_invoice_date', 'last_invoice_status']);
    expect($b['lifetime_paid'])->toBeFloat();
    expect($b['paid_count'])->toBeInt();
    expect($b['pending_count'])->toBeInt();
});

test('quotaGrid returns 4 cards each with percent 0-100', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $q = $svc->quotaGrid();

    expect($q)->toHaveKeys(['lists', 'campaigns', 'subscribers', 'sending']);
    foreach ($q as $key => $card) {
        expect($card)->toHaveKeys(['key', 'count', 'max', 'max_label', 'percent', 'unlimited']);
        expect($card['percent'])->toBeGreaterThanOrEqual(0);
        expect($card['percent'])->toBeLessThanOrEqual(100);
    }
});

test('metrics30d returns 4 metric entries with sparkline arrays', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $m = $svc->metrics30d();

    expect($m)->toHaveKeys(['sent', 'open_rate', 'click_rate', 'bounce_rate']);
    foreach ($m as $key => $metric) {
        expect($metric)->toHaveKeys(['value', 'sparkline', 'label']);
        expect($metric['sparkline'])->toBeArray();
    }
});

test('sendingTrend returns labels and data of equal length', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $t = $svc->sendingTrend(7);

    expect($t)->toHaveKeys(['labels', 'data', 'days', 'total']);
    expect(count($t['labels']))->toBe(7);
    expect(count($t['data']))->toBe(7);
    expect($t['days'])->toBe(7);
});

test('subscriberGrowth returns N months', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $g = $svc->subscriberGrowth(6);

    expect($g)->toHaveKeys(['labels', 'data', 'months']);
    expect(count($g['labels']))->toBe(6);
    expect(count($g['data']))->toBe(6);
});

test('sendingServers returns own + system counts', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $s = $svc->sendingServers();

    expect($s)->toHaveKeys(['own_count', 'system_count', 'types_in_use', 'total_available']);
    expect($s['own_count'])->toBeInt();
    expect($s['system_count'])->toBeInt();
    expect($s['types_in_use'])->toBeArray();
});

test('recentCampaigns returns at most N entries', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $rc = $svc->recentCampaigns(5);

    expect($rc->count())->toBeLessThanOrEqual(5);
});

test('activityFeed returns at most N entries', function () {
    $customer = Customer::first();
    if (!$customer) {
        $this->markTestSkipped('No customer in DB');
    }
    $svc = new AdminCustomerDashboardService($customer);
    $af = $svc->activityFeed(8);

    expect($af->count())->toBeLessThanOrEqual(8);
});

test('service handles all demo customers without exception', function () {
    $customers = Customer::limit(8)->get();
    if ($customers->isEmpty()) {
        $this->markTestSkipped('No customers in DB');
    }
    foreach ($customers as $customer) {
        $svc = new AdminCustomerDashboardService($customer);

        // Each method must return without throwing
        expect(fn() => $svc->identity())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->ribbon())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->users())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->subscription())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->billing())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->sendingCredits())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->evsCredits())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->quotaGrid())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->metrics30d())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->sendingTrend(7))->not->toThrow(\Throwable::class);
        expect(fn() => $svc->subscriberGrowth(6))->not->toThrow(\Throwable::class);
        expect(fn() => $svc->sendingServers())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->recentCampaigns())->not->toThrow(\Throwable::class);
        expect(fn() => $svc->activityFeed())->not->toThrow(\Throwable::class);
    }
});