Checkout (with Stripe SCA)
Overview
Tip
A SetupIntent
does not take the payment, it just collects card
details for later use.
Card Refresh
The ObjectPaymentPlanSetupIntentCardRefreshMixin
(checkout.views
)
requires login so we can find the ObjectPaymentPlan
for the user.
Some code snippets:
# url
path("card/refresh/",
# view
class CheckoutRefreshCardView(
LoginRequiredMixin,
ObjectPaymentPlanSetupIntentCardRefreshMixin,
BaseMixin,
UpdateView,
):
def get_success_url(self):
return reverse(
"web.checkout.refresh.card.success", args=[self.object.uuid]
)
# template
<link rel="stylesheet" href="{% static 'checkout/css/checkout.css' %}" type="text/css" charset="utf-8">
{% include 'checkout/_elements.html' %}
{% include 'checkout/_elements.js.html' with override_do_success_action=True %}
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 aCustomer
.
Management Commands
Retrieve the payment intent and update the checkout status:
django-admin payment_intent_fulfillment
Tip
For more info, see Asynchronously fulfill the customer’s order
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
asfail
.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…
find your payment plan e.g. http://localhost:8000/checkout/object/payment/plan/1234/
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).
No authentication (default U.S. card):
4242424242424242
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 thinkCustomerCardRefreshFormView
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 asetup_intent
? What would the customer expect us to do with the payment method?Not sure if we need
refresh_card_expiry_dates
orreport_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
ofPAY_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
(seecheckout/tests/helper.py
) andtest_check_checkout
(for theCustomer
). I was hoping we wouldn’t need these for the SCA version of checkout (search forPJK 04/09/2019
).Re-instated tests for
is_expiring
on theCustomer
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 nocontent_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
Our example
SalesLedgerSessionRedirectView
callscreate_checkout
.create_checkout
also creates a payment intent and saves it’s ID with theCheckout
object.The
SalesLedgerCheckoutView
inherits fromCheckoutMixin
.The
CheckoutMixin
adds theclient_secret
from the payment intent to the template context.The Stripe JavaScript code in the
_elements.js.html
template will collect the payment.The
stripe_intent_fulfillment
method (and management command) will update the state of pending checkout objects with thestatus
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
04/07/2019
Working on:
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 acontent_object
)ObjectPaymentPlanInstalment
(payment plan instalment for a payment plan)PaymentPlan
(template)PaymentRun
(date of payment run)PaymentRunItem
(instalment for payment run)