Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support EUR payments in stripe #470

Merged
merged 2 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,16 @@ PAYPAL_BUSINESS = "[email protected]"
# Stripe
# https://stripe.com/docs/tutorials/dashboard#api-keys
STRIPE_KEYS = {
"SECRET": "",
"PUBLISHABLE": "",
"WEBHOOK_SECRET": ""
"USD": {
"SECRET": "",
"PUBLISHABLE": "",
"WEBHOOK_SECRET": ""
},
"EUR": {
"SECRET": "",
"PUBLISHABLE": "",
"WEBHOOK_SECRET": ""
}
}

# if developing payment integration locally, change this to your localhost url
Expand Down
13 changes: 10 additions & 3 deletions consul_config.py.ctmpl
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,16 @@ PAYPAL_ACCOUNT_IDS = {
PAYPAL_BUSINESS = '''{{template "KEY" "payments/paypal/business_email"}}'''

STRIPE_KEYS = {
"SECRET": '''{{template "KEY" "payments/stripe/secret"}}''',
"PUBLISHABLE": '''{{template "KEY" "payments/stripe/publishable"}}''',
"WEBHOOK_SECRET": '''{{template "KEY" "payments/stripe/webhook_secret"}}''',
"USD": {
"SECRET": '''{{template "KEY" "payments/stripe/secret"}}''',
"PUBLISHABLE": '''{{template "KEY" "payments/stripe/publishable"}}''',
"WEBHOOK_SECRET": '''{{template "KEY" "payments/stripe/webhook_secret"}}''',
},
"EUR": {
"SECRET": '''{{template "KEY" "payments/stripe-eu/secret"}}''',
"PUBLISHABLE": '''{{template "KEY" "payments/stripe-eu/publishable"}}''',
"WEBHOOK_SECRET": '''{{template "KEY" "payments/stripe-eu/webhook_secret"}}''',
},
}

# MusicBrainz Base URL must have a trailing slash.
Expand Down
2 changes: 0 additions & 2 deletions metabrainz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,6 @@ def create_app(debug=None, config_path=None):
if app.config["QUICKBOOKS_CLIENT_ID"]:
admin.add_view(QuickBooksView(name='Invoices', endpoint="quickbooks/", category='Quickbooks'))

stripe.api_key = app.config["STRIPE_KEYS"]["SECRET"]

return app


Expand Down
19 changes: 14 additions & 5 deletions metabrainz/model/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,17 +300,26 @@ def _extract_paypal_ipn_options(form: dict) -> dict:
return options

@classmethod
def log_subscription_charge(cls, invoice):
def log_subscription_charge(cls, currency, invoice):
""" Log successful Stripe charges for a recurring payment/donation """
charge = stripe.Charge.retrieve(invoice["charge"], expand=["balance_transaction"])
if currency.lower() == "usd":
api_key = current_app.config["STRIPE_KEYS"]["USD"]["SECRET"]
else:
api_key = current_app.config["STRIPE_KEYS"]["EUR"]["SECRET"]
charge = stripe.Charge.retrieve(invoice["charge"], expand=["balance_transaction"], api_key=api_key)
metadata = invoice["lines"]["data"][0]["metadata"]
return cls._log_stripe_charge(charge, metadata)

@classmethod
def log_one_time_charge(cls, session):
def log_one_time_charge(cls, currency, session):
""" Log successful Stripe charge for one time payment/donation """
if currency.lower() == "usd":
api_key = current_app.config["STRIPE_KEYS"]["USD"]["SECRET"]
else:
api_key = current_app.config["STRIPE_KEYS"]["EUR"]["SECRET"]
payment_intent = stripe.PaymentIntent.retrieve(session["payment_intent"],
expand=["latest_charge.balance_transaction"])
expand=["latest_charge.balance_transaction"],
api_key=api_key)
charge = payment_intent["latest_charge"]
metadata = payment_intent["metadata"]
return cls._log_stripe_charge(charge, metadata)
Expand Down Expand Up @@ -343,7 +352,7 @@ def _log_stripe_charge(cls, charge, metadata):
last_name="",
amount=transaction["net"] / 100, # cents should be converted
fee=transaction["fee"] / 100, # cents should be converted
currency="usd",
currency=currency,
transaction_id=charge["id"],
payment_method=PAYMENT_METHOD_STRIPE,
is_donation=metadata["is_donation"],
Expand Down
4 changes: 2 additions & 2 deletions metabrainz/model/payment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def test_log_stripe_charge_donation(self, mock_stripe):
}
mock_stripe.retrieve.return_value = payment_intent
session = self.session_without_metadata.copy()
Payment.log_one_time_charge(session)
Payment.log_one_time_charge("usd", session)
self.assertEqual(len(Payment.query.all()), 1)

@patch("stripe.PaymentIntent")
Expand All @@ -461,5 +461,5 @@ def test_log_stripe_charge_payment(self, mock_stripe):
"invoice_number": 42,
}
mock_stripe.retrieve.return_value = payment_intent
Payment.log_one_time_charge(self.session_without_metadata)
Payment.log_one_time_charge("usd", self.session_without_metadata)
self.assertEqual(len(Payment.query.all()), 1)
25 changes: 18 additions & 7 deletions metabrainz/payments/stripe/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def pay():
return redirect(url_for("payments.error", is_donation=is_donation))

is_recurring = form.recurring.data
currency = form.currency.data
if currency.lower() == "usd":
api_key = current_app.config["STRIPE_KEYS"]["USD"]["SECRET"]
else:
api_key = current_app.config["STRIPE_KEYS"]["EUR"]["SECRET"]

charge_metadata = {
"is_donation": is_donation,
Expand All @@ -46,7 +51,7 @@ def pay():
{
"price_data": {
"unit_amount": int(form.amount.data * 100), # amount in cents
"currency": form.currency.data,
"currency": currency,
"product_data": {
"name": "Support the MetaBrainz Foundation",
"description": description
Expand Down Expand Up @@ -77,18 +82,24 @@ def pay():
}

try:
session = stripe.checkout.Session.create(**session_config)
session = stripe.checkout.Session.create(**session_config, api_key=api_key)
return redirect(session.url, code=303)
except Exception as e:
current_app.logger.error(e, exc_info=True)
return redirect(url_for("payments.error", is_donation=is_donation))


@payments_stripe_bp.route("/webhook/", methods=["POST"])
def webhook():
@payments_stripe_bp.route("/webhook/<currency>/", methods=["POST"])
amCap1712 marked this conversation as resolved.
Show resolved Hide resolved
def webhook(currency):
payload = request.data
sig_header = request.headers.get("Stripe-Signature")
webhook_secret = current_app.config["STRIPE_KEYS"]["WEBHOOK_SECRET"]

if currency.lower() == "usd":
webhook_secret = current_app.config["STRIPE_KEYS"]["USD"]["WEBHOOK_SECRET"]
elif currency.lower() == "eur":
webhook_secret = current_app.config["STRIPE_KEYS"]["EUR"]["WEBHOOK_SECRET"]
else:
return jsonify({"error": "invalid currency"}), 400

try:
event = stripe.Webhook.construct_event(payload, sig_header, webhook_secret)
Expand All @@ -105,8 +116,8 @@ def webhook():
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
if session["mode"] == "payment":
Payment.log_one_time_charge(session)
Payment.log_one_time_charge(currency, session)
elif event["type"] == "invoice.paid":
Payment.log_subscription_charge(event["data"]["object"])
Payment.log_subscription_charge(currency, event["data"]["object"])

return jsonify({"status": "ok"})
10 changes: 3 additions & 7 deletions metabrainz/payments/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import division
from flask import Blueprint, request, render_template, url_for, redirect, current_app, jsonify
from flask_babel import gettext
from metabrainz.payments import Currency, SUPPORTED_CURRENCIES
from metabrainz.payments import SUPPORTED_CURRENCIES
from metabrainz.model.payment import Payment
from metabrainz.payments.forms import DonationForm, PaymentForm
from metabrainz import flash
Expand All @@ -18,9 +18,7 @@
@payments_bp.route('/donate')
def donate():
"""Regular donation page."""
stripe_public_key = current_app.config['STRIPE_KEYS']['PUBLISHABLE']
return render_template('payments/donate.html', form=DonationForm(),
stripe_public_key=stripe_public_key)
return render_template('payments/donate.html', form=DonationForm())


@payments_bp.route('/payment/')
Expand All @@ -35,9 +33,7 @@ def payment(currency):
currency = currency.lower()
if currency not in SUPPORTED_CURRENCIES:
return redirect('.payment_selector')
stripe_public_key = current_app.config['STRIPE_KEYS']['PUBLISHABLE']
return render_template('payments/payment.html', form=PaymentForm(), currency=currency,
stripe_public_key=stripe_public_key)
return render_template('payments/payment.html', form=PaymentForm(), currency=currency)


@payments_bp.route('/donors')
Expand Down
15 changes: 0 additions & 15 deletions metabrainz/templates/payments/donate.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ <h3>{{ _('US Check') }}</h3>
$.each(buttons, function( index, button ) {
button.removeAttr('disabled');
});
updateButtonState();

} else { // Disabled
$.each(buttons, function( index, button ) {
Expand All @@ -184,19 +183,6 @@ <h3>{{ _('US Check') }}</h3>
}
}

function updateButtonState() {
// This function disables or enables payment buttons depending on what
// features they support.

// We don't support payments in EUR via Stripe currently.
if (selectedCurrency === CURRENCY.Euro) {
buttons.stripe.attr('disabled', 'disabled');
} else {
buttons.stripe.removeAttr('disabled');
}
}


///////////////
// AMOUNT INPUT
///////////////
Expand Down Expand Up @@ -226,7 +212,6 @@ <h3>{{ _('US Check') }}</h3>
} else {
console.error("Unknown currency:", this.value)
}
updateButtonState();
});

///////////////
Expand Down
14 changes: 0 additions & 14 deletions metabrainz/templates/payments/payment.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,6 @@
// PAYMENTS
///////////

function updateRecurringState() {
// We don't support recurring payments via Stripe
if ($('#recurring-flag').is(":checked")) {
buttons.stripe.attr('disabled', 'disabled');
} else {
buttons.stripe.removeAttr('disabled');
}
}

$('#recurring-flag').change(function() {
updateRecurringState();
});


// Stripe
buttons.stripe.on('click', function(e) {
setButtonsState(false);
Expand Down
Loading