@base44/app-plugin-commerce 0.1.5 → 0.1.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.
Files changed (95) hide show
  1. package/README.md +11 -11
  2. package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
  3. package/base44/entities/commerce.Cart.jsonc +1 -1
  4. package/base44/entities/commerce.Coupon.jsonc +5 -0
  5. package/base44/entities/commerce.Order.jsonc +6 -7
  6. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  7. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  8. package/base44/entities/commerce.Product.jsonc +6 -16
  9. package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
  10. package/base44/entities/commerce.ProductVariation.jsonc +1 -9
  11. package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
  12. package/base44/entities/commerce.Webhook.jsonc +1 -1
  13. package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
  14. package/base44/functions/commerce/admin-products/entry.ts +11 -17
  15. package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
  16. package/base44/functions/commerce/admin-reports/entry.ts +3 -3
  17. package/base44/functions/commerce/admin-tools/entry.ts +9 -36
  18. package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
  19. package/base44/functions/commerce/payments/entry.ts +46 -42
  20. package/base44/functions/commerce/seed-store/defaults.ts +35 -42
  21. package/base44/functions/commerce/seed-store/entry.ts +84 -31
  22. package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
  23. package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
  24. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
  25. package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
  26. package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
  27. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
  28. package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
  29. package/base44/shared/commerce/card-payment.ts +80 -0
  30. package/base44/shared/commerce/coupons.ts +16 -10
  31. package/base44/shared/commerce/emails.ts +30 -18
  32. package/base44/shared/commerce/money.ts +11 -24
  33. package/base44/shared/commerce/payments.ts +55 -286
  34. package/base44/shared/commerce/scan.ts +1 -1
  35. package/base44/shared/commerce/sequence.ts +1 -1
  36. package/base44/shared/commerce/settings.ts +4 -9
  37. package/base44/shared/commerce/shipping.ts +65 -133
  38. package/base44/shared/commerce/tax.ts +48 -96
  39. package/base44/shared/commerce/totals.ts +77 -91
  40. package/package.json +1 -1
  41. package/scripts/install.js +28 -7
  42. package/skills/commerce/SKILL.md +14 -14
  43. package/skills/commerce/docs/api-admin.md +23 -26
  44. package/skills/commerce/docs/api-storefront.md +67 -61
  45. package/skills/commerce/installation-guidelines.md +8 -8
  46. package/skills/commerce/post-installation.md +76 -41
  47. package/skills/commerce/references/admin-product-form.md +15 -12
  48. package/skills/commerce/references/emails.md +2 -2
  49. package/skills/commerce/references/guest-access-security.md +2 -2
  50. package/skills/commerce/references/online-payments.md +24 -191
  51. package/skills/commerce/references/product-render.md +18 -18
  52. package/skills/commerce/references/reviews.md +14 -8
  53. package/skills/commerce/references/storefront-product-page.md +1 -1
  54. package/src/commerce/admin/README.md +4 -5
  55. package/src/commerce/admin/bot/Markdown.jsx +1 -1
  56. package/src/commerce/admin/hooks/useMoney.js +13 -22
  57. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  58. package/src/commerce/admin/lib/constants.js +2 -29
  59. package/src/commerce/admin/lib/order-utils.js +1 -1
  60. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
  61. package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
  62. package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
  63. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
  64. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
  65. package/src/commerce/admin/pages/products/Categories.jsx +147 -177
  66. package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
  67. package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
  68. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
  69. package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
  70. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
  71. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
  72. package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
  73. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
  74. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
  75. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
  76. package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
  77. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
  78. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
  79. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
  80. package/src/commerce/admin/routes.jsx +6 -12
  81. package/src/commerce/utils/index.js +2 -2
  82. package/src/commerce/utils/shipping-promos.js +45 -49
  83. package/src/commerce/utils/variants.js +1 -1
  84. package/base44/entities/commerce.ShippingClass.jsonc +0 -30
  85. package/base44/entities/commerce.ShippingZone.jsonc +0 -41
  86. package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
  87. package/base44/entities/commerce.TaxClass.jsonc +0 -23
  88. package/base44/entities/commerce.TaxRate.jsonc +0 -68
  89. package/base44/shared/commerce/stripe.ts +0 -463
  90. package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
  91. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
  92. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
  93. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
  94. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
  95. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
package/README.md CHANGED
@@ -2,25 +2,25 @@
2
2
 
3
3
  A **Commerce backend + admin UI** for [Base44](https://base44.com) apps, delivered as a copyable file set. Drop `base44/` and `src/commerce/` into an existing Base44 app to add a full store: catalog, orders, coupons, customers, reviews, tax, shipping, webhooks, reports and transactional emails — plus a public storefront API for building your own shopfront.
4
4
 
5
- It provides a full-featured **commerce data model and behavior** (variant-driven products, order lifecycle, coupon rules, tax priority/compound math, shipping zones) using Base44-idiomatic primitives (entity JSON schemas, Deno functions, the Base44 SDK).
5
+ It provides a full-featured **commerce data model and behavior** (variant-driven products, order lifecycle, coupon rules, per-location shipping and tax) using Base44-idiomatic primitives (entity JSON schemas, Deno functions, the Base44 SDK).
6
6
 
7
7
  ## What's included
8
8
 
9
- - **24 entities** — Products (a product sells variants when it carries attributes; no type field), variations, categories, tags, attributes + values, reviews, orders (embedded line/shipping/tax/fee/coupon lines), order notes, refunds, coupons, customers, tax classes/rates, shipping zones/methods, payment gateways, store settings, webhooks + deliveries, carts, download permissions, email log.
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
- - **Online card payments, implemented** — hosted payment page, payment links for unpaid orders, two idempotent confirmation paths (customer return + signed webhook) and refunds through the provider. Wired to **Stripe** out of the box behind a provider-neutral payment utility, so the store takes cards as soon as the connector is connected — no charge flow to write — and moving to another provider means implementing one adapter. See [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md).
12
- - **Shared commerce engine** (`base44/shared/commerce/`) — totals, tax, shipping, coupons, stock, order lifecycle, webhook dispatch (HMAC-signed), emails, payments utility + Stripe adapter, plus static country/currency/continent data.
11
+ - **Online card payments, order side premade** — checkout routing, payment links for unpaid orders, two idempotent confirmation paths (customer return + webhook) and refund records are all built. Wiring a provider (Stripe, PayPal, a local PSP…) means implementing **exactly two files** — `base44/shared/commerce/card-payment.ts` and the payment webhook — and nothing else. Every other payment option is manual (on-hold + instructions) and needs no code; the admin can add more in Settings → Payments. See [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md).
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 functions for the shopfront you build: `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
15
  - **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.
16
- - **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 so agents pick it up natively.
16
+ - **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
17
 
18
18
  ## Repo map
19
19
 
20
20
  ```
21
21
  base44-commerce-template/
22
22
  ├── base44/
23
- │ ├── entities/ 24 .jsonc entity schemas (commerce.*.jsonc)
23
+ │ ├── entities/ 20 .jsonc entity schemas (commerce.*.jsonc)
24
24
  │ ├── functions/
25
25
  │ │ └── commerce/ 16 Deno functions (entry.ts each), invoked as "commerce/<name>"
26
26
  │ ├── agents/
@@ -34,13 +34,13 @@ base44-commerce-template/
34
34
  ├── scripts/
35
35
  │ └── install.js static installer (run from <app>/examples/commerce/scripts/)
36
36
  ├── skills/
37
- │ └── commerce/ commerce skill — copied into the app's skills/ so agents
38
- │ │ know the store natively
37
+ │ └── commerce/ commerce skill — copied into the app's .agents/skills/
38
+ │ │ so agents know the store natively
39
39
  │ ├── SKILL.md the map: short overview + links to everything below
40
40
  │ ├── installation-guidelines.md installing into an app (scripted or manual)
41
41
  │ ├── post-installation.md embedding the admin pages, seeding, storefront quick start
42
42
  │ ├── references/ per-topic guides (product rendering, product page & variants,
43
- │ │ Stripe, scheduled work, emails, webhooks, media & downloads,
43
+ │ │ online payments, scheduled work, emails, webhooks, media & downloads,
44
44
  │ │ limits, security)
45
45
  │ └── docs/
46
46
  │ ├── api-admin.md admin function/entity reference
@@ -86,7 +86,7 @@ From your existing Base44 app:
86
86
  <Route path="/store-admin/*" element={<AdminApp />} />
87
87
  ```
88
88
  6. **Grant yourself the `admin` role** (Base44 dashboard → users, or `users.inviteUser(email, "admin")`). The admin UI refuses non-admins.
89
- 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, gateways, tax classes and a fallback shipping zone, plus the catalog: pass `products` (whole products with attributes — variants, categories 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.
89
+ 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, gateways 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.
90
90
 
91
91
  ## Quick start (Base44 MCP / hosted apps)
92
92
 
@@ -99,7 +99,7 @@ If you build on Base44's hosted platform, use the Base44 agent/MCP to write the
99
99
  ## What's NOT included
100
100
 
101
101
  - **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 **helper logic**: [`src/commerce/utils/`](./src/commerce/utils/) — framework-free variant-selection functions (map a Size/Color selection to a `ProductVariation` and back, per-option availability, variant price ranges) — 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.
102
- - **No payment credentials** — the card integration itself *is* included (see above), but a store can only charge once someone connects a payment provider's connector for the app. Until then the card option stays hidden from customers and the offline gateway (bank transfer, cash on delivery) carries checkout.
102
+ - **No payment provider** — the order side of card payments is premade (see above), but charging a card needs a provider: implement the two payment files to wire one. Until then the card option answers "card payments are not available yet" at checkout (or switch it off in Settings → Payments), and the manual gateways (bank transfer, cash on delivery, anything the admin adds) carry checkout.
103
103
  - **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).
104
104
 
105
105
  ## Next steps
@@ -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, with_sample_data?, products?, coupons?, tax_rates?} with no action key — store_name is REQUIRED on a first seed, and products can bootstrap a whole catalog in one call, variants included; see 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, tax rates, shipping zones, 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 → Tax](admin:settings/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 and price formatting, payment return path | Settings → General | admin:settings/general |\n| Catalog defaults, review settings | Settings → Products | admin:settings/products |\n| Stock thresholds, hold minutes, inventory recipient | Settings → Inventory | admin:settings/inventory |\n| Tax classes, tax rates, price display | Settings → Tax | admin:settings/tax |\n| Shipping zones, methods, shipping classes | Settings → Shipping | admin:settings/shipping |\n| Enabling a gateway, offline bank details | Settings → Payments | admin:settings/payments |\n| Store name (subjects + sender), admin notification recipients, per-email overrides | Settings → Emails | admin:settings/emails |\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 in the store's tax settings. Open [Settings → Tax](admin:settings/tax) to 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, tags, 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 or reorder categories by hand. Tags and attributes have no screen of their own: attributes are edited in the Attributes section of a product's Price & Inventory tab, tags in the Tags 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 .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).",
14
14
  "tool_configs": [
15
15
  {
16
16
  "function_name": "commerce/admin-products",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  {
60
60
  "function_name": "commerce/storefront-catalog",
61
- "description": "Read-only public catalog browsing — use get-product BEFORE putting a product that carries attributes on an order, to list its variations and ask the operator which one. Actions: get-store-info, list-products {q?, category_id?, tag_id?, attribute_id?, attribute_term?, featured?, on_sale?, in_stock_only?, min_price?, max_price?, sort? (default \"-created_date\"), page?, per_page?}, get-product {id | slug} → {product, variations, ...}, list-categories, list-tags, list-attributes. There is no list-reviews — a product's reviews come back inside get-product, and moderation is commerce/admin-reviews."
61
+ "description": "Read-only public catalog browsing — use get-product BEFORE putting a product that carries attributes on an order, to list its variations and ask the operator which one. Actions: get-store-info, list-products {q?, category_id?, ribbon_id?, attribute_id?, attribute_term?, featured?, on_sale?, in_stock_only?, min_price?, max_price?, sort? (default \"-created_date\"), page?, per_page?}, get-product {id | slug} → {product, variations, ...}, list-categories, list-ribbons, list-attributes. There is no list-reviews — a product's reviews come back inside get-product, and moderation is commerce/admin-reviews."
62
62
  }
63
63
  ]
64
64
  }
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "chosen_shipping_method": {
52
52
  "type": "string",
53
- "description": "ShippingZoneMethod id"
53
+ "description": "The chosen shipping rate id (ShippingTaxLocation.shipping_rates[].id)"
54
54
  },
55
55
  "status": {
56
56
  "type": "string",
@@ -8,6 +8,11 @@
8
8
  "minLength": 1,
9
9
  "description": "Coupon code, stored lowercase, unique (enforced in commerce/admin-coupons)"
10
10
  },
11
+ "enabled": {
12
+ "type": "boolean",
13
+ "default": true,
14
+ "description": "A disabled coupon cannot be applied and stops validating on carts that hold it"
15
+ },
11
16
  "discount_type": {
12
17
  "type": "string",
13
18
  "enum": ["percent", "fixed_cart", "fixed_product"],
@@ -117,7 +117,7 @@
117
117
  "sku": { "type": "string" },
118
118
  "quantity": { "type": "integer" },
119
119
  "price": { "type": "number", "description": "Per-unit price after discounts, ex tax" },
120
- "tax_class": { "type": "string" },
120
+ "tax_group": { "type": "string" },
121
121
  "subtotal": { "type": "number", "description": "Line total before discounts, ex tax" },
122
122
  "subtotal_tax": { "type": "number" },
123
123
  "total": { "type": "number", "description": "Line total after discounts, ex tax" },
@@ -127,7 +127,7 @@
127
127
  "items": {
128
128
  "type": "object",
129
129
  "properties": {
130
- "rate_id": { "type": "string" },
130
+ "label": { "type": "string" },
131
131
  "total": { "type": "number" },
132
132
  "subtotal": { "type": "number" }
133
133
  }
@@ -164,8 +164,8 @@
164
164
  "type": "object",
165
165
  "properties": {
166
166
  "line_id": { "type": "string" },
167
- "method_id": { "type": "string", "description": "flat_rate|free_shipping|local_pickup" },
168
- "instance_id": { "type": "string", "description": "ShippingZoneMethod id" },
167
+ "method_id": { "type": "string", "description": "Always \"rate\" — kept for shape stability" },
168
+ "instance_id": { "type": "string", "description": "The chosen shipping rate id (ShippingTaxLocation.shipping_rates[].id)" },
169
169
  "method_title": { "type": "string" },
170
170
  "total": { "type": "number" },
171
171
  "total_tax": { "type": "number" },
@@ -174,7 +174,7 @@
174
174
  "items": {
175
175
  "type": "object",
176
176
  "properties": {
177
- "rate_id": { "type": "string" },
177
+ "label": { "type": "string" },
178
178
  "total": { "type": "number" }
179
179
  }
180
180
  }
@@ -192,7 +192,6 @@
192
192
  "rate_code": { "type": "string" },
193
193
  "label": { "type": "string" },
194
194
  "rate_percent": { "type": "number" },
195
- "compound": { "type": "boolean" },
196
195
  "tax_total": { "type": "number" },
197
196
  "shipping_tax_total": { "type": "number" }
198
197
  }
@@ -206,7 +205,7 @@
206
205
  "properties": {
207
206
  "line_id": { "type": "string" },
208
207
  "name": { "type": "string" },
209
- "tax_class": { "type": "string" },
208
+ "tax_group": { "type": "string" },
210
209
  "tax_status": { "type": "string", "enum": ["taxable", "none"] },
211
210
  "total": { "type": "number" },
212
211
  "total_tax": { "type": "number" }
@@ -21,7 +21,7 @@
21
21
  "refunded_payment": {
22
22
  "type": "boolean",
23
23
  "default": false,
24
- "description": "True when a real gateway refund was made (Stripe wiring placeholder — see skills/commerce/references/online-payments.md)"
24
+ "description": "True when a real gateway refund was made (Stripe wiring placeholder — see .agents/skills/commerce/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}]}; stripe: {connector: \"stripe\"} — see skills/commerce/references/online-payments.md"
38
+ "description": "Per-gateway settings. offline: {account_details: [{account_name, account_number, bank_name, sort_code, iban, bic}]}; stripe: {connector: \"stripe\"} — see .agents/skills/commerce/references/online-payments.md"
39
39
  }
40
40
  },
41
41
  "required": ["slug"],
@@ -23,12 +23,6 @@
23
23
  "default": false,
24
24
  "description": "Featured product flag"
25
25
  },
26
- "catalog_visibility": {
27
- "type": "string",
28
- "enum": ["visible", "catalog", "search", "hidden"],
29
- "default": "visible",
30
- "description": "Where the product is visible in the catalog"
31
- },
32
26
  "description": {
33
27
  "type": "string",
34
28
  "format": "richtext",
@@ -104,14 +98,14 @@
104
98
  },
105
99
  "tax_status": {
106
100
  "type": "string",
107
- "enum": ["taxable", "shipping", "none"],
101
+ "enum": ["taxable", "none"],
108
102
  "default": "taxable",
109
103
  "description": "Tax status"
110
104
  },
111
- "tax_class": {
105
+ "tax_group": {
112
106
  "type": "string",
113
- "default": "standard",
114
- "description": "TaxClass slug"
107
+ "default": "Products",
108
+ "description": "Tax group name from the matched Shipping & Tax Location; 'Products' is the default group"
115
109
  },
116
110
  "manage_stock": {
117
111
  "type": "boolean",
@@ -156,10 +150,6 @@
156
150
  "height": { "type": "number" }
157
151
  }
158
152
  },
159
- "shipping_class_id": {
160
- "type": "string",
161
- "description": "ShippingClass id"
162
- },
163
153
  "average_rating": {
164
154
  "type": "number",
165
155
  "default": 0,
@@ -185,10 +175,10 @@
185
175
  "items": { "type": "string" },
186
176
  "description": "ProductCategory ids"
187
177
  },
188
- "tag_ids": {
178
+ "ribbon_ids": {
189
179
  "type": "array",
190
180
  "items": { "type": "string" },
191
- "description": "ProductTag ids"
181
+ "description": "ProductRibbon ids"
192
182
  },
193
183
  "images": {
194
184
  "type": "array",
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "commerce.ProductTag",
2
+ "name": "commerce.ProductRibbon",
3
3
  "type": "object",
4
- "description": "Product tag. Created and picked from the product form; storefronts filter by id (list-products tag_id).",
4
+ "description": "Product ribbon — a label like 'Best Seller' shown on product cards. Created and picked from the product form; storefronts filter by id (list-products ribbon_id).",
5
5
  "properties": {
6
6
  "name": {
7
7
  "type": "string",
@@ -85,14 +85,10 @@
85
85
  },
86
86
  "tax_status": {
87
87
  "type": "string",
88
- "enum": ["taxable", "shipping", "none", "parent"],
88
+ "enum": ["taxable", "none", "parent"],
89
89
  "default": "parent",
90
90
  "description": "Tax status; parent = inherit from parent product"
91
91
  },
92
- "tax_class": {
93
- "type": "string",
94
- "description": "TaxClass slug; empty = inherit from parent"
95
- },
96
92
  "manage_stock": {
97
93
  "type": "string",
98
94
  "enum": ["yes", "no", "parent"],
@@ -126,10 +122,6 @@
126
122
  "height": { "type": "number" }
127
123
  }
128
124
  },
129
- "shipping_class_id": {
130
- "type": "string",
131
- "description": "ShippingClass id; empty = inherit from parent"
132
- },
133
125
  "image": {
134
126
  "type": "object",
135
127
  "description": "Variation image",
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "commerce.ShippingTaxLocation",
3
+ "type": "object",
4
+ "description": "A sales location: the regions it covers plus everything charged there — shipping rates, tax groups (default group 'Products'; products pick a group by name via tax_group) and an optional shipping tax (% or fixed). Matched by 'order' ascending; a location with no regions is the 'Rest of the world' fallback. Replaces the former zone/method/class model.",
5
+ "properties": {
6
+ "name": {
7
+ "type": "string",
8
+ "minLength": 1
9
+ },
10
+ "order": {
11
+ "type": "integer",
12
+ "default": 0,
13
+ "description": "Match priority, ascending"
14
+ },
15
+ "regions": {
16
+ "type": "array",
17
+ "description": "Regions this location covers; empty = matches everything (fallback location)",
18
+ "items": {
19
+ "type": "object",
20
+ "properties": {
21
+ "type": {
22
+ "type": "string",
23
+ "enum": ["country", "state", "continent"],
24
+ "description": "Region type"
25
+ },
26
+ "code": {
27
+ "type": "string",
28
+ "description": "Country code (US), state code (US:CA), or continent code (EU)"
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "shipping_rates": {
34
+ "type": "array",
35
+ "description": "Shipping options offered at this location; each is a checkout choice",
36
+ "items": {
37
+ "type": "object",
38
+ "properties": {
39
+ "id": { "type": "string", "description": "Stable id — carts/orders reference the chosen rate by it" },
40
+ "name": { "type": "string", "description": "Customer-facing label, e.g. Standard delivery" },
41
+ "cost": { "type": "number", "default": 0 },
42
+ "free_over": {
43
+ "type": ["number", "null"],
44
+ "description": "Items subtotal (after discounts) at which this rate becomes free; null = never"
45
+ }
46
+ }
47
+ }
48
+ },
49
+ "tax_groups": {
50
+ "type": "array",
51
+ "description": "Named tax groups; 'Products' is the default group every product uses unless its tax_group says otherwise. Group tax = sum of its rates.",
52
+ "items": {
53
+ "type": "object",
54
+ "properties": {
55
+ "name": { "type": "string", "description": "Group name, e.g. Products" },
56
+ "rates": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "properties": {
61
+ "name": { "type": "string", "description": "Rate label shown at checkout, e.g. VAT" },
62
+ "rate": { "type": "number", "description": "Percentage, e.g. 18 for 18%" }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ },
69
+ "shipping_tax": {
70
+ "type": ["object", "null"],
71
+ "description": "Tax applied to the shipping cost at this location; null = shipping is not taxed",
72
+ "properties": {
73
+ "type": { "type": "string", "enum": ["percent", "fixed"], "description": "percent of the shipping cost, or a fixed amount" },
74
+ "value": { "type": "number" }
75
+ }
76
+ }
77
+ },
78
+ "required": ["name"],
79
+ "rls": {
80
+ "read": { "user_condition": { "role": "admin" } },
81
+ "create": { "user_condition": { "role": "admin" } },
82
+ "update": { "user_condition": { "role": "admin" } },
83
+ "delete": { "user_condition": { "role": "admin" } }
84
+ }
85
+ }
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "secret": {
36
36
  "type": "string",
37
- "description": "HMAC-SHA256 signing secret (see skills/commerce/references/webhooks.md on storage tradeoffs)"
37
+ "description": "HMAC-SHA256 signing secret (see .agents/skills/commerce/references/webhooks.md on storage tradeoffs)"
38
38
  },
39
39
  "api_version": {
40
40
  "type": "string",
@@ -12,20 +12,16 @@ import { uuid } from "../../../shared/commerce/sequence.ts";
12
12
 
13
13
  export interface PricingContext {
14
14
  settings: Record<string, any>;
15
- taxRates: any[];
16
- zones: any[];
17
- zoneMethods: any[];
15
+ locations: any[];
18
16
  }
19
17
 
20
18
  /** Everything calculateTotals needs from the catalog, in one fetch. */
21
19
  export async function loadPricingContext(sr: any): Promise<PricingContext> {
22
- const [settings, taxRates, zones, zoneMethods] = await Promise.all([
20
+ const [settings, locations] = await Promise.all([
23
21
  getSettings(sr),
24
- scanAll(sr.entities["commerce.TaxRate"], null, "menu_order"),
25
- scanAll(sr.entities["commerce.ShippingZone"], null, "order"),
26
- scanAll(sr.entities["commerce.ShippingZoneMethod"], null, "order"),
22
+ scanAll(sr.entities["commerce.ShippingTaxLocation"], null, "order"),
27
23
  ]);
28
- return { settings, taxRates, zones, zoneMethods };
24
+ return { settings, locations };
29
25
  }
30
26
 
31
27
  export interface ItemSpec {
@@ -112,7 +108,7 @@ export interface RepriceOpts {
112
108
  /** Coupon codes; defaults to the order's current coupon_lines codes. */
113
109
  couponCodes?: string[];
114
110
  /** Fees; defaults to the order's current fee_lines. */
115
- fees?: Array<{ name: string; amount: number; tax_class?: string; tax_status?: string }>;
111
+ fees?: Array<{ name: string; amount: number; tax_group?: string; tax_status?: string }>;
116
112
  billing?: any;
117
113
  shipping?: any;
118
114
  chosenShippingMethodId?: string;
@@ -158,7 +154,7 @@ export async function repriceOrder(sr: any, order: any, opts: RepriceOpts = {}):
158
154
  const fees = opts.fees ?? (order.fee_lines ?? []).map((f: any) => ({
159
155
  name: f.name,
160
156
  amount: f.total,
161
- tax_class: f.tax_class,
157
+ tax_group: f.tax_group,
162
158
  tax_status: f.tax_status,
163
159
  }));
164
160
 
@@ -175,9 +171,7 @@ export async function repriceOrder(sr: any, order: any, opts: RepriceOpts = {}):
175
171
  shipping_address: opts.shipping ?? order.shipping ?? order.billing,
176
172
  chosenShippingMethodId: chosen,
177
173
  settings: ctx.settings,
178
- taxRates: ctx.taxRates,
179
- zones: ctx.zones,
180
- zoneMethods: ctx.zoneMethods,
174
+ locations: ctx.locations,
181
175
  });
182
176
 
183
177
  if (manualMode) {
@@ -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 skills/commerce/docs/api-admin.md.
7
+ * Body: { action, ...payload } — see .agents/skills/commerce/docs/api-admin.md.
8
8
  */
9
9
  import { createClientFromRequest } from "npm:@base44/sdk";
10
10
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
@@ -97,11 +97,7 @@ async function save(sr: any, payload: any): Promise<any> {
97
97
  }
98
98
 
99
99
  await adjustTermCounts(sr, "commerce.ProductCategory", prev?.category_ids ?? [], saved.category_ids ?? []);
100
- await adjustTermCounts(sr, "commerce.ProductTag", prev?.tag_ids ?? [], saved.tag_ids ?? []);
101
- if ((prev?.shipping_class_id ?? "") !== (saved.shipping_class_id ?? "")) {
102
- if (prev?.shipping_class_id) await bumpCount(sr, "commerce.ShippingClass", prev.shipping_class_id, -1);
103
- if (saved.shipping_class_id) await bumpCount(sr, "commerce.ShippingClass", saved.shipping_class_id, +1);
104
- }
100
+ await adjustTermCounts(sr, "commerce.ProductRibbon", prev?.ribbon_ids ?? [], saved.ribbon_ids ?? []);
105
101
 
106
102
  // variations diff — only when the caller sends a variations array
107
103
  let savedVariations: any[] | undefined;
@@ -153,8 +149,7 @@ async function remove(sr: any, id: string): Promise<any> {
153
149
  for (const v of variations) await sr.entities["commerce.ProductVariation"].delete(v.id);
154
150
 
155
151
  await adjustTermCounts(sr, "commerce.ProductCategory", product.category_ids ?? [], []);
156
- await adjustTermCounts(sr, "commerce.ProductTag", product.tag_ids ?? [], []);
157
- if (product.shipping_class_id) await bumpCount(sr, "commerce.ShippingClass", product.shipping_class_id, -1);
152
+ await adjustTermCounts(sr, "commerce.ProductRibbon", product.ribbon_ids ?? [], []);
158
153
 
159
154
  await sr.entities["commerce.Product"].delete(id);
160
155
  await dispatch(sr, "product.deleted", product);
@@ -212,8 +207,7 @@ async function duplicate(sr: any, id: string): Promise<any> {
212
207
 
213
208
  // duplicated product participates in taxonomy counts too
214
209
  await adjustTermCounts(sr, "commerce.ProductCategory", [], created.category_ids ?? []);
215
- await adjustTermCounts(sr, "commerce.ProductTag", [], created.tag_ids ?? []);
216
- if (created.shipping_class_id) await bumpCount(sr, "commerce.ShippingClass", created.shipping_class_id, +1);
210
+ await adjustTermCounts(sr, "commerce.ProductRibbon", [], created.ribbon_ids ?? []);
217
211
 
218
212
  await dispatch(sr, "product.created", created);
219
213
  return { product: created, variations_copied: copied };
@@ -295,7 +289,7 @@ async function search(sr: any, payload: any): Promise<any> {
295
289
 
296
290
  const TAXONOMIES: Record<string, string> = {
297
291
  category: "commerce.ProductCategory",
298
- tag: "commerce.ProductTag",
292
+ ribbon: "commerce.ProductRibbon",
299
293
  attribute: "commerce.ProductAttribute",
300
294
  "attribute-term": "commerce.ProductAttributeTerm",
301
295
  };
@@ -309,7 +303,7 @@ function taxonomyEntity(taxonomy: string): string {
309
303
  }
310
304
 
311
305
  /**
312
- * Upsert a category, tag, attribute or attribute value. Exists so an agent (or
306
+ * Upsert a category, ribbon, attribute or attribute value. Exists so an agent (or
313
307
  * any API caller) can create these — referencing one from a product is useless
314
308
  * while the record itself can only be made in the admin UI. An attribute plus
315
309
  * its values is what a product's `attributes[].options` draws on.
@@ -341,8 +335,8 @@ async function saveTerm(sr: any, payload: any): Promise<any> {
341
335
  if (term.image !== undefined) fields.image = term.image;
342
336
  if (term.menu_order !== undefined) fields.menu_order = Number(term.menu_order) || 0;
343
337
  } else if (!term.id) {
344
- // Tags are created by typing a name on the product form, so "create" has to
345
- // mean get-or-create: a second Best Seller would split the tag in two.
338
+ // Ribbons are created by typing a name on the product form, so "create" has
339
+ // to mean get-or-create: a second Best Seller would split the ribbon in two.
346
340
  const existing = (await scanAll(sr.entities[entity], null, "name"))
347
341
  .find((t: any) => String(t.name ?? "").toLowerCase() === name.toLowerCase());
348
342
  if (existing) return existing;
@@ -396,8 +390,8 @@ async function renameAttributeValue(sr: any, attributeId: string, from: string,
396
390
  }
397
391
 
398
392
  /**
399
- * Delete a term. For a category or tag, products keep the id in
400
- * category_ids/tag_ids — same as the admin UI, and the storefront skips ids that
393
+ * Delete a term. For a category or ribbon, products keep the id in
394
+ * category_ids/ribbon_ids — same as the admin UI, and the storefront skips ids that
401
395
  * no longer resolve; detach: true strips it from every product instead.
402
396
  * Deleting an attribute always takes its terms with it: a term outliving its
403
397
  * attribute is unreachable.
@@ -426,7 +420,7 @@ async function deleteTerm(sr: any, payload: any): Promise<any> {
426
420
  }
427
421
  }
428
422
  } else if (entity !== "commerce.ProductAttributeTerm" && payload.detach) {
429
- const field = entity === "commerce.ProductCategory" ? "category_ids" : "tag_ids";
423
+ const field = entity === "commerce.ProductCategory" ? "category_ids" : "ribbon_ids";
430
424
  for (const p of await scanAll(sr.entities["commerce.Product"], null, "-created_date")) {
431
425
  if ((p[field] ?? []).includes(id)) {
432
426
  await sr.entities["commerce.Product"].update(p.id, {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { createClientFromRequest } from "npm:@base44/sdk";
8
8
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
9
- import { refundOnlinePayment } from "../../../shared/commerce/payments.ts";
9
+ import { refundCardOrder } from "../../../shared/commerce/payments.ts";
10
10
  import { round2 } from "../../../shared/commerce/money.ts";
11
11
  import { restockLine } from "../../../shared/commerce/stock.ts";
12
12
  import { transitionOrder } from "../../../shared/commerce/orders.ts";
@@ -57,20 +57,21 @@ async function create(sr: any, payload: any, actor: string): Promise<any> {
57
57
 
58
58
  // Refund the money first: a provider refund that fails must not leave a
59
59
  // recorded refund behind, so nothing local is written until this succeeds.
60
- // Returns null when the order wasn't paid online (manual gateways settle
61
- // outside the store, so the record alone is the refund).
62
- let gatewayRefund: Awaited<ReturnType<typeof refundOnlinePayment>> = null;
60
+ // Returns null when the order wasn't paid by card (manual gateways settle
61
+ // outside the store, so the record alone is the refund). While the store's
62
+ // card-payment.ts refund stub is unimplemented, this throws 501 —
63
+ // record the refund without `refund_payment` instead.
64
+ let gatewayRefund: Awaited<ReturnType<typeof refundCardOrder>> = null;
63
65
  if (payload.refund_payment) {
64
- gatewayRefund = await refundOnlinePayment(sr, order, {
66
+ gatewayRefund = await refundCardOrder(sr, order, {
65
67
  amount,
66
68
  reason: payload.reason,
67
- idempotencyKey: `refund-${order.id}-${amount}-${alreadyRefunded}`,
68
69
  });
69
70
  if (!gatewayRefund) {
70
71
  throw new HttpError(
71
72
  400,
72
- "This order has no online payment to refund — record the refund without `refund_payment`, and return the money the way it was taken.",
73
- "no_online_payment",
73
+ "This order has no card payment to refund — record the refund without `refund_payment`, and return the money the way it was taken.",
74
+ "no_card_payment",
74
75
  { payment_method: order.payment_method ?? "" },
75
76
  );
76
77
  }
@@ -128,7 +129,7 @@ async function create(sr: any, payload: any, actor: string): Promise<any> {
128
129
  if (gatewayRefund) {
129
130
  await sr.entities["commerce.OrderNote"].create({
130
131
  order_id: order.id,
131
- note: `Refunded ${amount} through ${gatewayRefund.provider} (${gatewayRefund.id}, ${gatewayRefund.status}).`,
132
+ note: `Refunded ${amount} through the payment provider (${gatewayRefund.refund_id}).`,
132
133
  is_customer_note: false,
133
134
  added_by: actor,
134
135
  });