@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.
- package/README.md +11 -11
- package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
- package/base44/entities/commerce.Cart.jsonc +1 -1
- package/base44/entities/commerce.Coupon.jsonc +5 -0
- package/base44/entities/commerce.Order.jsonc +6 -7
- package/base44/entities/commerce.OrderRefund.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
- package/base44/entities/commerce.Product.jsonc +6 -16
- package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
- package/base44/entities/commerce.ProductVariation.jsonc +1 -9
- package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
- package/base44/entities/commerce.Webhook.jsonc +1 -1
- package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
- package/base44/functions/commerce/admin-products/entry.ts +11 -17
- package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
- package/base44/functions/commerce/admin-reports/entry.ts +3 -3
- package/base44/functions/commerce/admin-tools/entry.ts +9 -36
- package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
- package/base44/functions/commerce/payments/entry.ts +46 -42
- package/base44/functions/commerce/seed-store/defaults.ts +35 -42
- package/base44/functions/commerce/seed-store/entry.ts +84 -31
- package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
- package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
- package/base44/shared/commerce/card-payment.ts +80 -0
- package/base44/shared/commerce/coupons.ts +16 -10
- package/base44/shared/commerce/emails.ts +30 -18
- package/base44/shared/commerce/money.ts +11 -24
- package/base44/shared/commerce/payments.ts +55 -286
- package/base44/shared/commerce/scan.ts +1 -1
- package/base44/shared/commerce/sequence.ts +1 -1
- package/base44/shared/commerce/settings.ts +4 -9
- package/base44/shared/commerce/shipping.ts +65 -133
- package/base44/shared/commerce/tax.ts +48 -96
- package/base44/shared/commerce/totals.ts +77 -91
- package/package.json +1 -1
- package/scripts/install.js +28 -7
- package/skills/commerce/SKILL.md +14 -14
- package/skills/commerce/docs/api-admin.md +23 -26
- package/skills/commerce/docs/api-storefront.md +67 -61
- package/skills/commerce/installation-guidelines.md +8 -8
- package/skills/commerce/post-installation.md +76 -41
- package/skills/commerce/references/admin-product-form.md +15 -12
- package/skills/commerce/references/emails.md +2 -2
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/online-payments.md +24 -191
- package/skills/commerce/references/product-render.md +18 -18
- package/skills/commerce/references/reviews.md +14 -8
- package/skills/commerce/references/storefront-product-page.md +1 -1
- package/src/commerce/admin/README.md +4 -5
- package/src/commerce/admin/bot/Markdown.jsx +1 -1
- package/src/commerce/admin/hooks/useMoney.js +13 -22
- package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
- package/src/commerce/admin/lib/constants.js +2 -29
- package/src/commerce/admin/lib/order-utils.js +1 -1
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
- package/src/commerce/admin/pages/products/Categories.jsx +147 -177
- package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
- package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
- package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
- package/src/commerce/admin/routes.jsx +6 -12
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +45 -49
- package/src/commerce/utils/variants.js +1 -1
- package/base44/entities/commerce.ShippingClass.jsonc +0 -30
- package/base44/entities/commerce.ShippingZone.jsonc +0 -41
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
- package/base44/entities/commerce.TaxClass.jsonc +0 -23
- package/base44/entities/commerce.TaxRate.jsonc +0 -68
- package/base44/shared/commerce/stripe.ts +0 -463
- package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
- 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,
|
|
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
|
-
- **
|
|
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,
|
|
12
|
-
- **Shared commerce engine** (`base44/shared/commerce/`) — totals, tax, shipping, coupons, stock, order lifecycle, webhook dispatch (HMAC-signed), emails,
|
|
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/
|
|
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/
|
|
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
|
-
│ │
|
|
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
|
|
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
|
|
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?,
|
|
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?,
|
|
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
|
}
|
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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": "
|
|
168
|
-
"instance_id": { "type": "string", "description": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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", "
|
|
101
|
+
"enum": ["taxable", "none"],
|
|
108
102
|
"default": "taxable",
|
|
109
103
|
"description": "Tax status"
|
|
110
104
|
},
|
|
111
|
-
"
|
|
105
|
+
"tax_group": {
|
|
112
106
|
"type": "string",
|
|
113
|
-
"default": "
|
|
114
|
-
"description": "
|
|
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
|
-
"
|
|
178
|
+
"ribbon_ids": {
|
|
189
179
|
"type": "array",
|
|
190
180
|
"items": { "type": "string" },
|
|
191
|
-
"description": "
|
|
181
|
+
"description": "ProductRibbon ids"
|
|
192
182
|
},
|
|
193
183
|
"images": {
|
|
194
184
|
"type": "array",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "commerce.
|
|
2
|
+
"name": "commerce.ProductRibbon",
|
|
3
3
|
"type": "object",
|
|
4
|
-
"description": "Product
|
|
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", "
|
|
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
|
-
|
|
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,
|
|
20
|
+
const [settings, locations] = await Promise.all([
|
|
23
21
|
getSettings(sr),
|
|
24
|
-
scanAll(sr.entities["commerce.
|
|
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,
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
345
|
-
// mean get-or-create: a second Best Seller would split the
|
|
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
|
|
400
|
-
* category_ids/
|
|
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" : "
|
|
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 {
|
|
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
|
|
61
|
-
// outside the store, so the record alone is the refund).
|
|
62
|
-
|
|
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
|
|
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
|
|
73
|
-
"
|
|
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
|
|
132
|
+
note: `Refunded ${amount} through the payment provider (${gatewayRefund.refund_id}).`,
|
|
132
133
|
is_customer_note: false,
|
|
133
134
|
added_by: actor,
|
|
134
135
|
});
|