Placing orders from the storefront

Everything a customer does between "logged in" and "order placed" happens on a single page: /store/{slug}/catalog. The catalog and the cart live there together — no separate checkout step, no multi-page funnel.

The catalog

Products shown are every product matching all of:

  • companyId = your company
  • isActive = true
  • stock > 0

Ordered by category ascending, then name ascending. Paginated 24 per page — customers see Showing 1-24 of 87 products and page controls at the bottom.

Each product row shows:

  • Image (or a gray initial tile if none).
  • Product name + SKU + category pill + truncated description.
  • Stock column — plain gray if ≥ 6, yellow if 1–5 (low stock), Out of stock in red if zero (but out-of-stock products don't show in the catalog today, so this rarely appears).
  • Price column — unit price, with /each (or whatever the product's unit is) underneath.
  • Add button+ Add drops the product into the cart with quantity 1.

Once a product is in the cart, the + Add button is replaced with a − 1 + stepper. The + button disables when quantity hits the product's stock — customers can't put a quantity larger than available stock into the cart from the UI.

Search, sort, and category filters

Above the product list:

Search

A search input that matches (case-insensitive substring) against:

  • Product name
  • SKU
  • Category
  • Description

Placeholder text: "Search by name, SKU, category…" (description is matched too, even though it's not called out in the placeholder — a useful accidental feature for customers searching for a word in the description).

Sort dropdown

Five options:

  • Name A–Z (default)
  • Name Z–A
  • Price: low → high
  • Price: high → low
  • Stock: most first

Category pills

If your catalog has two or more distinct categories, a row of pills appears below the search:

[ All ]  [ Beverages ]  [ Cleaning ]  [ Paper goods ]

Clicking a category filters to that category; clicking the active one again clears it.

Search + sort + category compose — typing a search term inside a category filter narrows further.

Empty results

If the filters yield nothing:

No products match your filters.
Clear filters

The cart panel

Sticky on the right side of the page. Shows:

  • Your order — heading with item count badge.
  • Line items — name, unit price, − qty + stepper.
  • Ship to — a dropdown of saved addresses (default selected).
  • PO # (optional) — free-form text field, max 100 chars.
  • Notes (optional) — textarea, max 2000 chars.
  • Total — sum of price × qty across all items.
  • Place order button.

When the cart is empty, Place order is disabled and the cart shows a Your cart is empty. placeholder.

Custom prices

If you've set per-customer price overrides for this customer, the cart uses them automatically:

  • The product row shows the override price where the catalog price would be.
  • A small green Your price label appears below the price.
  • The cart total reflects the override.

The override is enforced a second time on the server when the order is placed — a customer can't trick the client into sending the catalog price by editing local state. If your override gets removed between loading the page and hitting Place order, the order is saved at whatever price the server resolves at that moment (override if still present, catalog price otherwise).

Shipping address requirement

Storefront orders require a saved shipping address. If the customer has no addresses saved, the cart shows a yellow banner:

Add a shipping address to your account before placing your first order.
Manage addresses →

…and the Place order button is disabled. The link points at /store/{slug}/addresses where they can add one.

With addresses saved, the Ship to dropdown lists them in this order:

  1. Default address first.
  2. Then every other address, newest first.

Each option shows:

{Label, if set} — {line1}, {city}, {region} {postalCode}

The address chosen at checkout gets frozen as a snapshot on the order — every field captured as it was at the moment of submission. Even if the customer later edits or deletes that address, the order still shows the original. See Orders overview for more on the snapshot model.

Adding an address

From /store/{slug}/addresses, customers can add, edit, delete, and set a default. The form fields (all validated server-side):

FieldRule
LabelOptional, up to 60 chars (e.g. "Main warehouse")
Street addressRequired, up to 200 chars
Line 2Optional, up to 200 chars
CityRequired, up to 100 chars
State/regionRequired, up to 100 chars
Postal codeRequired, up to 20 chars
CountryRequired, 2-letter code (uppercased automatically)
PhoneOptional, up to 40 chars

The first address added is automatically the default. Deleting the default auto-promotes the most-recent remaining address.

PO # and notes

Both optional, both stored on the order.

  • PO # — any string up to 100 chars. Shown on the order detail page and, if set, on the invoice PDF as PO # {value}.
  • Notes — any string up to 2000 chars. Shown on the order detail page and, truncated to 6 lines, on the invoice PDF.

Most distributors use notes for delivery instructions ("Deliver to loading dock B before 11am") and PO # for their buyer's internal purchase order tracking.

Placing the order

On clicking Place order, the server:

  1. Re-checks the customer's session + slug match.
  2. Rejects VIEWER contacts with Your account does not have permission to place orders.
  3. Checks the customer isn't BLOCKED (belt-and-suspenders — the login path already blocks them).
  4. Enforces your monthly order limit (see your plan in Settings → Billing) — rejects with This store has reached its monthly order limit. Please try again next month or contact the store. if over cap.
  5. Validates the shipping address belongs to this customer.
  6. Fetches every product in the cart, scoped to this company and isActive, and rejects with One or more products are unavailable. if anything's missing.
  7. Validates stock — if any line item's quantity exceeds current stock, rejects with Not enough stock for "{name}". Only {N} available. and nothing is saved.
  8. Fetches per-customer price overrides and resolves the final unit price for each line.
  9. In a single transaction: creates the order and its line items, snapshots the shipping address, decrements stock for every product.
  10. Fires the order.created webhook.
  11. Sends a receipt email to the customer.
  12. Sends a new-order alert to every OWNER + ADMIN staff user.
  13. Checks for products that dropped to stock ≤ 5 after the decrement and sends a low-stock alert to OWNER + ADMIN if so.
  14. Redirects to /store/{slug}/orders/{id}.

The order's initial status is SUBMITTED — see Order status workflow for what happens next.

What can fail at checkout

Every error the cart can show:

TriggerMessage
No shipping address selectedSelect a shipping address.
Address doesn't belong to customerShipping address not found. Please choose one saved to your account.
One or more products no longer activeOne or more products are unavailable.
Stock insufficientNot enough stock for "{name}". Only {N} available.
VIEWER contactYour account does not have permission to place orders.
Blocked customerYour account is not able to place orders. Please contact the store.
Monthly limit reachedThis store has reached its monthly order limit. Please try again next month or contact the store.
Invalid payloadInvalid order data. (falls back from any unexpected schema failure)

Errors show as a red banner at the top of the cart. Fixing the problem and clicking Place order again retries — the cart state is preserved.

Reordering a previous order

Customers can one-click reorder from any past order:

  1. Open My orders (/store/{slug}/orders).
  2. Click an order to open its detail.
  3. Click Reorder on the detail page.

This sends them to the catalog at /store/{slug}/catalog?reorder={orderId} which:

  • Pre-fills the cart with every line item from that order.
  • Clamps each quantity to current stock (so reordering 50 of a product that's down to 3 puts 3 in the cart).
  • Drops any items that are no longer active or in stock.
  • Shows a blue banner at the top:
Reordered N items from your previous order. Adjust quantities as needed.

…or if some items were skipped:

Reordered N items from your previous order. M items are no longer available.

When reordering, the catalog is shown with no pagination (every product visible on one page) so the pre-filled cart matches what the customer sees in the list. Regular browsing paginates as usual.

Order history and invoices

After placing an order, customers can:

  • See it in /store/{slug}/orders alongside all their past orders.
  • Open /store/{slug}/orders/{id} for the full line-item breakdown with the shipping address snapshot.
  • Click Download invoice to get the PDF — same format as the dashboard's invoice. See Invoice PDFs for what's on it.

Customers cannot:

  • Edit an order after placing it.
  • Cancel an order. They have to contact you, and you do it from the dashboard.
  • See orders placed by other customers or contacts at different customers.

Contacts see orders for their parent customer regardless of who placed them — so a BUYER contact can see an order placed by the primary customer, and vice versa.


That's the storefront. Next up: REST API — the same functionality, but programmable.