Skip to content

Commit

Permalink
Subscriptions using Stripe (#375)
Browse files Browse the repository at this point in the history
* Interim checkin for subscriptions using Stripe

* test stripe subscriptions
  • Loading branch information
amCap1712 authored Aug 10, 2023
1 parent 247bad9 commit d11531e
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 41 deletions.
27 changes: 19 additions & 8 deletions metabrainz/model/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,19 +300,30 @@ def _extract_paypal_ipn_options(form: dict) -> dict:
return options

@classmethod
def log_stripe_charge(cls, session):
def log_subscription_charge(cls, invoice):
""" Log successful Stripe charges for a recurring payment/donation """
charge = stripe.Charge.retrieve(invoice["charge"], expand=["balance_transaction"])
metadata = invoice["lines"]["data"][0]["metadata"]
return cls._log_stripe_charge(charge, metadata)

@classmethod
def log_one_time_charge(cls, session):
""" Log successful Stripe charge for one time payment/donation """
payment_intent = stripe.PaymentIntent.retrieve(session["payment_intent"],
expand=["charges.data.balance_transaction"])
charge = payment_intent["charges"]["data"][0]
metadata = payment_intent["metadata"]
return cls._log_stripe_charge(charge, metadata)

@classmethod
def _log_stripe_charge(cls, charge, metadata):
"""Log successful Stripe charge.
Args:
session: The charge object from Stripe. More information about it is
available at https://stripe.com/docs/api/python#charge_object.
"""
current_app.logger.debug("Processing Stripe charge...")
metadata = session["metadata"]

payment_intent = stripe.PaymentIntent.retrieve(session["payment_intent"],
expand=["charges.data.balance_transaction"])
charge = payment_intent["charges"]["data"][0]

# Transaction already exists in the database, do not insert again
if db.session.query(exists().where(Payment.transaction_id == charge["id"])).scalar():
Expand All @@ -324,15 +335,15 @@ def log_stripe_charge(cls, session):
transaction = charge["balance_transaction"]
currency = transaction["currency"].lower()
if currency not in SUPPORTED_CURRENCIES:
current_app.logger.warning("Unsupported currency: ", session["currency"])
current_app.logger.warning("Unsupported currency: ", transaction["currency"])
return

new_donation = cls(
first_name=details["name"],
last_name="",
amount=transaction["net"] / 100, # cents should be converted
fee=transaction["fee"] / 100, # cents should be converted
currency=currency,
currency="usd",
transaction_id=charge["id"],
payment_method=PAYMENT_METHOD_STRIPE,
is_donation=metadata["is_donation"],
Expand Down
70 changes: 47 additions & 23 deletions metabrainz/payments/stripe/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,62 @@ def pay():
if not form.validate():
return redirect(url_for("payments.error", is_donation=is_donation))

is_recurring = form.recurring.data

charge_metadata = {
"is_donation": is_donation,
}
if is_donation:
# Using DonationForm
if is_donation: # Using DonationForm
charge_metadata["editor"] = form.editor.data
charge_metadata["anonymous"] = form.anonymous.data
charge_metadata["can_contact"] = form.can_contact.data
description = "Donation to the MetaBrainz Foundation"
else:
# Using PaymentForm
else: # Using PaymentForm
charge_metadata["invoice_number"] = form.invoice_number.data
description = f"Payment to the MetaBrainz Foundation for Invoice {form.invoice_number.data}"
# Add invoice number to description only for non-recurring payments
if is_recurring:
description = "Payment to the MetaBrainz Foundation"
else:
description = f"Payment to the MetaBrainz Foundation for Invoice {form.invoice_number.data}"

try:
session = stripe.checkout.Session.create(
billing_address_collection="required",
line_items=[{
session_config = {
"billing_address_collection": "required",
"line_items": [
{
"price_data": {
"unit_amount": int(form.amount.data * 100), # amount in cents
"unit_amount": int(form.amount.data * 100), # amount in cents
"currency": form.currency.data,
"product_data": {
"name": "Support the MetaBrainz Foundation",
"description": description
}
},
"quantity": 1
}],
payment_intent_data={
"description": description
},
payment_method_types=["card"],
mode="payment",
submit_type="donate" if is_donation else "pay",
# stripe wants absolute urls so url_for doesn't suffice
success_url=f'{current_app.config["SERVER_BASE_URL"]}/payment/complete?is_donation={is_donation}',
cancel_url=f'{current_app.config["SERVER_BASE_URL"]}/payment/cancelled?is_donation={is_donation}',
metadata=charge_metadata
)
}
],
"payment_method_types": ["card"],
"mode": "subscription",
# stripe wants absolute urls so url_for doesn't suffice
"success_url": f'{current_app.config["SERVER_BASE_URL"]}/payment/complete?is_donation={is_donation}',
"cancel_url": f'{current_app.config["SERVER_BASE_URL"]}/payment/cancelled?is_donation={is_donation}',
}

if is_recurring:
session_config["mode"] = "subscription"
# configure monthly subscription
session_config["line_items"][0]["price_data"]["recurring"] = {"interval": "month"}
session_config["subscription_data"] = {"metadata": charge_metadata}
else:
session_config["mode"] = "payment"
# submit_type and payment_intent_data are only allowed in payment mode
session_config["submit_type"] = "donate" if is_donation else "pay"
session_config["payment_intent_data"] = {
"description": description,
"metadata": charge_metadata
}

try:
session = stripe.checkout.Session.create(**session_config)
return redirect(session.url, code=303)
except Exception as e:
current_app.logger.error(e, exc_info=True)
Expand All @@ -82,7 +99,14 @@ def webhook():
current_app.logger.error("Stripe signature error, possibly fake event", exc_info=True)
return jsonify({"error": "invalid signature"}), 400

# for one time payments, mode is payment, and we use the checkout.session.completed event to log charges
# other option is mode = subscription i.e. recurring payments, for which payment_intent data is unavailable
# in this webhook. hence, we use invoice.paid event instead which contains it.
if event["type"] == "checkout.session.completed":
Payment.log_stripe_charge(event["data"]["object"])
session = event["data"]["object"]
if session["mode"] == "payment":
Payment.log_one_time_charge(session)
elif event["type"] == "invoice.paid":
Payment.log_subscription_charge(event["data"]["object"])

return jsonify({"status": "ok"})
12 changes: 2 additions & 10 deletions metabrainz/templates/payments/donate.html
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,8 @@ <h3>GitHub</h3>
// This function disables or enables payment buttons depending on what
// features they support.
// We don't support recurring payments via Stripe. Payments in EUR are
// also not supported currently.
if ($('#recurring-flag').is(":checked") || selectedCurrency === CURRENCY.Euro) {
// We don't support payments in EUR via Stripe currently.
if (selectedCurrency === CURRENCY.Euro) {
buttons.stripe.attr('disabled', 'disabled');
} else {
buttons.stripe.removeAttr('disabled');
Expand Down Expand Up @@ -234,13 +233,6 @@ <h3>GitHub</h3>
updateButtonState();
});

/////////////
// RECURRENCE
/////////////
$('#recurring-flag').change(function() {
updateButtonState();
});

///////////////
// EDITOR INPUT
///////////////
Expand Down

0 comments on commit d11531e

Please sign in to comment.