Building forms with the Forms module: how a form is put together and placed on a page, validating and branching, presenting it, where submissions are stored, and what happens after one arrives, from jobs and records to CRMs and payments.
L1–L2
Unit 06 · Acting on a submission
Taking payments
Acting on a submissionPage 16 of 20Updated
Taking a donation, fee or deposit inside a form: where a form ends and a checkout begins, the setup, what a payment creates, and sending a receipt.
Who this is for. The developer adding a donation, fee or deposit to a form. After this page you can tell whether a payment belongs in a form or a checkout, set the element up, say what a payment creates, and send the payer a receipt.
A form can take a payment: a donation, an event fee, a deposit, a membership renewal. The Payment element adds the provider's card fields or buttons to a step, charges the visitor when the form is submitted, and records the payment against the submission. It has a clear boundary: a single amount from a single payer is a form, and anything that looks like a purchase belongs in a checkout. This page covers that boundary, the setup, what is created when the payment goes through, and the receipt.
One amount: fixed, picked from a few presets, or typed by the payer
Products, quantities, or more than one item
A donation, a fee, a deposit, a single registration
Anything with stock that can run out
No tax, or tax already included in a fixed amount
Tax calculated by where the payer is or what they buy
Nothing to post
Shipping
No discounts
Discount codes, member prices, promotions
A receipt you send yourself
Order confirmation, fulfilment, returns and per-item refunds
The test: if the answer to "what did they buy?" is a product, it is a checkout. A form payment creates an invoice and nothing else: no order, no stock movement, no shipment, no tax line. Rebuilding those around a form is how a "simple payment form" becomes the hardest part of a site to maintain.
Setting it up
Payment methods. Each provider is a payment method record under Invoices › Payment methods: Stripe, PayPal, PayPal Donate, or the Mock Pay test provider. Their setup is under The four payment methods.
The element. Add a Payment element to the form, usually alone on the last step, after every question. Its settings:
Setting
What it does
Currency
The currency of every amount on the element
Amounts
Preset amounts the payer picks from. The server accepts only one of these, unless a custom amount is allowed
Allow Custom Amount
The payer may type any amount above zero. Show the amount back to the payer before they pay
Payment Methods
Which active methods to offer. Several are shown as tabs
Authorize & Capture / Authorize Only
Use Authorize & Capture, which takes the payment when the form is submitted
Billing address fields
Which address fields to ask for, and which are required. They are stored as the invoice's billing address
The script. The element prints the providers' mount points; the platform's script makes them work (js-payment-element.js reference). Load it in the form layout, with the page's nonce, on both submission models:
On a page-per-step form the script catches the submit, gets a token from the provider and submits again with it. On a JS form it takes over the container's data-submit-modifier-callback and data-submit-result-callback, handles the provider's responses, and then calls the callbacks the markup named before it ran. So the result callback from the validation page keeps working, as long as its attribute is in the markup when the page loads. Do not put an invisible captcha on the payment step: both take over the submit (see Captcha).
What happens on submit
The payment is attempted only when every other answer on the form is valid. A visitor who mistyped an email is told so before any money moves.
The server checks the amount (above zero, and one of the presets unless custom amounts are allowed) and the chosen method (offered on the element, and active).
It takes an invoice for the submission: an open invoice of the submission is reused and any other open one is voided, or a new one is created. The invoice has one line, named after the form, with no tax and no discount, and the billing address, and it records the submission as its source.
It charges the payment through the provider.
On success the invoice is paid, the payment is stored as the element's answer, and the submission completes, which fires the form's Completed jobs. The answer, under the element's name in data, holds invoiceId, amount (formatted in US English, so an Australian-dollar payment reads A$25.00) and transactionId.
On failure the payer stays on the payment step with the provider's message, and the submission stays incomplete.
Two providers change the order. Plain PayPal may send the payer to PayPal and back. PayPal Donate with a hosted button creates a draft invoice and leaves the submission incomplete until PayPal's notification confirms the donation, minutes later: Completed fires then, not when the payer clicked. Anything that reacts to the payment must hang off Completed, never off the visitor reaching the result step.
Sending a receipt
The platform sends no receipt for a form payment. Send one from a job on the form's Completed event, to the payer's address, with the amount and invoice number from the payment answer. Here the element is named donation:
{$answers = $event->getRecord()->data}
{$payment = $answers['donation']|default:[]}
<!DOCTYPE html>
<html><body style="font-family:Arial,sans-serif;font-size:14px;color:#333333;">
<p>Thank you for your donation of {$payment['amount']|default:''|escape}.</p>
<p>Receipt number: {$payment['invoiceId']|default:''|escape}</p>
<p>Keep this email for your records.</p>
</body></html>
{% set payment = event.getRecord().data.donation|default({}) %}
<!DOCTYPE html>
<html><body style="font-family:Arial,sans-serif;font-size:14px;color:#333333;">
<p>Thank you for your donation of {{ payment.amount|default('')|e }}.</p>
<p>Receipt number: {{ payment.invoiceId|default('')|e }}</p>
<p>Keep this email for your records.</p>
</body></html>
A tax receipt for a deductible donation has legal content of its own (the charity's registration number, wording the tax office requires); get it from the client, and keep it in the template, not in the form.
Make sure no Mock Pay method is active on the live account and no form offers one (see The mock provider). Mock Pay reports every payment as successful and takes no money.
Refunds are made from the invoice, through the provider.