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_without_pricing_cards_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) ->assertDontSee('Pricing plans', 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 $overrides * * @return array */ 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 $payload * * @return array */ 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; } }