Skip to content

feat(orders): commerce-first create order form running the app's own checkout - #136

Closed
roncodes wants to merge 47 commits into
release/v0.4.25from
feat/create-order-form
Closed

roncodes wants to merge 47 commits into
release/v0.4.25from
feat/create-order-form

Conversation

@roncodes

Copy link
Copy Markdown
Member

Summary

Create order from design board 10b. Stacked on #135; review the top commit.

Today orders.index.new renders nothing. Now it is a commerce-first form that runs the same cart, quote, promotion and checkout path the app does, so prices, fees, discounts, minimum order and totals are computed by the API and never retyped.

  • Form (right overlay, 1040px): three numbered steps on the left and a sticky live summary on the right.
    1. Customer: ModelSelect over customers with SelectOption::Customer rows; ?customer= preselects (the customer panel's New order lands here).
    2. Fulfillment: Delivery / Pickup, ASAP / Schedule (date-time), the store location it leaves from or is picked up at, and the customer's saved places as Place::Pill choices with the default marked, or any other place.
    3. Items: picker over the store's published products with search; a product with variants or add-ons opens an inline chooser (required variants, multi-select, add-on category limits, quantity) before it is added; lines show options, quantity stepper, unit and line price, and flag out-of-stock products. Order notes.
    • Summary: promotion code (applied or rejected with the API's reason), payment (cash on delivery, or mark as paid for a phone order), tip (none, fixed, 10%, custom), subtotal, discount, delivery fee, tip, total, the minimum-order check, and Place order · total. Placing opens the new order's panel.
  • Backend (ConsoleOrderController, internal): POST orders/console/quote builds a Cart from the lines (variants and add-ons in the app's shape), prices delivery through ServiceQuoteController::fromCart, evaluates promotions with PromotionEngine, and returns totals, applied/rejected promotions, the minimum-order check and the quote id. POST orders/console/place re-uses the cart and quote, runs CheckoutController::initializeCashCheckout and the same captureOrder that creates app orders, recording placed_via: console and payment_status (paid for phone orders) in the order meta. The storefront session keys the public middleware would set are scoped for the request and released after.

Not here: "Place and dispatch now" (auto-dispatch follows the store setting), send a payment link, guest orders (a customer record is required), the network-context store step and multi-store carts, delivery tips.

Related Issue

Storefront UX overhaul, design canvas board 10b.

Type of Change

  • Feature

Implementation Notes

No gateway code changed: console orders go through the existing cash path (is_cod), with "mark as paid" expressed in transactionDetails. The form re-prices with a 350ms debounce on every change that affects the total. This touches checkout, so it carries needs-human-review as the repository rules ask.

Validation

  • Tests / Lint / Build — not run locally per the maintainer's request (CI).
  • Manual validation — maintainer. Needs a store with a location, a service rate covering the destination, and published products.
php -l server/src/Http/Controllers/ConsoleOrderController.php: no syntax errors

Documentation Impact

  • Documentation needed but not included

API Reference Impact

  • No API reference changes needed
  • API reference updates required but not included

API reference notes: internal endpoints POST storefront/int/v1/orders/console/quote and POST storefront/int/v1/orders/console/place; no public API change.

Documentation Notes

docs/storefront → orders → creating an order from the console. needs-docs for the series.

Risk

Checkout and payment handling path: review requested. The quote and place endpoints write storefront keys into the staff session for the duration of the request and forget them afterwards; the public middleware overwrites them on every app request regardless.

Screenshots / Recordings

Design canvas board 10b: https://claude.ai/artifact/771ptEFmi4BLmYHBZuUhWF

The dummy app never ran: ember-classic-decorator (unused) crashes on Ember 5,
lazy loading kept the engine's modules out of the test bundle, and the host
services, config and intl data the panels need were missing. Mirrors what
fleetops already does: eager loading when `ember test` runs from this package,
an inert stand-in for the console's extensions module, the API config the
core adapter reads at module scope, and the legacy built-in components the
engine's templates still use.

The dummy intl service ignores missing locale data, since headless Chrome has
no number formats for every bundled locale, and the order details and network
controller tests stub the host router, fetch and app cache the way the widget
tests already do.
…rce card

Catalog, truck and network index pages use Layout::Resource::CardsGrid with
one card each on Layout::Resource::Card, following the Fleet-Ops anatomy:
identity and subtitle in the header, media or facts in the body, status,
icon actions and meta in the footer, with registry slots at each end.

A catalog card shows its categories with product counts, its hours as a
seven-day strip (the new Storefront::HoursStrip) and its product total; a
draft is dashed. A truck card is a Vehicle::Pill with the online dot, the
vehicle photo, where it serves, and a presence badge that says serving,
offline, inactive, or online with no catalog. A network card shows the logo on
its backdrop, the online state, the currency and member count.

"Food trucks" reads "Trucks" in the UI; the food-truck model, routes and
permissions keep their names. The catalogs and trucks pages gain the search
the header already offered.
@roncodes roncodes added needs-docs Requires documentation updates needs-api-spec Requires API specification updates needs-human-review Requires human review before proceeding type:feature Feature or enhancement labels Oct 10, 2026
@roncodes
roncodes force-pushed the feat/create-order-form branch from b7ca203 to 4075d9b Compare October 10, 2026 08:17
… and crashed createRecord; the store summary is store_summary
…s own sidebar for each

- storefront service tracks a network context derived from the network route
- StoreSelector becomes the context switcher with a Networks group and create-network
- application route loads networks; sidebar rewrites for the network context
- network page is a full page with an identity strip; the list hides while one is open
- new Trucks section for a network backed by FoodTruckFilter::network
- network orders and customers are scoped to the network being viewed, not the active store
- createNewNetwork lives on the storefront service
…enu without Networks

- trigger: context icon, name, STORE/NETWORK label and caret; blue for a store, indigo for a network; invitation badge
- menu: Stores and Networks groups with tiles, currency or member count, a check on the active one, readable in dark mode; footer actions read New store / New network
- store navigation follows the design order (Dashboard, Orders, Products, Catalogs, Customers, Trucks, Promotions, Settings); networks live in the switcher, not the menu
- network page loses the All networks button; leaving a network is the switcher's job
@roncodes
roncodes force-pushed the feat/create-order-form branch from 4075d9b to 5351206 Compare October 10, 2026 09:41
…e sidebar re-renders on a switch; networks report their store count
…d an invite panel

- network index is now an Overview (stat tiles, members with 7-day share, invitations, recent orders); settings move to networks.index.network.settings
- NetworkController gains overview, invitations, resend and revoke; one Invite per recipient carrying category, expiry, message and approval in meta
- OrderFilter/CustomerFilter gain a network filter on meta.storefront_network_id; the network orders and customers routes use it
- Storefront::Network::InvitePanel (by email, shareable link, add a store you manage) replaces the share-network and add-stores modals
- stores page: store identity column via a new store descriptor, All/Online/Offline/Invited tabs, bulk assign category and remove, invitation rows with resend/revoke/invite again
- store and network resource descriptors registered with the shared identity registry; SelectOption::Store wrapper; width-style helper
…ost re-export broke console boot before the lazy engine loaded
…ore stacks and next actions

- a multi-store checkout is one expandable row (customer, stores, fulfillment, total with tip split, shared driver, least-advanced status) that opens into each store's order
- status tabs (needs action, preparing, ready for pickup, out for delivery, completed, canceled) drive the status query param; grouped/flat toggle
- new cells: reference, stores (identity stack of store pills), fulfillment, total, driver (inline assign), status, placed, actions (next action promoted, rest behind an ellipsis)
- order index resource keeps meta.checkout_id so the console can group
…lide with the networks list's page, sort and query
…ive questions first

- header: id, status and store/network badges, the next workflow action promoted to a button, Edit, the rest behind a menu
- Storefront::Order::FlowSteps renders the activity flow as steps (custom order configs contribute their own)
- Storefront::Order::IdentityStrip: customer, driver with inline assign, fulfillment and timing, total with tip and promotion
- tabs: Items (with store block and the other orders of the checkout), Route (with tracking), Payment, Activity (with comments), Customer, Data (metadata and documents); registered tabs still appended
- panel widened to 600px
…out rules and a save bar

- settings shell: store identity header with copy-store-key, a section rail (General, Branding and media, Locations, Checkout rules, Payment gateways, Notification channels, Alerts, API keys); anchors on the General page, routes for the rest
- General page restructured into always-open sections: general, branding and media, checkout rules, alerts
- every rule the server honours is a Storefront::SettingRule row with its consequence written under it, detail inputs appear when it is on
- sticky save bar with unsaved-change count and the sections touched; Discard rolls back attributes, options and alerts
…f categories, products, hours and who serves it

- new full-page route catalogs.index.edit: categories on the left (add, rename, reorder, remove), the selected category's products in the middle with an in-place picker over the store's products that hides what is already in the catalog, health on the right; Hours (ScheduleManager), Served by (stores and trucks) and Details sections; sticky save bar with a summary of unsaved changes
- a store can now serve a catalog like a truck: Store gains catalog relations, PUT catalogs/{id}/subjects syncs stores and trucks, the catalog resource exposes hours and subjects
- category order persists; the catalog list opens the editor, the create modal only asks for name, description and status, and the card shows who serves the catalog
… issue badges and a save bar

- Storefront::Product::Form gains tabs: Details (basics, pricing, flags, a folded More for translations and metadata), Variants and add-ons, Availability, Media (images and videos)
- inline validation: missing name or price, sale price not below price, published without category or image; the tab holding the field carries a badge and the save bar links to it
- status select moves into the panel header; the save bar counts unsaved changes across the product, its variants, add-ons and queued uploads, with Discard
…ns on one screen

- header rebuilt: photo, name, type and preference badges, copyable id, since-date, and a lifetime strip (orders, spend, average, last order) from a new customers/{id}/insights endpoint scoped to the store or network
- actions: New order for this customer, Edit, copy id/email
- tabs: Overview (contact facts, saved places with the default marked, stores ordered from in network context, recent orders), Orders, Places (set default, open in Fleet-Ops from the pill), Activity and notes (first/last order, cancellations, internal notes via CommentThread); registered tabs keep working
- panel widened to 600px
…builder, a redemptions screen and promotion cards

- hub shell: title, subtitle, live counts (active, scheduled, redemptions, discount given) from promotions/hub, one tab strip across promotions, campaigns, segments, pushes and the new Redemptions tab
- promotions list: status tabs (all, active, scheduled, paused, ended, draft), cards or table, Storefront::Promotion::Card with value, trigger, window, targets and redemptions, row actions gain duplicate and view redemptions
- Storefront::Promotion::Builder replaces the modal: What, When it applies (with codes once saved), Targets (products, categories, exclusions, segment), Schedule and limits, Review and announce; done steps collapse to one line; save draft at any step
- Redemptions: PromotionRedemption controller, filter, resource and relations, frontend model, route and table
- Storefront::Promotion::Codes extracted from the codes modal so the builder and the modal share it
… can run their own trucks

- Storefront::Truck::Panel (Details: vehicle, service area, zone, status; Catalogs: assign with serving-now badges) replaces the create/edit and assign-catalogs modals
- trucks page gains All/Online/Offline/Inactive tabs; the network trucks page gets New truck, owned by the network
- food_trucks.network_uuid (nullable) with model relation, resource fields, filter (network: member stores' trucks plus network-owned; ownedByNetwork) and public API ownership rules
- card shows the owning network
… status tabs, bulk actions and draft hints

- category sidebar nests by parent, shows per-category counts from a new products/summary endpoint, and offers smart groups (out of stock, drafts, recommended, on sale, uncategorised)
- status tabs (all, published, draft, on sale) and smart groups drive new product filters: status, category, available, on_sale, recommended, uncategorized
- cards and rows are selectable; a bulk bar publishes, unpublishes, moves to a category, marks on sale or deletes the selection
- a draft card says what it is missing (category, image, price); an out-of-stock card that is still published says so
… sidebar and render the status tabs and bulk bar; the earlier edit never applied and the sidebar crashed on an undefined handler
…n today and deep-links into it

- analytics/attention returns orders to confirm (with the oldest wait), pickups ready, deliveries without a driver, published products out of stock, trucks offline and pending network invitations for the active store
- Widget::AttentionStrip renders them as clickable tiles and is registered as a default storefront dashboard widget
…twork invitation

- the invitation email now lands in the engine at storefront/networks/join/:uri, which showed nothing before
- join page reviews the network (identity, currency, category, approval, message, sender, expiry), picks which store joins, and accepts or declines; closed invitations say why
- endpoints: lookup, accept (creates the membership in the invited category; awaiting_approval when the network asked to review), decline, and pending invitations for a store by its email
- the store dashboard shows a banner per open invitation and the context switcher carries a badge
…own checkout

- orders.index.new is a real form: customer, fulfillment (delivery or pickup, ASAP or scheduled, store location, the customer's saved places), items from the store's published products with inline variant and add-on choices, notes, promotion code, payment (cash on delivery or mark as paid), tip, and a live summary
- prices, delivery fee, discounts, minimum order and totals come from the API: POST orders/console/quote builds a cart, quotes delivery through the service-quote logic and evaluates promotions; POST orders/console/place runs the cash checkout and the same capture that creates app orders
- placing opens the order panel; the customer panel's New order lands here with the customer preselected
…t_items_unavailable error naming them; seeders keep customer ids and their app accounts
…cts only that; Mongolian test customers use emails QPay accepts
…, SUCCESS callback reply, clean invoice codes, token reuse

Checked against QPay's API v2 documentation (with e-barimt 3.0):
- Payments: an order is created only for a payment/check row with payment_status PAID whose paid_amount covers the invoice amount recorded when the invoice was created. NEW, FAILED, PARTIAL, REFUNDED and underpaid payments create nothing, in the callback and in the status check alike.
- Callback: QPay's call is answered with HTTP 200 and the body SUCCESS, the only format QPay allows. `respond=1` still returns JSON for manual checks.
- sender_invoice_no and invoice_receiver_code contain letters and digits only (no special characters): the checkout id and the customer's id. invoice_receiver_code was the fixed value CITIZEN; the e-barimt receiver type is sent separately with ebarimt_v3/create.
- Sandbox e-barimt invoices use QPay's TEST_EB_INVOICE code.
- district_code: still taken from the gateway settings; a live e-barimt invoice without one is logged as a warning.
- Access tokens are reused per merchant and environment until five minutes before expiry (expires_in is an absolute UNIX timestamp), instead of a new token per call. A token QPay refuses (401) is dropped and the request retried once with a new one.
…and QPay gateway settings from the Console kept on re-seed
@roncodes
roncodes force-pushed the feat/create-order-form branch from 5351206 to ac59746 Compare October 10, 2026 10:20
…les, no network identity strip, settings/catalog/promotions use Layout::Section::Header, square save bars, switcher hides unknown store counts
… the payment at once

- Order creation (createOrderFromCheckout → captureOrder) read the storefront from the request, and QPay's callback carries no storefront key, so captureOrder stopped at "No storefront in request" and no order was created; orders only appeared when the app's status check made them. The checkout's own storefront (its network, else its store) is now set the way SetStorefrontSession does for the storefront API.
- Checkout's company/order/owner/serviceQuote relations called $this->setConnection(), switching the checkout itself to the Fleetbase database, so a later refresh failed ("Table 'fleetbase.checkouts' doesn't exist"). The related models choose their database themselves.
- The callback publishes "paid" to the checkout channel as soon as QPay confirms the payment, then the order once created.
- The status endpoint answers with the order when it exists and only asks QPay while it doesn't (QPay: no repeated payment checks).
…sh notifications get a section header, products sidebar rows are single-line, New product works from All products, grouped network orders use floating pagination, redemptions call the storefront API
…ompact icon-only order actions in the panel table; storefront service injects hostRouter
…list; panel order rows use a compact actions menu
… tabs into the panel, stack the heading so the status and identity strips sit under the title, 600px
… statuses; orders from the checkout's copy

- carts.status (open, checked_out, cleared); carts already linked to a checkout are migrated to checked_out.
- Cart::retrieve() returns only open carts. An id naming a checked-out or cleared cart (an app can still hold one) resolves to the device's open cart, instead of starting an anonymous cart no device finds again.
- Emptying a cart (PUT carts/{id}/empty) now closes it as cleared with its items kept and continues with the device's open cart. A checked-out or cleared cart is never touched.
- Checkout::checkedout() marks the cart checked_out.
- Orders are created from the checkout's saved copy (cart_state), the items and prices that were priced and charged, so a cart changed or cleared between payment and order creation can't change the order. The live cart only for checkouts saved without a copy.
- storefront:purge-carts deletes only expired carts that are open and empty. Checked out and cleared carts, and abandoned carts with items, are kept.
…er form; flow steps prefer the short step names
… to for_customer so it no longer collides with the orders list and abort routing
…to match the board; categories are assigned from row and bulk actions
@roncodes

Copy link
Copy Markdown
Member Author

Superseded by the single overhaul PR (one branch, one PR targeting release/v0.4.25, per maintainer request).

@roncodes roncodes closed this Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-api-spec Requires API specification updates needs-docs Requires documentation updates needs-human-review Requires human review before proceeding type:feature Feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant