Checkout (with Stripe SCA)

Overview

_images/checkout-overview.png

Tip

A SetupIntent does not take the payment, it just collects card details for later use.

One-off Payments

  • Are handled using a PaymentIntent.

  • Are not linked to a Stripe Customer. The customer may use a credit card for a payment and not want to update the card used for their payment plan.

Payment Plans

  • Are started with a SetupIntent which collects the customer details.

  • Are linked to a Stripe Customer.

  • The payments are taken by linking a PaymentIntent to a Customer.

Management Commands

Retrieve the payment intent and update the checkout status:

django-admin.py payment_intent_fulfillment
django-admin.py payment_intent_on_session_fulfillment

Retrieve the setup intent and update the checkout status:

django-admin.py setup_intent_fulfillment

Send emails to customers asking them to login and pay (on-session):

django-admin.py send_on_session_payment_emails

Audit Stripe setup intent (which we marked fail) (audit_failed_checkout.py):

  • For payment plans, we create a setup_intent before prompting the user to enter their card details.

  • If the setup intent is not complete within 60 minutes, we mark the linked Checkout as fail.

  • My worry was that we marked it as fail, before the user entered their card details.

django-admin.py audit_failed_checkout

Management Commands - Testing

To set the due date for the next instalment…

Warning

FOR TESTING ONLY…

django-admin.py testing_set_pending_instalment_due 1234
django-admin.py process_payments

Tasks

The send_on_session_payment_emails task sends an email to users who need to carry out an on-session payment.

The function checks to see if the user has an active user account and are able to login and pay:

  • If the user can login and pay, they are sent an email using the MAIL_TEMPLATE_ON_SESSION_PAYMENT_ANON template.

  • If the user is not able to login and pay, they are sent an email using the MAIL_TEMPLATE_ON_SESSION_PAYMENT_AUTH template.

Write a can_login_and_pay function to check if the user can login and pay. Some of our systems use this to check if the customer has an overdue payment. The function is passed to the _user_has_active_account method (ObjectPaymentPlanInstalmentManager class). It can be as simple as:

def can_login_and_pay(contact):
    return True

Note

The can_login_and_pay must have a single parameter (a contact).

Create your task, passing in the can_login_and_pay function:

@task()
def send_on_session_payment_emails():
    logger.info("send_on_session_payment_emails")
    count = ObjectPaymentPlanInstalment.objects.send_on_session_payment_emails(
        can_login_and_pay
    )
    logger.info(
        "send_on_session_payment_emails - {} records- complete".format(count)
    )
    return count

Add to scheduled tasks (using myapp as example app name):

# run every 20 minutes
"send_on_session_payment_emails": {
    "task": "myapp.tasks.send_on_session_payment_emails",
    "schedule": crontab(minute="*/20"),
},

You may like to implement a send_on_session_payment_emails.py management command in your project. For an example, see

Other scheduled tasks

"payment_intent_fulfillment": {
    "task": "checkout.tasks.payment_intent_fulfillment",
    "schedule": crontab(minute="*/16"),
},
"setup_intent_fulfillment": {
    "task": "checkout.tasks.setup_intent_fulfillment",
    "schedule": crontab(minute="*/14"),
},
#
# and don't forget to add your ``send_on_session_payment_emails`` task

Template

From Viewport meta tag requirements:

{% block meta_extra %}
  <meta name="viewport" content="width=device-width, initial-scale=1" />
{% endblock meta_extra %}

Add our _elements.html template:

{% block content %}
  <div class="pure-g">
    <div class="pure-u-1">
      {% include 'checkout/_elements.html' %}
    </div>
  </div>
{% endblock %}

Add our _elements.js.html template:

{% block script_extra %}
  {{ block.super }}
  {% include 'checkout/_elements.js.html' with override_do_success_action=True %}
  <script>
    function doSuccessAction() {
      successUrl = "{{ success_url }}";
      if (!successUrl && console) console.log("No 'successUrl' returned");
      window.open(successUrl, '_self');
    }
  </script>
{% endblock script_extra %}

Note

I am not clear on why we need override_do_success_action. Here are the notes from 14 Nov 2019, 18:31: The issue with the pay on session page is that it does not have a checkout form (id_checkout_form) it only has the stripe form (payment-form) so obviously the submit button on the checkout form never gets pressed and therefore the successUrl variable is never set. I’ve amended _elements.js.html to allow the doSuccessAction function to be overridden and defined a doSuccessAction function in checkout_pay_on_session.html in the … project.

Testing

Tip

See the testing_set_pending_instalment_due management command (above).

To test the decline_code of authentication_required, use the card number:

4000002760003184

Warning

This card requires authentication on all transactions, regardless of how the card is set up.

Other card numbers:

4000056655665556  Visa (debit)
5555555555554444  Mastercard

When testing Elements, you will be asked to enter a ZIP code. To allow entry of a UK postcode, use a UK/GB debit card number:

4000058260000005

Tip

Don’t forget to run the Management Commands

Testing Payments for SCA:

Changelog

  • We are not using the CustomerPayment model. It was used to take money from a customer. It probably needs to be updated to send an email to a customer asking them to authorise payment. I think CustomerCardRefreshFormView does something similar.

  • We have removed CustomerCheckoutRefreshUpdateView. It is used to update card details for a customer. I think we should probably send an email to the customer asking them to update their card details. CustomerCardRefreshFormView might do this (see below).

  • We have removed CustomerCardRefreshFormView. I am not sure what this view would achieve? Would it create a setup_intent? What would the customer expect us to do with the payment method?

  • Not sure if we need refresh_card_expiry_dates or report_card_expiry_dates. How would they work with the requirement for on-session authentication?

To Do

  • Write a task (+ management command) to iterate through payment plans with a CheckoutState of PAY_ON_SESSION. Send the user an email asking them to login and authenticate the payment. Make sure we send just one email… 27/08/2019, Complete (see ticket)

  • Test the transition from the old system (saving cards) to the new system (payment_methods). 27/08/2019, done without any issues. It just seems to work :)

To Do on MD’s 4075-stripe-sca-modal branch

  • Re-instated checkout_actions, checkout_can_charge (see checkout/tests/helper.py) and test_check_checkout (for the Customer). I was hoping we wouldn’t need these for the SCA version of checkout (search for PJK 04/09/2019).

  • Re-instated tests for is_expiring on the Customer model e.g. test_is_expiring_future. I was hoping we wouldn’t need these for the SCA version of checkout.

  • CustomerPayment has been re-instated. I was hoping we wouldn’t need these for the SCA version of checkout.

  • CustomerChargeCreateView has been re-instated. I was hoping we wouldn’t need these for the SCA version of checkout.

  • Write a test for Checkout, __str__. It fails with the following error when the checkout has no content_object:

    Checkout.objects.all()
    fails on the institute database with the error AttributeError:
    'NoneType' object has no attribute '_base_manager'
    

WIP

12/11/2019

The email template for pay on session should check to see if the user has an account and has logged in within the last X months before deciding which template to use.

11/11/2019

To find the pay on session instalments, use the pay_on_session_for_email method…

We need to create a redirect view which generates a checkout URL e.g:

checkout = Checkout.objects.create_checkout_pay_on_session(
    instalment, AnonymousUser()
)
url = reverse(
    "web.checkout.pay.on.session", args=[checkout.uuid]
)

Note

This code is from send_on_session_payment_emails

11/08/2019

  1. Our example SalesLedgerSessionRedirectView calls create_checkout.

  2. create_checkout also creates a payment intent and saves it’s ID with the Checkout object.

  3. The SalesLedgerCheckoutView inherits from CheckoutMixin.

  4. The CheckoutMixin adds the client_secret from the payment intent to the template context.

  5. The Stripe JavaScript code in the _elements.js.html template will collect the payment.

  6. The stripe_intent_fulfillment method (and management command) will update the state of pending checkout objects with the status of the intent.

Working on…

Step 5: Attach the PaymentMethod to a Customer after success: https://stripe.com/docs/payments/cards/saving-cards#save-payment-method-without-payment

03/07/2019

The current checkout view allows users to choose payment, payment plan or invoice. Our customer site has an initial page where the user can choose how they want to pay - so our new checkout page should already know…

17/06/2019

Current Models

  • Checkout (keep track of a payment request)

  • CheckoutAction (charge, card refresh, payment plan)

  • CheckoutAdditional (additional information e.g. address)

  • CheckoutSettings (default payment plan)

  • CheckoutState (pending, request, success)

  • Customer (Stripe customer - email address is the primary key)

  • CustomerPayment (payment taken from a customer) What is this for?

  • ObjectPaymentPlan (payment plan for a content_object)

  • ObjectPaymentPlanInstalment (payment plan instalment for a payment plan)

  • PaymentPlan (template)

  • PaymentRun (date of payment run)

  • PaymentRunItem (instalment for payment run)