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 companyisActive = truestock > 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 stockin 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 —
+ Adddrops 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 × qtyacross 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 pricelabel 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:
- Default address first.
- 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):
| Field | Rule |
|---|---|
| Label | Optional, up to 60 chars (e.g. "Main warehouse") |
| Street address | Required, up to 200 chars |
| Line 2 | Optional, up to 200 chars |
| City | Required, up to 100 chars |
| State/region | Required, up to 100 chars |
| Postal code | Required, up to 20 chars |
| Country | Required, 2-letter code (uppercased automatically) |
| Phone | Optional, 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:
- Re-checks the customer's session + slug match.
- Rejects
VIEWERcontacts withYour account does not have permission to place orders. - Checks the customer isn't
BLOCKED(belt-and-suspenders — the login path already blocks them). - 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. - Validates the shipping address belongs to this customer.
- Fetches every product in the cart, scoped to this company and
isActive, and rejects withOne or more products are unavailable.if anything's missing. - 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. - Fetches per-customer price overrides and resolves the final unit price for each line.
- In a single transaction: creates the order and its line items, snapshots the shipping address, decrements stock for every product.
- Fires the
order.createdwebhook. - Sends a receipt email to the customer.
- Sends a new-order alert to every OWNER + ADMIN staff user.
- Checks for products that dropped to
stock ≤ 5after the decrement and sends a low-stock alert to OWNER + ADMIN if so. - 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:
| Trigger | Message |
|---|---|
| No shipping address selected | Select a shipping address. |
| Address doesn't belong to customer | Shipping address not found. Please choose one saved to your account. |
| One or more products no longer active | One or more products are unavailable. |
| Stock insufficient | Not enough stock for "{name}". Only {N} available. |
| VIEWER contact | Your account does not have permission to place orders. |
| Blocked customer | Your account is not able to place orders. Please contact the store. |
| Monthly limit reached | This store has reached its monthly order limit. Please try again next month or contact the store. |
| Invalid payload | Invalid 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:
- Open My orders (
/store/{slug}/orders). - Click an order to open its detail.
- 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}/ordersalongside 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.
