Skip to content

Stripe SCA supportΒ #1052

@benjaoming

Description

@benjaoming

Background

New EU legislation means that online card payment has to go through the issuer's 2FA. This is known as "Strong Customer Authentication". Some background here: https://support.stripe.com/questions/strong-customer-authentication-sca-enforcement-date

Solution

AFAIK, in order to process payments for fundraising, Django needs to use newer API and patterns offered by Stripe. I have an existing implementation to refer to.

Firstly, a checkout session is created and the user goes through Stripe's pages for payments. This is almost the same as before, except there is no modal popup on the shop's own site, but you to through a branded Stripe page.

Following this, the main change is that now the backend processes the payment through an async callback to a webhook which it receives from Stripe.

In the frontend, the user is redirected to a custom success/failure page. But AFAIK, this page has to be generic because it cannot assume that the payment is successful until the webhook is called.

Here is an implementation of a webhook that processes the successful session:

import stripe
# ...

def stripe_config(currency):
    """
    Sets configuration of stripe module according to the currency that we are
    using - if for instance you have a Stripe account for Euro payments and
    one for Dollar payments
    """
    config = settings.STRIPE[currency]

    if settings.DEBUG:
        stripe.api_key = config["api_key_test"]
    else:
        stripe.api_key = config["api_key"]

    return config


@csrf_exempt
def stripe_callback(request, currency):
    """
    Handle callbacks from Stripe, for instance /payment/stripe/webhook/usd/
    """

    payload = request.body
    sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
    event = None

    # Call stripe_config - in case 
    config = stripe_config(currency)

    try:
        # Notice that the endpoint itself has a secret known only to the endpoint and Stripe!
        event = stripe.Webhook.construct_event(
            payload, sig_header, config["endpoint_secret"]
        )
    except ValueError:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError:
        # Invalid signature
        return HttpResponse(status=400)

    # Handle the checkout.session.completed event
    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]

        # Activate original language of this session
        if "metadata" in session and "language" in session["metadata"]:
            translation.activate(session["metadata"]["language"])

        if settings.DEBUG:
            order_id = 123
        else:
            # In this example, we stored an order ID with the payment
            order_id = session["client_reference_id"]

        order = models.Order.objects.get(pk=order_id)

        # Create a payment object with information from the Stripe session and
        # mark the order as paid, then notify customer and admins.
        try:
            payment = models.Payment.from_order(order)
            payment.stripe_session_id = session["id"]
            payment.save()
            order.is_paid = True
            order.save()

            # Inform admins
            mail_admins("Card payment success", "Payment ID: {}".format(payment.pk))

            # Inform customer
            m = mail.PaymentCreateMail(
                request, payment=payment, to=[payment.order.email]
            )
            m.send()

        except Exception as e:
            # The card has been declined
            mail_admins("Card payment err", str(e))
            raise

    else:
        mail_admins("Card payment unknown payload", str(event))

    return HttpResponse(status=200)

Reference

Metadata

Metadata

Labels

fundraisingpythonPull requests that update Python code

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions