@base44/app-plugin-commerce 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,96 +0,0 @@
1
- ---
2
- stage: install/01
3
- read_when: "The commerce kit's files were just copied into the app, or you are installing it now."
4
- skip_when: "src/App.jsx already declares the admin's screens as literal <Route> JSX under a /store-admin layout route, and / routes somewhere real."
5
- forget_when: "The checklist at the bottom of this file passes (admin mounts, / routes somewhere real, /order-received exists)."
6
- carry_forward:
7
- - "The admin's six picker-visible routes are literal <Route> JSX in src/App.jsx (the platform discovers pages by reading that file — an array or a .map() discovers nothing); the rest run off its <Route path=\"*\" element={<AdminRoutes />} />."
8
- - "Admin enforcement is three layers — AuthGuard (UI), admin-only entity RLS, requireAdmin() in every admin function. Never weaken any of them."
9
- - "/order-received must exist as a route: every payment link returns there, and confirming is what marks an order paid."
10
- - "The storefront header shows a visible \"Store manager\" link to /store-admin when the signed-in user's role is admin, and nothing for everyone else."
11
- - "Interleave: start image generation first → mount admin + build the storefront → seed with the image_url the generate_image result already returned (never poll, never write a function to fetch URLs) → payments last."
12
- - "Entities are dotted + bracket-syntax only (`base44.entities[\"commerce.X\"]`); the map is ../docs/entities.md — never scan base44/entities/."
13
- ---
14
-
15
- # 01 — Install
16
-
17
- **Inside the Base44 runtime, writing a resource file *is* the deploy** — the 20 `commerce.*` entities, the 16 `commerce/*` functions + shared engine, the StoreAdmin agent, the finished admin app (`src/commerce/admin/` — don't validate it, it ships tested) and the storefront hooks (`src/commerce/storefront/` + `utils/`) are live the moment the files exist. If `scripts/install.js` ran, all of that is already in place; there is no build step and nothing to push.
18
-
19
- **Anonymous function calls must be allowed** in the app's settings — the storefront functions are public by design (they verify per action: auth session, `cart_token`, or `order_key`). If the app blocks unauthenticated invocation, every guest hits errors on the entire storefront.
20
-
21
- **Dependencies.** `sonner`, `recharts`, `react-markdown` — all three ship with the default Base44 template, so check `package.json` and `npm i` only what is actually absent (the normal outcome is installing nothing).
22
-
23
- ## Work order — interleave, don't queue
24
-
25
- Image generation is the slowest step and nothing depends on it until seed time; the storefront doesn't wait on live data either.
26
-
27
- 1. **Start image generation first** — every product image, before anything else.
28
- ⚑ **The result already carries the real `image_url`** alongside a `placeholder_url`
29
- — use `image_url` and move on. Nothing is pending, there is nothing to poll, and a
30
- backend function calling `Core.GenerateImage` to "fetch the real URLs" is pure waste:
31
- you already have them. If a `/__generating__/…` value does reach a file or a seed
32
- payload, the platform swaps it for the real URL after the turn (in files *and* entity
33
- records) — so a placeholder that renders broken in a mid-build preview is expected and
34
- must not be "fixed".
35
- 2. **Mount the admin (below) and build the storefront** while images render. Every
36
- request and response shape the pages build against is written out in
37
- [`./02-storefront.md`](./02-storefront.md), so they are written from the docs,
38
- not from live data.
39
- 3. **Seed the moment the image URLs are back** — one `commerce/seed-store` call ([`./03-data.md`](./03-data.md)). Its writes run in parallel, so the call usually takes a few seconds — but don't idle on it either.
40
- 4. **Converge**: open the finished pages against the live catalog.
41
- 5. **Payments last, if at all** — cards are off by default; [`./03-data.md`](./03-data.md) decides it.
42
-
43
- The only dependency edges are *image URLs → seed payload* and *seed done → real products on the pages*.
44
-
45
- ## Mount the admin
46
-
47
- The admin mounts as a **layout route in the app's own `src/App.jsx`**: the six screens the store owner opens from the builder are declared there as literal `<Route>` JSX, and everything deeper goes to `<AdminRoutes />` on a splat. The platform discovers an app's pages by reading that file — a screen declared anywhere else is unreachable from the page picker.
48
-
49
- ```jsx
50
- import AdminApp, { AdminRoutes } from "@/commerce/admin";
51
- import Dashboard from "@/commerce/admin/pages/Dashboard"; // …and orders/OrdersList,
52
- // products/ProductsList, customers/CustomersList, coupons/CouponsList, reports/Reports
53
-
54
- {/* Literal JSX — the platform reads this file, it never runs it. Do not refactor into a map. */}
55
- <Route path="/store-admin" element={<AdminApp />}>
56
- <Route index element={<Dashboard />} />
57
- <Route path="orders" element={<OrdersList />} />
58
- <Route path="products" element={<ProductsList />} />
59
- <Route path="customers" element={<CustomersList />} />
60
- <Route path="coupons" element={<CouponsList />} />
61
- <Route path="reports" element={<Reports />} />
62
- <Route path="*" element={<AdminRoutes />} /> {/* editors, settings, webhooks */}
63
- </Route>
64
- <Route path="/" element={<Navigate to="/store-admin" replace />} /> {/* until a storefront exists */}
65
- <Route path="/order-received" element={<OrderReceived />} /> {/* mandatory — see below */}
66
- ```
67
-
68
- - **Those seven lines, as they are.** The splat is what keeps the app's listed pages to six instead of 26: `path="*"` is skipped, so the editors, settings tabs and webhook screens stay navigable without appearing there, and `<AdminRoutes />` still serves the admin's own 404.
69
- - **Don't add `settings` to the list** — it is a tabbed layout around a nested route, so it only renders correctly from the splat.
70
- - **Elsewhere than `/store-admin`**: change the layout route's path and pass the prefix — `<AdminApp basePath="/backoffice" />`; the children are unchanged.
71
- - **Name the group** in `base44/ui.jsonc` — app-owned, so edit it in place, keep any other keys, never recreate a deleted one: `{ "version": 1, "sections": [{ "path": "/store-admin/*", "name": "Store Management" }] }`
72
- - **Give `/` something** — a blank app has no `/` route, and "page not found" at the app's own URL reads like a broken install.
73
- - **Link the admin from the storefront header** — otherwise the merchant has no way in but typing the URL. Resolve the signed-in user once (`base44.auth.me()`, rejection/no session = not an admin, never blocking the page) and render a plainly visible "Store manager" link to `/store-admin` in the header when `role === "admin"` — and nothing at all for everyone else.
74
- - **`/order-received` is mandatory**, even offline-only: every payment link returns there, and confirming is what marks an order paid — without it a paying customer hits a 404 and the order stays unpaid. The page is one hook, `useOrderReturn()` ([`./02-storefront.md`](./02-storefront.md)). A different path must be set in Settings → General (`general.order_received_path`).
75
-
76
- ## Admin-role enforcement — do not weaken
77
-
78
- Three layers, all load-bearing; keep every one when touching routes or schemas:
79
-
80
- 1. **UI guard** — the shipped `AuthGuard` (requires `role === "admin"`; grant via the dashboard or `base44.users.inviteUser(email, "admin")`).
81
- 2. **Entity RLS** — every commerce entity is admin-only on all operations.
82
- 3. **Function guard** — every `commerce/admin-*` function and `commerce/seed-store` calls `requireAdmin()`: **401** unauthenticated, **403** not admin.
83
-
84
- Storefront functions are public on purpose (per-action verification, above). To check install state at any point: `commerce/admin-tools` `{ action: "status" }` ([`../docs/api-admin.md`](../docs/api-admin.md)).
85
-
86
- ## Done — forget this file
87
-
88
- - [ ] The `/store-admin` layout route is in `src/App.jsx` with its six literal `<Route>` screens and the `path="*"` → `<AdminRoutes />` splat; `base44/ui.jsonc` names the section; the three enforcement layers untouched.
89
- - [ ] `/` routes somewhere real; `/order-received` is a route.
90
- - [ ] The storefront header shows a visible `/store-admin` link to signed-in admins, and to nobody else.
91
- - [ ] Anonymous function invocation is allowed in the app's settings.
92
- - [ ] Missing dependencies (if any) installed; image generation already running.
93
-
94
- Then continue: **[`./02-storefront.md`](./02-storefront.md) when you start building UI**, **[`./03-data.md`](./03-data.md) when you start the seed payload**. Do **not** read them now, and open no `references/` or `docs/` file during an install — a file read early costs its size on every later call.
95
-
96
- Then copy this file's `carry_forward` lines (in its front matter) into your working notes, and do not re-read this file.
@@ -1,144 +0,0 @@
1
- ---
2
- stage: install/03
3
- read_when: "You are about to build the seed payload — the store's catalog, shipping, coupons, currency — or deciding about payments."
4
- skip_when: "commerce/seed-store has already returned success for this store and payments are decided."
5
- forget_when: "The seed response is received and recorded (slugs + warnings), and the checklist at the bottom passes."
6
- carry_forward:
7
- - "Product slugs come from the seed response's catalog.products[] — link pages by slug, never by a client-side map."
8
- - "Payments: report at handover where they landed (default = offline on, card off) — the owner must never learn it from a customer. Shipping the Stripe file is not a Stripe connection; only the user can authorise that."
9
- - "Turning card payments on or off later is one more seed call: { payment_methods: [\"offline\", \"card\"] }."
10
- - "Seed-time `locations` is THE shipping path; patching commerce.ShippingTaxLocation is the day-2 route."
11
- ---
12
-
13
- # 03 — Store data
14
-
15
- A fresh install has **no settings and no catalog**. One admin-only, idempotent call to `commerce/seed-store` creates both: the six settings groups, the two gateway rows (`offline` enabled, `card` disabled), and — from the payload — the catalog, coupons and shipping locations. Nothing in [`./02-storefront.md`](./02-storefront.md) waits on it: fire it when the image URLs are back.
16
-
17
- | Mode | Body |
18
- |---|---|
19
- | **Real catalog** | `{ store_name, products: [...] }` (+ optional `coupons`, `locations`) |
20
- | **Demo data** | `{ store_name, with_sample_data: true }` — 10 generic products; cannot combine with `products` (**400**) |
21
- | **No products** | `{ store_name }` — defaults only |
22
-
23
- **`store_name` is required on a first seed** — the app's name as the platform shows it (`base44/config.jsonc` → `name` can be stale; ask if unsure). **`currency`** is an ISO code (`"EUR"`); formatting follows the viewer's locale, nothing else to set. Explicit values always win, first seed and re-runs alike.
24
-
25
- The working call — `name` is the only required product key; give each product the keys its own catalog entry actually has and leave the rest out. The **full key list** (sale windows, downloads, tax, backorders, upsells…) lives in `api-admin.md` — open it only if the catalog needs one:
26
-
27
- ```js
28
- try {
29
- const res = await base44.functions.invoke("commerce/seed-store", {
30
- store_name: "Aurora Threads",
31
- currency: "EUR",
32
- products: [
33
- { name: "Linen Scarf", regular_price: 45 }, // minimal is complete
34
-
35
- { name: "Runner Sneaker",
36
- sku: "SNK-RUN", // optional; makes re-runs idempotent
37
- featured: true, // → useProductList({ featured: true }) rails
38
- regular_price: 89,
39
- sale_price: 79, // sets on_sale; storefront strikes regular_price
40
- stock_quantity: 12, // implies manage_stock: true
41
- sold_individually: false, // true → max 1 per order (no qty stepper)
42
- short_description: "Cushioned everyday runner.",
43
- description: "<p>Cut from recycled knit…</p>", // HTML, rendered as rich text
44
- images: ["https://…/sneaker.jpg"], // URLs or { src, alt } — see Images below
45
- categories: ["Shoes"], // get-or-created by display name
46
- ribbons: ["Best Seller"], // flat labels, not a hierarchy
47
-
48
- weight: 0.248, // real fields, in the store's units —
49
- dimensions: { length: 31, width: 12, height: 11 }, // never a meta_data row
50
-
51
- // Descriptive spec rows (productSpecs). NOT axes, NOT ribbons; `_` hides.
52
- // Qualities only — a weight or a size here is a string nothing can read.
53
- meta_data: [
54
- { key: "Material", value: "Recycled knit upper" },
55
- { key: "Care", value: "Machine wash cold" },
56
- ],
57
-
58
- attributes: [ // the axes → one selector each
59
- { name: "Size", options: ["41", "42"] },
60
- { name: "Color", options: ["Black", "White"] },
61
- ],
62
- default_options: { Size: "42", Color: "Black" },
63
- variations: [ // omit entirely → all combos auto-generated
64
- { options: { Size: "41", Color: "Black" }, stock_quantity: 4, weight: 0.242 },
65
- { options: { Size: "42", Color: "White" }, regular_price: 94, stock_quantity: 3,
66
- weight: 0.254, // per variant, never inherited from the parent
67
- image: "https://…/sneaker-white.jpg" }, // per-variation image for a visual axis
68
- ],
69
- },
70
- ],
71
- coupons: [{ code: "WELCOME10", discount_type: "percent", amount: 10 }],
72
- // ONLY with a coupon field in the cart or checkout (see ./02-storefront.md)
73
- // locations: [ … ], // shipping — next section; passing any makes them the store's ONLY ones
74
- });
75
- return res.data; // ← the { success, data } envelope: plain JSON
76
- } catch (e) {
77
- return { success: false, status: e.response?.status, ...(e.response?.data ?? { error: e.message }) };
78
- }
79
- ```
80
-
81
- **Running this through a code-execution tool? Return `res.data`, never the raw response** — the raw response carries circular objects and fails `Converting circular structure to JSON` *even when the seed succeeded*; a thrown error needs `e.response?.data` for the same reason.
82
-
83
- Reference taxonomy by **display name** — existing records are matched case-insensitively and reused. The seeder derives slugs, checks SKU uniqueness, prices variations, and **rolls the parent's price up from the cheapest publishable variant** — never set a variant parent's price yourself. Unknown keys are rejected, so typos surface. **Idempotency:** a product whose `sku` (or derived slug) exists is skipped and reported — safe to retry. Bad payloads fail **400** `invalid_payload` with `errors: [{ path, error }]` before anything is written; per-call limits (≤100 products, ≤500 variations, ≤50 locations) and the full key list are in [`../docs/api-admin.md`](../docs/api-admin.md#commerceseed-store).
84
-
85
- The response reports everything; these matter downstream:
86
-
87
- ```jsonc
88
- { "catalog": { "products": [{ "name": "Runner Sneaker", "id": "…", "slug": "runner-sneaker", "variation_count": 2 }] },
89
- "payment_methods": null, // null = the default (offline on, card off)
90
- "warnings": [] } // always present; read it — see shipping
91
- ```
92
-
93
- Link pages by the **`slug`** from `catalog.products[]`; never mirror the seed in client-side constants (the database is the source of truth and a mirror diverges on the first merchant edit).
94
-
95
- ## Shipping — declare it in the seed call
96
-
97
- Each location is a scope plus its rates and taxes; locations match in payload order. "€20 in Europe, €100 everywhere else" is two locations:
98
-
99
- ```js
100
- locations: [
101
- { name: "Europe", continents: ["EU"], shipping_rates: [{ name: "Standard", cost: 20 },
102
- { name: "Express", cost: 35 }] },
103
- { name: "Worldwide", rest_of_world: true, shipping_rates: [{ name: "International", cost: 100 }] },
104
- ]
105
- ```
106
-
107
- ⚑ **Speeds go in one location's `shipping_rates`; zones are separate locations.** Exactly one location matches an address, so several rates in it become the customer's pick at checkout (`shipping_status: "choice_required"`), while a second location with the same scope is simply dead — never matched, its rates never offered.
108
-
109
- - **`continents: ["EU"]`** spares you a 51-country list — and `EU` is the *continent* Europe, not the European Union.
110
- - **`rest_of_world: true`** is the catch-all — there is **no country code meaning "everywhere"**, and improvised ones (`["*"]`, `["ALL"]`, alpha-3 `["USA"]`) fail **400**. The narrower scope is `countries: ["IL", "DE"]`; matching is country + state only.
111
- - The matched location supplies the taxes too (`tax_groups`, `shipping_tax`) and `free_over` zeroes a rate above a subtotal — reference below.
112
-
113
- **The catch-all trap.** Passing any `locations` suppresses the seeded worldwide fallback, so scoped locations with nothing behind them answer `shipping_not_available` to every other address. Plausibly intended, so it warns rather than fails: **read `warnings` on every seed** and either add a `rest_of_world` location or state the restriction to the user. If the brief named tiered rates, price a cart against one address per zone to check them (`set-shipping-address` → `available_shipping_methods`; `[]` means no location matched).
114
-
115
- Continent codes in full, state regions, tax binding, VAT-on-shipping, day-2 edits: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
116
-
117
- ## Images
118
-
119
- **The URL you seed is the URL the store serves.** Resolve each to its final, permanent URL before seeding: the app's image generation, or `base44.integrations.Core.UploadFile({ file })` → public URL. A temporary/**signed** URL (expiry params in the query string are the tell) must be re-hosted via `UploadFile` first — the failure is silent at seed time and visible only in the store. Cheap insurance before seeding: fetch one or two of the URLs and check they answer 200 with an image content type. Seeding imageless and patching later is allowed but is open debt; close it before handover.
120
-
121
- ## Payments — the decision
122
-
123
- Online card payments are **off by default**: the seeded store takes the manual `offline` method and is complete and payable — the order goes on-hold and `/order-received` renders the gateway's payment instructions, no code, no credentials. **Enable `card` only if a provider is wired in the same stretch of work**: an enabled card option with nothing behind it answers **`503 no_card_payment_provider`** the moment a customer picks it.
124
-
125
- `payment_methods` is the on/off switch — the listed slugs are enabled, every other row disabled. ⚑ **Never write `enabled` on the `commerce.PaymentGateway` row instead.** **Seeding need not happen all at once:** every key is independent, so re-call `commerce/seed-store` whenever one slice of configuration changes, carrying only that slice — `{ payment_methods: ["offline", "card"] }` is a complete standalone call, as valid on a live store weeks later as during the install (`store_name` is required only on a store's first seed).
126
-
127
- | The request says | Decision |
128
- |---|---|
129
- | A provider is named ("use Stripe") | copy the provider file, enable `card` — whenever convenient |
130
- | Selling online implied, no provider named | raise the provider **after** the store works |
131
- | Paid another way (transfer, COD, invoice, pickup) | nothing to do — the default is exactly this |
132
- | Payments not mentioned | leave the default, **state it at handover** |
133
-
134
- *Cards only:* **if it is Stripe the code is already written** — copy `base44/shared/commerce/card-payment.stripe.ts` over `base44/shared/commerce/card-payment.ts` **whole** (`fs.copyFileSync`; a partial edit breaks every function's deploy), then enable via `payment_methods`. Nothing needs filling in. ⚑ **That is the code, not a connection** — the app is not connected to Stripe until the user authorises it and its secret key reaches the app's secrets, which only they can do. Say the card path is *ready* and ask them to connect Stripe; never report it as connected, and never ask for a key in chat. Any other provider: [`../references/online-payments.md`](../references/online-payments.md).
135
-
136
- ## Done — forget this file
137
-
138
- - [ ] Seed returned success — real products, final permanent image URLs; slugs recorded and pages link by them.
139
- - [ ] `warnings` is empty, or every warning is deliberate and stated to the user.
140
- - [ ] Shipping expressed in `locations` (catch-all present if the store ships worldwide); named tiers asserted.
141
- - [ ] `coupons` seeded only if a coupon field exists ([`./02-storefront.md`](./02-storefront.md)).
142
- - [ ] Cards off, or on with the provider file copied whole.
143
-
144
- Then copy this file's `carry_forward` lines (in its front matter) into your working notes, and do not re-read this file.