@base44/app-plugin-commerce 0.1.5 → 0.1.6
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/base44/agents/commerce/StoreAdmin.jsonc +1 -1
- package/base44/functions/commerce/seed-store/entry.ts +35 -4
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +1 -1
- package/skills/commerce/docs/api-admin.md +3 -3
- package/skills/commerce/post-installation.md +22 -8
- package/skills/commerce/references/online-payments.md +5 -30
|
@@ -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?, 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).",
|
|
14
14
|
"tool_configs": [
|
|
15
15
|
{
|
|
16
16
|
"function_name": "commerce/admin-products",
|
|
@@ -12,14 +12,18 @@
|
|
|
12
12
|
* (with_sample_data=true, only when the store has zero products) — with
|
|
13
13
|
* best-effort rollback on mid-failure. See seed-catalog.ts for the pipeline.
|
|
14
14
|
*
|
|
15
|
-
* Body: { store_name?, with_sample_data?, products?, coupons?, tax_rates? }
|
|
15
|
+
* Body: { store_name?, currency?, with_sample_data?, products?, coupons?, tax_rates? }
|
|
16
16
|
* — with_sample_data cannot be combined with the catalog keys.
|
|
17
17
|
*
|
|
18
18
|
* `store_name` is required on a first seed: a function's env is only
|
|
19
19
|
* BASE44_APP_ID, so it cannot read the app's name, and subjects need one.
|
|
20
|
+
* `currency` is an ISO code; passing it explicitly always wins — it sets
|
|
21
|
+
* general.currency (and the currency's standard num_decimals) whether the
|
|
22
|
+
* group is being created or already exists.
|
|
20
23
|
*/
|
|
21
24
|
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
22
25
|
import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
|
|
26
|
+
import { CURRENCIES } from "../../../shared/commerce/data/currencies.ts";
|
|
23
27
|
import {
|
|
24
28
|
GATEWAY_DEFAULTS,
|
|
25
29
|
REST_OF_WORLD_EXAMPLE_METHOD,
|
|
@@ -41,9 +45,16 @@ Deno.serve(async (req) => {
|
|
|
41
45
|
const body = await req.json().catch(() => ({}));
|
|
42
46
|
const withSample = !!body.with_sample_data;
|
|
43
47
|
const storeName = String(body.store_name ?? "").trim();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const currencyCode = String(body.currency ?? "").trim().toUpperCase();
|
|
49
|
+
const currencyInfo = currencyCode ? CURRENCIES.find((c) => c.code === currencyCode) : undefined;
|
|
50
|
+
|
|
51
|
+
// ── 0. validate the payload — pure, so bad input fails before canaries
|
|
52
|
+
// or any write ──────────────────────────────────────────────────────
|
|
53
|
+
if (currencyCode && !currencyInfo) {
|
|
54
|
+
return fail(400, "Invalid catalog payload — nothing was written.", "invalid_payload", {
|
|
55
|
+
errors: [{ path: "currency", error: `"${currencyCode}" is not a known ISO currency code` }],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
47
58
|
let catalogSpec = null;
|
|
48
59
|
try {
|
|
49
60
|
catalogSpec = normalizeCatalogPayload(body);
|
|
@@ -75,11 +86,30 @@ Deno.serve(async (req) => {
|
|
|
75
86
|
if (existingGroups.has(group.group_id)) continue;
|
|
76
87
|
const values = group.group_id === "emails"
|
|
77
88
|
? { ...group.values, store_name: storeName }
|
|
89
|
+
: group.group_id === "general" && currencyInfo
|
|
90
|
+
? { ...group.values, currency: currencyInfo.code, num_decimals: currencyInfo.decimals }
|
|
78
91
|
: group.values;
|
|
79
92
|
await sr.entities["commerce.StoreSettings"].create({ ...group, values });
|
|
80
93
|
seeded.settings_groups++;
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
// An explicit currency always wins — unlike store_name there is no "blank"
|
|
97
|
+
// to distinguish a merchant's choice from the seeded USD default, and a
|
|
98
|
+
// caller passing one is giving an instruction, not a fallback.
|
|
99
|
+
let currencyAction = currencyInfo ? "created" : "unchanged";
|
|
100
|
+
if (currencyInfo && existingGroups.has("general")) {
|
|
101
|
+
const general = existingSettings.find((r: any) => r.group_id === "general");
|
|
102
|
+
const values = general?.values ?? {};
|
|
103
|
+
if (values.currency === currencyInfo.code && values.num_decimals === currencyInfo.decimals) {
|
|
104
|
+
currencyAction = "unchanged";
|
|
105
|
+
} else {
|
|
106
|
+
await sr.entities["commerce.StoreSettings"].update(general.id, {
|
|
107
|
+
values: { ...values, currency: currencyInfo.code, num_decimals: currencyInfo.decimals },
|
|
108
|
+
});
|
|
109
|
+
currencyAction = "updated";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
// A passed store_name must never be silently dropped: fill a blank one on an
|
|
84
114
|
// already-seeded store, but never overwrite a name the merchant chose.
|
|
85
115
|
let storeNameAction = storeName ? "created" : "unchanged";
|
|
@@ -143,6 +173,7 @@ Deno.serve(async (req) => {
|
|
|
143
173
|
sample_data: sampleResult ?? false,
|
|
144
174
|
catalog: catalogResult,
|
|
145
175
|
store_name: { value: storeNameAction === "kept_existing" ? currentName : storeName, action: storeNameAction },
|
|
176
|
+
currency: currencyInfo ? { value: currencyInfo.code, action: currencyAction } : null,
|
|
146
177
|
});
|
|
147
178
|
} catch (e) {
|
|
148
179
|
if (e instanceof HttpError) return fail(e.status, e.message, e.code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -34,7 +34,7 @@ Agents keep shipping storefronts that miss these, and each one breaks buying out
|
|
|
34
34
|
|
|
35
35
|
2. **Checkout must present shipping options and send a choice.** After `set-shipping-address`, read `shipping_status` on the cart: `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **you must show `available_shipping_methods` and call `choose-shipping-method`** · `none_available` → say so. `place-order` refuses with `400 shipping_method_required` until then — that is not a bug to work around.
|
|
36
36
|
|
|
37
|
-
3. **Take card payments, and build `/order-received`.** (
|
|
37
|
+
3. **Take card payments, and build `/order-received`.** (Have the Stripe integration connected at the beginning of the implementation, so it's already live when you create the checkout.) Choosing the online gateway returns `payment.checkout_url` — redirect there. Every payment link comes back to `/order-received`, which **you must implement**: call `commerce/payments` `complete-return` with the query params and render its `state`. Without that page a customer pays into a 404 and the order is never marked paid.
|
|
38
38
|
|
|
39
39
|
4. **Never advertise what isn't configured.** "Free shipping over €150" must come from a real `free_shipping` zone method. Zones are admin-only data, so a storefront cannot read them: the live answer is the cart's `available_shipping_methods` after `set-shipping-address`, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner.
|
|
40
40
|
|
|
@@ -163,7 +163,7 @@ All actions scan orders on demand (counted = `date_paid` set, or status `process
|
|
|
163
163
|
Actions: `status` · `payment-connector-status` · `admin-email-recipients` · `recount-terms` · `recount-coupon-usage` · `recalculate-customer-stats-all` · `prune-webhook-deliveries` · `clear-abandoned-carts` · `regenerate-download-permissions`
|
|
164
164
|
|
|
165
165
|
- **`status`** — `{ template_version, seeded, settings_groups, counts: { "commerce.Product": n | "1000+", ... }, checks: { has_payment_gateways, has_default_zone } }` — `counts` is keyed by the **namespaced** entity name, and `checks` is an object, not an array. — mini system-status; also the seeded/health check for install verification.
|
|
166
|
-
- **`payment-connector-status`** — no payload → `{ provider, provider_label, gateway_slug, connected, error?, connector }`. Whether an online payment provider is usable **right now**, answered by the payment utility for whichever provider is wired — so UI derives payment readiness instead of hardcoding a "not set up" notice, and shows "No payment provider connected" rather than a brand. Connectors are service-role only, hence the round trip; any failure reports `connected: false`. A provider connected **after** the function's last deploy reads as `connected: false` until the functions are redeployed — env vars are injected at deploy time ([`references/online-payments.md`](../references/online-payments.md) §
|
|
166
|
+
- **`payment-connector-status`** — no payload → `{ provider, provider_label, gateway_slug, connected, error?, connector }`. Whether an online payment provider is usable **right now**, answered by the payment utility for whichever provider is wired — so UI derives payment readiness instead of hardcoding a "not set up" notice, and shows "No payment provider connected" rather than a brand. Connectors are service-role only, hence the round trip; any failure reports `connected: false`. A provider connected **after** the function's last deploy reads as `connected: false` until the functions are redeployed — env vars are injected at deploy time ([`references/online-payments.md`](../references/online-payments.md) §1). (`connector` repeats `provider` for callers written against the older shape.)
|
|
167
167
|
- **`admin-email-recipients`** — no payload → `{ recipients: string[], source: "settings" | "admin_users", admin_users: string[] }`. Where admin notifications go **right now**: `recipients` is the configured `emails.admin_recipients`, or the app's admin users when that is empty (the runtime fallback), with `source` saying which. `admin_users` is returned either way, so Settings → Emails can show the fallback as the field's placeholder even while explicit recipients are set. A client can't resolve it itself — listing users needs service role.
|
|
168
168
|
- **`recount-terms`** — repairs category/tag/term `count`.
|
|
169
169
|
- **`recount-coupon-usage`** — repairs `usage_count`/`used_by`.
|
|
@@ -185,8 +185,8 @@ Actions: `status` · `create-link` · `complete-return` · `verify` — the admi
|
|
|
185
185
|
|
|
186
186
|
## commerce/seed-store
|
|
187
187
|
|
|
188
|
-
Not action-routed. Body `{ store_name?, with_sample_data?, products?, coupons?, tax_rates? }`. **`store_name` is required** when the `emails` group doesn't exist yet (**400** `store_name_required` otherwise) — pass the app's name **as the platform shows it** — ask the user or read it from the dashboard. `base44/config.jsonc` → `name` is *not* authoritative: it can still say `New App` for an app the platform calls `Canvas`. A backend function can't read either, its environment being only `BASE44_APP_ID`. It lands in `emails.store_name` — one setting serving as both the store name in email subjects and the sender name on every transactional email; a nameless store renders subjects like `[]: New order #1002`, which is why seeding refuses one. Requires admin. Runs a **canary schema check** first — on any incompatibility returns **422** `{ success:false, code:"schema_incompatible", errors:[{ entity, error }] }` and writes nothing. Otherwise seeds defaults idempotently, then the catalog. On an already-seeded store a passed `store_name` fills a **blank** name and never overwrites one the merchant chose.
|
|
188
|
+
Not action-routed. Body `{ store_name?, currency?, with_sample_data?, products?, coupons?, tax_rates? }`. **`store_name` is required** when the `emails` group doesn't exist yet (**400** `store_name_required` otherwise) — pass the app's name **as the platform shows it** — ask the user or read it from the dashboard. `base44/config.jsonc` → `name` is *not* authoritative: it can still say `New App` for an app the platform calls `Canvas`. A backend function can't read either, its environment being only `BASE44_APP_ID`. It lands in `emails.store_name` — one setting serving as both the store name in email subjects and the sender name on every transactional email; a nameless store renders subjects like `[]: New order #1002`, which is why seeding refuses one. Requires admin. Runs a **canary schema check** first — on any incompatibility returns **422** `{ success:false, code:"schema_incompatible", errors:[{ entity, error }] }` and writes nothing. Otherwise seeds defaults idempotently, then the catalog. On an already-seeded store a passed `store_name` fills a **blank** name and never overwrites one the merchant chose. **`currency`** is an ISO code (validated against the shared currencies table, **400** `invalid_payload` on an unknown one) that sets `general.currency` plus the currency's standard `num_decimals`; unlike `store_name` it **always wins** — there is no blank state to distinguish a merchant's USD from the seeded default, so passing it on a re-run updates the store currency.
|
|
189
189
|
|
|
190
190
|
**`products`** is the one-call catalog bootstrap — the worked example and full semantics are in [`../post-installation.md` §2.1](../post-installation.md#21-the-products-payload). Each entry references categories/tags/attributes by **display name** (taxonomy is get-or-created: slugs/codes derived, existing records matched case-insensitively and reused, with the stored casing canonicalized into the product). `attributes: [{ name, options }]` (or `{ <name>: [options] }`) declares the variant axes; `variations: [{ options: { <name>: <option> }, ...overrides }]` lists the stocked combinations — omit it to auto-generate **all** combinations, each inheriting the product-level price/sale fields. A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without one it draws on the parent's pooled stock (`"parent"`). Variation SKUs are synthesized from the parent SKU when absent. Parent `price`/`regular_price`/`on_sale` are rolled up from the cheapest publishable variant, `stock_status` derived, category/tag counts maintained — same helpers as `admin-products` `save`, but **no `product.created` webhooks are dispatched** (bootstrap precedes webhook subscribers; use `admin-products` for webhook-visible creates). Payload problems fail before any write as **400** `invalid_payload` with `errors: [{ path, error }]`; an explicit variation SKU already in use is **409** `duplicate_sku`; a mid-write failure rolls back everything the call created (**500** `catalog_seed_failed`) without touching reused taxonomy. **Re-runs converge**: a product whose `sku` (or, without one, derived slug) already exists is skipped and reported, so retries never duplicate. Limits: ≤100 products, ≤50 variations per product, ≤500 variations per call, ≤50 coupons/tax rates. `coupons`/`tax_rates` are thin passthroughs (coupon `code` lowercased; both skip-if-exists). `with_sample_data: true` seeds the template's demo catalog instead (only when the store has zero products) and cannot be combined with the catalog keys.
|
|
191
191
|
|
|
192
|
-
→ `{ seeded: { settings_groups, gateways, tax_classes, zones, zone_methods }, sample_data: {...} | false, catalog: { categories|tags|attributes|terms: { created, reused }, products_created, products_skipped, variations_created, coupons, tax_rates, products: [{ name, id, slug, sku, variation_count } | { name, skipped: true, reason: "sku_exists"|"slug_exists", existing_id }] } | null, store_name: { value, action: "created" | "filled" | "unchanged" | "kept_existing" } }`.
|
|
192
|
+
→ `{ seeded: { settings_groups, gateways, tax_classes, zones, zone_methods }, sample_data: {...} | false, catalog: { categories|tags|attributes|terms: { created, reused }, products_created, products_skipped, variations_created, coupons, tax_rates, products: [{ name, id, slug, sku, variation_count } | { name, skipped: true, reason: "sku_exists"|"slug_exists", existing_id }] } | null, store_name: { value, action: "created" | "filled" | "unchanged" | "kept_existing" }, currency: { value, action: "created" | "updated" | "unchanged" } | null }`.
|
|
@@ -44,7 +44,7 @@ Even if the client guard were bypassed, layers 2 and 3 keep the store data safe.
|
|
|
44
44
|
|
|
45
45
|
## 2. Store data — seeding
|
|
46
46
|
|
|
47
|
-
A fresh install has **no settings and no catalog**. One call to `commerce/seed-store` (admin-only, idempotent) initializes both. It always creates the business defaults — the six settings groups (`general`, `products`, `inventory`, `tax`, `shipping`, `emails`; USD, kg/cm, taxes off prices), the `offline` and `stripe` payment gateways, three tax classes and a free "Rest of the world" fallback shipping zone — and, depending on the payload, the catalog
|
|
47
|
+
A fresh install has **no settings and no catalog**. One call to `commerce/seed-store` (admin-only, idempotent) initializes both. It always creates the business defaults — the six settings groups (`general`, `products`, `inventory`, `tax`, `shipping`, `emails`; USD, kg/cm, taxes off prices), the `offline` and `stripe` payment gateways, three tax classes and a free "Rest of the world" fallback shipping zone — and, depending on the payload, the catalog. Pass **`currency`** (an ISO code, e.g. `"EUR"`) to set the store currency instead of the USD default — an explicit currency always wins, on a first seed and a re-run alike, and brings the currency's standard decimal count with it.
|
|
48
48
|
|
|
49
49
|
| Mode | Body | Products created |
|
|
50
50
|
|---|---|---|
|
|
@@ -65,6 +65,7 @@ Reference everything by **display name** — categories, tags, attributes and th
|
|
|
65
65
|
```js
|
|
66
66
|
await base44.functions.invoke("commerce/seed-store", {
|
|
67
67
|
store_name: "Aurora Threads",
|
|
68
|
+
currency: "EUR", // optional — defaults to USD
|
|
68
69
|
products: [
|
|
69
70
|
{ // simple product
|
|
70
71
|
name: "Classic T-Shirt",
|
|
@@ -118,16 +119,17 @@ The response reports everything:
|
|
|
118
119
|
{ "name": "Runner Sneaker", "id": "…", "slug": "runner-sneaker", "sku": "SNK-RUN", "variation_count": 4 }
|
|
119
120
|
]
|
|
120
121
|
},
|
|
121
|
-
"store_name": { "value": "Aurora Threads", "action": "created" }
|
|
122
|
+
"store_name": { "value": "Aurora Threads", "action": "created" },
|
|
123
|
+
"currency": { "value": "EUR", "action": "created" } } // "updated" | "unchanged" on re-runs; null when not passed
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
**Images**: every product needs at least one. Use whatever image generation the app has available and store the returned URL, upload real assets with `base44.integrations.Core.UploadFile({ file })` → public URL, or fall back to stable public stock URLs (`base44/functions/commerce/seed-store/sample-data.ts` shows a working Unsplash pattern). Match the image to the product.
|
|
125
127
|
|
|
126
|
-
A successful response means the data is in — the catalog and settings are live exactly as reported. Write any store-specific settings into `commerce.StoreSettings` (direct CRUD, one record per `group_id` —
|
|
128
|
+
A successful response means the data is in — the catalog and settings are live exactly as reported. Write any remaining store-specific settings into `commerce.StoreSettings` (direct CRUD, one record per `group_id` — weight/dimension units are the usual ones; patch `values`, don't replace groups you weren't asked about).
|
|
127
129
|
|
|
128
130
|
### 2.2 Payments
|
|
129
131
|
|
|
130
|
-
Card payments are **already implemented** (hosted payment page, payment links, refunds)
|
|
132
|
+
Card payments are **already implemented** (hosted payment page, payment links, refunds). **Get the payment provider connected at the beginning of the implementation** — have the platform's **Stripe** integration configured for the app while you build, so it is already connected by the time you create the checkout and a test order can prove the whole path. The `offline` gateway (manual reconciliation) works with nothing to configure. Provider internals, webhooks and refunds are day-2 material: [`references/online-payments.md`](./references/online-payments.md).
|
|
131
133
|
|
|
132
134
|
---
|
|
133
135
|
|
|
@@ -219,9 +221,11 @@ Shipping is chosen **on the cart, before place-order** — this is the step stor
|
|
|
219
221
|
cart = await inv("commerce/storefront-cart", { action: "set-shipping-address", cart_token,
|
|
220
222
|
address: { country, state, postcode, city } });
|
|
221
223
|
switch (cart.shipping_status) {
|
|
222
|
-
case "auto_selected":
|
|
224
|
+
case "auto_selected": break; // only ONE option existed — the backend already applied it;
|
|
225
|
+
// no picker needed, just display cart.chosen_shipping_method
|
|
223
226
|
case "chosen": break; // customer's earlier choice still valid
|
|
224
|
-
case "choice_required": // MUST render cart.available_shipping_methods
|
|
227
|
+
case "choice_required": // several options — MUST render cart.available_shipping_methods
|
|
228
|
+
// [{ id, title, cost }] as a picker, then send the customer's pick:
|
|
225
229
|
cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method", cart_token,
|
|
226
230
|
method_id: picked.id }); // the entry's id, not its method_id type
|
|
227
231
|
break;
|
|
@@ -234,14 +238,24 @@ switch (cart.shipping_status) {
|
|
|
234
238
|
|
|
235
239
|
### 3.4 Checkout & order-received
|
|
236
240
|
|
|
237
|
-
|
|
241
|
+
The checkout page renders **two sets of options that are store data, never hardcoded**: the shipping methods (already resolved on the cart in step 3 — `place-order` refuses with `400 shipping_method_required` until `shipping_status` is `chosen`/`auto_selected`/`not_needed`) and the payment methods, which come from `info.payment_gateways` (step 1) — already filtered to what can actually take payment right now, so every entry you render is payable:
|
|
238
242
|
|
|
239
243
|
```js
|
|
240
244
|
// card payments ride the app's Stripe integration — setup & provider details:
|
|
241
245
|
// skills/commerce/references/online-payments.md
|
|
246
|
+
const gateways = info.payment_gateways; // [{ slug, title, description, online }] — admin-owned data
|
|
247
|
+
// several → render a picker using the admin's title/description as the labels
|
|
248
|
+
// exactly ONE → no picker: use it directly, but still show its title so the customer knows how they'll pay
|
|
249
|
+
// none → checkout cannot complete — say so instead of rendering a dead button
|
|
250
|
+
const payment_method = gateways.length === 1 ? gateways[0].slug : picked.slug; // never a hardcoded "stripe"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`online: true` marks the card/redirect gateway; `offline` is manual reconciliation. Then one call places the order:
|
|
254
|
+
|
|
255
|
+
```js
|
|
242
256
|
const res = await inv("commerce/storefront-checkout", {
|
|
243
257
|
action: "place-order", cart_token,
|
|
244
|
-
payment_method
|
|
258
|
+
payment_method, // the slug chosen above
|
|
245
259
|
billing: { first_name, last_name, address_1, city, country, email }, // the required set; phone, state, postcode optional
|
|
246
260
|
// shipping: { … } if it differs from billing; customer_note?; return_url: window.location.origin
|
|
247
261
|
});
|
|
@@ -10,38 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
## 1.
|
|
13
|
+
## 1. Connecting a provider
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**Connect the Stripe integration for the app** — early, ideally at the beginning of the implementation, so it is already connected by the time the checkout exists and a test order can prove the whole path. The platform holds the keys and injects them into backend functions as **`STRIPE_SECRET_KEY`** / **`STRIPE_PUBLISHABLE_KEY`**, which is where `stripe.ts` reads the credential from — never put keys in the store's data, in code, or in a settings field.
|
|
16
16
|
|
|
17
|
-
**
|
|
17
|
+
**Connecting to an already-deployed store? Redeploy the backend functions.** The key is injected **at deploy time**, so `commerce/*` functions deployed before the integration was connected keep the environment they started with and the store keeps reporting *no payment provider connected* even though Stripe is set up correctly. Redeploy them all (anything under `base44/shared/commerce/` is bundled into every function, so touching it redeploys the whole set) and allow up to a minute for the ~60s status cache.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
> *"Your store is set up. The last piece is taking money: card payments are already implemented, they just need Stripe connected for this app — that part is yours to do in the dashboard, and then I can place a test order end to end. Want to do that now, or start with manual payments (bank transfer / cash on delivery) for the moment?"*
|
|
22
|
-
|
|
23
|
-
Then:
|
|
24
|
-
|
|
25
|
-
1. **Ask the user to set Stripe up for the app.** On **Base44** that's the platform's own Stripe integration (app dashboard → Integrations → Stripe; it can start in Stripe's test mode and be claimed with a real account later) — *not* an OAuth connector: there is no Stripe connector in Base44's catalog. The platform holds the keys and injects them into backend functions as **`STRIPE_SECRET_KEY`** / **`STRIPE_PUBLISHABLE_KEY`**, which is exactly where `stripe.ts` reads the credential from. Never put keys in the store's data, in code, or in a settings field.
|
|
26
|
-
2. **Redeploy the backend functions — connecting is not enough.** The platform injects `STRIPE_SECRET_KEY` **at deploy time**, so functions that were already deployed keep the environment they started with and cannot see a key added afterwards. `stripe.ts` reads `Deno.env.get("STRIPE_SECRET_KEY")`, so until the functions are redeployed the store reports *no payment provider connected* even though Stripe is set up correctly. This is the single most common "it doesn't work" after connecting.
|
|
27
|
-
|
|
28
|
-
- **Hosted app (no CLI)** — you cannot run a deploy command, so cause one by **changing a file the functions bundle**: edit anything under `base44/shared/commerce/` and every `commerce/*` function redeploys, because `shared/` is bundled into all of them. A one-line comment edit is enough — `shared/commerce/payments.ts` carries a **deploy marker** comment at the top for exactly this: bump its number, save, and the whole set redeploys. (Editing a single function's `entry.ts` redeploys only that function, which is not enough: `payments`, `payment-webhook`, `storefront-checkout`, `storefront-catalog`, `admin-tools` and `admin-refunds` all read the credential.)
|
|
29
|
-
- **CLI** — `npx base44 functions deploy`.
|
|
30
|
-
|
|
31
|
-
3. **Then confirm it landed** — `commerce/admin-tools` → `payment-connector-status` returns `{ connected, provider, gateway_slug }`; the admin's Payments screen shows the same state. Still *not connected*? In order: was the integration actually completed in the dashboard, did the redeploy above really happen, and has the ~60s status cache expired (`onlinePaymentStatus` caches per isolate).
|
|
32
|
-
4. **Nothing else.** The `Credit card` gateway is enabled by default and starts appearing at checkout the moment a provider is connected. No code, no keys, no placeholder to replace. (`commerce/seed-store` is idempotent and never rewrites an existing gateway, so a store seeded *before* this default landed keeps its old `enabled: false` — switch it on in Payments settings.)
|
|
33
|
-
5. **Optional but recommended: register the webhook** (§4) so payments confirm even when the buyer closes the tab.
|
|
34
|
-
6. **Test with the provider's test mode** — place an order, pay, and check the order reaches `processing` with `date_paid` and a payment reference; then try a refund. This is the other reason to do payments *after* the catalog: with products, a shipping method and an address in place, you can prove the whole path works instead of just wiring it.
|
|
35
|
-
|
|
36
|
-
**To stop taking card payments, switch the gateway off** — don't rely on disconnecting the provider in the platform dashboard. Verified on Base44: after disconnecting Stripe there, the injected `STRIPE_SECRET_KEY` was still present *and still accepted by Stripe* (across a redeploy), so the store could genuinely still charge and correctly reported card payment as available. The store-level switch in **Payments settings** is the control that always works, because it's the store's own data.
|
|
37
|
-
|
|
38
|
-
**The user can decline, and that's a legitimate answer.** Some businesses genuinely want invoice, bank transfer or cash on delivery only. If they do:
|
|
39
|
-
|
|
40
|
-
- turn the `Credit card` gateway **off** in Payments settings — that is the whole opt-out;
|
|
41
|
-
- the storefront never offers it, and the admin order page's payment actions sit disabled with *"No payment provider connected"*;
|
|
42
|
-
- say plainly in the checkout UI how payment works (`payment_instructions` from the `place-order` response), and don't describe the store as taking cards anywhere in the copy.
|
|
43
|
-
|
|
44
|
-
Don't leave the in-between state unexplained: the gateway enabled with **no provider connected** is safe (customers never see it — the storefront filters it out live), but the user should know it's waiting on them.
|
|
19
|
+
That's it — the `Credit card` gateway ships enabled and stays hidden from customers until a provider is connected, so nothing is broken in the meantime. To *stop* taking cards, switch the gateway off in Payments settings — disconnecting the provider alone can leave a still-working key in the functions' environment.
|
|
45
20
|
|
|
46
21
|
## 2. How it works
|
|
47
22
|
|
|
@@ -194,7 +169,7 @@ Rules for any checkout UI you build:
|
|
|
194
169
|
|
|
195
170
|
Whether the store can take a card is a **live fact about the connector**, not a constant. Everything derives it:
|
|
196
171
|
|
|
197
|
-
- backend — `onlinePaymentStatus(sr)` from the payment utility. It doesn't just look for a key, it **verifies** it with the provider (cheapest authenticated call, answer cached ~60s): a credential that has been disconnected, rotated or revoked lingers in a function's environment until the next deploy, so presence alone would advertise card payment the store can no longer take. The same deploy-time injection is why a *newly* connected provider stays invisible until the functions are redeployed (§
|
|
172
|
+
- backend — `onlinePaymentStatus(sr)` from the payment utility. It doesn't just look for a key, it **verifies** it with the provider (cheapest authenticated call, answer cached ~60s): a credential that has been disconnected, rotated or revoked lingers in a function's environment until the next deploy, so presence alone would advertise card payment the store can no longer take. The same deploy-time injection is why a *newly* connected provider stays invisible until the functions are redeployed (§1). Expect up to a minute for a change to show. A *rejected* credential means not connected; a network blip does **not** flip a working store to "no payments" while a good answer is still cached;
|
|
198
173
|
- admin — the `usePaymentProvider()` hook (`commerce/admin-tools` → `payment-connector-status`);
|
|
199
174
|
- storefront — the filtered `payment_gateways` list.
|
|
200
175
|
|