Monetization app + RevenueCat webhooks (store IAP ledger) (#69)
## Summary - Rename `finance` → **`monetization`** Django app (keep `finance_*` tables via `label = "finance"`) - Add `services/stripe.py` + `services/revenuecat.py`; RevenueCat webhook upserts **subscription + Invoice/Payment** (billing history parity with Stripe) - Mount `/api/monetization/` + keep `/api/finance/` alias - Extend `Source`/`Provider` with `revenuecat`; product→plan mapping via `revenuecat_product_id` / `REVENUECAT_PRODUCT_PLAN_MAP` Closes #68. Companion to [chat_web_app#100](ai_ml_operations/chat_web_app#100). ## Test plan - [x] `manage.py test monetization.tests` (54 OK) - [x] Smoke `chat_backend.tests.test_views_documents` + `test_oauth` - [ ] Deploy: set `REVENUECAT_WEBHOOK_SECRET`; point RC webhook at `/api/finance/webhooks/revenuecat/` - [ ] Map store product IDs on `SubscriptionPlan.revenuecat_product_id` (or env JSON map) - [ ] Sandbox INITIAL_PURCHASE → subscription `source=revenuecat` + invoice in `/finance/invoices/`Reviewed-on: #69
This commit was merged in pull request #69.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Tests for Stripe Checkout Session API (mocked Stripe SDK)."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from chat_backend.tests.factories import make_company, make_user
|
||||
from monetization.models import Invoice
|
||||
|
||||
|
||||
class CreateCheckoutSessionViewTestCase(APITestCase):
|
||||
def setUp(self):
|
||||
self.company = make_company()
|
||||
self.user = make_user(company=self.company)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
self.url = reverse("finance_checkout")
|
||||
|
||||
@override_settings(
|
||||
STRIPE_SECRET_KEY="sk_test_fake",
|
||||
SUBSCRIPTION_PRICE_AMOUNT_CENTS=1000,
|
||||
SUBSCRIPTION_PRICE_CURRENCY="usd",
|
||||
SUBSCRIPTION_PRICE_INTERVAL="month",
|
||||
SUBSCRIPTION_PRODUCT_NAME="Chat Subscription",
|
||||
STRIPE_PRICE_ID="",
|
||||
STRIPE_CHECKOUT_SUCCESS_URL="http://localhost:3000/ok",
|
||||
STRIPE_CHECKOUT_CANCEL_URL="http://localhost:3000/cancel",
|
||||
)
|
||||
@patch("monetization.services.stripe.stripe.checkout.Session.create")
|
||||
def test_creates_checkout_session_and_draft_invoice(self, mock_create):
|
||||
mock_session = MagicMock()
|
||||
mock_session.id = "cs_test_abc"
|
||||
mock_session.url = "https://checkout.stripe.com/c/pay/cs_test_abc"
|
||||
mock_session.customer = None
|
||||
mock_create.return_value = mock_session
|
||||
|
||||
response = self.client.post(self.url, {}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(
|
||||
response.data["checkout_url"],
|
||||
"https://checkout.stripe.com/c/pay/cs_test_abc",
|
||||
)
|
||||
self.assertEqual(response.data["session_id"], "cs_test_abc")
|
||||
|
||||
mock_create.assert_called_once()
|
||||
kwargs = mock_create.call_args.kwargs
|
||||
self.assertEqual(kwargs["mode"], "subscription")
|
||||
line_item = kwargs["line_items"][0]
|
||||
self.assertEqual(line_item["price_data"]["unit_amount"], 1000)
|
||||
self.assertEqual(line_item["price_data"]["currency"], "usd")
|
||||
self.assertEqual(
|
||||
line_item["price_data"]["recurring"]["interval"], "month"
|
||||
)
|
||||
self.assertEqual(kwargs["metadata"]["user_id"], str(self.user.pk))
|
||||
|
||||
invoice = Invoice.objects.get(stripe_checkout_session_id="cs_test_abc")
|
||||
self.assertEqual(invoice.user, self.user)
|
||||
self.assertEqual(invoice.company, self.company)
|
||||
self.assertEqual(invoice.amount_due, 1000)
|
||||
self.assertEqual(invoice.status, Invoice.Status.OPEN)
|
||||
|
||||
@override_settings(STRIPE_SECRET_KEY="", STRIPE_PRICE_ID="")
|
||||
def test_missing_stripe_key_returns_503(self):
|
||||
response = self.client.post(self.url, {}, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
def test_unauthenticated_rejected(self):
|
||||
self.client.force_authenticate(user=None)
|
||||
response = self.client.post(self.url, {}, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@override_settings(
|
||||
STRIPE_SECRET_KEY="sk_test_fake",
|
||||
STRIPE_PRICE_ID="price_abc123",
|
||||
STRIPE_CHECKOUT_SUCCESS_URL="http://localhost:3000/ok",
|
||||
STRIPE_CHECKOUT_CANCEL_URL="http://localhost:3000/cancel",
|
||||
)
|
||||
@patch("monetization.services.stripe.stripe.checkout.Session.create")
|
||||
def test_uses_stripe_price_id_when_set(self, mock_create):
|
||||
mock_session = MagicMock()
|
||||
mock_session.id = "cs_test_price"
|
||||
mock_session.url = "https://checkout.stripe.com/c/pay/cs_test_price"
|
||||
mock_session.customer = None
|
||||
mock_create.return_value = mock_session
|
||||
|
||||
response = self.client.post(self.url, {}, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
line_item = mock_create.call_args.kwargs["line_items"][0]
|
||||
self.assertEqual(line_item, {"price": "price_abc123", "quantity": 1})
|
||||
|
||||
|
||||
class InvoicePaymentListViewTestCase(APITestCase):
|
||||
def setUp(self):
|
||||
self.company = make_company()
|
||||
self.user = make_user(company=self.company)
|
||||
self.other = make_user(
|
||||
email="other@test.com",
|
||||
username="other@test.com",
|
||||
company=self.company,
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
Invoice.objects.create(
|
||||
user=self.user,
|
||||
company=self.company,
|
||||
amount_due=1000,
|
||||
stripe_checkout_session_id="cs_mine",
|
||||
)
|
||||
Invoice.objects.create(
|
||||
user=self.other,
|
||||
company=self.company,
|
||||
amount_due=1000,
|
||||
stripe_checkout_session_id="cs_other",
|
||||
)
|
||||
|
||||
def test_list_own_invoices_only(self):
|
||||
response = self.client.get(reverse("finance_invoices"))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]["stripe_checkout_session_id"], "cs_mine")
|
||||
Reference in New Issue
Block a user