# app/modules/billing/tests/integration/test_admin_routes.py """ Integration tests for billing admin API routes. Tests the admin subscription management endpoints at: /api/v1/admin/subscriptions/* Uses super_admin_headers fixture which bypasses module access checks. """ import uuid from datetime import UTC, datetime, timedelta import pytest from app.modules.billing.models import ( BillingHistory, MerchantSubscription, SubscriptionStatus, SubscriptionTier, ) from app.modules.tenancy.models import Merchant, Platform, User # ============================================================================ # Fixtures # ============================================================================ BASE = "/api/v1/admin/subscriptions" @pytest.fixture def rt_platform(db): """Create a platform for route tests.""" platform = Platform( code=f"test_{uuid.uuid4().hex[:8]}", name="Test Platform", is_active=True, ) db.add(platform) db.commit() db.refresh(platform) return platform @pytest.fixture def rt_tiers(db, rt_platform): """Create subscription tiers for route tests.""" tiers = [] for i, (code, name, price) in enumerate([ ("essential", "Essential", 0), ("professional", "Professional", 2900), ("business", "Business", 7900), ]): tier = SubscriptionTier( code=code, name=name, description=f"{name} tier", price_monthly_cents=price, price_annual_cents=price * 10 if price > 0 else 0, display_order=i, is_active=True, is_public=True, platform_id=rt_platform.id, ) db.add(tier) tiers.append(tier) db.commit() for t in tiers: db.refresh(t) return tiers @pytest.fixture def rt_merchant(db, rt_platform): """Create a merchant with owner for route tests.""" from middleware.auth import AuthManager auth = AuthManager() owner = User( email=f"merchant_{uuid.uuid4().hex[:8]}@test.com", username=f"merchant_{uuid.uuid4().hex[:8]}", hashed_password=auth.hash_password("pass123"), role="store", is_active=True, ) db.add(owner) db.commit() db.refresh(owner) merchant = Merchant( name="Route Test Merchant", owner_user_id=owner.id, contact_email=owner.email, is_active=True, is_verified=True, ) db.add(merchant) db.commit() db.refresh(merchant) return merchant @pytest.fixture def rt_subscription(db, rt_merchant, rt_platform, rt_tiers): """Create a subscription for route tests.""" sub = MerchantSubscription( merchant_id=rt_merchant.id, platform_id=rt_platform.id, tier_id=rt_tiers[1].id, # professional status=SubscriptionStatus.ACTIVE.value, is_annual=False, period_start=datetime.now(UTC), period_end=datetime.now(UTC) + timedelta(days=30), ) db.add(sub) db.commit() db.refresh(sub) return sub @pytest.fixture def rt_billing_history(db, rt_merchant): """Create billing history entries for route tests.""" records = [] for i in range(3): record = BillingHistory( merchant_id=rt_merchant.id, invoice_number=f"INV-{1000 + i}", invoice_date=datetime.now(UTC) - timedelta(days=30 * i), subtotal_cents=2900, tax_cents=493, total_cents=3393, amount_paid_cents=3393, currency="EUR", status="paid", description=f"Invoice {i}", ) db.add(record) # noqa: PERF006 records.append(record) db.commit() for r in records: db.refresh(r) return records # ============================================================================ # Tier Endpoints # ============================================================================ class TestAdminListTiers: """Tests for GET /api/v1/admin/subscriptions/tiers.""" def test_list_tiers_success(self, client, super_admin_headers, rt_tiers): response = client.get(f"{BASE}/tiers", headers=super_admin_headers) assert response.status_code == 200 data = response.json() assert "tiers" in data assert "total" in data assert data["total"] >= 3 def test_list_tiers_active_only_by_default(self, client, super_admin_headers, rt_tiers, db): # Deactivate one tier rt_tiers[2].is_active = False db.commit() response = client.get(f"{BASE}/tiers", headers=super_admin_headers) assert response.status_code == 200 codes = [t["code"] for t in response.json()["tiers"]] assert "business" not in codes def test_list_tiers_include_inactive(self, client, super_admin_headers, rt_tiers, db): rt_tiers[2].is_active = False db.commit() response = client.get( f"{BASE}/tiers", params={"include_inactive": True}, headers=super_admin_headers, ) assert response.status_code == 200 codes = [t["code"] for t in response.json()["tiers"]] assert "business" in codes def test_list_tiers_filter_by_platform(self, client, super_admin_headers, rt_tiers, rt_platform): response = client.get( f"{BASE}/tiers", params={"platform_id": rt_platform.id}, headers=super_admin_headers, ) assert response.status_code == 200 assert response.json()["total"] == 3 def test_list_tiers_unauthorized(self, client): response = client.get(f"{BASE}/tiers") assert response.status_code in (401, 403) class TestAdminGetTier: """Tests for GET /api/v1/admin/subscriptions/tiers/{tier_code}.""" def test_get_tier_success(self, client, super_admin_headers, rt_tiers): response = client.get( f"{BASE}/tiers/professional", headers=super_admin_headers ) assert response.status_code == 200 data = response.json() assert data["code"] == "professional" assert data["name"] == "Professional" assert data["price_monthly_cents"] == 2900 def test_get_tier_not_found(self, client, super_admin_headers): response = client.get( f"{BASE}/tiers/nonexistent", headers=super_admin_headers ) assert response.status_code == 404 class TestAdminCreateTier: """Tests for POST /api/v1/admin/subscriptions/tiers.""" def test_create_tier_success(self, client, super_admin_headers, rt_platform): response = client.post( f"{BASE}/tiers", json={ "code": "starter", "name": "Starter", "description": "Starter plan", "price_monthly_cents": 990, "price_annual_cents": 9900, "display_order": 0, "is_active": True, "is_public": True, "platform_id": rt_platform.id, }, headers=super_admin_headers, ) assert response.status_code == 201 data = response.json() assert data["code"] == "starter" assert data["price_monthly_cents"] == 990 def test_create_tier_duplicate_code(self, client, super_admin_headers, rt_tiers): response = client.post( f"{BASE}/tiers", json={ "code": "essential", "name": "Essential Dup", "price_monthly_cents": 0, }, headers=super_admin_headers, ) assert response.status_code in (400, 409, 422) class TestAdminUpdateTier: """Tests for PATCH /api/v1/admin/subscriptions/tiers/{tier_code}.""" def test_update_tier_success(self, client, super_admin_headers, rt_tiers): response = client.patch( f"{BASE}/tiers/professional", json={"name": "Professional Plus", "price_monthly_cents": 3900}, headers=super_admin_headers, ) assert response.status_code == 200 data = response.json() assert data["name"] == "Professional Plus" assert data["price_monthly_cents"] == 3900 def test_update_tier_not_found(self, client, super_admin_headers): response = client.patch( f"{BASE}/tiers/nonexistent", json={"name": "Updated"}, headers=super_admin_headers, ) assert response.status_code == 404 class TestAdminDeleteTier: """Tests for DELETE /api/v1/admin/subscriptions/tiers/{tier_code}.""" def test_delete_tier_success(self, client, super_admin_headers, rt_tiers): response = client.delete( f"{BASE}/tiers/business", headers=super_admin_headers ) assert response.status_code == 204 def test_delete_tier_with_active_subs( self, client, super_admin_headers, rt_subscription, rt_tiers ): # Try to delete the tier used by rt_subscription (professional) response = client.delete( f"{BASE}/tiers/professional", headers=super_admin_headers ) assert response.status_code in (400, 409, 422) def test_delete_tier_not_found(self, client, super_admin_headers): response = client.delete( f"{BASE}/tiers/nonexistent", headers=super_admin_headers ) assert response.status_code == 404 # ============================================================================ # Subscription Endpoints # ============================================================================ class TestAdminListSubscriptions: """Tests for GET /api/v1/admin/subscriptions.""" def test_list_subscriptions_success( self, client, super_admin_headers, rt_subscription ): response = client.get(f"{BASE}", headers=super_admin_headers) assert response.status_code == 200 data = response.json() assert "subscriptions" in data assert "total" in data assert data["total"] >= 1 def test_list_subscriptions_pagination( self, client, super_admin_headers, rt_subscription ): response = client.get( f"{BASE}", params={"page": 1, "per_page": 5}, headers=super_admin_headers, ) assert response.status_code == 200 data = response.json() assert data["page"] == 1 assert data["per_page"] == 5 def test_list_subscriptions_filter_by_status( self, client, super_admin_headers, rt_subscription ): response = client.get( f"{BASE}", params={"status": "active"}, headers=super_admin_headers, ) assert response.status_code == 200 for sub in response.json()["subscriptions"]: assert sub["status"] == "active" class TestAdminCreateSubscription: """Tests for POST /api/v1/admin/subscriptions/merchants/{id}/platforms/{id}.""" def test_create_subscription_success( self, client, super_admin_headers, rt_merchant, rt_platform, rt_tiers ): response = client.post( f"{BASE}/merchants/{rt_merchant.id}/platforms/{rt_platform.id}", json={ "merchant_id": rt_merchant.id, "platform_id": rt_platform.id, "tier_code": "essential", "status": "trial", "trial_days": 14, "is_annual": False, }, headers=super_admin_headers, ) assert response.status_code == 201 data = response.json() assert data["merchant_id"] == rt_merchant.id assert data["platform_id"] == rt_platform.id class TestAdminGetSubscription: """Tests for GET /api/v1/admin/subscriptions/merchants/{id}/platforms/{id}.""" def test_get_subscription_success( self, client, super_admin_headers, rt_subscription, rt_merchant, rt_platform ): response = client.get( f"{BASE}/merchants/{rt_merchant.id}/platforms/{rt_platform.id}", headers=super_admin_headers, ) assert response.status_code == 200 data = response.json() assert data["merchant_id"] == rt_merchant.id assert data["status"] == "active" def test_get_subscription_not_found( self, client, super_admin_headers, rt_platform ): response = client.get( f"{BASE}/merchants/99999/platforms/{rt_platform.id}", headers=super_admin_headers, ) assert response.status_code == 404 class TestAdminUpdateSubscription: """Tests for PATCH /api/v1/admin/subscriptions/merchants/{id}/platforms/{id}.""" def test_update_subscription_status( self, client, super_admin_headers, rt_subscription, rt_merchant, rt_platform ): response = client.patch( f"{BASE}/merchants/{rt_merchant.id}/platforms/{rt_platform.id}", json={"status": "past_due"}, headers=super_admin_headers, ) assert response.status_code == 200 assert response.json()["status"] == "past_due" def test_update_subscription_tier( self, client, super_admin_headers, rt_subscription, rt_merchant, rt_platform, rt_tiers, ): response = client.patch( f"{BASE}/merchants/{rt_merchant.id}/platforms/{rt_platform.id}", json={"tier_code": "business"}, headers=super_admin_headers, ) assert response.status_code == 200 # ============================================================================ # Stats Endpoint # ============================================================================ class TestAdminStats: """Tests for GET /api/v1/admin/subscriptions/stats.""" def test_get_stats_success( self, client, super_admin_headers, rt_subscription ): response = client.get( f"{BASE}/stats", headers=super_admin_headers ) assert response.status_code == 200 data = response.json() assert "total_subscriptions" in data assert "active_count" in data assert "mrr_cents" in data assert "arr_cents" in data assert "tier_distribution" in data assert data["active_count"] >= 1 def test_get_stats_empty(self, client, super_admin_headers): response = client.get( f"{BASE}/stats", headers=super_admin_headers ) assert response.status_code == 200 data = response.json() assert data["total_subscriptions"] == 0 # ============================================================================ # Billing History Endpoint # ============================================================================ class TestAdminBillingHistory: """Tests for GET /api/v1/admin/subscriptions/billing/history.""" def test_list_billing_history_success( self, client, super_admin_headers, rt_billing_history ): response = client.get( f"{BASE}/billing/history", headers=super_admin_headers ) assert response.status_code == 200 data = response.json() assert "invoices" in data assert "total" in data assert data["total"] >= 3 def test_list_billing_history_pagination( self, client, super_admin_headers, rt_billing_history ): response = client.get( f"{BASE}/billing/history", params={"page": 1, "per_page": 2}, headers=super_admin_headers, ) assert response.status_code == 200 data = response.json() assert data["per_page"] == 2 assert len(data["invoices"]) <= 2 def test_list_billing_history_filter_by_merchant( self, client, super_admin_headers, rt_billing_history, rt_merchant ): response = client.get( f"{BASE}/billing/history", params={"merchant_id": rt_merchant.id}, headers=super_admin_headers, ) assert response.status_code == 200 assert response.json()["total"] == 3 def test_list_billing_history_empty(self, client, super_admin_headers): response = client.get( f"{BASE}/billing/history", headers=super_admin_headers ) assert response.status_code == 200 assert response.json()["total"] == 0