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);
}
});