Files
website/website/tests/Feature/Admin/DashboardStatsTest.php
Claude Dev b4ef90465c feat: complete pre-launch audit — frontend polish, churn prevention, login history, financial reports, configurable checkout
Includes all work from phases 6-9+ and frontend polish rounds 1 & 2:

- Login history with device trust, new device notifications, session management
- Churn prevention: cancellation surveys, winback campaigns with email sequences
- Financial reports: revenue, P&L, tax, aging, refund, subscription reports with PDF/CSV/JSON export
- Configurable checkout: plan config groups/options, build-your-own VPS
- Frontend polish: fix broken legal links, add SEO meta tags, favicon, font display=swap,
  Head titles on all 14 marketing pages, mobile responsive fixes, AuthLayout legal footer,
  remove false 24/7 claims, hide empty stats, correct uptime SLA to 99.9%,
  GameServers notify buttons linked to /contact, 301 redirects for /terms and /privacy
- WHMCS migration scripts
- Update legal page effective dates to March 16, 2026

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:39:25 -04:00

392 lines
14 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Invoice;
use App\Models\PaymentTransaction;
use App\Models\Plan;
use App\Models\PlanPrice;
use App\Models\Service;
use App\Models\User;
use Database\Seeders\RoleAndPermissionSeeder;
use Illuminate\Support\Facades\Cache;
use Laravel\Cashier\Subscription;
beforeEach(function (): void {
$this->seed(RoleAndPermissionSeeder::class);
Cache::flush();
$this->admin = User::factory()->admin()->create();
$this->adminUrl = 'http://'.config('app.domains.admin');
});
describe('Customer count and delta', function (): void {
it('returns correct totalCustomers and newCustomersThisMonth', function (): void {
// Customer from last month
User::factory()->customer()->create([
'created_at' => now()->subMonth(),
]);
// Two customers this month
User::factory()->customer()->count(2)->create([
'created_at' => now(),
]);
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('totalCustomers', 3)
->where('newCustomersThisMonth', 2)
);
});
});
describe('MRR normalization', function (): void {
it('normalizes MRR across billing cycles', function (): void {
$user = User::factory()->customer()->create();
// Monthly plan at $30/mo → contributes $30 MRR
$monthlyPlan = Plan::factory()->create(['billing_cycle' => 'monthly', 'price' => 30]);
PlanPrice::create([
'plan_id' => $monthlyPlan->id,
'billing_cycle' => 'monthly',
'price' => 30.00,
]);
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_monthly_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_monthly',
'plan_id' => $monthlyPlan->id,
'billing_cycle' => 'monthly',
]);
// Quarterly plan at $90/quarter → contributes $30 MRR
$quarterlyPlan = Plan::factory()->create(['billing_cycle' => 'quarterly', 'price' => 90]);
PlanPrice::create([
'plan_id' => $quarterlyPlan->id,
'billing_cycle' => 'quarterly',
'price' => 90.00,
]);
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_quarterly_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_quarterly',
'plan_id' => $quarterlyPlan->id,
'billing_cycle' => 'quarterly',
]);
// Annual plan at $120/year → contributes $10 MRR
$annualPlan = Plan::factory()->create(['billing_cycle' => 'annual', 'price' => 120]);
PlanPrice::create([
'plan_id' => $annualPlan->id,
'billing_cycle' => 'annual',
'price' => 120.00,
]);
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_annual_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_annual',
'plan_id' => $annualPlan->id,
'billing_cycle' => 'annual',
]);
// Expected MRR: $30 + $30 + $10 = $70
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('mrr', fn ($mrr) => (float) $mrr === 70.0)
->where('arr', fn ($arr) => (float) $arr === 840.0)
);
});
});
describe('MRR month-over-month change', function (): void {
it('calculates mrrChangePercent when previous month data exists', function (): void {
$user = User::factory()->customer()->create();
// Subscription created before last month (will count for both current and previous MRR)
$plan = Plan::factory()->create(['billing_cycle' => 'monthly', 'price' => 50]);
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 50.00,
]);
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_old_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_old',
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'created_at' => now()->subMonths(2),
]);
// New subscription this month (only counts for current MRR)
$plan2 = Plan::factory()->create(['billing_cycle' => 'monthly', 'price' => 50]);
PlanPrice::create([
'plan_id' => $plan2->id,
'billing_cycle' => 'monthly',
'price' => 50.00,
]);
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_new_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_new',
'plan_id' => $plan2->id,
'billing_cycle' => 'monthly',
'created_at' => now(),
]);
// Current MRR: $100, Previous MRR: $50 → change: +100%
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('mrr', fn ($mrr) => (float) $mrr === 100.0)
->where('mrrChangePercent', fn ($v) => (float) $v === 100.0)
);
});
it('returns null mrrChangePercent when no previous month data', function (): void {
$user = User::factory()->customer()->create();
// Subscription created this month only
$plan = Plan::factory()->create(['billing_cycle' => 'monthly', 'price' => 50]);
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 50.00,
]);
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_new_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_new',
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'created_at' => now(),
]);
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('mrrChangePercent', null)
);
});
});
describe('Fee estimation by gateway', function (): void {
it('estimates fees correctly for Stripe and PayPal transactions', function (): void {
$user = User::factory()->customer()->create();
// Stripe: $100 → fee = ($100 * 0.029) + $0.30 = $3.20
PaymentTransaction::create([
'user_id' => $user->id,
'gateway' => 'stripe',
'gateway_transaction_id' => 'txn_stripe_'.uniqid(),
'amount' => 100.00,
'currency' => 'USD',
'status' => 'succeeded',
'payment_method' => 'card',
'description' => 'Test stripe payment',
]);
// PayPal: $200 → fee = ($200 * 0.0349) + $0.49 = $7.47
PaymentTransaction::create([
'user_id' => $user->id,
'gateway' => 'paypal',
'gateway_transaction_id' => 'txn_paypal_'.uniqid(),
'amount' => 200.00,
'currency' => 'USD',
'status' => 'succeeded',
'payment_method' => 'paypal',
'description' => 'Test paypal payment',
]);
// Total fees: $3.20 + $7.47 = $10.67
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('totalTransactionRevenue', fn ($v) => (float) $v === 300.0)
->where('estimatedFees', 10.67)
);
});
});
describe('Net revenue', function (): void {
it('calculates net revenue as total minus fees', function (): void {
$user = User::factory()->customer()->create();
PaymentTransaction::create([
'user_id' => $user->id,
'gateway' => 'stripe',
'gateway_transaction_id' => 'txn_'.uniqid(),
'amount' => 100.00,
'currency' => 'USD',
'status' => 'succeeded',
'payment_method' => 'card',
'description' => 'Payment',
]);
// Fee: ($100 * 0.029) + $0.30 = $3.20
// Net: $100 - $3.20 = $96.80
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('netRevenue', fn ($v) => (float) $v === 96.8)
);
});
});
describe('Service breakdown', function (): void {
it('returns correct service counts by type', function (): void {
$customer = User::factory()->customer()->create();
Service::factory()->count(3)->create([
'user_id' => $customer->id,
'service_type' => 'vps',
'status' => 'active',
]);
Service::factory()->count(2)->create([
'user_id' => $customer->id,
'service_type' => 'dedicated',
'status' => 'active',
]);
// Suspended service should not appear
Service::factory()->create([
'user_id' => $customer->id,
'service_type' => 'vps',
'status' => 'suspended',
]);
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('activeServices', 5)
->where('serviceBreakdown.vps', 3)
->where('serviceBreakdown.dedicated', 2)
);
});
});
describe('Overdue tracking', function (): void {
it('returns correct overdue count and amount', function (): void {
$customer = User::factory()->customer()->create();
Invoice::factory()->count(2)->create([
'user_id' => $customer->id,
'status' => 'overdue',
'total' => 50.00,
]);
// Paid invoice should not count
Invoice::factory()->create([
'user_id' => $customer->id,
'status' => 'paid',
'total' => 100.00,
]);
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('overdueCount', 2)
->where('overdueAmount', fn ($v) => (float) $v === 100.0)
);
});
});
describe('Churn health status', function (): void {
it('returns healthy when churn rate is below 3%', function (): void {
// No subscriptions → 0% churn → healthy
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('currentChurnRate', 0)
->where('churnHealthStatus', 'healthy')
);
});
it('returns watch when churn rate is between 3% and 7%', function (): void {
$user = User::factory()->customer()->create();
// Create 20 subscriptions before this month
for ($i = 0; $i < 20; $i++) {
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_base_'.$i.'_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_base',
'created_at' => now()->subMonths(2),
]);
}
// Cancel 1 this month → 5% churn → watch
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_cancelled_'.uniqid(),
'stripe_status' => 'canceled',
'stripe_price' => 'price_cancelled',
'created_at' => now()->subMonths(2),
'cancelled_at' => now(),
]);
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('churnHealthStatus', 'watch')
);
});
it('returns high when churn rate exceeds 7%', function (): void {
$user = User::factory()->customer()->create();
// Create 10 subscriptions before this month
for ($i = 0; $i < 10; $i++) {
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_base_'.$i.'_'.uniqid(),
'stripe_status' => 'active',
'stripe_price' => 'price_base',
'created_at' => now()->subMonths(2),
]);
}
// Cancel 1 this month → ~9.1% churn (1/11) → high
Subscription::create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_cancelled_'.uniqid(),
'stripe_status' => 'canceled',
'stripe_price' => 'price_cancelled',
'created_at' => now()->subMonths(2),
'cancelled_at' => now(),
]);
$this->actingAs($this->admin)
->get($this->adminUrl.'/dashboard')
->assertOk()
->assertInertia(fn ($page) => $page
->where('churnHealthStatus', 'high')
);
});
});