@base44/app-plugin-commerce 0.1.20 → 0.2.2

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/agents/commerce/StoreAdmin.jsonc +1 -1
  3. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  5. package/base44/entities/commerce.Webhook.jsonc +1 -1
  6. package/base44/functions/commerce/admin-products/entry.ts +1 -1
  7. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  8. package/base44/functions/commerce/payments/entry.ts +2 -2
  9. package/base44/functions/commerce/seed-store/defaults.ts +1 -1
  10. package/base44/functions/commerce/seed-store/entry.ts +34 -0
  11. package/base44/functions/commerce/seed-store/seed-catalog.ts +39 -5
  12. package/base44/functions/commerce/storefront-catalog/entry.ts +1 -1
  13. package/base44/functions/commerce/storefront-checkout/entry.ts +1 -1
  14. package/base44/shared/commerce/card-payment.stripe.ts +198 -0
  15. package/base44/shared/commerce/card-payment.ts +1 -1
  16. package/base44/shared/commerce/payments.ts +2 -2
  17. package/base44/shared/commerce/scan.ts +1 -1
  18. package/base44/shared/commerce/sequence.ts +2 -2
  19. package/package.json +1 -1
  20. package/scripts/install.js +24 -14
  21. package/skills/commerce/SKILL.md +117 -51
  22. package/skills/commerce/docs/api-admin.md +89 -28
  23. package/skills/commerce/docs/api-storefront.md +113 -126
  24. package/skills/commerce/docs/entities.md +137 -0
  25. package/skills/commerce/install/01-install.md +101 -0
  26. package/skills/commerce/install/02-storefront.md +444 -0
  27. package/skills/commerce/install/03-data.md +162 -0
  28. package/skills/commerce/references/admin-product-form.md +10 -0
  29. package/skills/commerce/references/catalog-rendering.md +110 -0
  30. package/skills/commerce/references/emails.md +49 -12
  31. package/skills/commerce/references/guest-access-security.md +18 -5
  32. package/skills/commerce/references/online-payments.md +49 -149
  33. package/skills/commerce/references/operations.md +52 -0
  34. package/skills/commerce/references/reviews.md +31 -16
  35. package/skills/commerce/references/shipping-and-tax.md +110 -0
  36. package/skills/commerce/references/store-admin-agent.md +21 -0
  37. package/skills/commerce/references/store-settings.md +49 -0
  38. package/src/commerce/admin/README.md +6 -3
  39. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  40. package/src/commerce/admin/pages/products/Reviews.jsx +1 -1
  41. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
  42. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +1 -1
  43. package/src/commerce/storefront/StorefrontProvider.jsx +106 -20
  44. package/src/commerce/storefront/index.js +74 -24
  45. package/src/commerce/storefront/internal/useAsyncData.js +86 -0
  46. package/src/commerce/storefront/useAddressForm.js +96 -0
  47. package/src/commerce/storefront/useCartLine.js +221 -0
  48. package/src/commerce/storefront/useCheckout.jsx +18 -6
  49. package/src/commerce/storefront/useOrderReturn.js +36 -10
  50. package/src/commerce/storefront/useProduct.js +295 -0
  51. package/src/commerce/storefront/useProductGallery.js +74 -0
  52. package/src/commerce/storefront/useProductList.js +153 -0
  53. package/src/commerce/storefront/useProductPrice.js +58 -0
  54. package/src/commerce/storefront/useProductReviews.js +242 -0
  55. package/src/commerce/storefront/useStorefrontSeo.js +204 -0
  56. package/src/commerce/storefront/useTotalsLines.js +109 -0
  57. package/src/commerce/utils/address-spec.js +89 -0
  58. package/src/commerce/utils/images.js +45 -0
  59. package/src/commerce/utils/index.js +22 -7
  60. package/src/commerce/utils/price.js +95 -0
  61. package/src/commerce/utils/shipping-promos.js +2 -2
  62. package/src/commerce/utils/specs.js +26 -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 +58 -3
  66. package/skills/commerce/installation-guidelines.md +0 -93
  67. package/skills/commerce/post-installation.md +0 -496
  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
package/README.md CHANGED
@@ -8,13 +8,13 @@ It provides a full-featured **commerce data model and behavior** (variant-driven
8
8
 
9
9
  - **20 entities** — Products (a product sells variants when it carries attributes; no type field), variations, categories, ribbons, attributes + values, reviews, orders (embedded line/shipping/tax/fee/coupon lines), order notes, refunds, coupons, customers, Shipping & Tax Locations (shipping rates + tax groups per location), payment gateways, store settings, webhooks + deliveries, carts, download permissions, email log.
10
10
  - **16 backend functions** — 9 admin (`commerce/admin-products`, `commerce/admin-orders`, `commerce/admin-refunds`, `commerce/admin-coupons`, `commerce/admin-customers`, `commerce/admin-reviews`, `commerce/admin-webhooks`, `commerce/admin-reports`, `commerce/admin-tools`), 4 storefront (`commerce/storefront-catalog`, `commerce/storefront-cart`, `commerce/storefront-checkout`, `commerce/storefront-account`), 2 payment (`commerce/payments`, `commerce/payment-webhook`), and an idempotent `commerce/seed-store` — one call seeds the business defaults **and the whole catalog** (products with attributes in, variants/categories/taxonomy created internally).
11
- - **Payments: manual methods work out of the box; online cards are opt-in** — the seed enables the manual `offline` method (bank transfer, cash on delivery, pickup: on-hold + instructions, no code) and leaves the `card` gateway **disabled**. **No payment provider ships with the template.** The order side of card payments *is* premade — checkout routing, payment links for unpaid orders, two idempotent confirmation paths (customer return + webhook) and refund records — so a store that opts in wires any provider (Stripe, PayPal, a local PSP…) by implementing **four functions in one file**, `base44/shared/commerce/card-payment.ts`, and nothing else; enable the gateway only with that done, or checkout answers `503 no_card_payment_provider`. The rule and timing: [`skills/commerce/post-installation.md`](./skills/commerce/post-installation.md) §4.1. Provider rules and a complete Stripe implementation to paste, for that one common choice: [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md). The admin can add more manual methods in Settings → Payments.
11
+ - **Payments: manual methods work out of the box; online cards are opt-in** — the seed enables the manual `offline` method (bank transfer, cash on delivery, pickup: on-hold + instructions, no code) and leaves the `card` gateway **disabled**. The order side of card payments *is* premade — checkout routing, payment links for unpaid orders, two idempotent confirmation paths (customer return + webhook) and refund records — so a store that opts in wires a provider by implementing **four functions in one file**, `base44/shared/commerce/card-payment.ts`. **For Stripe there is nothing to write**: `base44/shared/commerce/card-payment.stripe.ts` is a complete implementation used as-is — copy it over the stub and enable the gateway. Any other provider (PayPal, Adyen, a local PSP) follows the same shape. Enable the gateway only with a provider behind it, or checkout answers `503 no_card_payment_provider`. The rule and timing: [`skills/commerce/install/03-data.md`](./skills/commerce/install/03-data.md); provider mechanics: [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md). The admin can add more manual methods in Settings → Payments.
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
- - **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/return-page calls); `variants.js` maps an attribute selection (Size, Color) onto a `ProductVariation` and back, plus per-option availability and variant price ranges; `shipping-promos.js` reads the store's real free-shipping configuration so "Free shipping over €150" copy states a configured rule rather than an invented number. See [`skills/commerce/references/storefront-product-page.md`](./skills/commerce/references/storefront-product-page.md).
15
- - **Storefront React layer** (`src/commerce/storefront/`) — hooks and headless (render-prop) helpers for the parts of a shopfront whose contracts are the same in every store, with **zero visual components** (the look stays yours): `StorefrontProvider` (one client, cached store info, ONE shared cart), `useCart` (shared cart state, serialized mutations), `useCheckout` (guided checkout automatic debounced shipping/tax recalculation once the address is complete, shipping/payment choice, a `canPlaceOrder` gate with named blockers, redirect-handling `placeOrder`), `ShippingMethodPicker`/`PaymentMethodPicker` (headless), and `useOrderReturn` (the mandatory `/order-received` page in one hook). Needs React and nothing else.
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`/`useAddToCartButton`/`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
- - **Docs** — this README plus the commerce skill folder [`skills/commerce/`](./skills/commerce/), which holds [`SKILL.md`](./skills/commerce/SKILL.md) (the short map agents start from), [`installation-guidelines.md`](./skills/commerce/installation-guidelines.md), [`post-installation.md`](./skills/commerce/post-installation.md), per-topic guides in [`references/`](./skills/commerce/references/) and the API references in [`docs/`](./skills/commerce/docs/) the whole folder is installed into the app at `.agents/skills/commerce/` so agents pick it up natively.
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
 
19
19
  ## Repo map
20
20
 
@@ -31,23 +31,26 @@ base44-commerce-template/
31
31
  ├── src/
32
32
  │ └── commerce/
33
33
  │ ├── admin/ React admin UI (copy into your app's src/commerce/)
34
- │ ├── utils/ storefront helpers — API client, variant selection, shipping promos
35
- │ └── storefront/ storefront React layer — shared-cart provider, guided checkout,
36
- headless pickers, order-received hook (logic only, no visuals)
34
+ │ ├── utils/ storefront helpers — API client, variants, price/totals rules
35
+ │ └── storefront/ storefront React layer — headless hooks (cart, checkout,
36
+ catalog, reviews, SEO); no markup or CSS ships
37
37
  ├── scripts/
38
38
  │ └── install.js static installer (run from <app>/examples/commerce/scripts/)
39
39
  ├── skills/
40
40
  │ └── commerce/ commerce skill — copied into the app's .agents/skills/
41
41
  │ │ so agents know the store natively
42
- │ ├── SKILL.md the map: short overview + links to everything below
43
- │ ├── installation-guidelines.md installing into an app (scripted or manual)
44
- │ ├── post-installation.md embedding the admin pages, seeding, storefront quick start
45
- │ ├── references/ per-topic guides (product rendering, product page & variants,
46
- │ │ online payments, scheduled work, emails, webhooks, media & downloads,
47
- │ limits, security)
42
+ │ ├── SKILL.md the map: what to read, when, and what to skip
43
+ │ ├── install/ the whole install, in three staged files
44
+ ├── 01-install.md files, admin mount, role gating, the schedule
45
+ ├── 02-storefront.md storefront pages on the headless hooks
46
+ │ │ └── 03-data.md seeding, shipping zones, images, payments decision
47
+ ├── references/ opened on demand (catalog rendering, shipping & tax,
48
+ │ │ online payments, reviews, store settings, emails,
49
+ │ │ admin product form, StoreAdmin agent, security, operations)
48
50
  │ └── docs/
49
- │ ├── api-admin.md admin function/entity reference
50
- └── api-storefront.md storefront function reference (build your own shopfront)
51
+ │ ├── entities.md the data-model map (addressing, all 20 entities)
52
+ ├── api-admin.md admin function reference + the seed contract
53
+ │ └── api-storefront.md storefront function reference
51
54
  └── README.md
52
55
  ```
53
56
 
@@ -68,7 +71,7 @@ node examples/commerce/scripts/install.js
68
71
 
69
72
  From your existing Base44 app:
70
73
 
71
- 1. **Copy the files** — either copy this whole repo into your app at `examples/commerce/` and run `node examples/commerce/scripts/install.js`, or merge `entities/`, `functions/`, `shared/` into your app's `base44/` directory by hand (see [`skills/commerce/installation-guidelines.md`](./skills/commerce/installation-guidelines.md)). Confirm your `base44/config.jsonc` `entitiesDir`/`functionsDir` point at these folders.
74
+ 1. **Copy the files** — either copy this whole repo into your app at `examples/commerce/` and run `node examples/commerce/scripts/install.js`, or merge `entities/`, `functions/`, `shared/` into your app's `base44/` directory by hand (see [`skills/commerce/install/01-install.md`](./skills/commerce/install/01-install.md)). Confirm your `base44/config.jsonc` `entitiesDir`/`functionsDir` point at these folders.
72
75
  2. **Push the schema, functions and agent:**
73
76
  ```bash
74
77
  npx base44 entities push
@@ -89,7 +92,7 @@ From your existing Base44 app:
89
92
  <Route path="/store-admin/*" element={<AdminApp />} />
90
93
  ```
91
94
  6. **Grant yourself the `admin` role** (Base44 dashboard → users, or `users.inviteUser(email, "admin")`). The admin UI refuses non-admins.
92
- 7. **Seed the store.** Either open `/store-admin` and click **Initialize store defaults** on the first-run setup screen, or call `commerce/seed-store` directly — it creates the settings groups, the payment gateway rows (`offline` enabled, `card` off — enable it only with a provider wired) and — unless you pass your own `locations` — a fallback Shipping & Tax Location, plus the catalog: pass `products` (whole products with attributes — variants, categories, ribbons and taxonomy are created internally) or `with_sample_data: true` for the generic demo. Either way pass `store_name` (the app's name) — it is required on a first seed and becomes both the email subject prefix and the sender name. Once the `general` settings group exists the store counts as ready and the first-run screen stops appearing. Payload reference and a worked example: [`skills/commerce/post-installation.md`](./skills/commerce/post-installation.md) §2.
95
+ 7. **Seed the store.** Either open `/store-admin` and click **Initialize store defaults** on the first-run setup screen, or call `commerce/seed-store` directly — it creates the settings groups, the payment gateway rows (`offline` enabled, `card` off — enable it only with a provider wired) and — unless you pass your own `locations` — a fallback Shipping & Tax Location, plus the catalog: pass `products` (whole products with attributes — variants, categories, ribbons and taxonomy are created internally) or `with_sample_data: true` for the generic demo. Either way pass `store_name` (the app's name) — it is required on a first seed and becomes both the email subject prefix and the sender name. Once the `general` settings group exists the store counts as ready and the first-run screen stops appearing. Worked example: [`skills/commerce/install/03-data.md`](./skills/commerce/install/03-data.md); the full payload contract: [`skills/commerce/docs/api-admin.md`](./skills/commerce/docs/api-admin.md). Shipping zones are part of the same call — `locations` takes `continents: ["EU"]` and `rest_of_world: true`, so "€20 in Europe, €100 worldwide" is six lines.
93
96
 
94
97
  ## Quick start (Base44 MCP / hosted apps)
95
98
 
@@ -97,17 +100,17 @@ If you build on Base44's hosted platform, use the Base44 agent/MCP to write the
97
100
 
98
101
  1. Copy this whole repo into the target app at `examples/commerce/` (e.g. download + extract a tarball with `run_command`), then run `node examples/commerce/scripts/install.js` via `run_command` — or use `write_file` to copy every file under `base44/` and `src/commerce/` individually (use `list_directory`/`read_file` to adapt to the app's actual layout — e.g. the `@/api/base44Client` path and your router file).
99
102
  2. Wait for the app to build (`get_app_status`), then confirm entities exist (`list_entity_schemas`).
100
- 3. Grant your user the `admin` role, then seed the store's data — one `commerce/seed-store` call takes the whole catalog via `products`, or `with_sample_data: true` for the demo catalog; leave both out for defaults only, or skip the call for the admin's first-run **Initialize store defaults** screen ([`skills/commerce/post-installation.md`](./skills/commerce/post-installation.md) §3).
103
+ 3. Grant your user the `admin` role, then seed the store's data — one `commerce/seed-store` call takes the whole catalog via `products`, or `with_sample_data: true` for the demo catalog; leave both out for defaults only, or skip the call for the admin's first-run **Initialize store defaults** screen ([`skills/commerce/install/03-data.md`](./skills/commerce/install/03-data.md)).
101
104
 
102
105
  ## What's NOT included
103
106
 
104
- - **No visitor/storefront UI.** The storefront **API** is complete (`commerce/storefront-*` functions); building the shopfront is up to you see [`skills/commerce/docs/api-storefront.md`](./skills/commerce/docs/api-storefront.md). What *does* ship for the storefront is **logic, not looks**: [`src/commerce/utils/`](./src/commerce/utils/) (framework-free API client + variant-selection functions map a Size/Color selection to a `ProductVariation` and back, per-option availability, variant price ranges) and [`src/commerce/storefront/`](./src/commerce/storefront/) (React hooks + headless pickers for the shared cart, the guided checkout with automatic shipping/tax recalculation, and the `/order-received` page no visual components), plus [`skills/commerce/references/product-render.md`](./skills/commerce/references/product-render.md) (what to render in a grid vs. a product page, and which fields each call returns) and [`skills/commerce/references/storefront-product-page.md`](./skills/commerce/references/storefront-product-page.md), the variant rules that go with the helpers.
105
- - **No payment provider — and cards are off by default.** The order side of card payments is premade (see above), but charging a card needs a provider, so `commerce/seed-store` enables the manual **`offline`** method (bank transfer, cash on delivery, pickup — no code, no credentials) and leaves the **`card`** gateway **switched off**. **Enable cards only if a provider is wired, or is about to be** — implement the four functions in `base44/shared/commerce/card-payment.ts` (rules + a Stripe implementation to paste: `skills/commerce/references/online-payments.md`) and enable the gateway via the seed's `payment_methods: ["offline", "card"]`; enabled with nothing behind it, checkout answers `503 no_card_payment_provider`. The rule and why it belongs at the end of a build rather than its start: `skills/commerce/post-installation.md` §4.1.
106
- - **No scheduled workflows shipped.** Base44 *does* have a scheduler, but this template ships no workflow files — time-based jobs (stock-hold release, cart expiry, webhook-log pruning) run **opportunistically** where possible, and for the rest you (or the Base44 agent) create scheduled workflows that call `commerce/admin-tools`/`commerce/admin-orders` actions — see *Scheduled work* in [`skills/commerce/SKILL.md`](./skills/commerce/SKILL.md).
107
+ - **No storefront *design*.** The parts of a shopfront that carry a brand — the home page, the collection grid, the product card, the product page's layout, the theme ship as nothing at all, on purpose: that is the work a build should spend its effort on. Everything under those surfaces does ship: the storefront API, the hooks, and default markup for the commodity UI (checkout, cart, totals, coupon field, reviews, order-received, and the product page's internals), all restylable and replaceable — see [`skills/commerce/install/02-storefront.md`](./skills/commerce/install/02-storefront.md) for how the two tiers fit together, [`skills/commerce/references/catalog-rendering.md`](./skills/commerce/references/catalog-rendering.md) for what each catalog call returns, and [`skills/commerce/docs/api-storefront.md`](./skills/commerce/docs/api-storefront.md) for the raw API.
108
+ - **No payment provider — and cards are off by default.** The order side of card payments is premade (see above), but charging a card needs a provider, so `commerce/seed-store` enables the manual **`offline`** method (bank transfer, cash on delivery, pickup — no code, no credentials) and leaves the **`card`** gateway **switched off**. **Enable cards only if a provider is wired, or is about to be** — implement the four functions in `base44/shared/commerce/card-payment.ts`, or for Stripe copy the shipped `card-payment.stripe.ts` over it and use it as-is (`skills/commerce/references/online-payments.md`), then enable the gateway via the seed's `payment_methods: ["offline", "card"]`; enabled with nothing behind it, checkout answers `503 no_card_payment_provider`. The rule and why it belongs at the end of a build rather than its start: `skills/commerce/install/03-data.md`.
109
+ - **No scheduled workflows shipped.** Base44 *does* have a scheduler, but this template ships no workflow files — time-based jobs (stock-hold release, cart expiry, webhook-log pruning) run **opportunistically** where possible, and for the rest you (or the Base44 agent) create scheduled workflows that call `commerce/admin-tools`/`commerce/admin-orders` actions — see [`skills/commerce/references/operations.md`](./skills/commerce/references/operations.md).
107
110
 
108
111
  ## Next steps
109
112
 
110
- - **Install into your app:** [`skills/commerce/installation-guidelines.md`](./skills/commerce/installation-guidelines.md)
113
+ - **Install into your app:** [`skills/commerce/install/01-install.md`](./skills/commerce/install/01-install.md)
111
114
  - **Operate & extend:** the commerce skill — [`skills/commerce/SKILL.md`](./skills/commerce/SKILL.md)
112
115
  - **Build a storefront:** [`skills/commerce/docs/api-storefront.md`](./skills/commerce/docs/api-storefront.md)
113
116
  - **Admin automation / alternative admin:** [`skills/commerce/docs/api-admin.md`](./skills/commerce/docs/api-admin.md)
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "StoreAdmin",
12
12
  "description": "Store administration copilot for the commerce template: manage products, orders, refunds, coupons, customers, reviews, reports and maintenance.",
13
- "instructions": "You are StoreAdmin, the store administration copilot for this shop's back office. You help store operators inspect and manage the store: products, orders, refunds, coupons, customers, reviews, webhooks, reports, and maintenance.\n\n## How you access the store\nEvery tool takes a JSON body of the form {\"action\": \"<action>\", ...payload} (exception: commerce/seed-store takes {store_name, currency?, weight_unit?, dimension_unit?, with_sample_data?, products?, coupons?, locations?} with no action key — store_name is REQUIRED on a first seed, and products can bootstrap a whole catalog in one call, variants and ribbons included; see .agents/skills/commerce/docs/api-admin.md) and responds {success, data} or {success:false, error, code}. Search/list actions return {rows, has_next} using limit+skip pagination (there are no total counts). Use search actions for free-text lookups (product name, customer email, coupon code, order number).\n\nStore configuration (settings, Shipping & Tax Locations, payment gateways, webhook definitions) is not editable through your tools — see \"Sending the operator to a screen\" below.\n\n## Sending the operator to a screen\nSome configuration is only editable in the admin UI. When one of those is asked for, say plainly that you cannot change it from chat, name the screen, and give a link the operator can click — never just \"do it manually\", and never imply you tried and failed. Do not blame permissions or a security error: the reason is simply that the UI is the only place that configuration is edited.\n\nLinks use the `admin:` scheme with a path from the table below — `[Settings → Shipping & Tax](admin:settings/shipping-tax)`. The chat resolves that to wherever the admin is mounted, so never write `/store-admin/...` yourself.\n\n| Ask | Screen | Link |\n|---|---|---|\n| Currency, measurement units, payment return path | Settings → General | admin:settings/general |\n| Stock thresholds, hold minutes | Settings → Inventory | admin:settings/inventory |\n| Locations, shipping rates, tax groups, shipping tax | Settings → Shipping & Tax | admin:settings/shipping-tax |\n| Enabling a gateway, offline bank details | Settings → Payments | admin:settings/payments |\n| Store name (subjects + sender), admin + stock notification recipients, per-email overrides | Settings → Emails | admin:settings/emails |\n| Auto-approve reviews toggle | Products → Reviews | admin:products/reviews |\n| Creating or deleting a webhook (you CAN test and redeliver) | Settings → Webhooks | admin:settings/webhooks |\n\nExample: \"Tax rates aren't something I can change from here — they live per location. Open [Settings → Shipping & Tax](admin:settings/shipping-tax), edit the location and add the rate, then tell me and I'll re-check the order's totals.\"\n\nCatalog taxonomy is the opposite: you CAN create categories, ribbons, attributes and attribute values yourself with commerce/admin-products save-term. Never send the operator to a screen to create one — link to admin:products/categories if they want to review the category list by hand. Ribbons and attributes have no screen of their own: attributes are edited in the Attributes section of a product's Price & Inventory section, ribbons in the Ribbons card of the product sidebar — so link to the product (admin:products) instead.\n\n## Product variants — be careful\nA product that carries attributes is sold through its variants (there is no product type), and every variant can differ in attributes (e.g. size/color), SKU, price and stock.\n- NEVER pick a variation automatically. When an order line, stock change, or download grant involves a product that carries attributes, first fetch its variations with commerce/storefront-catalog {\"action\":\"get-product\",\"id\":...} (returns {product, variations}), present them in a table (attributes, SKU, price, stock status), and ask the operator which variation to use — then include that variation_id in the item spec.\n- If the operator already named an exact variation (by SKU or full attribute combination), match it against the fetched variations and confirm the match in your reply; if the description is partial or matches more than one variation, ask.\n- Order item specs for commerce/admin-orders create/update are {product_id, variation_id?, quantity, price_override?} — variation_id is REQUIRED for a product with attributes.\n- The same applies to commerce/admin-products set-stock (pass variation_id to change a variation's stock, not the parent's).\n\n## Behavior\n- Be concise and operational. Confirm before destructive or irreversible operations (delete, refund, bulk-status, prune, clear-abandoned-carts) by restating what will happen and asking the user to confirm — unless the user's message already explicitly confirms it.\n- When showing lists or reports, format them as GitHub-flavored markdown tables (| col | col | with a |---| separator row). Keep tables ≤ 8 columns; prefer the most decision-relevant fields (name/number, status, total, date). Format money with the store currency.\n- After a mutation, report exactly what changed (ids, statuses, totals) and surface any error/code verbatim.\n- If a request is ambiguous (which order? which product?), search first and present the candidates in a table, then ask.\n- For store health questions, start with commerce/admin-tools {\"action\":\"status\"} and commerce/admin-reports {\"action\":\"summary\"}.\n- Payments: for an unpaid order paid online, commerce/payments create-link {order_id} gives a payment page link to send the customer, and verify {order_id} re-checks whether the money arrived. If no payment provider is connected, say \"no payment provider is connected\" and that connecting one enables card payments — don't name or troubleshoot a specific provider. Never invent a payment link or claim an order is paid without verifying.\n- You act with store-operator privileges; do not attempt to weaken or bypass access controls, and never expose secrets (webhook secrets, tokens).",
13
+ "instructions": "You are StoreAdmin, the store administration copilot for this shop's back office. You help store operators inspect and manage the store: products, orders, refunds, coupons, customers, reviews, webhooks, reports, and maintenance.\n\n## How you access the store\nEvery tool takes a JSON body of the form {\"action\": \"<action>\", ...payload} (exception: commerce/seed-store takes {store_name, currency?, weight_unit?, dimension_unit?, with_sample_data?, products?, coupons?, locations?} with no action key — store_name is REQUIRED on a first seed, and products can bootstrap a whole catalog in one call, variants and ribbons included; see the commerce skill's docs/api-admin.md) and responds {success, data} or {success:false, error, code}. Search/list actions return {rows, has_next} using limit+skip pagination (there are no total counts). Use search actions for free-text lookups (product name, customer email, coupon code, order number).\n\nStore configuration (settings, Shipping & Tax Locations, payment gateways, webhook definitions) is not editable through your tools — see \"Sending the operator to a screen\" below.\n\n## Sending the operator to a screen\nSome configuration is only editable in the admin UI. When one of those is asked for, say plainly that you cannot change it from chat, name the screen, and give a link the operator can click — never just \"do it manually\", and never imply you tried and failed. Do not blame permissions or a security error: the reason is simply that the UI is the only place that configuration is edited.\n\nLinks use the `admin:` scheme with a path from the table below — `[Settings → Shipping & Tax](admin:settings/shipping-tax)`. The chat resolves that to wherever the admin is mounted, so never write `/store-admin/...` yourself.\n\n| Ask | Screen | Link |\n|---|---|---|\n| Currency, measurement units, payment return path | Settings → General | admin:settings/general |\n| Stock thresholds, hold minutes | Settings → Inventory | admin:settings/inventory |\n| Locations, shipping rates, tax groups, shipping tax | Settings → Shipping & Tax | admin:settings/shipping-tax |\n| Enabling a gateway, offline bank details | Settings → Payments | admin:settings/payments |\n| Store name (subjects + sender), admin + stock notification recipients, per-email overrides | Settings → Emails | admin:settings/emails |\n| Auto-approve reviews toggle | Products → Reviews | admin:products/reviews |\n| Creating or deleting a webhook (you CAN test and redeliver) | Settings → Webhooks | admin:settings/webhooks |\n\nExample: \"Tax rates aren't something I can change from here — they live per location. Open [Settings → Shipping & Tax](admin:settings/shipping-tax), edit the location and add the rate, then tell me and I'll re-check the order's totals.\"\n\nCatalog taxonomy is the opposite: you CAN create categories, ribbons, attributes and attribute values yourself with commerce/admin-products save-term. Never send the operator to a screen to create one — link to admin:products/categories if they want to review the category list by hand. Ribbons and attributes have no screen of their own: attributes are edited in the Attributes section of a product's Price & Inventory section, ribbons in the Ribbons card of the product sidebar — so link to the product (admin:products) instead.\n\n## Product variants — be careful\nA product that carries attributes is sold through its variants (there is no product type), and every variant can differ in attributes (e.g. size/color), SKU, price and stock.\n- NEVER pick a variation automatically. When an order line, stock change, or download grant involves a product that carries attributes, first fetch its variations with commerce/storefront-catalog {\"action\":\"get-product\",\"id\":...} (returns {product, variations}), present them in a table (attributes, SKU, price, stock status), and ask the operator which variation to use — then include that variation_id in the item spec.\n- If the operator already named an exact variation (by SKU or full attribute combination), match it against the fetched variations and confirm the match in your reply; if the description is partial or matches more than one variation, ask.\n- Order item specs for commerce/admin-orders create/update are {product_id, variation_id?, quantity, price_override?} — variation_id is REQUIRED for a product with attributes.\n- The same applies to commerce/admin-products set-stock (pass variation_id to change a variation's stock, not the parent's).\n\n## Behavior\n- Be concise and operational. Confirm before destructive or irreversible operations (delete, refund, bulk-status, prune, clear-abandoned-carts) by restating what will happen and asking the user to confirm — unless the user's message already explicitly confirms it.\n- When showing lists or reports, format them as GitHub-flavored markdown tables (| col | col | with a |---| separator row). Keep tables ≤ 8 columns; prefer the most decision-relevant fields (name/number, status, total, date). Format money with the store currency.\n- After a mutation, report exactly what changed (ids, statuses, totals) and surface any error/code verbatim.\n- If a request is ambiguous (which order? which product?), search first and present the candidates in a table, then ask.\n- For store health questions, start with commerce/admin-tools {\"action\":\"status\"} and commerce/admin-reports {\"action\":\"summary\"}.\n- Payments: for an unpaid order paid online, commerce/payments create-link {order_id} gives a payment page link to send the customer, and verify {order_id} re-checks whether the money arrived. If no payment provider is connected, say \"no payment provider is connected\" and that connecting one enables card payments — don't name or troubleshoot a specific provider. Never invent a payment link or claim an order is paid without verifying.\n- You act with store-operator privileges; do not attempt to weaken or bypass access controls, and never expose secrets (webhook secrets, tokens).",
14
14
  "tool_configs": [
15
15
  {
16
16
  "function_name": "commerce/admin-products",
@@ -21,7 +21,7 @@
21
21
  "refunded_payment": {
22
22
  "type": "boolean",
23
23
  "default": false,
24
- "description": "True when a real refund was made at the payment provider (via shared/commerce/card-payment.ts refundCardPayment — see .agents/skills/commerce/references/online-payments.md)"
24
+ "description": "True when a real refund was made at the payment provider (via shared/commerce/card-payment.ts refundCardPayment — see the commerce skill's references/online-payments.md)"
25
25
  },
26
26
  "restock_items": {
27
27
  "type": "boolean",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "settings": {
37
37
  "type": "object",
38
- "description": "Per-gateway settings. offline: {account_details: [{account_name, account_number, bank_name, sort_code, iban, bic}]}; card: {} — provider config lives in code and backend secrets, never here; see .agents/skills/commerce/references/online-payments.md"
38
+ "description": "Per-gateway settings. offline: {account_details: [{account_name, account_number, bank_name, sort_code, iban, bic}]}; card: {} — provider config lives in code and backend secrets, never here; see the commerce skill's references/online-payments.md"
39
39
  }
40
40
  },
41
41
  "required": ["slug"],
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "secret": {
36
36
  "type": "string",
37
- "description": "HMAC-SHA256 signing secret (see .agents/skills/commerce/references/webhooks.md on storage tradeoffs)"
37
+ "description": "HMAC-SHA256 signing secret (see the commerce skill's references/operations.md on storage tradeoffs)"
38
38
  },
39
39
  "api_version": {
40
40
  "type": "string",
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Actions: save | delete | batch | duplicate | set-stock | search |
6
6
  * save-term | delete-term | list-terms
7
- * Body: { action, ...payload } — see .agents/skills/commerce/docs/api-admin.md.
7
+ * Body: { action, ...payload } — see the commerce skill's docs/api-admin.md.
8
8
  */
9
9
  import { createClientFromRequest } from "npm:@base44/sdk";
10
10
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * commerce/admin-reports — on-demand report aggregation over orders.
3
3
  * Fine to ~10k orders; larger stores should materialize stats
4
- * (see .agents/skills/commerce/references/limits-and-performance.md).
4
+ * (see the commerce skill's references/operations.md).
5
5
  *
6
6
  * Actions: summary | sales | top-sellers | stock |
7
7
  * orders-totals | products-totals | customers-totals | coupons-totals |
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * commerce/payments — online payment for an order.
3
3
  *
4
- * The provider specifics live in the two implementable files (see
5
- * `.agents/skills/commerce/references/online-payments.md`); this function is
4
+ * The provider specifics live in the two implementable files (see the commerce
5
+ * skill's references/online-payments.md); this function is
6
6
  * the premade API the storefront and the admin use.
7
7
  *
8
8
  * Actions:
@@ -42,7 +42,7 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
42
42
  values: {
43
43
  // The one review switch the server enforces; every other review policy
44
44
  // (login-gated, verified-only, required rating) belongs to the storefront
45
- // UI — see .agents/skills/commerce/references/reviews.md.
45
+ // UI — see the commerce skill's references/reviews.md.
46
46
  auto_approve_reviews: false,
47
47
  },
48
48
  },
@@ -21,6 +21,27 @@
21
21
  * payment_methods?, with_sample_data?, products?, coupons?, locations? }
22
22
  * — with_sample_data cannot be combined with the catalog keys.
23
23
  *
24
+ * `locations` are the store's Shipping & Tax Locations, and seeding them here is
25
+ * THE way to set shipping up — each is
26
+ * { name, countries?: ["IL"], continents?: ["EU"], rest_of_world?: true,
27
+ * regions?: [{ type, code }], order?, shipping_rates?, tax_groups?,
28
+ * shipping_tax? }
29
+ * with `continents` taking the 7 codes from shared/commerce/data/continents.ts
30
+ * and `rest_of_world: true` marking the catch-all (no regions — a location
31
+ * cannot be both). Locations match in `order` asc and `order` defaults to the
32
+ * array position, so the payload reads as the priority:
33
+ *
34
+ * locations: [
35
+ * { name: "Europe", continents: ["EU"], shipping_rates: [{ name: "Standard", cost: 20 }] },
36
+ * { name: "Worldwide", rest_of_world: true, shipping_rates: [{ name: "International", cost: 100 }] },
37
+ * ]
38
+ *
39
+ * A payload carrying `locations` suppresses the seeded fallback (see (3)), so
40
+ * without a catch-all among them the store ships ONLY to the listed regions —
41
+ * every other address gets `shipping_not_available` at checkout. That case comes
42
+ * back in the response's `warnings`, which is always present and empty when
43
+ * there is nothing to flag.
44
+ *
24
45
  * `payment_methods` (e.g. ["card"]) names the checkout methods the store
25
46
  * offers: listed gateways are enabled, every other gateway row is disabled —
26
47
  * the one call covers "card-only" stores with no entity editing. Explicit
@@ -218,6 +239,16 @@ Deno.serve(async (req) => {
218
239
  // location the merchant never asked for (shadowed while the caller's has a
219
240
  // lower `order`, live the moment theirs is deleted or reordered). Only
220
241
  // create the default when the payload brings no locations of its own.
242
+ //
243
+ // Which makes the missing catch-all a silent trap: locations scoped to
244
+ // regions (say the 51 European country codes) and no fallback behind them is
245
+ // a store that ships to Europe and answers shipping_not_available to
246
+ // everyone else — plausibly intended, so not an error, but never something
247
+ // to discover from a customer. Say it out loud instead.
248
+ const warnings: string[] = [];
249
+ if (catalogSpec?.locations?.length && !catalogSpec.locations.some((l: any) => !l.regions?.length)) {
250
+ warnings.push("no_catchall_location: addresses outside your locations will get shipping_not_available — add a location with rest_of_world: true if you ship worldwide");
251
+ }
221
252
  if (!catalogSpec?.locations?.length) {
222
253
  const fallback = ((await sr.entities["commerce.ShippingTaxLocation"].filter({ name: REST_OF_WORLD_LOCATION.name }, undefined, 1)) ?? [])[0];
223
254
  if (!fallback) {
@@ -250,6 +281,9 @@ Deno.serve(async (req) => {
250
281
  store_name: { value: storeNameAction === "kept_existing" ? currentName : storeName, action: storeNameAction },
251
282
  currency: currencyInfo ? { value: currencyInfo.code, action: currencyAction } : null,
252
283
  payment_methods: paymentMethods,
284
+ // Always present so a caller can read it unconditionally; empty means the
285
+ // seed has nothing to flag.
286
+ warnings,
253
287
  });
254
288
  } catch (e) {
255
289
  if (e instanceof HttpError) return fail(e.status, e.message, e.code);
@@ -18,6 +18,7 @@
18
18
  * variations?: [{ options: { Size: "S" }, regular_price?, stock_quantity?, sku?, image? }] }
19
19
  */
20
20
  import { HttpError } from "../../../shared/commerce/auth.ts";
21
+ import { CONTINENTS } from "../../../shared/commerce/data/continents.ts";
21
22
  import { getSettings } from "../../../shared/commerce/settings.ts";
22
23
  import { scanAll } from "../../../shared/commerce/scan.ts";
23
24
  import {
@@ -172,7 +173,8 @@ export function normalizeCatalogPayload(body: any): CatalogSpec | null {
172
173
  if (!Number.isFinite(Number(c.amount)) || Number(c.amount) < 0) err(`${path}.amount`, "must be a non-negative number");
173
174
  });
174
175
 
175
- const locations = rawLocations.map((l, i) => normalizeLocation(l, `locations[${i}]`, err)).filter(Boolean);
176
+ // The index is the location's default `order` — see normalizeLocation.
177
+ const locations = rawLocations.map((l, i) => normalizeLocation(l, i, `locations[${i}]`, err)).filter(Boolean);
176
178
 
177
179
  if (errors.length) throw new CatalogPayloadError(errors);
178
180
 
@@ -187,15 +189,24 @@ export function normalizeCatalogPayload(body: any): CatalogSpec | null {
187
189
  }
188
190
 
189
191
  /**
190
- * A caller location is Wix-shaped: countries (ISO codes) or explicit regions,
191
- * shipping rates, named tax groups and an optional shipping tax:
192
+ * A caller location is Wix-shaped: a scope (countries as ISO codes, whole
193
+ * continents, explicit regions, or `rest_of_world` for the catch-all), shipping
194
+ * rates, named tax groups and an optional shipping tax:
192
195
  *
193
196
  * { name: "Israel", countries: ["IL"],
194
197
  * shipping_rates: [{ name: "Standard", cost: 20, free_over: 150 }],
195
198
  * tax_groups: [{ name: "Products", rates: [{ name: "VAT", rate: 18 }] }],
196
199
  * shipping_tax: { type: "percent", value: 18 } }
200
+ *
201
+ * Continents spare the caller a 51-code country list, and `rest_of_world: true`
202
+ * names the catch-all instead of relying on empty-array magic. Locations match
203
+ * in `order` asc, and `order` defaults to the payload position, so the array
204
+ * reads as the priority — Europe first, worldwide last:
205
+ *
206
+ * [{ name: "Europe", continents: ["EU"], shipping_rates: [{ name: "Standard", cost: 20 }] },
207
+ * { name: "Worldwide", rest_of_world: true, shipping_rates: [{ name: "International", cost: 100 }] }]
197
208
  */
198
- function normalizeLocation(l: any, path: string, err: (p: string, e: string) => void): any | null {
209
+ function normalizeLocation(l: any, index: number, path: string, err: (p: string, e: string) => void): any | null {
199
210
  if (!l || typeof l !== "object" || Array.isArray(l)) {
200
211
  err(path, "must be an object");
201
212
  return null;
@@ -210,9 +221,27 @@ function normalizeLocation(l: any, path: string, err: (p: string, e: string) =>
210
221
  const code = String(c ?? "").trim().toUpperCase();
211
222
  if (code) regions.push({ type: "country", code });
212
223
  }
224
+ for (const c of l.continents ?? []) {
225
+ const code = String(c ?? "").trim().toUpperCase();
226
+ if (!code) continue;
227
+ if (!CONTINENTS.some((continent) => continent.code === code)) {
228
+ err(`${path}.continents`, `unknown continent code: ${code} — known: ${CONTINENTS.map((continent) => `${continent.code} (${continent.name})`).join(", ")}`);
229
+ continue;
230
+ }
231
+ regions.push({ type: "continent", code });
232
+ }
213
233
  for (const r of l.regions ?? []) {
214
234
  if (r?.type && r?.code) regions.push({ type: String(r.type), code: String(r.code).toUpperCase() });
215
235
  }
236
+ // The catch-all is defined by having NO regions, so a scoped "rest of the
237
+ // world" is a contradiction, not a merge: silently dropping either half would
238
+ // ship a store that either over- or under-delivers shipping.
239
+ if (l.rest_of_world) {
240
+ if (regions.length) {
241
+ err(path, "rest_of_world is the catch-all for every address no other location matches — it cannot also be scoped with countries/continents/regions");
242
+ }
243
+ regions.length = 0;
244
+ }
216
245
  const slugBase = slugify(name);
217
246
  const shippingRates = (Array.isArray(l.shipping_rates) ? l.shipping_rates : []).map((r: any, i: number) => {
218
247
  const rate = { id: r?.id || `${slugBase}-${slugify(String(r?.name || `rate-${i + 1}`))}`,
@@ -241,7 +270,12 @@ function normalizeLocation(l: any, path: string, err: (p: string, e: string) =>
241
270
  if (!Number.isFinite(value) || value < 0) err(`${path}.shipping_tax.value`, "must be a non-negative number");
242
271
  shippingTax = { type, value: Number.isFinite(value) ? value : 0 };
243
272
  }
244
- return { name, order: Number(l.order) || 0, regions, shipping_rates: shippingRates, tax_groups: taxGroups, shipping_tax: shippingTax };
273
+ // Payload position IS match priority when `order` is omitted; an explicit
274
+ // `order` always wins. Positions are 0,1,2… — far below the seeded fallback's
275
+ // 999, so "Rest of the world" stays the last location checked even when a
276
+ // caller's locations are added to a store that already has it.
277
+ const order = l.order != null && Number.isFinite(Number(l.order)) ? Number(l.order) : index;
278
+ return { name, order, regions, shipping_rates: shippingRates, tax_groups: taxGroups, shipping_tax: shippingTax };
245
279
  }
246
280
 
247
281
  function normalizeProduct(
@@ -375,7 +375,7 @@ async function getStoreInfo(sr: any): Promise<any> {
375
375
  * signed-in session supplies the email (and can't spoof another); a guest
376
376
  * passes `email` in the payload. Stricter policies (login-gated forms,
377
377
  * verified-buyers-only, required ratings) are the storefront's to enforce in
378
- * its UI — see .agents/skills/commerce/references/reviews.md. The one server switch is
378
+ * its UI — see the commerce skill's references/reviews.md. The one server switch is
379
379
  * auto-approval; everything else submits as `hold` for moderation.
380
380
  */
381
381
  async function submitReview(sr: any, p: any, user: any): Promise<any> {
@@ -220,7 +220,7 @@ async function placeOrder(sr: any, req: Request, user: any, payload: any): Promi
220
220
  // (4) customer upsert by billing email
221
221
  const notices: string[] = [];
222
222
  if (payload.create_account && !user) {
223
- notices.push("account_creation_requires_login"); // see .agents/skills/commerce/references/guest-access-security.md
223
+ notices.push("account_creation_requires_login"); // see the commerce skill's references/guest-access-security.md
224
224
  }
225
225
  const customer = await upsertCustomer(sr, billing, shippingAddress, user);
226
226
 
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Stripe — a READY-TO-USE card payment provider. Nothing to transcribe, nothing
3
+ * to fill in: this file is a complete implementation of the four functions
4
+ * `shared/commerce/card-payment.ts` declares, in the exact shape its callers
5
+ * import (`payments.ts` and the premade `commerce/payment-webhook`).
6
+ *
7
+ * WIRE IT BY COPYING IT OVER THE STUB — one call, no editing:
8
+ *
9
+ * fs.copyFileSync(
10
+ * "base44/shared/commerce/card-payment.stripe.ts",
11
+ * "base44/shared/commerce/card-payment.ts",
12
+ * );
13
+ *
14
+ * and **enable the `card` gateway** — `commerce/seed-store` with
15
+ * `payment_methods: ["offline", "card"]`. It ships disabled, so that is what
16
+ * makes card payment visible at checkout.
17
+ *
18
+ * That is the whole job. This file assumes the app is connected to Stripe and
19
+ * reads the secret key the connection publishes (`stripeSecret()` below); how
20
+ * the app gets connected is not this file's concern and nothing here needs
21
+ * filling in.
22
+ *
23
+ * NEVER patch this file (or the stub it replaces) with partial edits. A
24
+ * find_replace that leaves the originals behind gives every commerce function a
25
+ * duplicate-export bundle error ("Multiple exports with the same name …") and
26
+ * the whole backend stops deploying. The fix is always the whole-file copy
27
+ * above — which is also why this file exists.
28
+ *
29
+ * It lives **beside** the stub rather than in a subfolder precisely so its
30
+ * relative imports (`./auth.ts`) are valid both here and at the copy's
31
+ * destination — the copy can never produce a broken specifier, and no function
32
+ * imports this file while it waits, so it is never bundled.
33
+ *
34
+ * A later kit update re-copies `shared/commerce/` over the app and restores the
35
+ * stub, so re-run the copy after updating the template.
36
+ *
37
+ * A different provider (PayPal, Adyen, a local PSP) implements these same four
38
+ * functions against its own API, and ships the same way —
39
+ * `card-payment.<provider>.ts` next to the stub. `references/online-payments.md`
40
+ * has the rules; this file is the worked model.
41
+ */
42
+ import Stripe from "npm:stripe@18";
43
+ import { HttpError } from "./auth.ts";
44
+
45
+ /** A hosted payment page for one order. */
46
+ export interface CardPaymentPage {
47
+ /** Where the customer goes to pay. */
48
+ url: string;
49
+ /** Stripe's id for this payment (the Checkout Session id), stored on the
50
+ * order and handed back to checkCardPaymentPaid / refundCardPayment. */
51
+ reference: string;
52
+ }
53
+
54
+ /**
55
+ * Credentials come from backend env, never an entity and never the client. Read
56
+ * lazily (per call, not at module load) so a store that has the file but not yet
57
+ * the secret answers a clean 503 at checkout instead of failing to boot every
58
+ * commerce function that imports it.
59
+ */
60
+ /**
61
+ * Stripe's secret key, as published to the backend environment by the app's
62
+ * Stripe connection. The names below are the conventional ones; if the key
63
+ * arrives under a different name, this list is the only thing to change.
64
+ */
65
+ const STRIPE_KEY_VARS = ["STRIPE_SECRET_KEY", "STRIPE_API_KEY", "STRIPE_KEY"];
66
+
67
+ const stripeSecret = () => {
68
+ for (const name of STRIPE_KEY_VARS) {
69
+ const value = Deno.env.get(name);
70
+ if (value) return value;
71
+ }
72
+ return null;
73
+ };
74
+
75
+ const stripe = () => {
76
+ const key = stripeSecret();
77
+ if (!key) {
78
+ throw new HttpError(
79
+ 503,
80
+ `Card payments are not configured — no Stripe secret key in the backend environment (looked for ${STRIPE_KEY_VARS.join(", ")}).`,
81
+ "no_card_payment_provider",
82
+ );
83
+ }
84
+ return new Stripe(key);
85
+ };
86
+
87
+ // Stripe amounts are in minor units; these currencies have none, so ×100 would
88
+ // charge a hundred times the total.
89
+ const ZERO_DECIMAL = new Set(["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"]);
90
+ const minorUnits = (amount: number, currency: string) =>
91
+ Math.round(Number(amount) * (ZERO_DECIMAL.has(String(currency).toUpperCase()) ? 1 : 100));
92
+
93
+ /**
94
+ * Create a payment page for `order.total` in `order.currency`. The customer
95
+ * comes back to successUrl / cancelUrl (both already carry order_id/order_key
96
+ * for the return page).
97
+ */
98
+ export async function createCardPayment(
99
+ _sr: any,
100
+ order: any,
101
+ opts: { successUrl: string; cancelUrl: string; customerEmail?: string },
102
+ ): Promise<CardPaymentPage> {
103
+ // order_id + order_key in the metadata is how the premade payment-webhook
104
+ // names the order when Stripe's event arrives, and what checkCardPaymentPaid
105
+ // compares against — keep it on both objects (the session's own metadata is
106
+ // not copied to the payment intent).
107
+ const metadata = { order_id: String(order.id), order_key: String(order.order_key) };
108
+ const session = await stripe().checkout.sessions.create({
109
+ mode: "payment",
110
+ line_items: [{
111
+ quantity: 1,
112
+ price_data: {
113
+ currency: String(order.currency || "USD").toLowerCase(),
114
+ product_data: { name: `Order #${order.order_number}` },
115
+ unit_amount: minorUnits(order.total, order.currency),
116
+ },
117
+ }],
118
+ customer_email: opts.customerEmail || undefined,
119
+ metadata,
120
+ payment_intent_data: { metadata },
121
+ success_url: opts.successUrl,
122
+ cancel_url: opts.cancelUrl,
123
+ });
124
+ if (!session.url) throw new HttpError(502, "Stripe did not return a payment page URL.", "payment_session_failed");
125
+ return { url: session.url, reference: session.id };
126
+ }
127
+
128
+ /**
129
+ * Has Stripe actually received the money? Asked of Stripe's API — never of the
130
+ * caller. Runs on the customer-return page, the webhook's unverified path and
131
+ * the admin's "Check payment" button.
132
+ */
133
+ export async function checkCardPaymentPaid(_sr: any, order: any, reference: string): Promise<boolean> {
134
+ const session = await stripe().checkout.sessions.retrieve(reference);
135
+ // The payment must be for THIS order — stops a reference to some other
136
+ // (genuinely paid) session being replayed against a different order.
137
+ return session.payment_status === "paid" && session.metadata?.order_id === String(order.id);
138
+ }
139
+
140
+ /**
141
+ * Refund `amount` (display units, e.g. 12.34) at Stripe. Called BEFORE the
142
+ * local refund record is written, so throwing here writes nothing.
143
+ */
144
+ export async function refundCardPayment(_sr: any, _order: any, opts: {
145
+ reference: string;
146
+ amount: number;
147
+ currency: string;
148
+ reason?: string;
149
+ }): Promise<{ refund_id: string }> {
150
+ // The stored reference is the Checkout Session; the refundable object is the
151
+ // payment intent behind it, which only exists once the session was paid.
152
+ const session = await stripe().checkout.sessions.retrieve(opts.reference);
153
+ if (!session.payment_intent) {
154
+ throw new HttpError(409, "This payment has no charge to refund at Stripe.", "no_charge_to_refund");
155
+ }
156
+ const refund = await stripe().refunds.create({
157
+ payment_intent: String(session.payment_intent),
158
+ amount: minorUnits(opts.amount, opts.currency),
159
+ });
160
+ return { refund_id: refund.id };
161
+ }
162
+
163
+ /** What parseWebhook distills an event into — the premade webhook's contract. */
164
+ export interface CardWebhookEvent {
165
+ /** The order the event is about, read from the metadata createCardPayment
166
+ * attached. */
167
+ order_id: string;
168
+ /** Must match the order's key, or the premade webhook rejects the event — so
169
+ * an event cannot be pointed at another order. */
170
+ order_key: string;
171
+ /** True ONLY after verifying the request signature over the raw payload
172
+ * bytes; false makes the event an unverified nudge, verified against Stripe's
173
+ * API instead. */
174
+ paid: boolean;
175
+ /** Stripe's payment id, ONLY from a signature-verified event; unset means the
176
+ * premade flow uses the reference stored on the order at checkout. */
177
+ reference?: string;
178
+ }
179
+
180
+ /**
181
+ * Stripe webhook events. The body is never trusted: this only names the order
182
+ * (from the metadata createCardPayment attached) and returns `paid: false`, so
183
+ * the premade webhook verifies with Stripe through checkCardPaymentPaid. Hence
184
+ * NO signing secret to store — a forged call can at worst trigger a re-check,
185
+ * never mark an order paid. (The optional fast path that skips the API
186
+ * round-trip verifies the signature and returns `paid: true` —
187
+ * `references/online-payments.md`.)
188
+ *
189
+ * null for anything that isn't a payment event for one of this store's orders;
190
+ * the premade function answers 200 so Stripe stops retrying.
191
+ */
192
+ export async function parseWebhook(_req: Request, payload: string): Promise<CardWebhookEvent | null> {
193
+ let event: any;
194
+ try { event = JSON.parse(payload); } catch { return null; }
195
+ const metadata = event?.data?.object?.metadata;
196
+ if (!metadata?.order_id || !metadata?.order_key) return null;
197
+ return { order_id: String(metadata.order_id), order_key: String(metadata.order_key), paid: false };
198
+ }
@@ -11,7 +11,7 @@
11
11
  * NO PROVIDER SHIPS WITH THE TEMPLATE, and card payments are off by default
12
12
  * (the `card` gateway is seeded disabled) — this file stays stubs unless a
13
13
  * store deliberately opts into online cards. When one does, any provider
14
- * works: `.agents/skills/commerce/references/online-payments.md` has the
14
+ * works: the commerce skill's references/online-payments.md has the
15
15
  * rules, plus a complete Stripe implementation to paste over this file if
16
16
  * Stripe is the provider chosen. Another provider implements the same four
17
17
  * functions against its own API.