api.py 6.3 KB

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