import pytest from infrasynth.billing.models import ( BillingPlan, Invoice, PaymentGateway, PaymentTransaction, Subscription, ) from infrasynth.features.models import FeatureFlag from infrasynth.shared.enums import SubscriptionStatus pytestmark = pytest.mark.django_db GATEWAYS_URL = "/api/billing/gateways/" PLANS_URL = "/api/billing/plans/" SUBSCRIPTIONS_URL = "/api/billing/subscriptions/" INVOICES_URL = "/api/billing/invoices/" TRANSACTIONS_URL = "/api/billing/transactions/" WEBHOOK_URL = "/api/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 plan(gateway): return BillingPlan.objects.create( slug="pro", name="Pro", price_amount="49.00", price_currency="USD", interval="monthly", gateway=gateway, external_id="price_123", ) @pytest.fixture def subscription(user, plan, gateway): return Subscription.objects.create( user=user, 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(PLANS_URL).status_code == 404 assert authenticated_client.get(SUBSCRIPTIONS_URL).status_code == 404 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 TestBillingPlanViewSet: 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.id}/") assert response.status_code == 200 assert response.data["price_amount"] == "49.00" assert response.data["gateway_info"]["slug"] == "fake" 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.id}/").status_code == 404 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 class TestInvoiceViewSet: def test_list_invoices(self, authenticated_client, subscription, user): Invoice.objects.create( subscription=subscription, user=user, invoice_number="INV-1", amount=49, 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, user): assert api_client.get(INVOICES_URL).status_code == 401 class TestPaymentTransactionViewSet: def test_list_transactions(self, authenticated_client, subscription, user): PaymentTransaction.objects.create( invoice=None, gateway=subscription.gateway, amount=49, 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