import pytest from infrasynth.billing.models import App, Entitlement, Invoice, PaymentGateway, PaymentTransaction, Plan, Subscription from infrasynth.features.models import FeatureFlag from infrasynth.shared.enums import MonetizationModel, SubscriptionStatus pytestmark = pytest.mark.django_db GATEWAYS_URL = "/api/v1/billing/gateways/" APPS_URL = "/api/v1/billing/apps/" PLANS_URL = "/api/v1/billing/plans/" ENTITLEMENTS_URL = "/api/v1/billing/entitlements/" SUBSCRIPTIONS_URL = "/api/v1/billing/subscriptions/" INVOICES_URL = "/api/v1/billing/invoices/" TRANSACTIONS_URL = "/api/v1/billing/transactions/" CHECKOUT_URL = "/api/v1/billing/checkout/" WEBHOOK_URL = "/api/v1/billing/webhook/receive/" @pytest.fixture def billing_enabled(): return FeatureFlag.objects.create(slug="billing", name="Billing", is_active=True) @pytest.fixture def gateway(billing_enabled): return PaymentGateway.objects.create( slug="fake", display_name="Fake", gateway_class="tests.helpers.FakeGateway", is_active=True, ) @pytest.fixture def app(): return App.objects.create(slug="messenger", name="Messenger", monetization=MonetizationModel.SUBSCRIPTION) @pytest.fixture def plan(app, gateway): return Plan.objects.create( app=app, slug="pro", name="Pro", price_amount=4900, price_currency="USD", interval="monthly", gateway=gateway, external_id="price_123", ) @pytest.fixture def subscription(tenant, plan, gateway): return Subscription.all_objects.create( tenant=tenant, plan=plan, gateway=gateway, external_id="sub_1", status=SubscriptionStatus.ACTIVE, ) class TestFeatureGate: def test_disabled_feature_returns_404(self, authenticated_client): assert authenticated_client.get(GATEWAYS_URL).status_code == 404 assert authenticated_client.get(SUBSCRIPTIONS_URL).status_code == 404 assert authenticated_client.get(INVOICES_URL).status_code == 404 def test_catalog_is_public(self, api_client, plan): assert api_client.get(APPS_URL).status_code == 200 assert api_client.get(PLANS_URL).status_code == 200 class TestPaymentGatewayViewSet: def test_list_gateways(self, authenticated_client, gateway): response = authenticated_client.get(GATEWAYS_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["slug"] == "fake" def test_create_gateway(self, authenticated_client, billing_enabled): response = authenticated_client.post( GATEWAYS_URL, { "slug": "stripe", "display_name": "Stripe", "gateway_class": "infrasynth.billing.gateways.stripe.StripeGateway", }, format="json", ) assert response.status_code == 201 assert PaymentGateway.objects.count() == 1 def test_requires_auth(self, api_client, gateway): assert api_client.get(GATEWAYS_URL).status_code == 401 class TestPlanViewSet: def test_list_plans_public(self, api_client, plan): response = api_client.get(PLANS_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["slug"] == "pro" def test_retrieve_plan(self, api_client, plan): response = api_client.get(f"{PLANS_URL}{plan.slug}/") assert response.status_code == 200 assert response.data["price_amount"] == 4900 assert response.data["gateway_info"]["slug"] == "fake" assert response.data["app_info"]["slug"] == "messenger" def test_retrieve_inactive_plan_404(self, api_client, plan): plan.is_active = False plan.save(update_fields=["is_active"]) assert api_client.get(f"{PLANS_URL}{plan.slug}/").status_code == 404 class TestEntitlementViewSet: def test_list_entitlements(self, authenticated_client, tenant, plan): Entitlement.all_objects.create(tenant=tenant, app=plan.app, plan=plan) response = authenticated_client.get(ENTITLEMENTS_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["app_info"]["slug"] == "messenger" def test_requires_auth(self, api_client, tenant, plan): Entitlement.all_objects.create(tenant=tenant, app=plan.app, plan=plan) assert api_client.get(ENTITLEMENTS_URL).status_code == 401 class TestSubscriptionViewSet: def test_list_subscriptions(self, authenticated_client, subscription): response = authenticated_client.get(SUBSCRIPTIONS_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["status"] == "active" def test_requires_auth(self, api_client, subscription): assert api_client.get(SUBSCRIPTIONS_URL).status_code == 401 def test_subscribe_action(self, authenticated_client, plan): response = authenticated_client.post( f"{SUBSCRIPTIONS_URL}subscribe/", {"plan_slug": "pro"}, format="json", ) assert response.status_code == 200 assert response.data["checkout_url"] == "https://checkout.example.com/cs_fake_123" assert response.data["session_id"] == "cs_fake_123" assert response.data["gateway"] == "fake" def test_subscribe_invalid_plan(self, authenticated_client): response = authenticated_client.post( f"{SUBSCRIPTIONS_URL}subscribe/", {"plan_slug": "nope"}, format="json", ) assert response.status_code == 404 def test_checkout_action(self, authenticated_client, plan): response = authenticated_client.post( CHECKOUT_URL, {"app": "messenger", "plan": "pro"}, format="json", ) assert response.status_code == 200 assert response.data["checkout_url"] == "https://checkout.example.com/cs_fake_123" assert response.data["app_slug"] == "messenger" class TestInvoiceViewSet: def test_list_invoices(self, authenticated_client, tenant, subscription): Invoice.all_objects.create( tenant=tenant, subscription=subscription, invoice_number="INV-1", amount=4900, currency="USD", status="open", ) response = authenticated_client.get(INVOICES_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["invoice_number"] == "INV-1" def test_requires_auth(self, api_client, subscription): assert api_client.get(INVOICES_URL).status_code == 401 class TestPaymentTransactionViewSet: def test_list_transactions(self, authenticated_client, tenant, subscription): PaymentTransaction.all_objects.create( tenant=tenant, invoice=None, gateway=subscription.gateway, amount=4900, currency="USD", status="approved", ) response = authenticated_client.get(TRANSACTIONS_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["status"] == "approved" def test_requires_auth(self, api_client, billing_enabled): assert api_client.get(TRANSACTIONS_URL).status_code == 401 class TestWebhookReceiveView: def test_receive_processed(self, api_client, gateway): response = api_client.post( WEBHOOK_URL, { "gateway_slug": "fake", "event": "checkout.session.completed", "data": {"id": "cs_1"}, }, format="json", ) assert response.status_code == 200 assert response.data["status"] == "processed" assert response.data["event_type"] == "checkout.session.completed" def test_receive_ignored_event(self, api_client, gateway): response = api_client.post( WEBHOOK_URL, {"gateway_slug": "fake", "event": "ignored", "data": {}}, format="json", ) assert response.status_code == 200 assert response.data["status"] == "ignored" def test_receive_uses_first_active_gateway(self, api_client, gateway): response = api_client.post( WEBHOOK_URL, {"event": "checkout.session.completed", "data": {}}, format="json", ) assert response.status_code == 200 assert response.data["event_type"] == "checkout.session.completed" def test_receive_no_gateway_404(self, api_client): response = api_client.post(WEBHOOK_URL, {"event": "x", "data": {}}, format="json") assert response.status_code == 404 def test_receive_is_public(self, api_client, gateway): response = api_client.post( WEBHOOK_URL, {"gateway_slug": "fake", "event": "e", "data": {}}, format="json", ) assert response.status_code == 200