@base44/app-plugin-commerce 0.1.19 → 0.2.1

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.
Files changed (73) hide show
  1. package/README.md +25 -22
  2. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  3. package/base44/functions/commerce/seed-store/entry.ts +34 -0
  4. package/base44/functions/commerce/seed-store/seed-catalog.ts +39 -5
  5. package/base44/shared/commerce/card-payment.stripe.ts +178 -0
  6. package/base44/shared/commerce/scan.ts +1 -1
  7. package/base44/shared/commerce/sequence.ts +1 -1
  8. package/package.json +1 -1
  9. package/scripts/install.js +24 -14
  10. package/skills/commerce/SKILL.md +107 -51
  11. package/skills/commerce/docs/api-admin.md +89 -28
  12. package/skills/commerce/docs/api-storefront.md +113 -126
  13. package/skills/commerce/docs/entities.md +137 -0
  14. package/skills/commerce/install/01-install.md +101 -0
  15. package/skills/commerce/install/02-storefront.md +188 -0
  16. package/skills/commerce/install/03-data.md +162 -0
  17. package/skills/commerce/references/admin-product-form.md +10 -0
  18. package/skills/commerce/references/catalog-rendering.md +110 -0
  19. package/skills/commerce/references/emails.md +49 -12
  20. package/skills/commerce/references/guest-access-security.md +18 -5
  21. package/skills/commerce/references/online-payments.md +50 -149
  22. package/skills/commerce/references/operations.md +52 -0
  23. package/skills/commerce/references/reviews.md +31 -16
  24. package/skills/commerce/references/shipping-and-tax.md +110 -0
  25. package/skills/commerce/references/store-admin-agent.md +21 -0
  26. package/skills/commerce/references/store-settings.md +49 -0
  27. package/src/commerce/admin/README.md +2 -2
  28. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  29. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
  30. package/src/commerce/storefront/StorefrontProvider.jsx +106 -20
  31. package/src/commerce/storefront/blocks/AddToCartBlock.jsx +86 -0
  32. package/src/commerce/storefront/blocks/AddressFieldsBlock.jsx +96 -0
  33. package/src/commerce/storefront/blocks/BreadcrumbsBlock.jsx +52 -0
  34. package/src/commerce/storefront/blocks/CartLinesBlock.jsx +98 -0
  35. package/src/commerce/storefront/blocks/CheckoutBlock.jsx +247 -0
  36. package/src/commerce/storefront/blocks/CouponFieldBlock.jsx +84 -0
  37. package/src/commerce/storefront/blocks/OrderReceivedBlock.jsx +129 -0
  38. package/src/commerce/storefront/blocks/ProductGalleryBlock.jsx +66 -0
  39. package/src/commerce/storefront/blocks/ProductSpecsBlock.jsx +33 -0
  40. package/src/commerce/storefront/blocks/ProductStripBlock.jsx +55 -0
  41. package/src/commerce/storefront/blocks/QuantityStepper.jsx +62 -0
  42. package/src/commerce/storefront/blocks/ReviewsBlock.jsx +191 -0
  43. package/src/commerce/storefront/blocks/TotalsBlock.jsx +42 -0
  44. package/src/commerce/storefront/blocks/VariantSelectorBlock.jsx +81 -0
  45. package/src/commerce/storefront/blocks/index.js +44 -0
  46. package/src/commerce/storefront/index.js +59 -21
  47. package/src/commerce/storefront/internal/useAsyncData.js +86 -0
  48. package/src/commerce/storefront/pickers.jsx +20 -5
  49. package/src/commerce/storefront/useAddressForm.js +96 -0
  50. package/src/commerce/storefront/useCartLine.js +184 -0
  51. package/src/commerce/storefront/useCheckout.jsx +38 -11
  52. package/src/commerce/storefront/useProduct.js +227 -0
  53. package/src/commerce/storefront/useProductGallery.js +74 -0
  54. package/src/commerce/storefront/useProductList.js +153 -0
  55. package/src/commerce/storefront/useProductPrice.js +58 -0
  56. package/src/commerce/storefront/useProductReviews.js +242 -0
  57. package/src/commerce/storefront/useStorefrontSeo.js +204 -0
  58. package/src/commerce/storefront/useTotalsLines.js +109 -0
  59. package/src/commerce/utils/address-spec.js +89 -0
  60. package/src/commerce/utils/images.js +45 -0
  61. package/src/commerce/utils/index.js +18 -6
  62. package/src/commerce/utils/price.js +95 -0
  63. package/src/commerce/utils/storefront.js +47 -3
  64. package/src/commerce/utils/totals.js +110 -0
  65. package/src/commerce/utils/variants.js +10 -2
  66. package/skills/commerce/installation-guidelines.md +0 -93
  67. package/skills/commerce/post-installation.md +0 -495
  68. package/skills/commerce/references/limits-and-performance.md +0 -16
  69. package/skills/commerce/references/media-and-downloads.md +0 -4
  70. package/skills/commerce/references/product-render.md +0 -89
  71. package/skills/commerce/references/scheduled-work.md +0 -19
  72. package/skills/commerce/references/storefront-product-page.md +0 -83
  73. package/skills/commerce/references/webhooks.md +0 -10
@@ -1,89 +0,0 @@
1
- # Rendering products: list view and product page
2
-
3
- What to show for a product, and **which view can show it**. Both storefront surfaces render the same catalog record from a different call:
4
-
5
- | View | Call | Returns |
6
- |---|---|---|
7
- | Listing / grid / search results / ribbon & category pages | `commerce/storefront-catalog` `list-products` | `{ products: [row...], page, per_page, has_next }` |
8
- | Product page | `commerce/storefront-catalog` `get-product` | `{ product, variations, categories, ribbons, reviews, upsells, cross_sells }` |
9
-
10
- Request/response shapes: [`../docs/api-storefront.md`](../docs/api-storefront.md). Variant selection mechanics (axes, resolving a selection to a variation, unavailable combinations): [`storefront-product-page.md`](./storefront-product-page.md).
11
-
12
- You choose what belongs in each view — but you can only render what the call returns. The table below is the boundary.
13
-
14
- ---
15
-
16
- ## 1. Field availability
17
-
18
- A listing **row** is the product record itself (minus paywalled fields), plus resolved `ribbons`. `get-product` adds everything that needs a second read.
19
-
20
- | Data | `list-products` row | `get-product` | Notes |
21
- |---|---|---|---|
22
- | `id`, `name`, `slug`, `status` | ✅ | ✅ | There is no `type` field — a product sells variants when `attributes[]` is non-empty |
23
- | `price`, `regular_price`, `sale_price`, `on_sale` | ✅ | ✅ | With variants the parent `price` is a starting point, not the truth — see §3 |
24
- | `images[]`, `featured`, `short_description`, `description` | ✅ | ✅ | Cards normally use `images[0]` + `short_description`. Every entry is an **object** `{ src, name, alt, position }` — render `images[0]?.src`, never the entry itself (an object passed as `src` fails the load and your fallback shows a placeholder despite the image existing). Sub-fields aren't guaranteed on raw entries — the `normalizeImage` helper (`@/commerce/utils`) returns `{src, name, alt}` with a non-empty `src` or `null`. `images` can also be genuinely **empty** — render a placeholder, don't leave a broken `img` (on the product page `resolveSelection`'s `display.image` is `null` in that same case, already normalized) |
25
- | `sku`, `stock_status`, `stock_quantity`, `manage_stock`, `backorders` | ✅ | ✅ | |
26
- | `average_rating`, `rating_count`, `total_sales` | ✅ | ✅ | Enough for stars on a card; the reviews themselves are not in a row |
27
- | `downloadable`, `virtual`, `weight`, `dimensions`, `attributes[]`, `meta_data` | ✅ | ✅ | `attributes[]` is the variant axes; `meta_data` is the descriptive properties the admin calls **modifiers** |
28
- | **`ribbons`** (resolved `{id, name}`) | ✅ | ✅ | `get-product` returns the full ribbon records; rows carry the short form |
29
- | `ribbon_ids`, `category_ids` | ✅ | ✅ | Raw ids |
30
- | **`categories`** (resolved) | ❌ *ids only* | ✅ | See §5 to add |
31
- | **`variations[]`** (per-variant price/stock/image/attributes) | ❌ | ✅ | The reason a product with variants can't be fully priced from a row |
32
- | **`reviews`** (paged items + `average_rating`/`rating_count`) | ❌ | ✅ | Row still has the aggregate numbers |
33
- | **`upsells`, `cross_sells`** (summaries) | ❌ | ✅ | `{id, name, slug, price, on_sale, image}` |
34
- | `downloads[]`, `download_limit`, `download_expiry` | ❌ | ❌ | **Never** exposed publicly — paywalled. Gated by `commerce/storefront-account` `get-download` |
35
-
36
- ## 2. Ribbons — render them in **both** views
37
-
38
- Ribbons are flat, cross-cutting labels ("Best Seller", "New", "Gift"); categories are the hierarchical spine. Generated storefronts routinely omit ribbons entirely. Don't.
39
-
40
- - **Listing rows carry resolved `ribbons`** (`{id, name}`), so a card can render them with no extra call. One or two per card is useful (a corner label like "Best Seller"); more is noise. Hide the block when the array is empty — never a dangling "Ribbons:" label.
41
- - **On the product page**, render `product.ribbons` (the top-level `ribbons` array) as labels near the metadata, visually lighter than the category breadcrumb.
42
- - **Every ribbon can link to a filtered listing** — `list-products` with `ribbon_id` — never a dead label. A ribbon has no slug, so key the URL on its id (`/shop?ribbon=<ribbon_id>`) so the page is shareable and survives reload. Use `list-ribbons` for a name to label it with.
43
- - **Offer ribbons as a listing filter** from `list-ribbons`, which hides ribbons no published product carries and gives a `count` for labels like "Gift (12)". `ribbon_id` stacks with `category_id`, price, `on_sale`, `featured`, `in_stock_only`.
44
- - **Ribbon landing page:** `list-products` filtered by `ribbon_id`, with the ribbon's `name` as the heading.
45
- - Don't put ribbons in the breadcrumb, and don't use them as variant options — a size or colour is an `attribute`, not a ribbon.
46
- - Descriptive properties (Material, Care) are **modifiers** in `meta_data`, not ribbons and not attributes. Render them as a spec table on the product page.
47
-
48
- ## 3. Product shapes in each view
49
-
50
- There are only two shapes, and **`attributes[]` tells them apart** — there is no `type` field:
51
-
52
- | Shape | Test | Card | Product page |
53
- |---|---|---|---|
54
- | **Single item** | `attributes` is empty | price, **Add to cart** straight from the card if you want one | single **Add to cart** — `resolveSelection` returns no axes, `complete: true`, a ready `addToCart` |
55
- | **Has variants** | `attributes` is non-empty | price **range or `From €19`** (see below), never selectors — link through | one selector per attribute: [`storefront-product-page.md`](./storefront-product-page.md) |
56
-
57
- A product with attributes is **only** sellable through a variant: `add-item` without a `variation_id` is `400 variation_required`, and there is no fall-back to the parent. So one with attributes but no variations is unsellable by design, not by accident.
58
-
59
- Downloadable/virtual products are either shape with `downloadable: true` — see [`media-and-downloads.md`](./media-and-downloads.md).
60
-
61
- ### A variant parent's `price` is its *from* price
62
-
63
- `admin-products` rolls both **stock** and **price** up from the variations: a parent's `regular_price`, `price` and `on_sale` are derived from the cheapest publishable variant on every save. So the parent price is real and safe to sort and filter on — but it is the **lowest** price, not *the* price.
64
-
65
- A listing row has no `variations`, so a card still cannot compute the full range from a row alone: render `From {product.price}`, never the parent price presented as *the* price. On the product page you have `variations`, so show the real range via `priceRange` from the variant helpers.
66
-
67
- ## 4. Sensible defaults per view
68
-
69
- **Card:** image, name, price (or range), sale badge from `on_sale`, stars from `average_rating`/`rating_count`, out-of-stock state from `stock_status`, one or two ribbons. Link the whole card to the product page.
70
-
71
- **Product page:** gallery, name, price, variant selectors, stock, `short_description` then `description`, SKU, categories breadcrumb, ribbons, reviews, then upsells/cross-sells.
72
-
73
- ## 5. Adding a `get-product`-only field to the list view
74
-
75
- If a customer asks for something the row doesn't carry — resolved categories are the common one — resolve it in the listing call rather than firing `get-product` per card (that's N requests for one grid).
76
-
77
- `list-products` resolves its page slice through a helper in `base44/functions/commerce/storefront-catalog/entry.ts`:
78
-
79
- ```ts
80
- const pageItems = await withRibbons(sr, products.slice(start, start + perPage).map(publicProduct));
81
- ```
82
-
83
- `withRibbons` reads the taxonomy **once per request** and maps ids on the rows — not once per row. Extend that pattern: read the entity for the whole page, build an id→record `Map`, attach the short shape each card needs. Keep it to the fields the UI renders; a listing payload is served on every browse.
84
-
85
- What not to do:
86
-
87
- - Don't call `get-product` per card.
88
- - Don't add `variations` to every row — that is the heaviest read in the catalog and a grid does not need it. If cards must show real price ranges, add a precomputed `price_min`/`price_max` instead.
89
- - Don't relax entity RLS to read the catalog from the client: `commerce.Product` and the taxonomy entities are admin-only by design ([`guest-access-security.md`](./guest-access-security.md)). Widen the function's response instead.
@@ -1,19 +0,0 @@
1
- # Scheduled work (workflows)
2
-
3
- Base44 **has** a scheduler — the recurring maintenance below is run by **scheduled workflows**, and the Base44 agent can create them for you. **This template ships no workflow/schedule files on purpose**: don't look for them under `base44/` and don't add them here — create the workflows directly in the target app.
4
-
5
- **Instruct the agent (or set up yourself): create one scheduled workflow per row below.** Each workflow does one thing — invoke the given function action on the chosen cadence:
6
-
7
- ```js
8
- await base44.functions.invoke("commerce/admin-tools", { action: "clear-abandoned-carts", older_than_days: 2 });
9
- ```
10
-
11
- | Workflow to create | What it does | Action to invoke | Payload | Suggested cadence |
12
- |---|---|---|---|---|
13
- | Release expired stock holds | Frees stock held by unpaid orders past `hold_stock_minutes`. `commerce/storefront-checkout` `place-order` already runs this at the start of every checkout, so the schedule mainly covers quiet periods. | `commerce/admin-orders` `release-expired-holds` | — | every 5–15 min |
14
- | Abandoned cart cleanup | Marks carts past the 48 h TTL as `abandoned`. Also happens lazily on next cart access. | `commerce/admin-tools` `clear-abandoned-carts` | `{ older_than_days: 2 }` | hourly–daily |
15
- | Prune webhook delivery log | Deletes old `commerce.WebhookDelivery` rows so the log doesn't grow unbounded. **No opportunistic fallback — this one genuinely needs a schedule.** | `commerce/admin-tools` `prune-webhook-deliveries` | `{ keep_days: 30 }` | daily–weekly |
16
-
17
- **Optional — drift repair.** Denormalized counters can drift without transactions (see [`limits-and-performance.md`](./limits-and-performance.md)). If you want them self-healing, add a nightly/weekly workflow that calls the `commerce/admin-tools` recount actions: `recount-terms`, `recount-coupon-usage`, `recalculate-customer-stats-all`, `regenerate-download-permissions`.
18
-
19
- Every action above is guarded by `requireAdmin()`, so each scheduled workflow must run with **admin privileges** (an admin identity / service context), not as an anonymous caller.
@@ -1,83 +0,0 @@
1
- # Variant selection on the product page
2
-
3
- The one interaction agents reliably get wrong: turning a product with variants into controls a customer can buy from. Everything else about the page — what to show, and which fields each call returns — is [`product-render.md`](./product-render.md); request/response shapes are [`docs/api-storefront.md`](../docs/api-storefront.md).
4
-
5
- The logic ships: **`src/commerce/utils/variants.js`** (framework-free, no I/O, full JSDoc on every export — read it for signatures). Import it and spend your effort on the markup.
6
-
7
- ---
8
-
9
- ## 1. The model, and the rule
10
-
11
- `get-product` returns `{ product, variations }`. **Every** `product.attributes[]` entry is an **axis**, in `position` order; each `variations[]` record is one combination. There is no product type and no per-attribute flag — carrying attributes is what makes a product sell variants. Descriptive properties are `meta_data` (the admin calls them modifiers) — a spec table, **never** a selector.
12
-
13
- **One control per axis. Never one option per variation.** Do not flatten `variations[]` into `Red / S`, `Red / M`, `Blue / S`, … — that is `n × m` noise, it hides the product's structure, and it stops scaling immediately.
14
-
15
- ```
16
- Size ( ○ S )( ● M )( ○ L ) ← one control per attribute
17
- Color ( ● Red )( ○ Blue )
18
- → resolved variation: Red / M · $21.99 · 10 in stock
19
- ```
20
-
21
- Pick the control by option count: swatches for colours, pills for a few short options, a `Select` for many or long labels. The axis label is `attribute.name`.
22
-
23
- ## 2. One call gives you the whole view
24
-
25
- ```jsx
26
- import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
27
-
28
- const [selection, setSelection] = useState(() => defaultSelection(product, variations));
29
- const view = resolveSelection(product, variations, selection);
30
- const pick = (axisKey, option) =>
31
- setSelection((prev) => selectOption(product, variations, prev, axisKey, option));
32
- ```
33
-
34
- Bind the UI to `view` — not to `product.*` — so a selection actually changes the page:
35
-
36
- | `view` field | Binds to |
37
- |---|---|
38
- | `axes` | the controls: `[{ key, name, attribute_id, options[] }]`, dead options already dropped |
39
- | `display` | image, price, `regular_price`, `on_sale`, SKU, stock, weight/dimensions, description — variation-first with parent fallback. `display.image` is an **object** — always the normalized `{src, name, alt}` (`src` guaranteed non-empty, `alt` defaulted; stored entries are not uniform, so never read `variation.image`/`product.images` raw when this field exists), or **`null` when neither the variation nor the product has a renderable image** (the one case needing a placeholder in the UI). Render `display.image?.src` / `display.image?.alt`; **passing the object itself to an `<img src>` or Image component fails the load and shows the placeholder for every product**. For galleries, run raw entries through the exported `normalizeImage`: `product.images.map(normalizeImage).filter(Boolean)` |
40
- | `availability[axisKey][option]` | per-option state: `"available"` / `"out_of_stock"` / `"unavailable"` — §3 |
41
- | `priceRange` | `{ min, max, on_sale, count }` while the selection is incomplete — §3 |
42
- | `complete` / `missingAxes` | the button label: `Select a ${view.missingAxes[0]?.name}` |
43
- | `purchasable` | gate **Add to cart** on this (resolved *and* buyable) |
44
- | `addToCart` | `{ product_id, variation_id }` to spread into the cart call, or `null` |
45
-
46
- `view.variation` and `view.candidates` are there when you need the record itself or the variations still reachable. For a product with no attributes `resolveSelection` returns no axes, `complete: true` and a ready `addToCart` — the same code path; it branches on whether there are axes, so `variations` can be `[]`.
47
-
48
- A **product with attributes but no usable variations** — the state attaching an attribute and stopping leaves you in — is deliberately *not* given a parent fallback, because `add-item` would reject it. You get axes whose `options` arrays are empty, `purchasable: false` and `addToCart: null`. Render it as unavailable rather than painting selector groups with no buttons, and guard the "Select a …" label: with no attributes at all, `missingAxes` is empty and `view.missingAxes[0]?.name` is `undefined`.
49
-
50
- The other exports cover what the view model doesn't: `selectionForVariation` (variation → selection, to hydrate controls from a cart line or `?variation=` link), `selectionToParams`/`selectionFromParams` (URL round-trip), `priceRange`, and the individual pieces if you build your own view model. Signatures and edge cases are in the JSDoc; two behaviors are easy to miss when hand-rolling: non-publishable variations never leak an option into the UI, and an empty `option` on an axis means **"any"**.
51
-
52
- ## 3. The three UI decisions the helpers can't make for you
53
-
54
- - **Unavailable vs out of stock.** `"unavailable"` (no such combination) → **disable, don't hide**; options that vanish and reappear as the customer clicks are disorienting. `"out_of_stock"` → keep it visible and label it. `onbackorder` counts as available and purchasable — label it ("Ships in 2–3 weeks"), don't disable it.
55
- - **Incomplete selection.** Show `view.priceRange` (`$21.99 – $23.99`, or `From $21.99`) — never `$0`, and never the parent `price`, which for a product with variants is not its price ([`product-render.md` §3](./product-render.md#3-product-shapes-in-each-view)). Keep **Add to cart** disabled with a hint at what's missing.
56
- - **Initial state and linkability.** Start from `defaultSelection` (honours the merchant's `default_attributes`, preselects single-option axes). Mirror the selection into the URL with `selectionToParams` and hydrate with `selectionFromParams`, so a chosen variant is shareable and survives a refresh.
57
-
58
- Route every click through `selectOption` and dead ends take care of themselves: it keeps the just-picked option and clears only conflicting axes (Red/L → click Blue → `{ Color: Blue }`, size re-open) instead of leaving the customer on a combination that resolves to nothing.
59
-
60
- ## 4. Add to cart
61
-
62
- ```js
63
- await base44.functions.invoke("commerce/storefront-cart", {
64
- action: "add-item",
65
- cart_token,
66
- ...view.addToCart, // { product_id, variation_id } — variation_id is REQUIRED once a product has attributes
67
- quantity,
68
- });
69
- ```
70
-
71
- Handle `400 variation_required` (empty `variation_id` on a product with attributes — the page has a bug), the stock codes (`out_of_stock`, `insufficient_stock`, …), and `404 variation_not_found` (catalog changed underneath the page: refetch `get-product`). `product.sold_individually` caps the quantity control at 1.
72
-
73
- ## 5. Checklist
74
-
75
- - [ ] One control per axis in `view.axes` — no combination list anywhere in the UI.
76
- - [ ] `meta_data` modifiers render as specs, not selectors.
77
- - [ ] Image, price, SKU and availability all come from `view.display`; the image is rendered from `display.image.src` (it's an object, not a URL string), with a placeholder **only** for `display.image === null`.
78
- - [ ] Incomplete selection shows `view.priceRange` — never `$0`, never the parent `price`.
79
- - [ ] `"unavailable"` disabled, `"out_of_stock"` labelled; every click routed through `selectOption`.
80
- - [ ] Initial state from `defaultSelection`; selection mirrored into the URL.
81
- - [ ] **Add to cart** gated on `view.purchasable`, sending `view.addToCart`.
82
- - [ ] A product with empty `axes[].options` shows as unavailable — no empty selector groups, no "Select a undefined".
83
- - [ ] Ribbons, ratings, upsells and the rest per [`product-render.md`](./product-render.md).
@@ -1,10 +0,0 @@
1
- # Webhooks
2
-
3
- Managed in the admin at **Settings → Webhooks** (`settings/webhooks`; the source still lives under `pages/status/`).
4
-
5
- `webhooks.ts` `dispatch()` fires on `order.*`, `product.*`, `customer.*`, `coupon.*` (created/updated/deleted, note that `restored` is selectable on a webhook but **never emitted** — nothing dispatches it). Each active `commerce.Webhook` matching the topic receives an HTTP POST with headers `X-Commerce-Webhook-Topic/-Resource/-Event/-ID/-Delivery-ID/-Signature`. The signature is **base64 HMAC-SHA256** of the body, keyed by the webhook's `secret` (Web Crypto).
6
-
7
- - Every attempt is logged as a `commerce.WebhookDelivery` (request/response bodies truncated to 32 KB). Prune with `commerce/admin-tools` `prune-webhook-deliveries`.
8
- - `failure_count` increments on non-2xx/timeout and resets on success; a webhook auto-disables after **5** consecutive failures.
9
- - The `secret` is stored on the (admin-only-RLS) `commerce.Webhook` entity. For higher assurance, move it to Base44 secrets and read it in `dispatch()`.
10
- - Verify deliveries on the receiver by recomputing the HMAC over the raw body with your secret.