Checkout
Customers send an order request from the storefront. The shop confirms it through the usual order lifecycle; nothing is reserved or charged before that (ADR-014). Uklon courier delivery follows ADR-015. Online payment is a later milestone (3B).
Settings → Checkout
Section titled “Settings → Checkout”Owners and admins set the checkout rules per shop. They are stored in the workspace settings under checkout (App\Support\Checkout\CheckoutSettings).
| Setting | Default | Rule |
|---|---|---|
| Accept order requests | off | When off, the Storefront API refuses quotes and submissions, and themes keep “contact us to order”. Existing orders and status links keep working |
| Pickup | on | At the shop’s address from Settings → Storefront |
| Delivery for a flat fee | off | Within one named city; addresses elsewhere are refused. The fee and the optional “free from” amount are exact in the shop’s currency |
| Uklon courier | off | Estimated live at checkout, booked at dispatch. The shop absorbs a price change up to its tolerance (default 15%). Requires the bank details below, because Uklon orders are paid by bank transfer before dispatch. Hidden from customers until the shop’s Uklon account is connected |
| Bank transfer details | none | An IBAN (validated with ISO 13616 check digits) and the account holder, shown to customers of Uklon orders after confirmation |
| Lead time | 2 hours | The earliest pickup or delivery is now plus the lead time, in the shop’s timezone |
| Same-day orders until | 18:00 | After this time, today is not offered |
| Time slots | all three | Morning 09:00–12:00, afternoon 12:00–17:00, evening 17:00–21:00. The slot ends match the fulfilment cutoffs that reservations use |
| Closed dates | none | Up to 60 dates on which no pickup or delivery is offered |
| Payment | cash and card | On pickup or delivery; staff record the payment |
| Open requests per phone | 3 | Unconfirmed requests one phone number may have at once |
Checkout can be switched on only with at least one way to receive an order and one way to pay. A new shop can also switch it on from Getting started with one click: pickup, cash or card, everything else unchanged (onboarding). While the shop shows the demo shop, every order placed is a test order: marked demo, labelled “[Демо]” in both emails, never sent to Uklon, and removed with the demo (ADR-016).
When an order can be received
Section titled “When an order can be received”The windows are the delivery windows already in place. Morning 09:00–12:00, afternoon 12:00–17:00 and evening 17:00–21:00 are defined once, in App\Support\Fulfilment\TimeSlots. The staff order form shows them (Ранок, День, Вечір), reservations use their ends as the fulfilment cutoff, and checkout, the status page and the emails show them. Before 6 October 2026 the same times were written separately in the order model, the cutoffs, the checkout settings and the staff interface strings.
App\Support\Checkout\FulfilmentRules decides, in the shop’s timezone. A window on a date is offered when:
- the shop offers that slot and the date is not closed;
- the date is today or later and at most 30 days away;
- the same-day cutoff has not passed (for today);
- for pickup, the shop is open then: the window is cut to that weekday’s opening hours from the storefront profile, and a day without hours has no pickup (a shop with no hours set is not restricted);
- the window starts no earlier than now plus the lead time.
At 10:00 with a two-hour lead time, today’s morning window is gone and the afternoon (from 12:00) is still offered. A shop open 09:00–20:00 offers evening pickup as 17:00–20:00; delivery keeps 17:00–21:00, as its delivery note says. GET /checkout returns a calendar per method, and the status page and emails show a pickup’s window cut the same way.
- Pickup costs nothing and has no address.
- Delivery needs a street and the shop’s city (compared without case or extra spaces). Its fee is the flat fee, or nothing once items and packaging reach the “free from” amount.
- Uklon is refused until the shop’s account is connected (PR 7).
A request outside these rules is refused with the field that is wrong, never moved to another time.
Quote, request and status link
Section titled “Quote, request and status link”The Storefront API carries the flow (endpoints):
- Options (
GET /checkout): methods, the fee, payment methods and the dates and slots offered now. - Quote (
POST /checkout/quote):- Prices the visitor’s cart from the catalogue now. A line whose price changed since it was added says so.
- Refuses a cart with a product or packaging that can no longer be ordered.
- Returns the total and a fingerprint, an HMAC over the lines, packaging, fee, total, currency, date, slot and expiry. The fingerprint expires after ten minutes and needs no storage.
- Request (
POST /checkout):- Locks the cart and quotes it again at the confirmed quote’s expiry.
- Creates the order only if the fingerprints match and the quote has not expired. Otherwise it answers 409 with the current quote, for the customer to check and confirm.
- The street, contact and card message are not part of the price, so they can change between quote and request.
- Status link (
GET /orders/{token}):- Shows the order’s status, items, totals, fulfilment and payment, and nothing else.
- Works on its own shop until 60 days after the delivery date, also after checkout is switched off.
One order per cart. A checkout_submissions row is unique per cart. A repeat (double click, another tab, a lost answer) returns the same order and link with status 200, whatever details it carries. The submitted cart becomes converted; the visitor’s next item starts a new cart.
Contact.
- The customer’s name, phone and email are required.
- Phones are stored as
+and digits; a local Ukrainian number (050…) becomes+38050…. - A phone with as many unconfirmed requests as the shop allows (default 3) is refused, however it is typed.
- The recipient’s name and phone are kept for delivery only.
Emails.
- After the order commits, the shop’s owners, admins and managers get “New order request” in the shop’s default language.
- The customer gets a confirmation in the language they used: the items, total, time, payment and the status link, and the shop’s phone and reply-to address when its profile has them.
- Both are queued (the production worker sends them). The customer’s job is encrypted because it carries the link.
- Amounts in both are formatted by
Money::format()in the email’s language, as the website shows them: the narrow symbol where the locale puts it, no decimals when the amount is whole (“1 190 ₴”, “₴1,190”, “1 190,50 ₴”). Until 26 October 2026 they read “1190.00 UAH”. - A failure to queue is reported and never loses the order.
Rate limits. Per visitor address: 30 quotes and 5 requests a minute.
Uklon courier
Section titled “Uklon courier”Per ADR-015. The integration is built against Uklon’s published spec with recorded answers; no shop has Uklon access yet.
Connecting a shop (Settings → Checkout, “Your Uklon account”; owners and admins, since these are credentials):
- Enter the Client ID and secret Uklon gives the business, and the sender name and phone couriers call. Flowery checks them with Uklon at once. The secret is stored encrypted and never shown again; leave it empty later to keep it.
- Load Uklon’s cities, choose the shop’s, and find the pickup point with Uklon’s own address search.
- Check the connection. This runs a test estimate from the pickup point. Only then, with Uklon courier switched on and the bank details filled in, do customers see the option. Any later change to the account needs a new check.
At checkout (App\Services\Uklon\UklonCourier):
- The customer picks the address from Uklon’s search (
GET /checkout/uklon/addresses), plus an optional apartment. - The fee is Uklon’s live estimate from the pickup point: a courier on foot, or a car when no courier can take it (for example beyond 10 km). It is shown as Uklon’s price now, which the shop absorbs small changes to at dispatch.
- If Uklon cannot price the address or does not answer, the option is refused; a price is never invented. A changed Uklon price between quote and request is a changed quote (409).
- Uklon orders are paid by bank transfer only, since couriers cannot take money. The status page shows the IBAN, recipient, amount and purpose (the order number) once the shop confirms, until the payment is recorded.
- The order keeps Uklon’s address, coordinates, product and agreed fee (
uklon_deliveries).
Dispatching (App\Services\Uklon\UklonDispatch; the Uklon courier card on the order page):
- Who: someone with
orders.edit(managers and up) books the courier, because it spends the shop’s money. - When: the order must be confirmed or later and not yet delivered, and its bank transfer recorded as paid (ADR-015 §4).
- Price check: booking re-estimates. Within the shop’s tolerance over what the customer agreed to (default 15%), it books at Uklon’s price now and the shop absorbs the difference. Above it, the card shows Uklon’s price and asks staff to contact the customer first, then Book anyway.
- Never twice: a click claims the delivery first, so a second click is refused. Uklon has no idempotency key, so a booking whose answer is lost becomes “Uklon did not confirm”. Check with Uklon then finds it by our order number (
external_tracking_number) instead of booking again; if Uklon has none, staff may book again. - What we send: the sender is the shop’s name and phone from the account. The receiver is the recipient, or else the customer, with our order number and the apartment as the comment.
- Following the courier:
- Uklon’s signed webhooks (
POST /webhooks/uklon/{account}, HMAC-SHA256 inX-Signature, hex or base64, eachevent_idonce) update the card: Uklon’s status, courier, phone, vehicle and final charge. The webhook is registered when the connection check passes. uklon:syncpolls every five minutes as a fallback, which needs the scheduler (deployment).- When Uklon completes, the order moves to delivered through the legal steps.
- When Uklon cancels a courier staff did not cancel, owners, admins and managers get an email.
- Staff can cancel a booked courier.
- Uklon’s signed webhooks (
- What the customer sees: only the stage: Uklon is finding a courier, a courier is coming, on its way, delivered. The courier’s name and phone stay with the shop.
Staff side
Section titled “Staff side”- New requests. The order list badges storefront orders, highlights the ones still pending (“New request”), and filters them with Storefront requests, which shows how many wait. The order page shows a request card: how the order is received, the customer’s language, and when the status link was issued. Staff check availability and the time, call the customer, then confirm, which reserves the flowers (B01).
- Payments (
App\Services\Orders\OrderPayments, tableorder_payments):- Staff record what the customer paid, by cash, card or bank transfer, with an optional note; the time and the person are kept. Staff hold the
orders.paymentspermission because they take the money at pickup and on delivery; viewers do not. - Amounts are exact in the order’s currency, above zero and at most what is outstanding; a cancelled order takes no payment.
- Entries are never edited or deleted. A mistake is reversed by a negative entry that names it, once, by someone with
orders.edit. - Once an order has entries, its payment status follows them: paid when they cover the total, otherwise pending, also after an amendment changes the total. Orders without entries keep the status staff set by hand.
- Customers can never set a payment: the checkout refuses
payment_status, and only staff routes write payments.
- Staff record what the customer paid, by cash, card or bank transfer, with an optional note; the time and the person are kept. Staff hold the
- A new status link. For a customer who lost theirs, someone with
orders.editissues a new link from the request card. The old link stops working at once, a retry of the original checkout returns the new one, the link is shown once to copy, and it can be emailed to the customer in their language.
Found while building it (6 October 2026): the order page never showed the agreed delivery window when there was no exact time (it now shows, for example, “Evening (17:00–21:00)”), and a staff email queued by the previous release failed once a field was added to it (the field now has a default, with a test).
How a request becomes an order
Section titled “How a request becomes an order”Checkout and the staff order form write orders through the same App\Services\Orders\OrderCreator:
- Prices come from the catalogue on the server; a caller never supplies a price or a total.
- Each line keeps its packaging as a snapshot (name, unit price, known cost) and is charged per item, as the cart charges it: 2 bouquets at 200 with packaging at 30 plus 100 delivery is 2 × (200 + 30) + 100 = 560.
- The order starts
pendingwith revision 1 and reserves nothing until staff confirm it (B01). - A guest order has no staff creator (
orders.created_byis empty). It recordssource = storefront, how it is received (fulfilment), the customer’s language (locale) and the hash of its status link. A pickup has no address, and staff can amend it without inventing one. - When staff later amend the order, lines left on the same product and packaging keep the price they were agreed at; see business rules.