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

Stripe SCA support #1052

Open
benjaoming opened this issue Dec 6, 2020 · 11 comments
Open

Stripe SCA support #1052

benjaoming opened this issue Dec 6, 2020 · 11 comments
Assignees
Labels
fundraising python Pull requests that update Python code

Comments

@benjaoming
Copy link
Contributor

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

@carltongibson carltongibson self-assigned this Dec 7, 2020
@carltongibson
Copy link
Member

Hi @benjaoming Thanks for this! I will take a proper look tomorrow.

@carltongibson
Copy link
Member

Hey @benjaoming — Thanks again here. It's very clear. I shall have a pop at it and ping you when it doesn't work. 😉

@carltongibson
Copy link
Member

Ok, post Thursday update.

We need a slight update to the flow we're using. We'll use the new hosted checkout page. That'll let us use the Portal for managing subscriptions too, which is less code for us.

I'll potter over the weekend and finish next week now.

@carltongibson
Copy link
Member

Part 1 is #1058 — main donation page.

I'll follow-up shortly updating the backend Manage your donations page.

Finally I'll cleanup and update the Stripe API version to latest and so on.

@carltongibson
Copy link
Member

Note to self: I can add max value validator to the donation form (for silly values, which Sentry occasionally reports).

@sentry-io
Copy link

sentry-io bot commented Feb 11, 2021

Sentry issue: DJANGOPROJECTCOM-EW

carltongibson added a commit to carltongibson/djangoproject.com that referenced this issue Mar 9, 2021
Stripe's API rejects very large values. Sentry reports folks enjoying spending
the weekend entering such large values. So reject as invalid, and show an
appropriate alert before attempting to contact Stripe.
carltongibson added a commit to carltongibson/djangoproject.com that referenced this issue Mar 9, 2021
Stripe's API rejects very large values. Sentry reports folks enjoying spending
the weekend entering such large values. So reject as invalid, and show an
appropriate alert before attempting to contact Stripe.
carltongibson added a commit that referenced this issue Mar 9, 2021
Stripe's API rejects very large values. Sentry reports folks enjoying spending
the weekend entering such large values. So reject as invalid, and show an
appropriate alert before attempting to contact Stripe.
@stale
Copy link

stale bot commented Oct 4, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Oct 4, 2022
@MarkusH
Copy link
Member

MarkusH commented Oct 4, 2022

/unstale

@stale stale bot removed the stale label Oct 4, 2022
@carltongibson
Copy link
Member

@cgl This was the holding issue for the Stripe updates we were discussing at DjangoCon.

Basically, we'd like to use the Stripe hosted portal, and then remove the direct API integration code, closing the related issues (We have OK from the board for that, and things like privacy policy and T&C to add to the site and all that Jazz — but we ran out of steam into 🦠 ...)

@cgl
Copy link
Contributor

cgl commented Oct 5, 2022

Hey @carltongibson thanks for pointing out. I can work on it. Is there any draft work I should be aware of other than the merged PR? I will have a better look later.

@carltongibson
Copy link
Member

Hey @cgl.

Right, yes... 😜 — I got to a certain point with this implementing the Stripe portal, and then we needed Privacy Policy and such, which I got drafts for some time later, but never got back to.

  • You'll need access to Stripe. (I'll email ref that.)
  • Then the first pass is to redirect from the Hero page (≈"Manage your donation") to the Portal, and let folks use that. (I have a very minimal sketch of this I can send you.)
  • Then it would be good to tie the Hero page into actual user accounts, rather than just unique URLs, so they can manage "profile" details, such as logo and such. This ties into Add an FAQ about changing a recurration donation #775 and more recently Improvements to the Corporate Sponsor Experience #1171 — it's about helping corporate sponsors see the value in backing the DSF.

I think first pass would be get a feel for how it is, and then a sketch of a plan for the later bits before necessarily coding too much is a good idea. But launching the Stripe session for the portal isn't too hard — you just need the customer ID — so that first part should be doable. (I recall thinking I wasn't that far away...)

Thanks for the interest here. It's precisely one of those areas where there's lots of ideas, but a real need for somebody to lead the effort 🎁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fundraising python Pull requests that update Python code
Projects
None yet
Development

No branches or pull requests

5 participants