create(); PlatformPrice::query()->delete(); PlatformPrice::create($this->pricePayload(['code' => 'pro', 'name' => 'Pro', 'sort_order' => 30])); PlatformPrice::create($this->pricePayload(['code' => 'basic', 'name' => 'Basic', 'sort_order' => 10])); PlatformPrice::create($this->pricePayload(['code' => 'legacy', 'name' => 'Legacy', 'is_active' => false, 'sort_order' => 1])); $response = $this->actingAs($user)->get(route('platform.pricing.index')); $response->assertOk() ->assertSee('Pricing', false) ->assertSee('Basic', false) ->assertSee('Pro', false) ->assertDontSee('Legacy', false) ->assertSeeInOrder(['Basic', 'Pro']); } /** * Ensure the admin pricing index remains an Administration CRUD area. */ public function test_platform_admin_can_view_price_crud_index(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); PlatformPrice::query()->delete(); PlatformPrice::create($this->pricePayload(['code' => 'advanced', 'name' => 'Advanced'])); $this->actingAs($admin) ->get(route('platform.prices.index')) ->assertOk() ->assertSee('Pricing Administration', false) ->assertSee('Advanced', false) ->assertSee('Add price', false) ->assertSee(route('platform.prices.create'), false); } /** * Ensure only MPBA platform admins can reach price administration. */ public function test_non_platform_admin_cannot_access_price_crud(): void { $user = User::factory()->create(); $this->actingAs($user) ->get(route('platform.prices.index')) ->assertForbidden(); } /** * Ensure create renders with default form values. */ public function test_platform_admin_can_open_create_price_form(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); $this->actingAs($admin) ->get(route('platform.prices.create')) ->assertOk() ->assertSee('Create Price', false) ->assertSee('Currency', false); } /** * Ensure store normalises amounts, code, booleans, currency and feature rows. */ public function test_platform_admin_can_create_price(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); $response = $this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.prices.store'), $this->csrfPayload([ 'code' => '', 'name' => 'Ultimate Plan', 'strapline' => 'Full control', 'description' => 'Unlimited warehouses and users.', 'monthly_amount' => '199.99', 'annual_amount' => '1999.00', 'currency' => 'GBP', 'badge_label' => 'Best Value', 'features' => "Unlimited warehouses\nUnlimited users\nPriority support\n", 'sort_order' => 40, 'is_featured' => '1', 'is_active' => '1', ])); $response->assertRedirect(route('platform.prices.index')); $this->assertDatabaseHas('platform_prices', [ 'code' => 'ultimate-plan', 'name' => 'Ultimate Plan', 'monthly_amount' => 19999, 'annual_amount' => 199900, 'currency' => 'gbp', 'is_featured' => true, 'is_active' => true, ]); $this->assertSame( ['Unlimited warehouses', 'Unlimited users', 'Priority support'], PlatformPrice::where('code', 'ultimate-plan')->firstOrFail()->featureRows(), ); } /** * Ensure validation protects required fields and amount limits. */ public function test_create_price_validates_required_fields(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); $this->actingAs($admin) ->from(route('platform.prices.create')) ->withSession(['_token' => 'test-token']) ->post(route('platform.prices.store'), $this->csrfPayload([ 'name' => '', 'monthly_amount' => '-1', 'currency' => 'GB', 'sort_order' => -5, ])) ->assertRedirect(route('platform.prices.create')) ->assertSessionHasErrors(['name', 'monthly_amount', 'currency', 'sort_order']); } /** * Ensure edit renders the current price. */ public function test_platform_admin_can_open_edit_price_form(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); PlatformPrice::query()->delete(); $price = PlatformPrice::create($this->pricePayload(['code' => 'basic-edit', 'name' => 'Basic'])); $this->actingAs($admin) ->get(route('platform.prices.edit', $price)) ->assertOk() ->assertSee('Edit Basic', false) ->assertSee('Basic', false); } /** * Ensure update preserves unique-code validation for the current record. */ public function test_platform_admin_can_update_price(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); PlatformPrice::query()->delete(); $price = PlatformPrice::create($this->pricePayload(['code' => 'basic', 'name' => 'Basic'])); $this->actingAs($admin)->withSession(['_token' => 'test-token'])->put(route('platform.prices.update', $price), $this->csrfPayload([ 'code' => 'basic', 'name' => 'Basic Updated', 'strapline' => 'Entry level stock control', 'description' => 'Updated description.', 'monthly_amount' => '29.00', 'annual_amount' => '', 'currency' => 'GBP', 'badge_label' => '', 'features' => "2 warehouses\n10 users", 'sort_order' => 5, 'is_active' => '1', ]))->assertRedirect(route('platform.prices.index')); $this->assertDatabaseHas('platform_prices', [ 'id' => $price->id, 'code' => 'basic', 'name' => 'Basic Updated', 'monthly_amount' => 2900, 'annual_amount' => null, 'sort_order' => 5, 'is_featured' => false, 'is_active' => true, ]); } /** * Ensure duplicate codes are rejected on update. */ public function test_price_code_must_remain_unique(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); PlatformPrice::query()->delete(); PlatformPrice::create($this->pricePayload(['code' => 'basic', 'name' => 'Basic'])); $price = PlatformPrice::create($this->pricePayload(['code' => 'advanced', 'name' => 'Advanced'])); $this->actingAs($admin) ->from(route('platform.prices.edit', $price)) ->withSession(['_token' => 'test-token']) ->put(route('platform.prices.update', $price), $this->csrfPayload([ ...$this->requestPayload(), 'code' => 'basic', 'name' => 'Advanced', ])) ->assertRedirect(route('platform.prices.edit', $price)) ->assertSessionHasErrors(['code']); } /** * Ensure destroy removes the price. */ public function test_platform_admin_can_delete_price(): void { $admin = User::factory()->mpbaPlatformAdmin()->create(); PlatformPrice::query()->delete(); $price = PlatformPrice::create($this->pricePayload(['code' => 'legacy', 'name' => 'Legacy'])); $this->actingAs($admin) ->withSession(['_token' => 'test-token']) ->delete(route('platform.prices.destroy', $price), ['_token' => 'test-token']) ->assertRedirect(route('platform.prices.index')); $this->assertDatabaseMissing('platform_prices', ['id' => $price->id]); } /** * Ensure currencies render with correct symbols. */ #[DataProvider('currencySymbolProvider')] public function test_price_currency_symbol_and_formatting(string $currency, string $expected): void { $price = new PlatformPrice(['currency' => $currency]); $this->assertSame($expected, $price->currencySymbol()); $this->assertStringStartsWith($expected, $price->formatAmount(12345)); $this->assertSame('—', $price->formatAmount(null)); } /** * @return array */ public static function currencySymbolProvider(): array { return [ ['gbp', '£'], ['eur', '€'], ['usd', '$'], ['aud', 'AUD '], ]; } /** * @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 array */ private function requestPayload(): array { return [ 'code' => 'advanced', 'name' => 'Advanced', 'strapline' => 'Expanded controls.', 'description' => 'Larger warehouse estate support.', 'monthly_amount' => '99.00', 'annual_amount' => '999.00', 'currency' => 'GBP', 'badge_label' => 'Popular', 'features' => "5 warehouses\n100 users", 'sort_order' => 20, 'is_featured' => '1', 'is_active' => '1', ]; } /** * 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. */ }