@base44/app-plugin-commerce 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +68 -76
- package/skills/commerce/docs/api-admin.md +11 -50
- package/skills/commerce/docs/api-storefront.md +25 -118
- package/skills/commerce/install/01-install.md +20 -49
- package/skills/commerce/install/02-storefront.md +204 -431
- package/skills/commerce/install/03-data.md +43 -109
- package/skills/commerce/references/admin-product-form.md +26 -0
- package/skills/commerce/references/catalog-rendering.md +9 -9
- package/skills/commerce/references/online-payments.md +4 -15
- package/skills/commerce/references/operations.md +19 -1
- package/skills/commerce/references/storefront-verification.md +47 -0
- package/src/commerce/storefront/StorefrontProvider.jsx +8 -3
- package/src/commerce/storefront/cartUI.jsx +118 -0
- package/src/commerce/storefront/index.js +39 -15
- package/src/commerce/storefront/pickers.jsx +84 -23
- package/src/commerce/storefront/useAddressForm.js +63 -26
- package/src/commerce/storefront/useCartLine.js +9 -4
- package/src/commerce/storefront/useCheckout.jsx +13 -0
- package/src/commerce/storefront/useOrderReturn.js +7 -5
- package/src/commerce/storefront/usePlaceOrder.js +63 -0
- package/src/commerce/storefront/useProduct.js +112 -41
- package/src/commerce/storefront/useProductList.js +7 -0
- package/src/commerce/storefront/useUpsell.js +90 -0
- package/src/commerce/utils/specs.js +6 -2
- package/src/commerce/utils/totals.js +23 -12
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ It provides a full-featured **commerce data model and behavior** (variant-driven
|
|
|
12
12
|
- **Shared commerce engine** (`base44/shared/commerce/`) — totals, tax, shipping, coupons, stock, order lifecycle, webhook dispatch (HMAC-signed), emails, card-payment plumbing, plus static country/currency/continent data.
|
|
13
13
|
- **Admin UI** (`src/commerce/admin/`) — a React/Tailwind/shadcn admin with a familiar store back-office information architecture: dashboard, orders, products, coupons, customers, reports, and full settings including webhooks. Admin-role gated.
|
|
14
14
|
- **Storefront helpers** (`src/commerce/utils/`) — framework-free, dependency-free modules for the shopfront you build: `storefront.js` is the API client (`createStorefront(base44)` — cart-token lifecycle, cached store-info, catalog/cart/checkout/reviews/return-page calls); `variants.js` maps an attribute selection (Size, Color) onto a `ProductVariation` and back, plus per-option availability and price ranges; `price.js` encodes the from-price and price-range rules; `totals.js` projects a cart *or* an order into one summary shape; `address-spec.js` is the checkout address form as data; `images.js` normalizes catalog images; `shipping-promos.js` reads the store's real free-shipping configuration so "Free shipping over €150" states a configured rule rather than an invented number.
|
|
15
|
-
- **Storefront React layer** (`src/commerce/storefront/`) — **headless: the logic is premade, the UI never is.** Nothing in the layer renders markup or carries CSS; every element, class and word of copy in the storefront you build is yours, so a brief like "make it feel like <site>" applies to the whole store, checkout included. What ships is every piece of logic that is the same in all stores, as hooks returning complete view-models: `StorefrontProvider`, `useProductList`/`useCategories`/`useRibbons`, `useProduct`/`useAddToCart`/`
|
|
15
|
+
- **Storefront React layer** (`src/commerce/storefront/`) — **headless: the logic is premade, the UI never is.** Nothing in the layer renders markup or carries CSS; every element, class and word of copy in the storefront you build is yours, so a brief like "make it feel like <site>" applies to the whole store, checkout included. What ships is every piece of logic that is the same in all stores, as hooks returning complete view-models: `StorefrontProvider`, `useProductList`/`useCategories`/`useRibbons`, `useProduct`/`useAddToCart`/`useProductPrice`/`useProductGallery` (+ the `variantAxes`/`productSpecs` render-model helpers), `useProductReviews`, `useCart`/`useCartLine`/`useCoupon`, `useCheckout` + `useAddressForm`/`useTotalsLines`/`useCheckoutBlockers`, `useOrderReturn`, and `useStorefrontSeo` — plus three render-prop components that stay just as headless (`ShippingMethodPicker`/`PaymentMethodPicker` for the two checkout choices that are store data, `CartLine` for per-row cart bindings). Each hook's doc comment states the render rules that keep a store correct (an unbuyable variant option renders disabled, not hidden; a receipt page must render `paymentInstructions`; …). Needs React and nothing else.
|
|
16
16
|
- **StoreAdmin agent + bot** — an AI copilot (`base44/agents/commerce/StoreAdmin.jsonc`, registered as `commerce/StoreAdmin`) with the `commerce/*` functions attached directly as tools (calls run as the chatting user → `requireAdmin()` still applies), variant-aware order editing, plus a chat panel in the admin sidebar with GFM markdown-table rendering.
|
|
17
17
|
- **Docs** — this README plus the commerce skill folder [`skills/commerce/`](./skills/commerce/): [`SKILL.md`](./skills/commerce/SKILL.md) is the map every agent starts from (and the only path the platform needs to know); [`install/`](./skills/commerce/install/) holds the three stage files that are the whole install (`01-install` → `02-storefront` → `03-data`, each read at the moment its work starts and dropped when its checklist passes); [`references/`](./skills/commerce/references/) holds per-topic guides opened only on demand; [`docs/`](./skills/commerce/docs/) holds the data-model map ([`entities.md`](./skills/commerce/docs/entities.md)) and the two API references. The whole folder is installed into the app at `.agents/skills/commerce/` so agents pick it up natively.
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -10,77 +10,70 @@ Nothing else in this skill needs to be open right now.
|
|
|
10
10
|
|
|
11
11
|
The kit ships, already written and tested: 20 `commerce.*` entities, the
|
|
12
12
|
`commerce/*` backend functions (storefront API, admin API, payments, webhooks,
|
|
13
|
-
emails), the shared
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
**Paths in this skill
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`install/03-data.md` means `.agents/skills/commerce/install/03-data.md`. Resolve
|
|
23
|
-
them from the skill folder, never from the directory of the file that mentioned
|
|
24
|
-
them — a comment in `src/…` naming `install/03-data.md` is not
|
|
25
|
-
`src/install/03-data.md`. (Markdown links between skill files are ordinary
|
|
26
|
-
relative links and resolve as written.)
|
|
13
|
+
emails), the shared engine, a complete admin back office at `/store-admin`, the
|
|
14
|
+
`commerce/StoreAdmin` copilot, and the storefront's entire logic layer
|
|
15
|
+
(`@/commerce/storefront`). None of that costs you generation — your work is the
|
|
16
|
+
store's *UI and data*, not the plumbing.
|
|
17
|
+
|
|
18
|
+
**Paths in this skill** are relative to the skill's folder
|
|
19
|
+
(`.agents/skills/commerce/` in an installed app), wherever they are named —
|
|
20
|
+
docs, checklists, code comments. Resolve them from the skill folder, never from
|
|
21
|
+
the file that mentioned them.
|
|
27
22
|
|
|
28
23
|
## Installing right now?
|
|
29
24
|
|
|
30
25
|
Read **[`install/01-install.md`](./install/01-install.md)** and follow it. It
|
|
31
26
|
routes you to `install/02-storefront.md` when you start the UI and
|
|
32
27
|
`install/03-data.md` when you seed the catalog — in that order, at those
|
|
33
|
-
moments. Read nothing else up front
|
|
34
|
-
and each one says when a reference is genuinely needed.
|
|
28
|
+
moments. Read nothing else up front.
|
|
35
29
|
|
|
36
30
|
## Four things to hold from the start
|
|
37
31
|
|
|
38
32
|
- **Entity names are dotted; SDK access is bracket syntax only** —
|
|
39
33
|
`base44.entities["commerce.Product"]`. `commerce__Product` and `Product` do
|
|
40
|
-
not exist. The
|
|
41
|
-
|
|
42
|
-
- **Store configuration is declared in one seed call
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
manual (`offline`) payment and works end to end. Cards are a late, deliberate
|
|
52
|
-
step, and the decision plus the timing live in
|
|
53
|
-
[`install/03-data.md`](./install/03-data.md).
|
|
34
|
+
not exist. The map is [`docs/entities.md`](./docs/entities.md); never scan
|
|
35
|
+
`base44/entities/` for a name.
|
|
36
|
+
- **Store configuration is declared in one seed call** — catalog, shipping
|
|
37
|
+
zones, currency, payment methods — never assembled by editing records
|
|
38
|
+
([`install/03-data.md`](./install/03-data.md)).
|
|
39
|
+
- **Don't weaken the admin gating** — three layers (UI guard, entity RLS,
|
|
40
|
+
`requireAdmin()`); keep all three when touching routes or schemas
|
|
41
|
+
([`install/01-install.md`](./install/01-install.md)).
|
|
42
|
+
- **Online card payments are optional and off by default.** A seeded store
|
|
43
|
+
takes manual (`offline`) payment and works end to end; cards are a late,
|
|
44
|
+
deliberate step ([`install/03-data.md`](./install/03-data.md)).
|
|
54
45
|
|
|
55
46
|
## The storefront: logic is premade, UI never is
|
|
56
47
|
|
|
57
|
-
**The UI is yours, all of it** — every page, element, class and word of copy
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
**The UI is yours, all of it** — every page, element, class and word of copy.
|
|
49
|
+
The kit deliberately ships **no markup and no CSS anywhere**; a brief like
|
|
50
|
+
"make it feel like <site>" is your work, done as you would with no kit. Encode
|
|
51
|
+
that identity **once** — design classes in `index.css`, spent across every
|
|
52
|
+
page — not as decoration re-typed on every element;
|
|
53
|
+
[`install/02-storefront.md`](./install/02-storefront.md) opens with the
|
|
54
|
+
method.
|
|
62
55
|
|
|
63
56
|
**The logic ships as headless hooks** (`@/commerce/storefront`) — checkout
|
|
64
|
-
repricing
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
repricing, variant resolution, cart state, coupons, reviews, order-return
|
|
58
|
+
verification, cart-drawer state. Each returns a complete view-model as **plain
|
|
59
|
+
states and handlers** (`status` to branch on, ready-to-map arrays, `label`s
|
|
60
|
+
and `disabled` flags, callbacks) — you write every element and attribute from
|
|
61
|
+
them. **Never re-implement what a hook does** — that is where storefront bugs
|
|
62
|
+
cluster.
|
|
70
63
|
|
|
71
64
|
```jsx
|
|
72
|
-
import { useCart, CartLine
|
|
65
|
+
import { useCart, CartLine } from "@/commerce/storefront";
|
|
73
66
|
const { status, lines } = useCart(); // branch on status, map lines into YOUR rows
|
|
74
67
|
```
|
|
75
68
|
|
|
76
69
|
The admin UI (`src/commerce/admin/`) is finished and **also yours to change** —
|
|
77
|
-
restyle it, add pages, rework flows.
|
|
78
|
-
|
|
70
|
+
restyle it, add pages, rework flows ([`docs/api-admin.md`](./docs/api-admin.md)
|
|
71
|
+
is the backend it talks to).
|
|
79
72
|
|
|
80
73
|
Four rules the API enforces — a storefront that skips them cannot complete a
|
|
81
|
-
purchase. In
|
|
82
|
-
|
|
83
|
-
[`docs/api-storefront.md`](./docs/api-storefront.md)
|
|
74
|
+
purchase. In React the hooks implement all four; your markup renders what they
|
|
75
|
+
hand back (API-level statements for non-React clients:
|
|
76
|
+
[`docs/api-storefront.md`](./docs/api-storefront.md)):
|
|
84
77
|
|
|
85
78
|
1. A product with variants needs **one selector per attribute**, resolved to a
|
|
86
79
|
`variation_id` (`useProduct` + `variantAxes`).
|
|
@@ -88,8 +81,8 @@ render what they hand back. The API-level statements are in
|
|
|
88
81
|
(`useCheckout` — automatic).
|
|
89
82
|
3. **`/order-received` must exist** and render the return state, including a
|
|
90
83
|
manual order's payment instructions (`useOrderReturn`).
|
|
91
|
-
4. **Never advertise what isn't configured** — no free-shipping banner without
|
|
92
|
-
real rate, no coupon codes without a field to redeem them in.
|
|
84
|
+
4. **Never advertise what isn't configured** — no free-shipping banner without
|
|
85
|
+
a real rate, no coupon codes without a field to redeem them in.
|
|
93
86
|
|
|
94
87
|
All backend functions return the envelope `{ success, data }`; with the SDK the
|
|
95
88
|
payload is `res.data.data`:
|
|
@@ -101,32 +94,31 @@ const { products, has_next } = res.data.data;
|
|
|
101
94
|
|
|
102
95
|
## Where to look for what
|
|
103
96
|
|
|
104
|
-
Open a file when its work starts — not while planning.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
| [`install/
|
|
110
|
-
| [`install/
|
|
111
|
-
| [`
|
|
112
|
-
| [`
|
|
113
|
-
| [`references/
|
|
114
|
-
| [`references/
|
|
115
|
-
| [`references/
|
|
116
|
-
| [`references/reviews.md`](./references/reviews.md) | moderation, or a policy beyond the `policy` prop |
|
|
117
|
-
| [`references/store-settings.md`](./references/store-settings.md) | changing store behavior through settings keys |
|
|
118
|
-
| [`references/emails.md`](./references/emails.md) | order-email recipients, subjects,
|
|
119
|
-
| [`references/admin-product-form.md`](./references/admin-product-form.md) | editing the shipped product editor |
|
|
120
|
-
| [`references/store-admin-agent.md`](./references/store-admin-agent.md) | changing the StoreAdmin copilot
|
|
121
|
-
| [`references/guest-access-security.md`](./references/guest-access-security.md) | **adding your own function or entity
|
|
122
|
-
| [`references/operations.md`](./references/operations.md) | scheduled maintenance, scaling limits, outbound webhooks |
|
|
123
|
-
| [`docs/api-storefront.md`](./docs/api-storefront.md) | filters, customer accounts, refunds, a non-React client
|
|
124
|
-
| [`docs/api-admin.md`](./docs/api-admin.md) | changing admin pages, automating
|
|
97
|
+
Open a file when its work starts — not while planning.
|
|
98
|
+
|
|
99
|
+
| Topic | Open when | Size |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| [`install/01-install.md`](./install/01-install.md) | installing — routes you to 02 and 03 | 6K |
|
|
102
|
+
| [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages | 27K |
|
|
103
|
+
| [`install/03-data.md`](./install/03-data.md) | seeding catalog/shipping, payments decision | 11K |
|
|
104
|
+
| [`docs/entities.md`](./docs/entities.md) | any direct entity read/write ("which entity holds X") | 11K |
|
|
105
|
+
| [`references/catalog-rendering.md`](./references/catalog-rendering.md) | which fields each catalog call returns, variant edge cases | 13K |
|
|
106
|
+
| [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond 03's recipe, taxes, day-2 edits | 8K |
|
|
107
|
+
| [`references/online-payments.md`](./references/online-payments.md) | the store opted into cards and you are wiring the provider **now** | 8K |
|
|
108
|
+
| [`references/storefront-verification.md`](./references/storefront-verification.md) | driving the storefront from a browser script | 3K |
|
|
109
|
+
| [`references/reviews.md`](./references/reviews.md) | moderation, or a policy beyond the `policy` prop | 4K |
|
|
110
|
+
| [`references/store-settings.md`](./references/store-settings.md) | changing store behavior through settings keys | 5K |
|
|
111
|
+
| [`references/emails.md`](./references/emails.md) | order-email recipients, subjects, the log | 5K |
|
|
112
|
+
| [`references/admin-product-form.md`](./references/admin-product-form.md) | editing the shipped product editor | 6K |
|
|
113
|
+
| [`references/store-admin-agent.md`](./references/store-admin-agent.md) | changing the StoreAdmin copilot | 3K |
|
|
114
|
+
| [`references/guest-access-security.md`](./references/guest-access-security.md) | **adding your own function or entity**; RLS/identity questions | 5K |
|
|
115
|
+
| [`references/operations.md`](./references/operations.md) | scheduled maintenance, scaling limits, outbound webhooks | 7K |
|
|
116
|
+
| [`docs/api-storefront.md`](./docs/api-storefront.md) | filters, customer accounts, refunds, a non-React client | 32K |
|
|
117
|
+
| [`docs/api-admin.md`](./docs/api-admin.md) | changing admin pages, automating admin functions, the full seed contract | 24K |
|
|
125
118
|
|
|
126
119
|
**The read budget.** Content you pull into context is re-read on every later
|
|
127
|
-
call, so a file opened while planning costs many times what
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
value, its doc comment), the code wins over both.
|
|
120
|
+
call, so a file opened while planning costs many times what it costs opened at
|
|
121
|
+
the moment it is used. Open one reference when its task starts, take what you
|
|
122
|
+
need, and when a stage's checklist passes, record its carry-forward lines and
|
|
123
|
+
treat the file as gone. If a *rule* appears in code (a hook's JSDoc), the code
|
|
124
|
+
wins over any doc.
|
|
@@ -10,11 +10,7 @@ Two styles. **Reads are direct** entity SDK calls. **Mutations with side effects
|
|
|
10
10
|
|
|
11
11
|
## Invocation & envelope
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const res = await base44.functions.invoke("commerce/admin-products", { action: "search", q: "shirt", limit: 20 });
|
|
15
|
-
const { rows, has_next } = res.data.data; // res.data = { success, data }; .data = payload
|
|
16
|
-
```
|
|
17
|
-
Success: `{ success: true, data }`. Failure: `{ success: false, error, code }` with the HTTP status set. Auth: every `commerce/admin-*` function calls `requireAdmin()` → **401** if not signed in, **403** if signed in without the `admin` role, before any data access (via the service role).
|
|
13
|
+
Standard invoke envelope — payload at `res.data.data` (SKILL.md owns it). Auth: every `commerce/admin-*` function calls `requireAdmin()` → **401** if not signed in, **403** if signed in without the `admin` role, before any data access (via the service role).
|
|
18
14
|
|
|
19
15
|
## Pagination & search
|
|
20
16
|
|
|
@@ -44,10 +40,10 @@ Actions: `save` · `delete` · `batch` · `duplicate` · `set-stock` · `search`
|
|
|
44
40
|
| `attribute` | commerce.ProductAttribute | `id?, name, code?, order?` |
|
|
45
41
|
| `attribute-term` | commerce.ProductAttributeTerm | `id?, attribute_id` (**required**, must exist)`, name, order?` |
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
Cascade semantics — category slug/`parent_id` coercion, attribute `code` derivation, ribbon get-or-create, the attribute-value rename rewrite across products and variations: [`../references/admin-product-form.md`](../references/admin-product-form.md#taxonomy-cascade-semantics-save-term--delete-term).
|
|
48
44
|
|
|
49
45
|
This is how a non-UI caller (notably the StoreAdmin agent) creates the records that `category_ids`/`ribbon_ids`/`attributes[].attribute_id` reference — assigning an id is useless if the record can't be created. **A product's `commerce.ProductAttribute` and its values must exist first**, so create those, then `save` the product with `attributes[]`/`variations[]`. (At install time, `commerce/seed-store` can do all of this in one call — it takes whole products by display name and get-or-creates the taxonomy internally; see below.)
|
|
50
|
-
- **`delete-term`** — `{ taxonomy, id, detach? }` → `{ deleted, detached, terms_deleted }`.
|
|
46
|
+
- **`delete-term`** — `{ taxonomy, id, detach? }` → `{ deleted, detached, terms_deleted }`. What each taxonomy cascades and what `detach` does — including where it is a no-op: same reference as above.
|
|
51
47
|
- **`list-terms`** — `{ taxonomy, q?, attribute_id?, limit?, skip? }` → `{ rows, has_next }`. Categories sort by `menu_order`, attributes and attribute values by `order`, ribbons by `name`; `attribute_id` filters values to one attribute. Use it to reuse an existing record instead of creating a duplicate.
|
|
52
48
|
|
|
53
49
|
## commerce/admin-orders
|
|
@@ -60,7 +56,7 @@ Actions: `create-draft` · `create` · `update` · `preview` · `update-status`
|
|
|
60
56
|
- Any time: `billing`, `shipping`, `customer_id`, `customer_note`, `payment_method`, `meta_data`, `status`.
|
|
61
57
|
- **Line edits require pending/on-hold** (else `409 order_locked`): `items` (specs `{ product_id, variation_id?, quantity, price_override? }`), `fees`, `coupon_codes`, `chosen_shipping_method`. Providing any of these — or changing addresses — triggers a full reprice.
|
|
62
58
|
- A `status` in the patch is delegated to the transition engine last (fires the side effects below); otherwise an `order.updated` webhook fires.
|
|
63
|
-
- **`preview`** — `{ order_id, patch }`. **Reads only — writes nothing, fires no webhooks.** Prices the same patch shape `update` takes and returns `{ fields }`: the Order fields `update` *would* have written (`line_items` with per-line tax, `shipping_lines`, `fee_lines`, `coupon_lines`, `tax_lines` and every total). Allowed on locked orders, since nothing is persisted. Only the pricing keys matter (`items`, `fees`, `shipping_lines`, `billing`, `shipping`); the rest are ignored. This is how the admin order editor shows live totals for **unsaved** edits
|
|
59
|
+
- **`preview`** — `{ order_id, patch }`. **Reads only — writes nothing, fires no webhooks.** Prices the same patch shape `update` takes and returns `{ fields }`: the Order fields `update` *would* have written (`line_items` with per-line tax, `shipping_lines`, `fee_lines`, `coupon_lines`, `tax_lines` and every total). Allowed on locked orders, since nothing is persisted. Only the pricing keys matter (`items`, `fees`, `shipping_lines`, `billing`, `shipping`); the rest are ignored. This is how the admin order editor shows live totals for **unsaved** edits without reimplementing tax and coupon rules on the client; the preview always equals what saving would write.
|
|
64
60
|
- **`update-status`** — `{ order_id, status, note? }` → runs the transition engine (side-effect matrix below).
|
|
65
61
|
- **`bulk-status`** — `{ ids, status }` → `{ results: [{ id, success, error? }] }`.
|
|
66
62
|
- **`status-counts`** — no payload → `{ all, pending, processing, "on-hold", completed, cancelled, refunded, failed }` (includes the `all` key). Powers the list tabs.
|
|
@@ -126,21 +122,8 @@ Actions: `test` · `redeliver` (webhook definitions themselves are direct `comme
|
|
|
126
122
|
|
|
127
123
|
## commerce/admin-reports
|
|
128
124
|
|
|
129
|
-
All
|
|
130
|
-
|
|
131
|
-
| Action | Payload | Returns |
|
|
132
|
-
|---|---|---|
|
|
133
|
-
| `summary` | — | `{ sales_today, sales_month, orders_by_status, low_stock_count, out_of_stock_count, top_seller }` where `sales_*` = `{ gross_sales, net_sales, orders, items, tax, shipping, discount, refunds }` |
|
|
134
|
-
| `sales` | `{ date_min?, date_max?, interval? }` (`day`\|`week`\|`month`) | `{ totals: <agg>, series: [{ period, ...agg }] }` (net = gross − refunds − tax − shipping) |
|
|
135
|
-
| `top-sellers` | `{ date_min?, date_max?, limit? }` | `{ rows: [{ product_id, name, sku, quantity, net_revenue }] }` |
|
|
136
|
-
| `stock` | — | `{ low_stock: [...], out_of_stock: [...] }` |
|
|
137
|
-
| `orders-totals` | — | `{ [status]: count }` |
|
|
138
|
-
| `products-totals` | — | `{ total, by: { [status]: count } }` |
|
|
139
|
-
| `customers-totals` | — | `{ total, guests, registered, paying }` |
|
|
140
|
-
| `coupons-totals` | — | `{ total, by: { [discount_type]: count } }` |
|
|
141
|
-
| `reviews-totals` | — | `{ total, by: { [status]: count } }` |
|
|
142
|
-
| `categories-totals` / `ribbons-totals` | — | `{ total, terms: [{ id, name, count }] }` |
|
|
143
|
-
| `attributes-totals` | — | `{ total, attributes: [{ id, name, terms }] }` |
|
|
125
|
+
Actions: `summary` · `sales` · `top-sellers` · `stock` · a `*-totals` count action per resource (`orders` `products` `customers` `coupons` `reviews` `categories` `ribbons` `attributes`). All scan orders on demand (counted = `date_paid` set, or status `processing`/`completed`).
|
|
126
|
+
The full payload/return table and scaling guidance: [`../references/operations.md`](../references/operations.md#commerceadmin-reports-actions).
|
|
144
127
|
|
|
145
128
|
## commerce/admin-tools
|
|
146
129
|
|
|
@@ -157,11 +140,8 @@ Actions: `status` · `admin-email-recipients` · `recount-terms` · `recount-cou
|
|
|
157
140
|
|
|
158
141
|
## commerce/payments
|
|
159
142
|
|
|
160
|
-
Actions: `create-link` · `complete-return` · `verify` — the admin side of online payments
|
|
161
|
-
|
|
162
|
-
- **`create-link`** — `{ order_id }` → `{ url, reference }`: a provider-hosted payment page for an unpaid order — the **payment link** to send a customer. Accepts an order on **any** payment method (including none, as admin-created orders start) and switches it onto the card gateway, logging the change: refunds key off `payment_method`, so this keeps the order honest about how it was paid. `409 already_paid`, `400 card_payments_disabled`, `503 no_card_payment_provider`.
|
|
163
|
-
- **`verify`** — `{ order_id }` → `{ paid, already_confirmed, status, order }`: re-asks the provider and moves the order to `processing` when the money is there. Idempotent.
|
|
164
|
-
- **`complete-return`** — what the storefront's mandatory `/order-received` page calls; full contract in [`api-storefront.md`](./api-storefront.md#commercepayments--online-payment-for-an-order).
|
|
143
|
+
Actions: `create-link` · `complete-return` · `verify` — the admin side of online payments. [`api-storefront.md`](./api-storefront.md#commercepayments--online-payment-for-an-order) owns the action table (payloads, returns, error codes, the mandatory `/order-received` flow).
|
|
144
|
+
An admin authorizes with the role and passes `{ order_id }` alone where a customer passes `order_key`; `create-link` mints the payment link for an unpaid order on **any** current method (it is switched onto the card gateway, logged).
|
|
165
145
|
|
|
166
146
|
`commerce/payment-webhook` is the provider's server-to-server callback (raw body, no `action` envelope). The function is **premade** — it validates every event through `card-payment.ts`'s `parseWebhook` and never trusts an event body on its own: an unverified event only names an order, and payment is verified through `checkCardPaymentPaid` against the provider's API, so no signing secret is needed. Until `card-payment.ts` is implemented it answers `400 webhook_not_implemented`; events it can't tie to an order are acknowledged with `200 { ignored: true }` so providers don't retry. See [`../references/online-payments.md`](../references/online-payments.md).
|
|
167
147
|
|
|
@@ -197,7 +177,7 @@ The one-call catalog bootstrap. Entries reference categories/ribbons/attributes
|
|
|
197
177
|
| `attributes` | `[{ name, options }]` or `{ <name>: [options] }` | declares the variant axes |
|
|
198
178
|
| `variations` | `[{ options: { <name>: <option> }, ...overrides }]` | the stocked combinations. **Omit it to auto-generate all** combinations, each inheriting the product-level price/sale fields |
|
|
199
179
|
| `default_options` | `{ <name>: <option> }` | the pre-selected combination |
|
|
200
|
-
| everything else | `commerce.Product` fields | `name` (required), `slug`, `sku`, `status`, `regular_price`, `sale_price`, `images`, `featured`, `virtual`, `downloadable`, `downloads`, `tax_status`, `tax_group`, `stock_quantity`, `weight`, `dimensions`, `meta_data` |
|
|
180
|
+
| everything else | `commerce.Product` fields | `name` (required), `slug`, `sku`, `status`, `regular_price`, `sale_price`, `date_on_sale_from`/`date_on_sale_to`, `short_description`, `description`, `images`, `featured`, `virtual`, `downloadable`, `downloads`, `download_limit`, `download_expiry`, `tax_status`, `tax_group`, `stock_quantity`, `manage_stock`, `low_stock_amount`, `backorders`, `sold_individually`, `weight`, `dimensions`, `meta_data` (`upsell_ids`/`cross_sell_ids` take Product *ids*, which exist only after this call — cross-link via `admin-products` later) |
|
|
201
181
|
|
|
202
182
|
A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without one it draws on the parent's pooled stock (`"parent"`), and its SKU is synthesized from the parent's when absent. Parent price and `stock_status` are derived by the same helpers as `save` ([derived fields](./entities.md#derived-fields--never-write-these)), but **no `product.created` webhooks fire** — bootstrap precedes subscribers, so use `admin-products` for webhook-visible creates. **Re-runs converge**: a product whose `sku` (or, with none, derived slug) already exists is skipped and reported, so retries never duplicate; an explicit variation SKU already in use is **`409 duplicate_sku`**. `coupons` is a thin passthrough (code lowercased, skip-if-exists).
|
|
203
183
|
|
|
@@ -205,28 +185,9 @@ A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without on
|
|
|
205
185
|
|
|
206
186
|
### Payload — `locations`
|
|
207
187
|
|
|
208
|
-
Seeding `locations` is **the** way to set shipping up. Each entry becomes a `commerce.ShippingTaxLocation`, skip-if-exists **by name
|
|
209
|
-
|
|
210
|
-
| Key | Shape | Notes |
|
|
211
|
-
|---|---|---|
|
|
212
|
-
| `name` | string, required | the skip-if-exists match key |
|
|
213
|
-
| `countries` | `["IL", "DE"]` | ISO country codes → `{ type: "country" }` regions |
|
|
214
|
-
| `continents` | `["EU"]` | one of `AF` `AN` `AS` `EU` `NA` `OC` `SA`, sparing you a 51-code country list. `EU` is the **continent** Europe, not the European Union. An unknown code fails listing all seven |
|
|
215
|
-
| `regions` | `[{ type, code }]` | raw escape hatch (`country`\|`state`\|`continent`; state codes are `US:CA`) |
|
|
216
|
-
| `rest_of_world` | `true` | the **catch-all** every unmatched address falls to. It has no regions, so combining it with `countries`/`continents`/`regions` is a contradiction and fails validation |
|
|
217
|
-
| `order` | integer | match priority, **ascending**. Defaults to the **payload position**, so the array reads as the priority; an explicit value wins |
|
|
218
|
-
| `shipping_rates` | `[{ id?, name, cost, free_over? }]` | one checkout choice each. `id` is minted from the location + rate names when omitted, and must stay stable (carts and orders reference the chosen rate by it). `free_over` = discounted items subtotal at which it becomes free. Negative `cost` fails |
|
|
219
|
-
| `tax_groups` | `[{ name, rates: [{ name, rate }] }]` | a `Products` group is prepended when missing (products pick one by name via `tax_group`). A `rate` outside 0–100 fails |
|
|
220
|
-
| `shipping_tax` | `{ type: "percent"\|"fixed", value }` | tax on the shipping line |
|
|
221
|
-
|
|
222
|
-
```js
|
|
223
|
-
locations: [
|
|
224
|
-
{ name: "Europe", continents: ["EU"], shipping_rates: [{ name: "Standard", cost: 20 }] },
|
|
225
|
-
{ name: "Worldwide", rest_of_world: true, shipping_rates: [{ name: "International", cost: 100 }] },
|
|
226
|
-
]
|
|
227
|
-
```
|
|
188
|
+
Seeding `locations` is **the** way to set shipping up. Each entry becomes a `commerce.ShippingTaxLocation`, skip-if-exists **by name**: a scope (`countries: ["IL", "DE"]`, `continents: ["EU"]` — the **continent**, not the EU — raw `regions`, or `rest_of_world: true`, the catch-all, which contradicts the other scope keys), `order` (match priority ascending, defaults to payload position), `shipping_rates: [{ id?, name, cost, free_over? }]` (a minted `id` must stay stable — carts/orders reference it), `tax_groups` (a `Products` group prepended when missing) and `shipping_tax`. Unknown codes, negative `cost` or a tax `rate` outside 0–100 fail validation. Field-by-field semantics, matching and recipes: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
|
|
228
189
|
|
|
229
|
-
|
|
190
|
+
A payload carrying `locations` suppresses the seeded catch-all fallback — without your own, unlisted addresses get `shipping_not_available`, flagged as `warnings: ["no_catchall_location: …"]` (same reference; [`../install/03-data.md`](../install/03-data.md) also owns this).
|
|
230
191
|
|
|
231
192
|
### Response
|
|
232
193
|
|
|
@@ -2,65 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
The raw HTTP/SDK surface behind a customer-facing shopfront. **A React storefront should not call most of it directly** — `src/commerce/storefront/` ships the logic layer, and this file is the reference for what lies beyond it, for non-React and headless clients, and for the exact payloads and error codes.
|
|
4
4
|
|
|
5
|
-
**Logic is premade, UI never is.** In a React app, every surface here has a headless hook in `@/commerce/storefront` — `useProductList`, `useProduct` (+ `variantAxes`, `
|
|
5
|
+
**Logic is premade, UI never is.** In a React app, every surface here has a headless hook in `@/commerce/storefront` — `useProductList`, `useProduct` (+ `variantAxes`, `useAddToCart`, `useProductSpecs`, `useUpsell`), `useCart`/`useCartLine`/`useCoupon`/`useCartUI` (+ `CartUIProvider`), `useCheckout`/`usePlaceOrder` (+ `useAddressForm` and the pickers), `useOrderReturn`, `useProductReviews`, `useStorefrontSeo`. The hooks own the API calls and the branching below and hand you a view-model; **all markup and styling are yours** — nothing in the kit renders UI. Never hand-roll a hook's logic. Framework-free helpers (API client, variant resolution, price and totals rules, free-shipping rules) live in `src/commerce/utils/`.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- [Required behaviors](#required-behaviors--a-storefront-that-skips-these-cannot-sell) (the four hard rules) · [capabilities](#what-the-api-already-gives-you-read-before-designing-the-ui) · [conventions](#conventions) (envelope, auth, money)
|
|
10
|
-
- [`storefront-catalog`](#commercestorefront-catalog): [`get-store-info`](#get-store-info) · [`list-products`](#list-products) · [`get-product`](#get-product) · [`list-categories`](#list-categories) · [`list-ribbons`](#list-ribbons) · [`list-attributes`](#list-attributes) · [`submit-review`](#submit-review)
|
|
11
|
-
- [`storefront-cart`](#commercestorefront-cart): [priced view](#the-priced-cart-view-returned-by-every-action) · [actions](#actions) · [`cart_token`](#cart_token--the-storefronts-one-job) · [**shipping rules**](#shipping-is-not-optional--read-this-before-building-checkout) · [never advertise unconfigured offers](#never-advertise-what-the-store-isnt-configured-to-do)
|
|
12
|
-
- [`storefront-checkout`](#commercestorefront-checkout): [`place-order`](#place-order) · [`confirm-payment`](#confirm-payment) · [`cancel-order`](#cancel-order)
|
|
13
|
-
- [`payments`](#commercepayments--online-payment-for-an-order): payment links, `complete-return` for `/order-received`
|
|
14
|
-
- [`storefront-account`](#commercestorefront-account): orders, notes, downloads, addresses, my reviews
|
|
15
|
-
- [Walkthrough A — guest checkout](#walkthrough-a--guest-checkout) · [B — member](#walkthrough-b--member-experience) (non-React clients)
|
|
16
|
-
- Entities and direct CRUD → [`entities.md`](./entities.md) · admin surface → [`api-admin.md`](./api-admin.md)
|
|
7
|
+
Entities and direct CRUD → [`entities.md`](./entities.md) · admin surface → [`api-admin.md`](./api-admin.md)
|
|
17
8
|
|
|
18
9
|
## Required behaviors — a storefront that skips these cannot sell
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| # | Requirement | Enforced by |
|
|
23
|
-
|---|---|---|
|
|
24
|
-
| 1 | **Products with variants: one selector per attribute**, resolved to a `variation_id` before adding to the cart — never a flat list of combinations | `add-item` → `400 variation_required` ([product page](#get-product)) |
|
|
25
|
-
| 2 | **Present shipping options and send a choice** — drive it off the cart's `shipping_status`; a single option is auto-applied, several mean you must ask | `place-order` → `400 shipping_method_required` ([shipping](#shipping-is-not-optional--read-this-before-building-checkout)) |
|
|
26
|
-
| 3 | **Redirect to `payment.checkout_url`** when the customer pays by card | the order stays `pending` otherwise |
|
|
27
|
-
| 4 | **Implement the return page** (`/order-received`, or your own route set in Settings → General → *Payment return path*) and call `commerce/payments` `complete-return` there | without it a paid order is never confirmed, and a wrong path is a 404 |
|
|
28
|
-
| 5 | **Only advertise offers the store is configured for** (free-shipping thresholds come from a location's shipping rates) | nothing — this one is on you |
|
|
11
|
+
Five hard rules — the first four API-enforced, the fifth on you:
|
|
29
12
|
|
|
30
|
-
|
|
13
|
+
1. **Variants: one selector per attribute**, resolved to a `variation_id` before `add-item` (`400 variation_required`, [product page](#get-product)) — never a flat list of combinations.
|
|
14
|
+
2. **Present shipping options and send a choice**, driven by the cart's `shipping_status` (`place-order` → `400 shipping_method_required`, [shipping](#shipping-is-not-optional--read-this-before-building-checkout)).
|
|
15
|
+
3. **Redirect to `payment.checkout_url`** for card payments — the order stays `pending` otherwise.
|
|
16
|
+
4. **Implement the return page** (`/order-received`, or the route set in Settings → General → *Payment return path*) and call `commerce/payments` `complete-return` there — otherwise a paid order is never confirmed, and a wrong path is a 404.
|
|
17
|
+
5. **Only advertise offers the store is configured for** (free-shipping thresholds come from a location's rates) — nothing enforces this one.
|
|
31
18
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
**Build to this surface, not to the basics** — everything below already exists server-side, so leaving it out ships a storefront weaker than the store behind it.
|
|
35
|
-
|
|
36
|
-
| Capability | Already supported | Where |
|
|
37
|
-
|---|---|---|
|
|
38
|
-
| **Product discovery** | full-text `search`; filters: category (incl. descendants), ribbon, attribute+term, price range, `featured`, `on_sale`, `in_stock_only`; sort by name / price / **newest** (default) / **popularity** / **rating**; paging | [`list-products`](#list-products) |
|
|
39
|
-
| **Rich product pages** | galleries, per-variant price/stock/image, categories, ribbons, **upsells**, **cross-sells** | [`get-product`](#get-product) |
|
|
40
|
-
| **Taxonomy navigation** | category tree, **ribbons with counts**, attributes + terms for filter UIs | [`list-categories`](#list-categories) · [`list-ribbons`](#list-ribbons) · [`list-attributes`](#list-attributes) |
|
|
41
|
-
| **Customer reviews** | paginated per product with **average rating + count**, `verified` flag, **submission by anyone with an email** (no login), "my reviews" | [`submit-review`](#submit-review) |
|
|
42
|
-
| **Cart** | guest carts via `cart_token`, add/update/remove, quantity merging, `sold_individually` caps, live re-pricing, stock revalidation | [`storefront-cart`](#commercestorefront-cart) |
|
|
43
|
-
| **Coupons** | apply/remove by code, fully validated server-side (eligibility, limits, per-user usage). Codes are admin-only data, so a store with coupons needs a **code field** in the buy path or they can never be redeemed | [`apply-coupon`](#actions) |
|
|
44
|
-
| **Shipping** | address → matched location → **selectable rates with live costs**, free-over thresholds | [`set-shipping-address`](#actions) |
|
|
45
|
-
| **Tax** | inclusive/exclusive pricing, per-location tax groups, itemized display — resolved server-side | [`get-store-info`](#get-store-info) |
|
|
46
|
-
| **Stock states** | in stock / out of stock / **on backorder**, low-stock signalling, out-of-stock hiding | [`list-products`](#list-products) |
|
|
47
|
-
| **Checkout** | guest or authenticated, billing/shipping, per-gateway routing, **payment instructions** for manual methods, cancel, post-payment confirm | [`storefront-checkout`](#commercestorefront-checkout) |
|
|
48
|
-
| **Accounts** | order history, saved addresses, **guest order tracking by `order_key`** with customer-visible notes | [`storefront-account`](#commercestorefront-account) |
|
|
49
|
-
| **Digital products** | entitlement-checked downloads with remaining-count, expiry, signed URLs for private files | [`get-download`](#commercestorefront-account) |
|
|
50
|
-
| **Store config** | currency, units, catalog/cart price display — honour it instead of hardcoding | [`get-store-info`](#get-store-info) |
|
|
51
|
-
|
|
52
|
-
Not in the backend: the visitor UI (all of it is yours — the hooks above carry the logic), and the card-payment provider integration, which only a store that opts into cards needs — cards are off by default, the decision and its timing live in [`../install/03-data.md`](../install/03-data.md), the provider code in [`../references/online-payments.md`](../references/online-payments.md). Payment methods and the currency are admin-owned data — always render them from `get-store-info`, never a hardcoded list, and format prices with `Intl.NumberFormat(undefined, { style: "currency", currency })`.
|
|
19
|
+
Four public functions — **`storefront-catalog`**, **`storefront-cart`**, **`storefront-checkout`**, **`storefront-account`** — all invoked the same way, all returning the same envelope. Discovery filters and sorts, reviews, coupons, live shipping rates, tax, stock states, digital downloads and guest order tracking all already exist server-side — build to the sections below, not to a subset.
|
|
53
20
|
|
|
54
21
|
## Conventions
|
|
55
22
|
|
|
56
|
-
- **Invoke:** `base44.functions.invoke("commerce/<function>", { action, ...payload })
|
|
57
|
-
```js
|
|
58
|
-
const res = await base44.functions.invoke("commerce/storefront-catalog", { action: "get-store-info" });
|
|
59
|
-
const info = res.data.data; // envelope → payload
|
|
60
|
-
```
|
|
23
|
+
- **Invoke:** `base44.functions.invoke("commerce/<function>", { action, ...payload })`; the `{ success, data }` / `{ success, error, code }` envelope and `res.data.data` unwrapping are covered in SKILL.md.
|
|
61
24
|
- **Auth modes:** most actions are anonymous. Some require an authenticated Base44 session (marked **auth**). Two bearer credentials (HTTPS only): `order_key` for guest order access, `cart_token` for the cart — whose lifecycle rules are [in the cart section](#cart_token--the-storefronts-one-job).
|
|
62
25
|
- **Identity is never a payload field.** Anything attributed to a person — a review's author, a saved profile — comes from the session. An email in the body does not make you that customer, and no request can assert that an order was paid. A feature built on client-supplied identity will be ignored.
|
|
63
|
-
- **Money:** numbers, 2-dp. **Dates:** ISO strings.
|
|
26
|
+
- **Money:** numbers, 2-dp — format with `Intl.NumberFormat(undefined, { style: "currency", currency })` using the `get-store-info` currency, never a hardcoded symbol. **Dates:** ISO strings.
|
|
64
27
|
- **Errors:** every failure has a stable `code` (listed per action) plus a human `error` message.
|
|
65
28
|
|
|
66
29
|
---
|
|
@@ -131,15 +94,7 @@ await cat({ category_id, sort: "popularity", per_page: 4 }); // top in catego
|
|
|
131
94
|
```
|
|
132
95
|
**Errors:** `404 not_found` (missing / not published / hidden).
|
|
133
96
|
|
|
134
|
-
> **`variations[]` is not a list of choices to show
|
|
135
|
-
> ```js
|
|
136
|
-
> import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
|
|
137
|
-
> const view = resolveSelection(product, variations, selection); // selection from defaultSelection(), updated via selectOption()
|
|
138
|
-
> // view.axes → one control per axis view.display → image/price/sku/stock of the resolved variation
|
|
139
|
-
> // view.availability[axis.key][option] → "available" | "out_of_stock" | "unavailable" (disable, don't hide)
|
|
140
|
-
> // view.purchasable + view.addToCart → { product_id, variation_id } for add-item
|
|
141
|
-
> ```
|
|
142
|
-
> Field availability per view, ribbons, from-price rules and the variant deep-dive (availability states, incomplete-selection pricing, URL round-trip): [`../references/catalog-rendering.md`](../references/catalog-rendering.md).
|
|
97
|
+
> **`variations[]` is not a list of choices to show** — variant rule 1 applies, and variant prices come from `variations[]`, never `product.price` (a rolled-up from-price). React: `useProduct` + `useAddToCart`. The non-React resolver sample (`resolveSelection` from `@/commerce/utils`) and the variant deep-dive: [`../references/catalog-rendering.md`](../references/catalog-rendering.md).
|
|
143
98
|
|
|
144
99
|
### `list-categories`
|
|
145
100
|
No payload. Returns a nested tree: `{ "categories": [ { ...category, "children": [...] } ] }` sorted by `menu_order` then name.
|
|
@@ -157,7 +112,7 @@ This is the **only** way a storefront can enumerate ribbons (the entity is admin
|
|
|
157
112
|
No payload. Returns `{ "attributes": [ { ...attribute, "terms": [ ...values ] } ] }` — each attribute (`id, name, code, order`) with its values (`id, attribute_id, name, order, count`), both sorted by `order`; for filter UIs. Filter with `list-products` `attribute_id` (id or attribute **name**) + `attribute_term` (the value name); `code` is the stable key for a URL.
|
|
158
113
|
|
|
159
114
|
### `submit-review`
|
|
160
|
-
**Payload:** `{ product_id, reviewer?, review, rating }
|
|
115
|
+
**Payload:** `{ product_id, email?, reviewer?, review, rating? }` — `email` is required for guests (`400 email_required`); a signed-in caller's session email always wins.
|
|
161
116
|
|
|
162
117
|
> A React storefront gets the whole reviews surface — this call, the paginated list from `get-product`, refresh, and the store's moderation policy — from **`useProductReviews`**; don't hand-roll it. Read on for the raw contract.
|
|
163
118
|
|
|
@@ -239,15 +194,9 @@ rather than assuming:
|
|
|
239
194
|
| `missing_address` | options undeterminable until an address is set (several locations, none matchable yet) | collect the address, call `set-shipping-address`; cost is not calculated yet |
|
|
240
195
|
| `none_available` | nothing ships to this address | say so and block checkout (`set-shipping-address` already failed `shipping_not_available`) |
|
|
241
196
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
- **A single available method is auto-selected.** One option is not a choice, and requiring a round trip for it is how a storefront ends up never sending a method at all.
|
|
245
|
-
- **A single location answers before the address.** A store with exactly **one** location gets its rates (and, via auto-select, a cost) with no address set — the options are the same everywhere. With several, the cart reports `missing_address` until `set-shipping-address` resolves one.
|
|
246
|
-
- **An unsupported address fails on entry**, with `400 shipping_not_available` — surface it on the address form instead of letting the customer reach payment.
|
|
247
|
-
- **A stale choice is dropped.** If the address or cart changes so the chosen method is no longer offered, it is cleared (and re-auto-selected when one remains) rather than silently pricing zero shipping.
|
|
248
|
-
- **Costs change with the cart** — free-shipping thresholds and coupon-gated methods appear and disappear, so **call `set-shipping-address` as soon as an address exists and re-read the cart after every mutation**; never carry an options list you fetched earlier.
|
|
197
|
+
Rates re-resolve on every cart change (a stale choice is dropped, re-auto-selected when one remains) — call `set-shipping-address` as soon as an address exists and re-read the cart after every mutation; never cache an options list. A store with exactly one location answers before any address is set.
|
|
249
198
|
|
|
250
|
-
|
|
199
|
+
`place-order` errors: none chosen of several → `400 shipping_method_required`; `chosen_shipping_method` no longer offered → `400 invalid_shipping_method` (never silently swapped); nothing offered → `400 no_shipping_available`; an unsupported address already failed `set-shipping-address` with `400 shipping_not_available`. Each error body carries `available_shipping_methods`, no order is created, and a created order's `shipping_total` matches what was shown.
|
|
251
200
|
|
|
252
201
|
**In React this is `useCheckout`**: address state debounces into `set-shipping-address`, `ShippingMethodPicker` renders the options, and `canPlaceOrder` blocks with a named blocker until a method resolves — the whole table above is the hook's behavior, not code you write. Location/zone/tax configuration: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
|
|
253
202
|
|
|
@@ -255,19 +204,7 @@ What `place-order` does with all this: a single offered method is applied for yo
|
|
|
255
204
|
|
|
256
205
|
Copy like *"Free shipping on orders over €150"* is a **claim about store configuration**, and checkout honours only what is configured. Don't write such a line unless a matching rule exists — configure it (with the operator's agreement) or leave the copy out. The same goes for a discount claim (it needs a real `commerce.Coupon`, and only a code the operator gave you) and for delivery-time or returns promises the template cannot enforce.
|
|
257
206
|
|
|
258
|
-
**Locations are admin-only data
|
|
259
|
-
|
|
260
|
-
1. **The cart** — the live answer for a real address: after `set-shipping-address`, `available_shipping_methods` reflects every rule checkout will honour, free shipping included. Prefer this.
|
|
261
|
-
2. **A projection you add**, for a threshold banner *before* an address exists: widen `get-store-info` (or add an action) to return the free-shipping rules, then normalize them:
|
|
262
|
-
|
|
263
|
-
```js
|
|
264
|
-
import { freeShippingRules, freeShippingThreshold, freeShippingProgress } from "@/commerce/utils";
|
|
265
|
-
// locations come from your own storefront action — NOT a client entity read
|
|
266
|
-
const threshold = freeShippingThreshold(freeShippingRules(locations)); // null → no rule: no banner
|
|
267
|
-
const progress = freeShippingProgress(threshold, cart.totals.subtotal - cart.totals.discount_total);
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
A free-shipping rule is a rate that is free (`cost: 0`) or carries a `free_over` threshold. Thresholds are **per location** — a rule in one location must not become a site-wide banner.
|
|
207
|
+
**Locations are admin-only data**, so a visitor cannot list them. Two honest sources: the cart's `available_shipping_methods` after `set-shipping-address` (prefer this — it reflects every rule checkout will honour), or a server projection you add (widen `get-store-info` or add an action — never a client entity read) normalized with `freeShippingRules` / `freeShippingThreshold` / `freeShippingProgress` from `@/commerce/utils` (a `null` threshold means no rule: no banner). Thresholds are **per location** — a rule in one location must not become a site-wide banner.
|
|
271
208
|
|
|
272
209
|
---
|
|
273
210
|
|
|
@@ -352,68 +289,38 @@ Two access modes: **auth** (Base44 session) or **`order_key` bearer** (guest tra
|
|
|
352
289
|
|
|
353
290
|
---
|
|
354
291
|
|
|
355
|
-
## Walkthrough
|
|
292
|
+
## Walkthrough — guest checkout
|
|
356
293
|
|
|
357
|
-
Raw-call
|
|
294
|
+
Raw-call spine for a non-React client (a React app gets this from `useCart` + `useCheckout`).
|
|
358
295
|
|
|
359
296
|
```js
|
|
360
297
|
const inv = (fn, payload) => base44.functions.invoke(fn, payload).then(r => r.data.data);
|
|
361
298
|
|
|
362
|
-
// 1. Browse, then 2. open a cart with one item
|
|
363
299
|
const { products } = await inv("commerce/storefront-catalog", { action: "list-products", per_page: 12 });
|
|
364
|
-
let cart = await inv("commerce/storefront-cart", { action: "create",
|
|
365
|
-
|
|
366
|
-
const token = cart.cart_token; // re-read it from every response
|
|
300
|
+
let cart = await inv("commerce/storefront-cart", { action: "create", items: [{ product_id: products[0].id, quantity: 1 }] });
|
|
301
|
+
const token = cart.cart_token; // re-read it from every response
|
|
367
302
|
|
|
368
|
-
// 3. (variants) fetch the options, then add the chosen variation
|
|
369
303
|
const detail = await inv("commerce/storefront-catalog", { action: "get-product", id: products[0].id });
|
|
370
|
-
if ((detail.product.attributes ?? []).length)
|
|
304
|
+
if ((detail.product.attributes ?? []).length) // variants: add the resolved variation
|
|
371
305
|
cart = await inv("commerce/storefront-cart", { action: "add-item",
|
|
372
306
|
cart_token: token, product_id: detail.product.id, variation_id: detail.variations[0].id });
|
|
373
|
-
}
|
|
374
307
|
|
|
375
|
-
// 4. Coupon (optional)
|
|
376
308
|
cart = await inv("commerce/storefront-cart", { action: "apply-coupon", cart_token: token, code: "welcome10" });
|
|
377
309
|
|
|
378
|
-
// 5. Shipping: address as soon as it's known, then a method if asked for one
|
|
379
310
|
cart = await inv("commerce/storefront-cart", { action: "set-shipping-address",
|
|
380
311
|
cart_token: token, address: { country: "US", state: "CA", postcode: "90210", city: "Los Angeles" } });
|
|
381
312
|
if (cart.shipping_status === "choice_required") {
|
|
382
313
|
const picked = await askCustomer(cart.available_shipping_methods); // your UI
|
|
383
|
-
cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method",
|
|
384
|
-
cart_token: token, method_id: picked.id });
|
|
314
|
+
cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method", cart_token: token, method_id: picked.id });
|
|
385
315
|
}
|
|
386
316
|
|
|
387
|
-
|
|
388
|
-
const order = await inv("commerce/storefront-checkout", { action: "place-order",
|
|
317
|
+
const order = await inv("commerce/storefront-checkout", { action: "place-order", // offline → on-hold
|
|
389
318
|
cart_token: token, payment_method: "offline",
|
|
390
319
|
billing: { first_name: "Ada", last_name: "Lovelace", address_1: "1 St",
|
|
391
320
|
city: "Los Angeles", state: "CA", postcode: "90210", country: "US", email: "ada@example.com" } });
|
|
392
321
|
|
|
393
|
-
|
|
394
|
-
const tracked = await inv("commerce/storefront-account", { action: "get-order",
|
|
322
|
+
const tracked = await inv("commerce/storefront-account", { action: "get-order", // guest tracking
|
|
395
323
|
order_id: order.order_id, order_key: order.order_key });
|
|
396
324
|
```
|
|
397
325
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
Same, for an authenticated caller (the SDK sends the session automatically).
|
|
401
|
-
|
|
402
|
-
```js
|
|
403
|
-
// Carts made while logged out merge into this one automatically on `get`/`create`.
|
|
404
|
-
let cart = await inv("commerce/storefront-cart", { action: "create", items: [{ product_id, quantity: 2 }] });
|
|
405
|
-
|
|
406
|
-
// Saved addresses speed up checkout
|
|
407
|
-
await inv("commerce/storefront-account", { action: "update-my-addresses",
|
|
408
|
-
billing: { first_name: "Ada", last_name: "Lovelace", address_1: "1 St", city: "LA",
|
|
409
|
-
state: "CA", postcode: "90210", country: "US", email: "ada@example.com" } });
|
|
410
|
-
|
|
411
|
-
const order = await inv("commerce/storefront-checkout", { action: "place-order",
|
|
412
|
-
cart_token: cart.cart_token, payment_method: "offline", billing: { /* ... */ } });
|
|
413
|
-
// offline → on-hold; show order.payment_instructions.account_details
|
|
414
|
-
|
|
415
|
-
// Order history, downloads, reviews — all auth, no order_key needed
|
|
416
|
-
const { orders } = await inv("commerce/storefront-account", { action: "my-orders", per_page: 10 });
|
|
417
|
-
const { downloads } = await inv("commerce/storefront-account", { action: "my-downloads" });
|
|
418
|
-
const file = await inv("commerce/storefront-account", { action: "get-download", permission_id: downloads[0]?.permission_id });
|
|
419
|
-
```
|
|
326
|
+
**Authenticated caller:** same sequence — the SDK sends the session automatically, carts made while logged out merge in on `create`/`get`, and `update-my-addresses` lets checkout prefill saved addresses. The account actions (`my-orders`, `my-downloads`, `my-reviews`, `get-download`) then need no `order_key`.
|