# Stripe

**Six products priced and reconciling, three coupons live, and the store's side of the hand-off built. No payment link, no promotion code, 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 — and a fourth coupon for DOORSOPEN** — 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. There are eight strings in data/discounts.yml, not seven — DOORSOPEN was added after that count was written. Seven of the eight map onto the three existing coupons; five of those seven are at a hundred per cent and are kept separate so an order record says which one produced it.The eighth needs a coupon of its own, and this is the trap. DOORSOPEN is levels: [pack, vault] in the store's data, and that restriction cannot be expressed on a promotion code: a promotion code restricts by first transaction and by minimum amount, and scoping a discount to particular products is a property of the coupon, through applies_to.products. Attached to the shared hundred-per-cent coupon it would take the whole of a level-four deposit off — £300 of somebody's day, from a code published on a page on purpose. So: a fourth coupon, 100 per cent, applies_to the level-one and level-two products only, and DOORSOPEN attached to that one.No redemption caps, by ruling on 16 September — purchases are managed directly and what a hundred-per-cent code skips paying for is already published free. Expiries yes: every code carries an until: date in the store's data and the promotion code should carry the same one, or the two disagree and the store is the one that looks wrong.
4. **Switch promotion codes ON on every link — the store already prefills them** — 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. It is one checkbox per link and it is not optional, because the store's checkout now builds prefilled_promo_code onto every button when the buyer arrived with a code, and a prefill into a field that is switched off is silently dropped.One limitation, stated rather than hidden. The store keeps the discount record's id and never the code itself — what ships is a sha256, and a build check greps the whole tree to keep that true. So the plaintext is in hand for the life of one page and no longer. Open the checkout by a route that does not carry the code and the price shown is still discounted but the button cannot prefill; the page says so in as many words and tells the buyer to reopen from their link or type it at the provider. That warning is the only thing standing between a buyer and being charged a price they were not shown, which is why it is a callout and not a footnote.
5. **Create the four payment links, and set the return address** — Four links, not six. ABP-T1 £10 · ABP-T2 £50 · ABP-T3-DEPOSIT £100 · ABP-T4-DEPOSIT £300. The two DELIVERY halves get no link, because the store's own copy says the balance is invoiced; adding them later is one line each in data/offers.yml.Per link: confirmation page set to redirect to https://store.sgit.ai/paid/<id>/?cs={CHECKOUT_SESSION_ID} — that placeholder is literal and the provider substitutes it; promotion codes on; adjustable quantity on for the two lower levels and off for the two deposits, because a deposit is not a thing anybody buys two of.The ?order= question is settled and the answer is that it cannot be done. A payment link's return address is configured once and is the same for every buyer, so the store's reference cannot ride back in it. It does not need to: the browser that built the order still holds it, and the page after paying renders from that. What travels the other way is client_reference_id, which the checkout already appends, and which is how the several links of one order find each other in the dashboard. What comes back is the provider's own session id, which is a string to search on and not a receipt — the page says you came back, and never that you paid.Then paste the four URLs into checkout_url in data/offers.yml. The gate pins each to buy.stripe.com or checkout.stripe.com, and the build refuses to run until the claim ledger's checkout-links-not-issued entry is corrected in the same commit.
6. **Turn the emails on — and understand which of them a £0 order will not send** — Neither side is emailed today, and one of them cannot be.The buyer's receipt is a dashboard setting, not a property of the payment: Settings → Business → Customer emails → Successful payments. With it off the buyer completes a checkout and hears nothing, which on a store whose whole argument is that it collects nothing means the buyer has no record from anybody. Turn it on.The seller's notification is a per-person preference on the Stripe account (Profile → Notifications), plus push from their mobile app. It is not part of the link and it is not part of the product, so it has to be set by whoever is going to act on a sale rather than assumed.AND NEITHER FIRES ON A £0 ORDER, which is the half worth reading twice. A hundred-per-cent code takes the total to zero, and a zero-total checkout creates no charge — the session completes with nothing to pay. Both of those emails hang off a successful payment. So the exact journey this rail was built for — hand somebody a code at an event, watch them go through a real purchase, end up with a person in a system — is the journey that notifies nobody, by any route, and the only place it shows up is the dashboard. The webhook below is not a nicety for that journey. It is the only thing that can see it.A post-payment invoice PDF (on the link, at 0.4% capped near $2) is the other built-in way to email a buyer, and it carries more than a receipt does. Whether it is raised and sent for a zero-total order has not been checked here and should be, on the £0 walkthrough, before it is relied on either way.
7. **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._
8. **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.
9. **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. Worse than editable: the two can simply disagree, and the buyer sees the browser's number and is charged the provider's. It comes out; the code field becomes a thing that carries a string to the provider rather than a thing that computes. _Blocked on: Blocked on the promotion codes 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.
- **Is VAT on, and is the price inclusive?** — Nobody has ruled on this and the rail cannot be turned on without it. These are digital goods sold to consumers and to businesses, in the United Kingdom and outside it. With the provider's tax handling off, £10 is £10 and what it is inclusive of is whatever is declared elsewhere. With it on, tax is added at the checkout and the total the buyer is charged stops matching the total this store showed them — and every price on this site is frozen by the gate against data/offers.yml, so the store cannot quietly follow. It is a ruling about what the published number means, not a setting, and the answer changes copy on the checkout either way.
- **Nothing tells anybody a sale happened** — This is the same gap the webhook step names, said from the buyer's side. The store hands a buyer to the provider and cannot know what happened next: there is no server here and nowhere to receive a callback. So a £50 order that completes produces a Stripe Customer and an email receipt from the provider, and no signal at all to the person who owes them a vault within twenty-four hours. Until the webhook exists that person is reading the dashboard. That is workable at the volume this store has and it should be a decision rather than a thing discovered on the first Monday somebody misses.
- **The journey this rail exists for is the one nothing can see** — Said once more here because it is an argument, not a setting. The point of the rail was never the money — it is that somebody handed a code at an event goes through a real purchase and comes out the other side as a Customer with an email address. But a hundred-per-cent code means a zero total, a zero total means no charge, and every email Stripe sends by itself — to the buyer and to us — hangs off a charge. So the free journey produces a record and no signal, and the paid journey produces both. The store cannot close that gap from here: it is static, it opens no connection, and it has nowhere to receive a callback. Until `checkout.session.completed` is being listened to by something, a free order is a thing you find by looking rather than a thing you are told.

---

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