api.py 6.6 KB

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