/ rails / stripe
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.
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
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.
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.
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.
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.
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.
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
- Create the products and their prices
Done on 16 September. Six products, GBP, one price each, and the split into
DEPOSITandDELIVERYat 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-DEPOSITand the rest — in the Stripe price description field, because the dashboard's product view surfaces no lookup key. That is not the same object asPrice.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. - 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.
- 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.ymlare 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. - 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.
- Put the reference in
client_reference_idand settle the return addressThe 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.
- 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.completedhandler 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.
- 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.
- 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.
The catalogue, reconciled
The products exist. Six of them, created on 16 September, and the two upper levels are split into a DEPOSIT and a DELIVERY that sum to the price — which is the store's own "a fifth now and the rest on delivery" as two rows rather than a rule somebody has to remember. A buyer paying in full adds both lines.
So this table is a reconciliation rather than a shopping list: the left is what data/offers.yml implies, the right is what the dashboard actually holds. This site cannot ask Stripe anything — it opens no connection — so the other side is the dashboard's own CSV export, committed at data/admin/stripe-products.csv, with the amounts read off the product list the same day. Weaker than an API call, and the strongest thing a static site can honestly do: it catches a price changed here and not there, which is the direction that actually happens.
Served beside this page as catalogue.json. It carries no key and no shape. A product id is not a secret and is printed on purpose.
ABP-T1Can I just have the pack? — £10 · prod_VGeQVaPGj6zhj0
ABP - The pack, downloaded
ABP-T2Can I have a vault I hold the keys to? — £50 · prod_VGeSCLNy40qFU1
ABP - A working vault
ABP-T3-DEPOSITCan you correct it for my situation? — 20% of £500 · prod_VGeTgWs8HPSiPB
ABP - Corrected for your situation (DEPOSIT)
ABP-T3-DELIVERYCan you correct it for my situation? — the remaining 80% · prod_VGeWBbtxXyIzi7
ABP - Corrected for your situation (DELIVERY)
ABP-T4-DEPOSITCan somebody go through it with my team? — 20% of £1,500 · prod_VGeYrfTOzFhf7z
ABP - Two sessions and a custom vault (DEPOSIT)
ABP-T4-DELIVERYCan somebody go through it with my team? — the remaining 80% · prod_VGeZ3hF7RyfZVl
ABP - Two sessions and a custom vault (DELIVERY)
The account carries one more product that is not listed and is not reconciled: it belongs to a different part of the estate and this store neither sells it nor tracks it.
The coupons, and the codes that are still missing
Three coupons exist. Zero promotion codes do, and that is the whole gap. A coupon carries a percentage. A promotion code is the string a person is actually handed, it is a separate object attached to a coupon, and it is what a link can carry pre-applied. Until one exists there is nothing to give anybody.
Three onto seven is the right shape, not a shortfall. Five of this store's seven codes are at a hundred per cent, and they exist as five so that an order record says which one produced it — a beta tester, an agent driving a script, somebody at a stand. That lives in the promotion code, not in the coupon.
6 codes in data/discounts.yml at this percentage, 4 of them printable · coupon ABP-Sep-100pc, once, 0 redemptions · no cap and no expiry
1 code in data/discounts.yml at this percentage · coupon ABP-Sep-50pc, once, 0 redemptions · no cap and no expiry
1 code in data/discounts.yml at this percentage · coupon ABP-Sep-25pc, once, 0 redemptions · no cap and no expiry
The hundred-per-cent coupon has no cap and no expiry
Which is fine today, because no promotion code exists over it and no payment link exists to use one on — so there is nothing to redeem and nothing to leak. It stops being fine the moment either of those is true. A code at a hundred per cent, printed on a card or on a page, cannot be recalled; the cap is the thing that makes printing it survivable, and it belongs on the coupon before the first promotion code is created rather than after the first one gets screenshotted. check_a_hundred_per_cent_coupon_is_capped fails the release the moment a rail goes live and a cap is still missing.