api.py 6.7 KB

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