Coverage for src/couchers/servicers/donations.py: 96%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import json
2import logging
4import grpc
5import stripe
7from couchers import errors, urls
8from couchers.config import config
9from couchers.db import session_scope
10from couchers.models import DonationInitiation, DonationType, Invoice, User
11from couchers.sql import couchers_select as select
12from couchers.tasks import send_donation_email
13from proto import donations_pb2, donations_pb2_grpc, stripe_pb2_grpc
14from proto.google.api import httpbody_pb2
16logger = logging.getLogger(__name__)
19class Donations(donations_pb2_grpc.DonationsServicer):
20 def InitiateDonation(self, request, context):
21 if not config["ENABLE_DONATIONS"]:
22 context.abort(grpc.StatusCode.UNAVAILABLE, errors.DONATIONS_DISABLED)
24 with session_scope() as session:
25 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
27 if request.amount < 2:
28 # we don't want to waste *all* of the donation on processing fees
29 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DONATION_TOO_SMALL)
31 if not user.stripe_customer_id:
32 # create a new stripe id for this user
33 customer = stripe.Customer.create(
34 email=user.email,
35 # metadata allows us to store arbitrary metadata for ourselves
36 metadata={"user_id": user.id},
37 api_key=config["STRIPE_API_KEY"],
38 )
39 user.stripe_customer_id = customer.id
40 # commit since we only ever want one stripe customer id per user, so if the rest of this api call fails, this will still be saved in the db
41 session.commit()
43 if request.recurring:
44 item = {
45 "price": config["STRIPE_RECURRING_PRODUCT_ID"],
46 "quantity": request.amount,
47 }
48 else:
49 item = {
50 "price_data": {
51 "currency": "usd",
52 "unit_amount": request.amount * 100, # input is in cents
53 "product_data": {
54 "name": f"Couchers financial supporter (one-time)",
55 "images": ["https://couchers.org/img/share.jpg"],
56 },
57 },
58 "quantity": 1,
59 }
61 checkout_session = stripe.checkout.Session.create(
62 client_reference_id=user.id,
63 submit_type="donate" if not request.recurring else None,
64 customer=user.stripe_customer_id,
65 success_url=urls.donation_success_url(),
66 cancel_url=urls.donation_cancelled_url(),
67 payment_method_types=["card"],
68 mode="subscription" if request.recurring else "payment",
69 line_items=[item],
70 api_key=config["STRIPE_API_KEY"],
71 )
73 session.add(
74 DonationInitiation(
75 user_id=user.id,
76 amount=request.amount,
77 stripe_checkout_session_id=checkout_session.id,
78 donation_type=DonationType.recurring if request.recurring else DonationType.one_time,
79 )
80 )
82 return donations_pb2.InitiateDonationRes(
83 stripe_checkout_session_id=checkout_session.id, stripe_checkout_url=checkout_session.url
84 )
87class Stripe(stripe_pb2_grpc.StripeServicer):
88 def Webhook(self, request, context):
89 # We're set up to receive the following webhook events (with explanations from stripe docs):
90 # For both recurring and one-off donations, we get a `charge.succeeded` event and we then send the user an
91 # invoice. There are other events too, but we don't handle them right now.
92 headers = dict(context.invocation_metadata())
94 event = stripe.Webhook.construct_event(
95 payload=request.data,
96 sig_header=headers.get("stripe-signature"),
97 secret=config["STRIPE_WEBHOOK_SECRET"],
98 api_key=config["STRIPE_API_KEY"],
99 )
100 data = event["data"]
101 event_type = event["type"]
102 event_id = event["id"]
103 data_object = data["object"]
105 # Get the type of webhook event sent - used to check the status of PaymentIntents.
106 logger.info(f"Got signed Stripe webhook, {event_type=}, {event_id=}")
108 if event_type == "charge.succeeded":
109 customer_id = data_object["customer"]
110 with session_scope() as session:
111 user = session.execute(select(User).where(User.stripe_customer_id == customer_id)).scalar_one()
112 # amount comes in cents
113 amount = int(data_object["amount"]) // 100
114 receipt_url = data_object["receipt_url"]
115 session.add(
116 Invoice(
117 user_id=user.id,
118 amount=amount,
119 stripe_payment_intent_id=data_object["payment_intent"],
120 stripe_receipt_url=receipt_url,
121 )
122 )
123 send_donation_email(user, amount, receipt_url)
124 else:
125 logger.info(f"Unhandled event from Stripe: {event_type}")
127 return httpbody_pb2.HttpBody(
128 content_type="application/json",
129 # json.dumps escapes non-ascii characters
130 data=json.dumps({"success": True}).encode("ascii"),
131 )