Files
WMS/tests/Feature/Platform/PlatformAdministrationTest.php
T
2026-06-12 15:11:51 +01:00

287 lines
10 KiB
PHP

<?php
namespace Tests\Feature\Platform;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Modules\Platform\app\Models\Payment;
use Modules\Platform\app\Models\PlatformPrice;
use Modules\Warehouse\app\Models\Warehouse;
use Tests\TestCase;
/**
* Covers PlatformDashboardController, CompanyController, LicenseKeyController, PaymentController and StripeWebhookController.
*/
class PlatformAdministrationTest extends TestCase
{
use RefreshDatabase;
/**
* Ensure guests are redirected away from platform administration.
*/
public function test_guest_is_redirected_from_platform_dashboard(): void
{
$this->get(route('platform.dashboard'))->assertRedirect(route('login'));
}
/**
* Ensure non-platform users cannot access admin administration.
*/
public function test_regular_user_is_forbidden_from_platform_dashboard(): void
{
$user = User::factory()->create();
$this->actingAs($user)->get(route('platform.dashboard'))->assertForbidden();
}
/**
* Ensure dashboard renders the single platform header and pricing cards.
*/
public function test_platform_dashboard_renders_counts_pricing_and_recent_companies(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
PlatformPrice::query()->delete();
$company = Company::create(['name' => 'Customer One', 'slug' => 'customer-one']);
LicenseKey::issueFor($company, 'Customer licence', 'enterprise', 25);
Warehouse::create(['company_id' => $company->id, 'code' => 'MAIN', 'name' => 'Main Warehouse']);
Payment::create(['company_id' => $company->id, 'amount' => 12500, 'currency' => 'gbp', 'status' => 'paid']);
PlatformPrice::create($this->pricePayload(['code' => 'basic', 'name' => 'Basic']));
PlatformPrice::create($this->pricePayload(['code' => 'advanced', 'name' => 'Advanced', 'sort_order' => 20]));
PlatformPrice::create($this->pricePayload(['code' => 'pro', 'name' => 'Pro', 'sort_order' => 30]));
$this->actingAs($admin)
->get(route('platform.dashboard'))
->assertOk()
->assertSee('Platform Dashboard', false)
->assertSee('Pricing plans', false)
->assertSee('Basic', false)
->assertSee('Advanced', false)
->assertSee('Pro', false)
->assertSee('Customer One', false);
}
/**
* Ensure companies can be created by platform admins.
*/
public function test_platform_admin_can_create_company(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.companies.store'), $this->csrfPayload([
'name' => 'Abbott Logistics Limited',
'billing_email' => 'billing@abbott.example',
'status' => 'active',
]))->assertRedirect();
$this->assertDatabaseHas('companies', [
'name' => 'Abbott Logistics Limited',
'slug' => 'abbott-logistics-limited',
'billing_email' => 'billing@abbott.example',
'status' => 'active',
'timezone' => 'Europe/London',
]);
}
/**
* Ensure invalid company submissions are rejected.
*/
public function test_company_create_validates_input(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
$this->actingAs($admin)
->from(route('platform.companies.index'))
->withSession(['_token' => 'test-token'])
->post(route('platform.companies.store'), $this->csrfPayload([
'name' => '',
'billing_email' => 'not-an-email',
'status' => 'unknown',
]))
->assertRedirect(route('platform.companies.index'))
->assertSessionHasErrors(['name', 'billing_email', 'status']);
}
/**
* Ensure licences are issued hashed and shown once in session.
*/
public function test_platform_admin_can_issue_license_key(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
$company = Company::create(['name' => 'Tenant A', 'slug' => 'tenant-a']);
$response = $this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.licenses.store'), $this->csrfPayload([
'company_id' => $company->id,
'label' => 'Production licence',
'plan' => 'enterprise',
'max_users' => 500,
]));
$response->assertRedirect()->assertSessionHas('plain_license_key');
$plainKey = session('plain_license_key');
$license = LicenseKey::firstOrFail();
$this->assertStringStartsWith('WMS-', $plainKey);
$this->assertSame(hash('sha256', $plainKey), $license->key_hash);
$this->assertSame('enterprise', $license->plan);
$this->assertSame(500, $license->max_users);
}
/**
* Ensure licence input is validated.
*/
public function test_license_create_validates_input(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
$this->actingAs($admin)
->from(route('platform.licenses.index'))
->withSession(['_token' => 'test-token'])
->post(route('platform.licenses.store'), $this->csrfPayload([
'company_id' => 999,
'label' => '',
'plan' => 'ultimate',
'max_users' => 0,
]))
->assertRedirect(route('platform.licenses.index'))
->assertSessionHasErrors(['company_id', 'label', 'plan', 'max_users']);
}
/**
* Ensure manual payments are recorded with paid_at for paid statuses.
*/
public function test_platform_admin_can_create_manual_payment(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
$company = Company::create(['name' => 'Tenant Payment', 'slug' => 'tenant-payment']);
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.payments.store'), $this->csrfPayload([
'company_id' => $company->id,
'amount' => 12345,
'currency' => 'gbp',
'status' => 'paid',
'description' => 'Manual bank transfer',
]))->assertRedirect();
$payment = Payment::firstOrFail();
$this->assertSame($company->id, $payment->company_id);
$this->assertSame(12345, $payment->amount);
$this->assertSame('manual_admin', $payment->metadata['source']);
$this->assertNotNull($payment->paid_at);
}
/**
* Ensure unpaid statuses do not set paid_at.
*/
public function test_pending_payment_does_not_set_paid_at(): void
{
$admin = User::factory()->mpbaPlatformAdmin()->create();
$company = Company::create(['name' => 'Tenant Pending', 'slug' => 'tenant-pending']);
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.payments.store'), $this->csrfPayload([
'company_id' => $company->id,
'amount' => 5000,
'currency' => 'gbp',
'status' => 'pending',
'description' => 'Awaiting payment',
]))->assertRedirect();
$this->assertNull(Payment::firstOrFail()->paid_at);
}
/**
* Ensure the Stripe webhook creates or updates paid records.
*/
public function test_stripe_webhook_records_successful_payment_intent(): void
{
$company = Company::create([
'name' => 'Stripe Tenant',
'slug' => 'stripe-tenant',
'stripe_customer_id' => 'cus_test_123',
]);
$payload = [
'type' => 'payment_intent.succeeded',
'data' => [
'object' => [
'id' => 'pi_123',
'customer' => 'cus_test_123',
'amount_received' => 7800,
'currency' => 'gbp',
],
],
];
$this->withSession(['_token' => 'test-token'])->withHeader('X-CSRF-TOKEN', 'test-token')->postJson(route('stripe.webhook'), $payload)
->assertOk()
->assertJson(['received' => true]);
$this->assertDatabaseHas('payments', [
'company_id' => $company->id,
'stripe_payment_intent_id' => 'pi_123',
'amount' => 7800,
'currency' => 'gbp',
'status' => 'paid',
]);
}
/**
* Ensure unrelated Stripe events are acknowledged but do not create payments.
*/
public function test_stripe_webhook_ignores_unhandled_events(): void
{
$this->withSession(['_token' => 'test-token'])->withHeader('X-CSRF-TOKEN', 'test-token')->postJson(route('stripe.webhook'), ['type' => 'customer.created'])
->assertOk()
->assertJson(['received' => true]);
$this->assertDatabaseCount('payments', 0);
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
private function pricePayload(array $overrides = []): array
{
return [
'code' => 'basic',
'name' => 'Basic',
'strapline' => 'Starter WMS platform access.',
'description' => 'Core warehouse management tools.',
'monthly_amount' => 4900,
'annual_amount' => 49000,
'currency' => 'gbp',
'badge_label' => 'Starter',
'features' => ['2 warehouses', '10 users', 'Standard support'],
'sort_order' => 10,
'is_featured' => false,
'is_active' => true,
...$overrides,
];
}
/**
* Return payload with a valid test CSRF token.
*
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function csrfPayload(array $payload = []): array
{
return ['_token' => 'test-token'] + $payload;
}
/**
* Seed the test session with the matching CSRF token.
*/
private function withCsrfSession(): self
{
$this->withSession(['_token' => 'test-token']);
return $this;
}
}