Introduction

This service acts as a gateway between Visit(cloud) and Stripe and its main purpose is to be able to customize the checkout process for people registering for TNC2025 and enable the following features:

  • Payment through bank transfer or credit card

  • Customization of the invoice

    • allow the visitors to set a PO number and VAT number

    • add the VAT in GBP as well as EUR

    • Use a specific invoice template

These features are not supported by Visit’s implementation of the Stripe integration. This service acts as an in-between layer and does support these features.

When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the final step of the registration form. Here they get the opportunity to fill out a purchase order and/or VAT number. Upon clicking a confirmation button an invoice is created for them and they are redirected to Stripe where they can view the invoice, pay directly by credit card and/or see payment instructions for paying by bank transfer. They also receive an e-mail with all payment information.

Note

Throughout this documentation, there are instructions to invoke django management commands. These instructions use the django-admin cli tool to invoke these commands. Depending on the situation you may want to use another script to invoke these commands, such as manage-dev.py or cmd.sh. See also Invoking management commands

Design

The design of the stripe-checkout service is roughly as following:

Whenever a visitor registers for TNC, they fill out a form in Visit. This form sets their registrationType. While filling out the form, the visitor may also want to purchase some extras, such as access to side meetings or social events. The information for extras is stored in the visitors profile as answers to certain questions. For example, there is a question for “Side meetings” with answers “Friday” and “Sunday”

  • As the final page in the registration form, the visitor is redirected to the stripe checkout service. Here they see what they’ve checked out and how much they need to pay.

    • The checkout service reads the visitor’s profile through the Visit API (the visitor is identified by their visitor id in the url).

    • From the visitor’s profile, the Stripe products that the visitor needs to pay for are determined. A visitor can have a single Registration Type item and multiple extras. Every category of extras has two questions: One that the visitor answers with the extras they want, and one that is goverened by the stripe checkout service when the visitor’s payment has been processed. Every answer in the paid-questions represents an extra that the visitor has paid for.

      These are defined as PricedItem objects in the database. See also: Adding new products or price information to the database

    • All items that the visitor has not previously purchased (see Order below) are added to a ShoppingCart and this collection is shown to the user in their checkout page

  • In their checkout page, the visitor can add some additional information that will be shown on the invoice. These are a Purchase Order (PO) number and/or a VAT id. These fields are optional. Then the visitor clicks Confirm to finalize the purchase

  • The stripe checkout service now creates an invoice in Stripe for the items that they need to pay for:

    • All PricedItem objects that the visitor needs to pay for are collected in an Order and stored in the database. Any items for which the visitor already has an Order are excluded from the new Order, so that the visitor is charged only once for an item.

    • The visitor’s billing address is read from their Visit profile. contact.addresses should contain an address object of "type": "billing".

      Note

      In order for any billing information to be shown in the Invoice, it is required that the country field is set to a valid country code in the billing address.

    • The visitor is identified in stripe by their email address. If the visitor’s email address is already bound to a customer in Stripe, we use that customer, but update the billing details using their Visit profile

    • Additional fields are added to the the invoice as custom_fields. These are:

      • Purchase Order: Supplied by the visitor (optional)

      • VAT number: Supplied by the visitor (optional)

      • GBP VAT: The VAT amount in GBP. The exchange rate read from the database and is periodically updated by management command getexchangerate

    • We store the visitor_id and order_id as metadata fields to the invoice. That way we can always find back the order if we have the Stripe invoice.

    • We tell Stripe to finalize and send the invoice by email to the visitor. Stripe also generates a url where the visitor can directly pay their invoice. We redirect the user to this url. The visitor facing part of the stripe checkout service is now finished.

    • When creating and finalizing the Invoice, Stripe also creates a payment intent. Before redirecting the visitor to their Stripe payment page, we store the payment intent’s id (starting with pi_) along with the Order so that we can reference it later.

  • We now wait for the visitor to pay. The visitor may pay by credit card or by bank transfer. While credit card payments are processed almost instantly, bank transfers may take time to be processed. First, the visitor needs to actually pay, and then Stripe needs to process the payment. The total time between placing the order and strip processing the payment may take weeks or longer.

  • When the visitor’s payment has been succesfully processed by Stripe, Stripe calls our webhook at the stripe-event-webhook/ endpoint. This endpoint does not process the event, but merely stores it in the database as an Event. Stripe requires that this endpoint returns quickly so processing the events happens asynchronously by periodically running the django managemement command processevents

Processing stripe events

  • The processevents command looks in the database for Events that are not processed yet. It looks at two types of events: payment_intent.succeeded and payment_intent.canceled.

    • payment_intent.succeeded events indicate that the visitor has paid. We look in the Order table for a matching Order, and update the visitor’s Profile in Visit. We add the PAID tag to the visitor to indicate that they’ve paid for their main registration, and add the relevant answers to the “paid”-question for extra’s. In certain cases there is no Order that is linked to the payment_intent. This can happen in the following circumstances and we handle these differently

      • If during processing of the invoice, the invoice (and payment intent) gets created, but for some reason we are unable to write back the payment intent id to the Order, we end up with an Order without a payment intent reference. In this case we do a reverse lookup. We retrieve the invoice from the payment intent and look in the metadata for a visitor_id and and order_id. If we can find an order that match those parameters we use it and continue as normal

      • If the original invoice was credited and another invoice was created manually, this has a new payment intent attached to it. The Order now has an incorrect payment intent reference. We try the reverse lookup using the invoice’s metadata, but if that fails, we send an email with a notification that we encountered a payment intent that we cannot process. The payment_intent / invocie must be processed manually

    • payment_intent.canceled events happen when an invoice was credited. We add the CANCELLED tag to the visitor, to indicate that we now no longer expect them to pay

Fetching exchange rates

  • Another job that runs periodically is the getexchangerate command. Every day it looks for a new GBP/EUR exchange rate on https://www.trade-tariff.service.gov.uk/exchange_rates and if it finds one, adds it to the database to be used from now on. The trade-tariff service publishes the exchange rates every month.