Skip to content

Switching a shop's checkout on

Checkout is per shop and off by default (checkout, ADR-014). A customer who sends a request gets a reference and a private status link; nothing is reserved or charged until staff confirm. Turning it on is the shop’s decision; this page is the order of steps and the checks around it. Anyone with the control panel can do the shop steps; the platform steps are read-only checks.

Done once per release, not per shop.

  1. The storefront answers the checkout paths. nginx sends only listed paths to the storefront (storefront operations). Check, read-only, with any shop host:
    Terminal window
    curl -sI https://<shop>.flowery.com.ua/checkout | grep -i content-security-policy
    The header comes from the storefront. Without it, the path reaches Laravel’s “back in a moment” page and the shop’s checkout must stay off. Checked 25 October 2026 on demo.flowery.com.ua/checkout: the storefront answered (404 for the reserved slug, with its CSP).
  2. The release is the tested one. curl https://flowery.com.ua/release matches main’s HEAD, and the last main run’s deploy job is green (deployment). The deploy waits for /release and /__storefront/release to report the commit, so a green deploy is evidence for that commit.
  3. The worker runs. Both checkout emails are queued; without the worker the order exists and nobody is told. Forge → Daemons shows queue:work running, and the scheduler is on (uklon:sync needs it).
  4. Mail is delivered. Resend’s dashboard shows recent deliveries, or send a test with php artisan mail:test on the server (deployment).

In the shop’s control panel, as an owner or admin.

  1. Something to sell. Active bouquets with stock in eligible batches. A bouquet whose flowers have no eligible batch shows “sold out today” and cannot be added to the cart; the request would otherwise be refused at the quote. Settings → Catalogue and Supplies, or the demo shop.
  2. The storefront is published (Settings → Storefront) with the profile filled in: address (pickup shows it), phone and email (both go into the customer’s email and onto the status page), and opening hours (pickup windows are cut to them). An “opening soon” shop takes no requests.
  3. Settings → Checkout.
    • Pickup, delivery for a flat fee (fee, optional “free from”, the city), or both. Uklon needs a verified account and bank details first (checkout).
    • Lead time, same-day cutoff, slots and closed dates. The defaults are 2 hours, 18:00, all three slots, none.
    • At least one payment method (cash, card). Without a way to receive an order and a way to pay, the switch refuses.
    • Accept order requests: on. Save.
  4. Place a test request yourself. While the shop still shows the demo, every request is a test order: marked “Демо”, labelled in both emails, removed with the demo (ADR-016). Otherwise place a small pickup order and cancel it from the order page afterwards; an order with no reservation or status history can also be deleted.
    • Open the website, add a bouquet, open /checkout, send the request.
    • The status page opens on “Запит отримано”.
    • The staff email (“Новий запит на замовлення”) reaches the owners, admins and managers; the customer email, with the status link, reaches the address given.
    • In the control panel the order carries “Новий запит” and the Запити із сайту filter counts it. Confirm it: the reservations card lists the batches. Record the payment: the status page shows it paid.
    • php artisan inventory:reconcile --workspace=<slug> on the server stays green.
  • The first real requests. Someone reads the staff email or the orders list every day the shop is open; a request outside the shop’s hours waits until then, and the customer’s email says the shop will call. Open requests per phone are limited (default 3), so a customer who gets no answer cannot flood the list.
  • The status link is private. It is shown once in the customer’s email and in the control panel on request; a customer who lost it gets a new one from the order page, which stops the old one.
  • Prices. A catalogue change after a customer’s quote makes them confirm the new total before the request is created, so the shop never receives an order at a price it no longer offers.

Settings → Checkout → Accept order requests: off → Save. The website goes back to “contact us to order”, quotes and requests are refused at once, and every existing order and status link keeps working. Nothing else changes.

Evidence (Stage 3A closeout, 25 October 2026)

Section titled “Evidence (Stage 3A closeout, 25 October 2026)”

The runbook above was walked on the production build of the storefront run locally (npm run build, STOREFRONT_PROXY_API=true) against the pilot shop, as a customer at desktop and 390 px and as a manager in the control panel; the staff side and the recovery cases are also the named CI gate tests/Feature/Checkout/GoldenScenariosTest.php (G1 and G6). The report is improvement-plans/Stage-3A-Closeout.md. What it found:

Observation Where it is recorded
The /checkout page is clean at 390 px; a headless Chrome --window-size=390 screenshot clips the right edge because the window, not the viewport, is 390 wide. Use the Playwright CLI with --viewport-size (browser testing) Browser testing page
The sixth checkout request from one address within a minute is throttled (429), as the rate limit says GoldenScenariosTest G6
Local demo stock expires with time, so every bouquet reads “sold out” until a delivery is recorded; the storefront was right, the fixture was old This page, step 1 of the shop checks
Images on the locally served production build are missing because /storage is served by Laravel, which that run does not proxy; production nginx does No change
Percentages on the order page use a dot (53.49%) while money uses a comma Already listed under PROJECT_STATE.md next step 8

The “place a test request yourself” step, walked on production by the owner’s account through the batata shop (storefront on its own domain, still showing the demo, so the order is marked “Демо” and goes with the demo removal), in Chrome through the extension:

Step Seen
Platform checks /release = main HEAD (ae67793); the storefront answers /checkout with its CSP on a shop host; two queue:work daemons and the storefront Node process running; the scheduler log rewritten this minute
Shop checks Checkout was off; Settings → Checkout: pickup and both payment methods were already on, “Приймати замовлення” switched on and saved (on after reload). The profile had no phone, email, address or hours, so pickup had no windows: filled with placeholder contacts and 09:00–18:00 every day, draft saved (PATCH 200), published (POST …/publish 200); the storefront showed the contacts at once. Every bouquet read “Сьогодні розпродано” (the demo batches had expired, as on the local run): a supply of 30 white lilies (batch E2E-3A, 35 each, expiry 5 November) recorded through Поставки → Записати поставку; “Біла класика” became purchasable
Request Bouquet page → kraft paper → POST /cart/items 201 → /checkout: pickup at the address, free, today’s morning slot first, 1 190 ₴, “Це запит… Зараз ви нічого не сплачуєте”. Empty submit stopped at the name field with no request sent. Filled and sent: POST /storefront-api/v1/checkout 201; status page ORD-20261026-0001 “Запит отримано”, “Ви ще нічого не сплатили”, the privacy line. No console errors
Control panel Orders list: the order with “Новий запит” and “Демо”, the Запити із сайту filter counting 1. Order page: the request card (“Запит із сайту · Самовивіз · UK”), “Видати нове посилання на статус”, the costing card “невідомо” because kraft paper has no purchase price (honest, as designed)
Confirm and pay Status → Підтверджено (PATCH …/status 200): reservations card “E2E-3A, придатні до 05/11/2026, × 10, зарезервовано”, status history with the owner’s name. 1 190 cash recorded (POST …/payments 200): paid 1 190, outstanding 0, “Сторнувати” on the entry
Status page after “Підтверджено”, “Магазин підтвердив замовлення й відклав для вас квіти”, “Готівкою — оплачено”; headers X-Robots-Tag: noindex, nofollow and Cache-Control: private, no-store; a guessed token 404; the same token on another shop’s host 404
Server inventory:reconcile --workspace=batata green (two operational exceptions on the old demo batches, as designed); queue:failed empty; no errors in today’s log
Emails Both arrived in the owner’s Gmail inbox within the minute (Resend, from [email protected]). Customer: “[Демо] ion отримав ваш запит на замовлення ORD-20261026-0001”, the demo warning, “Це запит: ion підтвердить наявність квітів…”, the line, the total, pickup date, slot and address, “Оплата готівкою під час самовивозу”, the “Переглянути запит” button, the privacy line. Staff: the demo warning, the customer’s name, phone and email, the same order lines, “Відкрити замовлення” to the control panel, “Нічого не зарезервовано, доки ви не підтвердите замовлення”
Money in emails Amounts read 1190.00 UAH in both emails while the storefront shows 1 190 ₴ and the control panel 1 190,00 грн: correct, but not the shop’s format. Fixed the same day: Money::format() renders them as the website does (#97)

Left as it was: the batata shop now accepts requests with placeholder contacts; switch it off or replace the contacts before the shop is shown to anyone. The order and the supply go away with the demo removal.