api.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. from contextlib import asynccontextmanager
  2. from asgiref.sync import sync_to_async
  3. from django.conf import settings
  4. from django.db.models import Prefetch
  5. from django.http import HttpResponse
  6. from django.shortcuts import aget_object_or_404
  7. from djstripe.models import Customer, Price, Product, Subscription, SubscriptionItem
  8. from djstripe.settings import djstripe_settings
  9. from ninja import Router
  10. from ninja.errors import HttpError
  11. from ninja.pagination import paginate
  12. from stripe import AIOHTTPClient, StripeClient
  13. from apps.organizations_ext.constants import OrganizationUserRole
  14. from apps.organizations_ext.models import Organization
  15. from glitchtip.api.authentication import AuthHttpRequest
  16. from .schema import (
  17. CreateSubscriptionResponse,
  18. PriceIDSchema,
  19. ProductPriceSchema,
  20. SubscriptionIn,
  21. SubscriptionSchema,
  22. )
  23. router = Router()
  24. @asynccontextmanager
  25. async def get_stripe_client():
  26. client = StripeClient(
  27. djstripe_settings.STRIPE_SECRET_KEY,
  28. http_client=AIOHTTPClient(),
  29. stripe_version=djstripe_settings.STRIPE_API_VERSION,
  30. )
  31. try:
  32. yield client
  33. finally:
  34. # Close the client
  35. # https://github.com/stripe/stripe-python/issues/874
  36. await client._requestor._client.close_async()
  37. @router.get(
  38. "subscriptions/{slug:organization_slug}/", response=SubscriptionSchema | None
  39. )
  40. async def get_subscription(request: AuthHttpRequest, organization_slug: str):
  41. return await (
  42. Subscription.objects.filter(
  43. livemode=settings.STRIPE_LIVE_MODE,
  44. customer__subscriber__users=request.auth.user_id,
  45. customer__subscriber__slug=organization_slug,
  46. )
  47. .exclude(status="canceled")
  48. .select_related("customer")
  49. .prefetch_related(
  50. Prefetch(
  51. "items",
  52. queryset=SubscriptionItem.objects.select_related("price__product"),
  53. )
  54. )
  55. .order_by("-created")
  56. .afirst()
  57. )
  58. @router.post("subscriptions/", response=CreateSubscriptionResponse)
  59. async def create_subscription(request: AuthHttpRequest, payload: SubscriptionIn):
  60. organization = await aget_object_or_404(
  61. Organization,
  62. id=payload.organization,
  63. organization_users__role=OrganizationUserRole.OWNER,
  64. organization_users__user=request.auth.user_id,
  65. )
  66. price = await aget_object_or_404(Price, id=payload.price, unit_amount=0)
  67. customer, _ = await sync_to_async(Customer.get_or_create)(subscriber=organization)
  68. if (
  69. await Subscription.objects.filter(customer=customer)
  70. .exclude(status="canceled")
  71. .aexists()
  72. ):
  73. raise HttpError(400, "Customer already has subscription")
  74. subscription = await sync_to_async(customer.subscribe)(items=[{"price": price}])
  75. subscription = (
  76. await Subscription.objects.filter(id=subscription.id)
  77. .select_related("customer")
  78. .prefetch_related(
  79. Prefetch(
  80. "items",
  81. queryset=SubscriptionItem.objects.select_related("price__product"),
  82. )
  83. )
  84. .aget()
  85. )
  86. return {
  87. "price": price.id,
  88. "organization": organization.id,
  89. "subscription": subscription,
  90. }
  91. @router.get("subscriptions/{slug:organization_slug}/events_count/")
  92. async def get_subscription_events_count(
  93. request: AuthHttpRequest, organization_slug: str
  94. ):
  95. org = await aget_object_or_404(
  96. Organization.objects.with_event_counts(),
  97. slug=organization_slug,
  98. users=request.auth.user_id,
  99. )
  100. return {
  101. "eventCount": org.issue_event_count,
  102. "transactionEventCount": org.transaction_count,
  103. "uptimeCheckEventCount": org.uptime_check_event_count,
  104. "fileSizeMB": org.file_size,
  105. }
  106. @router.post("organizations/{slug:organization_slug}/create-billing-portal/")
  107. async def stripe_billing_portal(request: AuthHttpRequest, organization_slug: str):
  108. """See https://stripe.com/docs/billing/subscriptions/integrating-self-serve-portal"""
  109. organization = await aget_object_or_404(
  110. Organization,
  111. slug=organization_slug,
  112. organization_users__role=OrganizationUserRole.OWNER,
  113. organization_users__user=request.auth.user_id,
  114. )
  115. customer, _ = await sync_to_async(Customer.get_or_create)(subscriber=organization)
  116. domain = settings.GLITCHTIP_URL.geturl()
  117. async with get_stripe_client() as client:
  118. session = await client.billing_portal.sessions.create_async(
  119. params={
  120. "customer": customer.id,
  121. "return_url": domain
  122. + "/"
  123. + organization.slug
  124. + "/settings/subscription?billing_portal_redirect=true",
  125. }
  126. )
  127. return session
  128. @router.post(
  129. "organizations/{slug:organization_slug}/create-stripe-subscription-checkout/"
  130. )
  131. async def create_stripe_subscription_checkout(
  132. request: AuthHttpRequest, organization_slug: str, payload: PriceIDSchema
  133. ):
  134. """
  135. Create Stripe Checkout, send to client for redirecting to Stripe
  136. See https://stripe.com/docs/api/checkout/sessions/create
  137. """
  138. organization = await aget_object_or_404(
  139. Organization,
  140. slug=organization_slug,
  141. organization_users__role=OrganizationUserRole.OWNER,
  142. organization_users__user=request.auth.user_id,
  143. )
  144. price = await aget_object_or_404(Price, id=payload.price)
  145. customer, _ = await sync_to_async(Customer.get_or_create)(subscriber=organization)
  146. domain = settings.GLITCHTIP_URL.geturl()
  147. async with get_stripe_client() as client:
  148. session = await client.checkout.sessions.create_async(
  149. params={
  150. "payment_method_types": ["card"],
  151. "line_items": [
  152. {
  153. "price": price.id,
  154. "quantity": 1,
  155. }
  156. ],
  157. "mode": "subscription",
  158. "customer": customer.id,
  159. "automatic_tax": {
  160. "enabled": settings.STRIPE_AUTOMATIC_TAX,
  161. },
  162. "customer_update": {"address": "auto", "name": "auto"},
  163. "tax_id_collection": {
  164. "enabled": True,
  165. },
  166. "success_url": domain
  167. + "/"
  168. + organization.slug
  169. + "/settings/subscription?session_id={CHECKOUT_SESSION_ID}",
  170. "cancel_url": domain + "",
  171. }
  172. )
  173. return session
  174. @router.get("products/", response=list[ProductPriceSchema])
  175. @paginate
  176. async def list_products(request: AuthHttpRequest, response: HttpResponse):
  177. return (
  178. Product.objects.filter(
  179. active=True,
  180. livemode=settings.STRIPE_LIVE_MODE,
  181. prices__active=True,
  182. metadata__events__isnull=False,
  183. metadata__is_public="true",
  184. )
  185. .prefetch_related(
  186. Prefetch("prices", queryset=Price.objects.filter(active=True))
  187. )
  188. .distinct()
  189. )