# Stripe

**Six products priced and reconciling, three coupons live. No promotion code, no link, no webhook.** — The rail for everything bought from a screen, and the one that turns a buyer into a customer — including a buyer who pays nothing.

## Why

The point of this rail is not the money. It is the customer record. A hundred-per-cent discount code taken on this rail produces a Stripe Customer with an email, a name, a country and a completed checkout against a named product at a stated price — for £0. That is the whole shape of what the event needs: hand somebody a code, they go through the real purchase, and at the end of it there is a person in a system rather than a business card in a pocket.

A payment link is an online payment, so no cross-border rule applies to it. That is the reason this store is links rather than a card reader: a United Kingdom card account cannot tap in Portugal, the provider declines on detecting a different country, and a reader bought for the event would have been a box that says no in public to the first person who tried it.

And a catalogue-backed checkout is the only version that can discount. A link that asks the buyer to type an amount has nothing to take a percentage of. Once the price is an object in Stripe, a coupon is a percentage of it, a promotion code is a string a human can be handed, and the arithmetic happens on the rail rather than in a browser where it is a demonstration.

## What this rail is handed

- **The catalogue, in full** — Six products, six prices, all in GBP, all one-off, all created. ABP-T1 £10 · ABP-T2 £50 · ABP-T3-DEPOSIT £100 and ABP-T3-DELIVERY £400 · ABP-T4-DEPOSIT £300 and ABP-T4-DELIVERY £1,200. Every number is read from data/offers.yml, which stays the only place a price exists on this side, and the build reconciles all six against the dashboard's own export every release.
- **Who the buyer becomes** — A Customer. Stripe Checkout always collects an email; what is not the default is keeping it — in payment mode Stripe creates a Customer only if required, and a £0 order requires nothing. Customer creation has to be set to always, or the hundred-per-cent code produces a completed checkout and no customer, which is the exact opposite of why this rail was chosen. This is the single setting most likely to be missed.
- **What the discount is** — A Coupon carrying the percentage, and a Promotion Code carrying the string a person is handed. Three coupons — 25, 50 and 100 — and one promotion code per audience, each with its own redemption cap and expiry, so a code printed on a card at a stand can be killed without touching the code given to a beta tester.
- **What the link carries** — client_reference_id, set to the order reference. Stripe stores it against the session and shows it in the dashboard. Optionally prefilled_promo_code, which is what a leaked-on-the-page code actually is: a link the buyer follows with the discount already applied and visible before they type anything.
- **Where it lands** — riskmandate.ai's page for that level. The store's existing contract is ?order= filled in by us, and a standing link cannot do that — the redirect URL is fixed per link and the only thing Stripe will substitute into it is the session id. So either the handover contract gains a session parameter and riskmandate.ai resolves it, or the buyer carries their reference on paper. That is a decision with another team in it and it is written up as one.
- **What the buyer types** — Email, name, and — unless the total is zero — a card. On Stripe's pages. Nothing on this domain has a field any of it could go in, and check_no_forms fails the release if one appears.

## The steps, in order

1. **Create the products and their prices** — Done on 16 September. Six products, GBP, one price each, and the split into DEPOSIT and DELIVERY at the two upper levels was the project lead's and is better than the shape this plan first proposed: the pair sums to the price, so paying in full is adding both lines. Every price carries its code — ABP-T1, ABP-T3-DEPOSIT and the rest — in the Stripe price description field, because the dashboard's product view surfaces no lookup key. That is not the same object as Price.lookup_key, which exists on the API and is the field a webhook would use to map a line item back to a level. It works for a person reading the dashboard and it is not yet queryable, and that difference is written down here rather than found later by a handler that cannot tell what was bought.
2. **Turn on customer creation, and prove it on a £0 order** — Set customer creation to always. Then run one hundred-per-cent order end to end and look for the Customer in the dashboard. If it is not there the rail is doing the wrong job silently, and it will look like it worked — the buyer still sees a confirmation, the session still completes, and nothing anywhere says the record was dropped.
3. **Create the promotion codes over the three coupons** — The coupons are done — 25, 50 and 100 per cent, created 16 September. The promotion codes are the gap. A coupon carries the percentage; a promotion code is the string a person is actually handed, and it is what a link can carry pre-applied. Until one exists there is nothing to give anybody. The seven strings in data/discounts.yml are the ones to create and they map onto the three coupons — five of them at a hundred per cent, kept separate so an order record says which one produced it. Put a redemption cap and an expiry on every one, and a cap on the hundred-per-cent coupon itself, before the first one is printed rather than after the first one is screenshotted.
4. **Enable promotion codes on every link, and prefill where a code was leaked deliberately** — A link with the promotion-code field switched off refuses a valid code with no explanation, which is the worst failure available here — the buyer blames themselves. Where the store publishes a code on a page as part of a journey, the link on that page should carry it already applied rather than ask the buyer to re-type what they just read.
5. **Put the reference in client_reference_id and settle the return address** — The reference is already in the URL the pay page builds. What is unsettled is the landing: the ?order= contract cannot be filled by a standing link. Resolve it with the RiskMandate team before a first sale, not after — a buyer who lands on a page that cannot name what they bought has been failed at the one moment they were paying attention. _Blocked on: Blocked on another team. Everything above it is not._
6. **Stand up the webhook** — This is the step that unblocks a first paid order at £50, £500 and £1,500. Their pages promise a person within 24 hours and nothing carries a sale from here to that person. A checkout.session.completed handler that emails the follow-up mailbox the level, the reference, the customer and — at £500 — the template chosen is the smallest thing that closes it. Listen to the session event, not to the payment. A hundred-per-cent order creates no charge, so a handler keyed on a succeeded payment sees every paid order and none of the free ones, which is precisely the set this rail exists to capture. _Blocked on: It does not run on this site. This site is static and opens no connection; the handler is somebody else's process holding the signing secret, and it never appears in this repository._
7. **Take one real payment, for £5, and read the receipt** — Against the live rail, with a real card. Then read what the receipt actually says, whether the reference is on it under a name the buyer would recognise, and what the fee was. Every sentence on this page about a receipt is currently unverified.
8. **Retire the browser-side discount arithmetic** — A code is recognised in the browser today, which is a demonstration and the page says so. The moment a promotion code exists on the rail there are two implementations of one rule, and the browser one is the one that can be edited by the person it is discounting. It comes out; the code field becomes a thing that carries a string to Stripe rather than a thing that computes. _Blocked on: Blocked on the coupons above._

## Still open

- **The hundred-per-cent coupon has no cap — and that is a ruling, not an oversight** — Ruled on 16 September by the project lead, and the reasoning is worth keeping. Every purchase is managed directly, so a redemption is seen rather than found in a monthly total. Abuse at a volume worth doing is abuse at a volume that shows. And what a hundred-per-cent code skips paying for is already free — the templates are published with read keys, so the code removes a payment for material anybody could have downloaded. What it does not skip is the work at the upper levels, and that is done by a person who would notice. The gate reports the state on every release rather than failing on it, because the day the first of those reasons stops being true is a day somebody should be reminded this was a decision.
- **Does the deposit want one payment or two?** — The two upper levels take a fifth now and the rest on delivery, which is why there are eight prices and not six. Simplest: charge the deposit price and invoice the balance, which is what every page already says. The alternative — an authorisation held and captured later — changes what the buyer sees at the moment they pay, and the pages would have to change with it.
- **What happens to a deposit on a run that cannot be scheduled?** — Both upper levels disclose that they have never been sold. Neither says what happens to the money if the first one goes badly. That is a hole the honesty opened and the rail cannot close it.
- **Does a £0 checkout collect an address, and should it?** — A hundred-per-cent order collects no card, and the address fields Stripe would otherwise gather come with the card. If the event's purpose is a contact record then an address is probably worth asking for explicitly — and probably is not worth making required, because the person is standing at a stand and every extra field is somebody walking away.
- **Eight rows in two places. What catches the drift?** — Nothing yet. The generated catalogue file makes the copy mechanical; it does not make it checked, because this site cannot read Stripe. The candidate is a small script run by hand at release that lists prices from the dashboard and diffs them against the file — out of the build, because the build opens no connection and that rule is not moving for this.

---

This document is released under the Creative Commons Attribution 4.0 International licence (CC BY 4.0).
