@base44/app-plugin-commerce 0.1.17 → 0.1.18

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 CHANGED
@@ -8,7 +8,7 @@ It provides a full-featured **commerce data model and behavior** (variant-driven
8
8
 
9
9
  - **20 entities** — Products (a product sells variants when it carries attributes; no type field), variations, categories, ribbons, attributes + values, reviews, orders (embedded line/shipping/tax/fee/coupon lines), order notes, refunds, coupons, customers, Shipping & Tax Locations (shipping rates + tax groups per location), payment gateways, store settings, webhooks + deliveries, carts, download permissions, email log.
10
10
  - **16 backend functions** — 9 admin (`commerce/admin-products`, `commerce/admin-orders`, `commerce/admin-refunds`, `commerce/admin-coupons`, `commerce/admin-customers`, `commerce/admin-reviews`, `commerce/admin-webhooks`, `commerce/admin-reports`, `commerce/admin-tools`), 4 storefront (`commerce/storefront-catalog`, `commerce/storefront-cart`, `commerce/storefront-checkout`, `commerce/storefront-account`), 2 payment (`commerce/payments`, `commerce/payment-webhook`), and an idempotent `commerce/seed-store` — one call seeds the business defaults **and the whole catalog** (products with attributes in, variants/categories/taxonomy created internally).
11
- - **Online card payments, order side premade** — checkout routing, payment links for unpaid orders, two idempotent confirmation paths (customer return + webhook, both premade) and refund records are all built. Wiring a provider (Stripe, PayPal, a local PSP…) means implementing **four functions in one file** `base44/shared/commerce/card-payment.ts` and nothing else; a complete Stripe implementation to paste in ships in [`skills/commerce/post-installation.md`](./skills/commerce/post-installation.md) §4.2. **Cards are off by default**: the seed leaves the `card` gateway disabled and the manual `offline` method on — enable cards only with a provider wired, or about to be, or checkout answers `503 no_card_payment_provider` (§4.1 — the rule, plus when in a build to raise it). 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).
11
+ - **Payments: manual methods work out of the box; online cards are opt-in** — the seed enables the manual `offline` method (bank transfer, cash on delivery, pickup: on-hold + instructions, no code) and leaves the `card` gateway **disabled**. **No payment provider ships with the template.** The order side of card payments *is* premade — checkout routing, payment links for unpaid orders, two idempotent confirmation paths (customer return + webhook) and refund records so a store that opts in wires any provider (Stripe, PayPal, a local PSP…) by implementing **four functions in one file**, `base44/shared/commerce/card-payment.ts`, and nothing else; enable the gateway only with that done, or checkout answers `503 no_card_payment_provider`. The rule and timing: [`skills/commerce/post-installation.md`](./skills/commerce/post-installation.md) §4.1. Provider rules and a complete Stripe implementation to paste, for that one common choice: [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md). The admin can add more manual methods in Settings → Payments.
12
12
  - **Shared commerce engine** (`base44/shared/commerce/`) — totals, tax, shipping, coupons, stock, order lifecycle, webhook dispatch (HMAC-signed), emails, card-payment plumbing, plus static country/currency/continent data.
13
13
  - **Admin UI** (`src/commerce/admin/`) — a React/Tailwind/shadcn admin with a familiar store back-office information architecture: dashboard, orders, products, coupons, customers, reports, and full settings including webhooks. Admin-role gated.
14
14
  - **Storefront helpers** (`src/commerce/utils/`) — framework-free, dependency-free modules for the shopfront you build: `storefront.js` is the API client (`createStorefront(base44)` — cart-token lifecycle, cached store-info, catalog/cart/checkout/return-page calls); `variants.js` maps an attribute selection (Size, Color) onto a `ProductVariation` and back, plus per-option availability and variant price ranges; `shipping-promos.js` reads the store's real free-shipping configuration so "Free shipping over €150" copy states a configured rule rather than an invented number. See [`skills/commerce/references/storefront-product-page.md`](./skills/commerce/references/storefront-product-page.md).
@@ -102,7 +102,7 @@ If you build on Base44's hosted platform, use the Base44 agent/MCP to write the
102
102
  ## What's NOT included
103
103
 
104
104
  - **No visitor/storefront UI.** The storefront **API** is complete (`commerce/storefront-*` functions); building the shopfront is up to you — see [`skills/commerce/docs/api-storefront.md`](./skills/commerce/docs/api-storefront.md). What *does* ship for the storefront is **logic, not looks**: [`src/commerce/utils/`](./src/commerce/utils/) (framework-free API client + variant-selection functions — map a Size/Color selection to a `ProductVariation` and back, per-option availability, variant price ranges) and [`src/commerce/storefront/`](./src/commerce/storefront/) (React hooks + headless pickers for the shared cart, the guided checkout with automatic shipping/tax recalculation, and the `/order-received` page — no visual components), plus [`skills/commerce/references/product-render.md`](./skills/commerce/references/product-render.md) (what to render in a grid vs. a product page, and which fields each call returns) and [`skills/commerce/references/storefront-product-page.md`](./skills/commerce/references/storefront-product-page.md), the variant rules that go with the helpers.
105
- - **No payment provider — and cards are off by default.** The order side of card payments is premade (see above), but charging a card needs a provider, so `commerce/seed-store` enables the manual **`offline`** method (bank transfer, cash on delivery, pickup — no code, no credentials) and leaves the **`card`** gateway **switched off**. **Enable cards only if a provider is wired, or is about to be** — implement the four functions in `base44/shared/commerce/card-payment.ts` (Stripe: paste-in in `skills/commerce/post-installation.md` §4.2) and enable the gateway via the seed's `payment_methods: ["offline", "card"]`; enabled with nothing behind it, checkout answers `503 no_card_payment_provider`. The rule and why it belongs at the end of a build rather than its start: `skills/commerce/post-installation.md` §4.1.
105
+ - **No payment provider — and cards are off by default.** The order side of card payments is premade (see above), but charging a card needs a provider, so `commerce/seed-store` enables the manual **`offline`** method (bank transfer, cash on delivery, pickup — no code, no credentials) and leaves the **`card`** gateway **switched off**. **Enable cards only if a provider is wired, or is about to be** — implement the four functions in `base44/shared/commerce/card-payment.ts` (rules + a Stripe implementation to paste: `skills/commerce/references/online-payments.md`) and enable the gateway via the seed's `payment_methods: ["offline", "card"]`; enabled with nothing behind it, checkout answers `503 no_card_payment_provider`. The rule and why it belongs at the end of a build rather than its start: `skills/commerce/post-installation.md` §4.1.
106
106
  - **No scheduled workflows shipped.** Base44 *does* have a scheduler, but this template ships no workflow files — time-based jobs (stock-hold release, cart expiry, webhook-log pruning) run **opportunistically** where possible, and for the rest you (or the Base44 agent) create scheduled workflows that call `commerce/admin-tools`/`commerce/admin-orders` actions — see *Scheduled work* in [`skills/commerce/SKILL.md`](./skills/commerce/SKILL.md).
107
107
 
108
108
  ## Next steps
@@ -8,9 +8,13 @@
8
8
  * refundCardPayment → admin refunds through the provider
9
9
  * parseWebhook → validate the provider's server-to-server event
10
10
  *
11
- * Any provider works; a ready-made reference implementation (Stripe) is in
12
- * `.agents/skills/commerce/post-installation.md` §4 to paste over this
13
- * file another provider implements the same functions against its own API.
11
+ * NO PROVIDER SHIPS WITH THE TEMPLATE, and card payments are off by default
12
+ * (the `card` gateway is seeded disabled) this file stays stubs unless a
13
+ * store deliberately opts into online cards. When one does, any provider
14
+ * works: `.agents/skills/commerce/references/online-payments.md` has the
15
+ * rules, plus a complete Stripe implementation to paste over this file if
16
+ * Stripe is the provider chosen. Another provider implements the same four
17
+ * functions against its own API.
14
18
  *
15
19
  * REPLACE THIS FILE WHOLE (one write of the full new content). Never
16
20
  * find_replace into these stubs: a partial patch leaves the originals behind
@@ -4,10 +4,10 @@
4
4
  * The provider-specific half lives in exactly ONE file an agent implements
5
5
  * when wiring a provider: `shared/commerce/card-payment.ts` — four functions
6
6
  * (create a payment page, check it was paid, refund it, validate a webhook
7
- * event). Any provider works;
8
- * a reference implementation (Stripe) is in
9
- * `.agents/skills/commerce/post-installation.md` §4, and the
10
- * provider-agnostic rules are in
7
+ * event). No provider ships with the template and the `card` gateway is
8
+ * seeded disabled, so this half is dormant until a store opts into online
9
+ * cards. Any provider works; the rules — and a complete Stripe
10
+ * implementation, for that one common choice — are in
11
11
  * `.agents/skills/commerce/references/online-payments.md`.
12
12
  *
13
13
  * Everything here — return URLs, storing the payment reference on the order,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44/app-plugin-commerce",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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",
@@ -15,7 +15,7 @@ Operational guidance for extending, operating and building on the Base44 Commerc
15
15
 
16
16
  ## IMPORTANT — first-time installation
17
17
 
18
- If the template was just installed (or you are installing it right now), read [`.agents/skills/commerce/post-installation.md`](./post-installation.md) **before anything else — and, unless the user has a special requirement, read nothing else**: it alone covers embedding the admin pages, the three-layer admin-role enforcement (do not weaken), seeding the store's data — **one `commerce/seed-store` call takes the whole catalog** (products with attributes; variants, categories, ribbons and Shipping & Tax Locations created internally — §3) — **card payments as an optional, late step** (§4.1: off by default — the seed enables offline payment and leaves cards off; enable cards only with a provider wired or about to be, and raise it at the end of the install, never its start — then §4.2's complete Stripe implementation to paste over `shared/commerce/card-payment.ts`; the payment webhook is premade), and the **storefront quick start** (§2): logic-only chunks for product list → product page → optional cart → checkout. Its §0 schedules the whole install: **storefront components are written while image generation and the seed call (parallel writes — usually a few seconds) run — never after them, and never behind a payment-provider round-trip**. The references below and the API docs are for requests that go beyond that happy path, not for the install. The full install-from-scratch steps are in [`.agents/skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
18
+ If the template was just installed (or you are installing it right now), read [`.agents/skills/commerce/post-installation.md`](./post-installation.md) **before anything else — and, unless the user has a special requirement, read nothing else**: it alone covers embedding the admin pages, the three-layer admin-role enforcement (do not weaken), seeding the store's data — **one `commerce/seed-store` call takes the whole catalog** (products with attributes; variants, categories, ribbons and Shipping & Tax Locations created internally — §3) — **card payments as an optional, late step** (§4.1: off by default — the seed enables offline payment and leaves cards off; **no payment provider ships with the kit**, so cards mean deliberately wiring one, raised at the end of the install and never at its start; §4.2 is the four-step how-to, and the provider code itself lives in [`references/online-payments.md`](./references/online-payments.md) — read it only if the store opts in), and the **storefront quick start** (§2): logic-only chunks for product list → product page → optional cart → checkout. Its §0 schedules the whole install: **storefront components are written while image generation and the seed call (parallel writes — usually a few seconds) run — never after them, and never behind a payment-provider round-trip**. The references below and the API docs are for requests that go beyond that happy path, not for the install. The full install-from-scratch steps are in [`.agents/skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
19
19
 
20
20
  ## Working on the UI
21
21
 
@@ -36,7 +36,7 @@ Agents keep shipping storefronts that miss these, and each one breaks buying out
36
36
 
37
37
  2. **Checkout must recalculate shipping from the address and send a choice.** `useCheckout` does all of it: it calls `set-shipping-address` automatically once the address is complete (recalculating options, cost and tax; `400 shipping_not_available` → its `addressError`), resolves `shipping_status`, and blocks `placeOrder` until the choice is made. Hand-rolled flows must do the same by hand: call `set-shipping-address` as soon as the customer provides an address, then read `shipping_status` on the returned cart — `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **show `available_shipping_methods` and call `choose-shipping-method`** · `missing_address` → collect the address first (a single-location store shows its options even before one). `place-order` refuses with `400 shipping_method_required` until then — that is not a bug to work around. (`chosen_shipping_method` on the cart is the rate **id**, a string — display it by looking up its entry's `title`/`cost`, never by rendering the id.)
38
38
 
39
- 3. **Handle card payments if the store offers them, and build `/order-received` either way.** Render whatever gateways `get-store-info` reports — a default-seeded store offers `offline` only (cards are off by default, and enabled only with a provider wired: [`post-installation.md` §4.1](./post-installation.md#41-off-by-default--enable-only-with-a-provider-wired)), so never hardcode a card option. Choosing the card gateway returns `payment.checkout_url` — `useCheckout().placeOrder` redirects there for you (until a payment provider is implemented — one file, Stripe paste-in in [`post-installation.md` §4](./post-installation.md#42-wiring-a-provider--one-file) — it answers `503 no_card_payment_provider`; offer the other methods). Every payment link comes back to `/order-received`, which **you must implement**: one `useOrderReturn()` call rendered by `status` (hand-rolled: `commerce/payments` `complete-return` with the query params). Without that page a customer pays into a 404 and the order is never marked paid. Also persist the `cart_token` from **every** cart response (the provider/client do this) — `add-item` silently starts a fresh cart when the stored token is stale.
39
+ 3. **Handle card payments if the store offers them, and build `/order-received` either way.** Render whatever gateways `get-store-info` reports — a default-seeded store offers `offline` only (cards are off by default, and enabled only with a provider wired: [`post-installation.md` §4.1](./post-installation.md#41-off-by-default--enable-only-with-a-provider-wired)), so never hardcode a card option. Choosing the card gateway returns `payment.checkout_url` — `useCheckout().placeOrder` redirects there for you (with no provider implemented — one file, [`post-installation.md` §4.2](./post-installation.md#42-wiring-a-provider--one-file) — it answers `503 no_card_payment_provider`; offer the other methods). Every payment link comes back to `/order-received`, which **you must implement**: one `useOrderReturn()` call rendered by `status` (hand-rolled: `commerce/payments` `complete-return` with the query params). Without that page a customer pays into a 404 and the order is never marked paid. Also persist the `cart_token` from **every** cart response (the provider/client do this) — `add-item` silently starts a fresh cart when the stored token is stale.
40
40
 
41
41
  4. **Never advertise what isn't configured — and never configure what can't be reached.** "Free shipping over €150" must come from a real shipping rate that is free or carries a `free_over` threshold on a Shipping & Tax Location. Locations are admin-only data, so a storefront cannot read them: the live answer is the cart's `available_shipping_methods` after the address is set, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner. **Coupons are the same rule in reverse:** codes are admin-only data that a customer can only use by typing them, so a store with coupons needs a code field — `useCart().applyCoupon`, in the cart or, when there's no cart page, in the checkout — rendering `discount_total` and invalid codes inline. No field means no coupons: don't seed them, don't name a code in the copy ([`post-installation.md` §2.3/§2.4](./post-installation.md#23-cart)).
42
42
 
@@ -57,7 +57,7 @@ Open the matching file under `.agents/skills/commerce/references/` only when a t
57
57
  | Variant selection | attribute-level selectors, resolving a selection to a variation, availability states, incomplete-selection pricing, add-to-cart contract | [`references/storefront-product-page.md`](./references/storefront-product-page.md) |
58
58
  | Reviews | stars on cards and the product page, the review list + submit form (public by email, backend ships complete), the auto-approve toggle, UI-enforced policies (login-gated, verified-only, required rating) | [`references/reviews.md`](./references/reviews.md) |
59
59
  | Admin product form | changing the product editor — its stacked sections are **Price & Inventory** (tax group, then the attributes, then a row per variant, or a single *Base price* row when there are none), **Modifiers** (`meta_data`), **Downloads**, **Linked products**; one **Visible** toggle drives `status`. Variants reconcile from the attribute values automatically: no generate step, no per-variant delete. Weight and dimensions are per variant. | [`references/admin-product-form.md`](./references/admin-product-form.md) |
60
- | Online payments | **any storefront or checkout work** — the order side (checkout, confirmation, payment links, refund records) *and* the payment webhook are premade; wiring a provider means implementing the four functions in one file (`shared/commerce/card-payment.ts`; Stripe = paste [`post-installation.md` §4](./post-installation.md#42-wiring-a-provider--one-file)) | [`references/online-payments.md`](./references/online-payments.md) |
60
+ | Online payments | a store that has **opted into card payments** (they are off by default and no provider ships) — the order side (checkout, confirmation, payment links, refund records) *and* the payment webhook are premade; wiring a provider means implementing the four functions in one file (`shared/commerce/card-payment.ts`), and this reference holds the rules plus a complete Stripe implementation | [`references/online-payments.md`](./references/online-payments.md) |
61
61
  | Scheduled work | recurring maintenance — stock-hold release, abandoned-cart cleanup, webhook-log pruning, counter-drift repair | [`references/scheduled-work.md`](./references/scheduled-work.md) |
62
62
  | Emails | transactional order emails, per-type overrides, deliverability, the email log | [`references/emails.md`](./references/emails.md) |
63
63
  | Webhooks | outbound webhooks, HMAC signing, delivery log, auto-disable behavior | [`references/webhooks.md`](./references/webhooks.md) |
@@ -36,7 +36,7 @@ The backend is considerably richer than a minimal "grid → cart → pay" shop.
36
36
  | **Digital products** | entitlement-checked downloads with remaining-count, expiry, and signed URLs for private files | [`get-download`](#commercestorefront-account) |
37
37
  | **Store config** | currency (format it with `Intl.NumberFormat`), units, catalog/cart price display — read it and honour it instead of hardcoding | [`get-store-info`](#get-store-info) |
38
38
 
39
- Two things that are **not** in the backend and are yours to build: the visitor UI itself, and the payment provider integration four functions in one file (Stripe = paste [`post-installation.md` §4](../post-installation.md#42-wiring-a-provider--one-file); other providers: [`references/online-payments.md`](../references/online-payments.md)). Payment methods and the currency are admin-owned data — always render them from `get-store-info` rather than a hardcoded list, and format prices with `Intl.NumberFormat(undefined, { style: "currency", currency })`.
39
+ Two things that are **not** in the backend and are yours to build: the visitor UI itself, and — only for a store that opts into card payments, which are off by default with no provider shipped — the payment provider integration: four functions in one file ([`post-installation.md` §4.2](../post-installation.md#42-wiring-a-provider--one-file) for the steps, [`references/online-payments.md`](../references/online-payments.md) for the provider code). Payment methods and the currency are admin-owned data — always render them from `get-store-info` rather than a hardcoded list, and format prices with `Intl.NumberFormat(undefined, { style: "currency", currency })`.
40
40
 
41
41
  ## Conventions
42
42
 
@@ -1,10 +1,10 @@
1
1
  # Post-installation
2
2
 
3
- What to do right after the static installation ([`installation-guidelines.md`](./installation-guidelines.md)): embed the admin pages into the app, build the storefront from the quick start below, and seed the store's data. Online card payments are **optional and off by default** — [§4](#4-payments--optional-per-store-one-file-when-needed-stripe-as-the-reference) has the rule (enable them only with a provider wired) and the wiring; they are never the opening move. Installed into the app at `.agents/skills/commerce/post-installation.md`.
3
+ What to do right after the static installation ([`installation-guidelines.md`](./installation-guidelines.md)): embed the admin pages into the app, build the storefront from the quick start below, and seed the store's data. Online card payments are **optional and off by default** — [§4](#4-payments--optional-cards-off-by-default) has the rule (enable them only with a provider wired) and the wiring; they are never the opening move. Installed into the app at `.agents/skills/commerce/post-installation.md`.
4
4
 
5
- > **This file is intentionally the whole job.** Unless the user has a special requirement (a non-Stripe payment provider, signature-verified webhooks, filters/reviews/accounts beyond the happy path), do **not** open the `references/` guides or the `docs/` API references — everything needed to ship a working store (admin mount, the storefront — catalog pages, optional cart, checkout, order-received — seeding, and card payments for the stores that need them) is on this page, ending with the [Done when](#5-done-when) checklist. Reading more first just burns time.
5
+ > **This file is intentionally the whole job.** Unless the user has a special requirement (wiring a card payment provider, signature-verified webhooks, filters/reviews/accounts beyond the happy path), do **not** open the `references/` guides or the `docs/` API references — everything needed to ship a working store (admin mount, the storefront — catalog pages, optional cart, checkout, order-received — seeding, and card payments for the stores that need them) is on this page, ending with the [Done when](#5-done-when) checklist. Reading more first just burns time.
6
6
 
7
- > **Payments, in short** (full rules in [§4](#4-payments--optional-per-store-one-file-when-needed-stripe-as-the-reference)): **online card payments are optional and off by default.** The seed enables the manual `offline` method, which needs no code or credentials, and leaves the `card` gateway disabled. **Enable cards only if you wire a payment provider — or are about to, in the same stretch of work** (§4.2: one file): an enabled card option with no provider behind it answers `503 no_card_payment_provider` at checkout. Choosing to add cards is fine and often right; just do it **late** — nothing else depends on it, so unless the user brought it up first, asking for a provider key before their store exists only stalls the build.
7
+ > **Payments, in short** (full rules in [§4](#4-payments--optional-cards-off-by-default)): **online card payments are optional and off by default.** The seed enables the manual `offline` method, which needs no code or credentials, and leaves the `card` gateway disabled. **Enable cards only if you wire a payment provider — or are about to, in the same stretch of work** (§4.2: one file): an enabled card option with no provider behind it answers `503 no_card_payment_provider` at checkout. Choosing to add cards is fine and often right; just do it **late** — nothing else depends on it, so unless the user brought it up first, asking for a provider key before their store exists only stalls the build.
8
8
 
9
9
  ## 0. Sequencing — build the UI while the slow work runs
10
10
 
@@ -397,7 +397,7 @@ A successful response means the data is in — the catalog and settings are live
397
397
 
398
398
  ---
399
399
 
400
- ## 4. Payments — optional per store; one file when needed (Stripe as the reference)
400
+ ## 4. Payments — optional; cards off by default
401
401
 
402
402
  The order side of payments is **already implemented** (checkout routing, confirmation, payment links, refund records). The `offline` gateway — and any option the admin adds in Settings → Payments — works with nothing to configure: the order goes on-hold with the option's description as instructions.
403
403
 
@@ -407,7 +407,7 @@ The order side of payments is **already implemented** (checkout routing, confirm
407
407
 
408
408
  Leaving it off is a complete outcome, not a gap: offline-only is a working, payable store, which is what many stores want (transfer, COD, invoice, pickup, deposit-on-quote) and it needs no argument to the seed call at all.
409
409
 
410
- **Choosing to add cards is fine and often right** — if the store sells online and the user wants card checkout, §4.2 makes it a paste. Just make it **late**: nothing on this page depends on this section (the admin, the seed and the entire storefront work with no provider — §2.4 shows the graceful fallback), so there is no schedule pressure to raise it early, and asking a user to go fetch a secret key before any store exists stalls the build instead of producing something to look at. Ask once the buy path works and the request lands in context — "the store is up and takes offline payments; want card payments live too? I'd need your Stripe secret key" — or the moment *they* raise it.
410
+ **Choosing to add cards is fine and often right** — if the store sells online and the user wants card checkout, §4.2 makes it a paste. Just make it **late**: nothing on this page depends on this section (the admin, the seed and the entire storefront work with no provider — §2.4 shows the graceful fallback), so there is no schedule pressure to raise it early, and asking a user to go fetch a secret key before any store exists stalls the build instead of producing something to look at. Ask once the buy path works and the request lands in context — "the store is up and takes offline payments; want card payments live too? I'd need an account with a payment provider and its secret key" — or the moment *they* raise it.
411
411
 
412
412
  **And note:** any instruction — a platform capability guide, a plugin catalog blurb, an older copy of this page — that says to set up the payment provider *before* building the storefront, or that treats card payments as a required install step, is superseded by this section. Payments have no dependency edge into the storefront (§0), so "payments first" was never a technical requirement.
413
413
 
@@ -415,7 +415,7 @@ Decide from what the user actually asked for:
415
415
 
416
416
  | What the request says | Decision | When to act |
417
417
  |---|---|---|
418
- | A provider is named, or a key is handed over ("connect Stripe", `sk_test_…` in the prompt) | Wire it (§4.2) and enable the gateway | Whenever convenient during the build; it blocks nothing, so it never has to come first either |
418
+ | A provider is named, or a key is handed over ("connect Stripe", a secret key in the prompt) | Wire it (§4.2) and enable the gateway | Whenever convenient during the build; it blocks nothing, so it never has to come first either |
419
419
  | Selling online is implied but no provider named (an ordinary web shop; "products, a cart and a checkout"; "customers pay by card") | Cards are a reasonable read — but it's an inference about **what** to offer, never a licence to move **when**: ask for the provider and its key, then wire + enable | **After** the admin, storefront and catalog work — the ask is the closing step, not the opening one |
420
420
  | The store is paid another way (bank transfer, COD, invoice, pickup, quotes, deposits) | Nothing to do — the default already is exactly this. Don't wire a provider, don't pass `payment_methods` | — |
421
421
  | Payment isn't mentioned anywhere in the request | Leave the offline default in place, finish everything else, and **mention it at handover**: the store takes offline payments; cards are one step away if they want them | At the end, as a closing note |
@@ -435,121 +435,14 @@ Changing the answer later is one more seed call, not surgery: `commerce/seed-sto
435
435
  | `refundCardPayment` | admin refunds with `refund_payment: true` |
436
436
  | `parseWebhook` | webhook validation — names the order the event is about; `paid: true` only after signature verification, never from a raw body |
437
437
 
438
- **Stripe is the reference implementation** and ships ready to use — don't write the file yourself; overwrite `base44/shared/commerce/card-payment.ts` with this as-is, **as one whole-file write. Never `find_replace` into the stub**: a partial patch leaves the original stubs behind and breaks every commerce function's deploy with duplicate-export bundle errors (`Multiple exports with the same name "createCardPayment"` → rewrite the file whole). For another provider, implement the same four functions against its API instead (same shape: hosted page in, paid-check and refund by `reference`, event naming an order — rules in [`references/online-payments.md`](./references/online-payments.md)):
438
+ Four steps, whichever provider it is:
439
439
 
440
- ```ts
441
- // base44/shared/commerce/card-payment.tsStripe implementation
442
- import Stripe from "npm:stripe@18";
443
- import { HttpError } from "./auth.ts";
440
+ 1. **Implement the four functions** in `base44/shared/commerce/card-payment.ts` against the provider's API. **No provider ships with the template** — the file arrives as stubs, and [`references/online-payments.md`](./references/online-payments.md) holds the per-provider rules plus a complete **Stripe** implementation to paste if Stripe is the provider the store chose. Write the file **whole, in one write. Never `find_replace` into the stubs**: a partial patch leaves the originals behind and breaks every commerce function's deploy with duplicate-export bundle errors (`Multiple exports with the same name "createCardPayment"` → rewrite the file whole).
441
+ 2. **Store the provider's API credential as a backend app secret** (read with `Deno.env.get(...)`never in code, never in an entity). Test credentials work end to end. This is the only step that needs the user, so it sets the timing of the whole section (§4.1): ask when the store is standing, not in the install's first message — and if the answer takes a while, keep the rest of the work moving rather than idling on it.
442
+ 3. **Register the premade webhook URL** — `https://<app-domain>/functions/commerce/payment-webhook` — with the provider, for its "payment succeeded" event, so orders are confirmed even when the buyer pays and closes the tab. There is **no signing secret to store**: events are treated as nudges and verified against the provider's API. (Per-provider registration calls are in the reference; the user can also do it in the provider's dashboard.)
443
+ 4. **Enable the gateway** — the `card` row is seeded **off** (§4.1), so the file alone changes nothing a customer sees. Call `commerce/seed-store` with `{ payment_methods: ["offline", "card"] }` (or `["card"]` for card-only) — safe on a seeded store: no catalog needed, products are skipped, and it converges every gateway row to that set. Skipping this step is the usual reason a freshly wired provider "doesn't show up at checkout"; doing it *without* steps 1–2 is what produces `503 no_card_payment_provider`.
444
444
 
445
- export interface CardPaymentPage {
446
- url: string; // where the customer goes to pay
447
- reference: string; // the provider's id for this payment, stored on the order
448
- }
449
-
450
- const stripe = () => {
451
- const key = Deno.env.get("STRIPE_SECRET_KEY");
452
- if (!key) {
453
- throw new HttpError(503, "Card payments are not configured — the STRIPE_SECRET_KEY secret is missing.", "no_card_payment_provider");
454
- }
455
- return new Stripe(key);
456
- };
457
-
458
- // Stripe amounts are in minor units; these currencies have none.
459
- const ZERO_DECIMAL = new Set(["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"]);
460
- const minorUnits = (amount: number, currency: string) =>
461
- Math.round(Number(amount) * (ZERO_DECIMAL.has(String(currency).toUpperCase()) ? 1 : 100));
462
-
463
- export async function createCardPayment(
464
- _sr: any,
465
- order: any,
466
- opts: { successUrl: string; cancelUrl: string; customerEmail?: string },
467
- ): Promise<CardPaymentPage> {
468
- // order_id + order_key in the metadata is how the premade payment-webhook
469
- // names the order when Stripe's event arrives — keep it on both objects.
470
- const metadata = { order_id: String(order.id), order_key: String(order.order_key) };
471
- const session = await stripe().checkout.sessions.create({
472
- mode: "payment",
473
- line_items: [{
474
- quantity: 1,
475
- price_data: {
476
- currency: String(order.currency || "USD").toLowerCase(),
477
- product_data: { name: `Order #${order.order_number}` },
478
- unit_amount: minorUnits(order.total, order.currency),
479
- },
480
- }],
481
- customer_email: opts.customerEmail || undefined,
482
- metadata,
483
- payment_intent_data: { metadata },
484
- success_url: opts.successUrl,
485
- cancel_url: opts.cancelUrl,
486
- });
487
- if (!session.url) throw new HttpError(502, "Stripe did not return a payment page URL.", "payment_session_failed");
488
- return { url: session.url, reference: session.id };
489
- }
490
-
491
- export async function checkCardPaymentPaid(_sr: any, order: any, reference: string): Promise<boolean> {
492
- const session = await stripe().checkout.sessions.retrieve(reference);
493
- // The payment must be for THIS order — stops a reference to some other
494
- // (paid) session being replayed against a different order.
495
- return session.payment_status === "paid" && session.metadata?.order_id === String(order.id);
496
- }
497
-
498
- export async function refundCardPayment(_sr: any, _order: any, opts: {
499
- reference: string; amount: number; currency: string; reason?: string;
500
- }): Promise<{ refund_id: string }> {
501
- const session = await stripe().checkout.sessions.retrieve(opts.reference);
502
- if (!session.payment_intent) {
503
- throw new HttpError(409, "This payment has no charge to refund at Stripe.", "no_charge_to_refund");
504
- }
505
- const refund = await stripe().refunds.create({
506
- payment_intent: String(session.payment_intent),
507
- amount: minorUnits(opts.amount, opts.currency),
508
- });
509
- return { refund_id: refund.id };
510
- }
511
-
512
- /** What parseWebhook distills an event into — the premade webhook's contract. */
513
- export interface CardWebhookEvent {
514
- order_id: string;
515
- order_key: string;
516
- paid: boolean; // true only after signature verification — never from a raw body
517
- reference?: string; // only if signature-verified; otherwise the order's stored reference is used
518
- }
519
-
520
- /**
521
- * Stripe webhook events. The event body is never trusted: this only names
522
- * the order (from the metadata createCardPayment attached) and returns
523
- * paid: false, so the premade webhook verifies against Stripe's API through
524
- * checkCardPaymentPaid — a forged call can never mark an order paid, and no
525
- * signing secret is needed. (Optional fast path that skips the API
526
- * round-trip: verify the signature instead — references/online-payments.md.)
527
- */
528
- export async function parseWebhook(_req: Request, payload: string): Promise<CardWebhookEvent | null> {
529
- let event: any;
530
- try { event = JSON.parse(payload); } catch { return null; }
531
- const metadata = event?.data?.object?.metadata;
532
- if (!metadata?.order_id || !metadata?.order_key) return null;
533
- return { order_id: String(metadata.order_id), order_key: String(metadata.order_key), paid: false };
534
- }
535
- ```
536
-
537
- Then three steps and payments are done (every provider follows this same shape — an API-credential secret, the premade webhook URL registered, and the gateway switched on; only the Stripe specifics below vary):
538
-
539
- 1. **Secret**: ask the user for their Stripe **secret key** and store it as the `STRIPE_SECRET_KEY` app secret (backend env — never in code, never in an entity). Test keys (`sk_test_…`) work end to end. This is the only step that needs the user, so it sets the timing of the whole section (§4.1): ask when the store is standing, not in the install's first message — and if the answer takes a while, keep the rest of the work moving rather than idling on it.
540
- 2. **Webhook endpoint** — so orders are confirmed even when the buyer pays and closes the tab: register `https://<app-domain>/functions/commerce/payment-webhook` with Stripe for the `checkout.session.completed` event. There is **no signing secret to store** — events are treated as nudges and verified against Stripe's API. Registration is one call with the same secret key (or the user can do it in the Stripe dashboard):
541
-
542
- ```js
543
- await fetch("https://api.stripe.com/v1/webhook_endpoints", {
544
- method: "POST",
545
- headers: { Authorization: `Bearer ${Deno.env.get("STRIPE_SECRET_KEY")}`, "Content-Type": "application/x-www-form-urlencoded" },
546
- body: new URLSearchParams({ url: "https://<app-domain>/functions/commerce/payment-webhook", "enabled_events[]": "checkout.session.completed" }),
547
- });
548
- ```
549
-
550
- 3. **Enable the gateway** — the `card` row is seeded **off** (§4.1), so the file alone changes nothing a customer sees. Call `commerce/seed-store` with `{ payment_methods: ["offline", "card"] }` (or `["card"]` for card-only) — safe on a seeded store: no catalog needed, products are skipped, and it converges every gateway row to that set. Skipping this step is the usual reason a freshly wired provider "doesn't show up at checkout"; doing it *without* steps 1–2 is what produces `503 no_card_payment_provider`.
551
-
552
- That's it — checkout redirect, `/order-received` confirmation, the webhook, the admin's "Check payment" button, payment links and provider refunds all run through this one file. **Nothing else to read or edit**: [`references/online-payments.md`](./references/online-payments.md) is only for *other* providers or signature-verified webhooks.
445
+ That's it checkout redirect, `/order-received` confirmation, the webhook, the admin's "Check payment" button, payment links and provider refunds all run through this one file.
553
446
 
554
447
  ---
555
448
 
@@ -13,7 +13,7 @@ What it does **not** ship with is a payment provider. Wiring one (Stripe, PayPal
13
13
  | `refundCardPayment` | admin refunds through the provider (optional — leave the stub to keep refunds manual) |
14
14
  | `parseWebhook` | webhook validation: name the order a provider event is about (from the echoed metadata), and vouch `paid: true` **only** after verifying the request signature over the raw body bytes |
15
15
 
16
- **Wiring Stripe? Don't start here** [`post-installation.md` §4](../post-installation.md#42-wiring-a-provider--one-file) has the complete implementation of all four functions to paste in, plus the secret and webhook-endpoint steps. This reference is for other providers and for webhook customization.
16
+ **Wiring a payment provider?** Whichever one the store chose, the job is the same: implement those four functions against its API, following the rules on this page. As a worked example, [a complete Stripe implementation](#reference-implementation--stripe) sits at the bottom of the page with its secret and webhook-endpoint steps usable as-is if Stripe happens to be the provider, and as a model of the four contracts if it isn't. It is a reference for one common choice, **not a bundled provider**: no provider ships with the template.
17
17
 
18
18
  The card option ships **disabled**, so on a default store none of this is customer-visible. Where the gateway has been enabled without the file, picking Credit card at checkout answers `503 no_card_payment_provider` (the storefront should offer the other methods) — implement the file, or switch the option back off in Settings → Payments. Every other payment option is **manual**: the order goes on-hold with the option's description as payment instructions, and the operator moves it on once the money arrives — those need no code at all, and the admin can add more of them in Settings → Payments.
19
19
 
@@ -41,12 +41,130 @@ The card option ships **disabled**, so on a default store none of this is custom
41
41
 
42
42
  `parseWebhook(req, payload)` lives in `card-payment.ts` with the other three; `commerce/payment-webhook` (the function file) is premade and calls it with the raw request and the raw body — the exact bytes, so signature schemes work. It returns `CardWebhookEvent | null`:
43
43
 
44
- - **The simple, secure default — the nudge**: parse the event, read the `order_id`/`order_key` metadata `createCardPayment` attached, return `{ order_id, order_key, paid: false }`. No signing secret; the premade flow verifies via `checkCardPaymentPaid` against the provider's API, so forgery is impossible by construction. This is what the Stripe reference implementation does.
44
+ - **The simple, secure default — the nudge**: parse the event, read the `order_id`/`order_key` metadata `createCardPayment` attached, return `{ order_id, order_key, paid: false }`. No signing secret; the premade flow verifies via `checkCardPaymentPaid` against the provider's API, so forgery is impossible by construction. This is what the Stripe implementation below does.
45
45
  - **The signature-verified fast path** (optional): verify the provider's signature over the raw `payload` bytes (Stripe: `constructEventAsync` with a webhook signing secret) and return `paid: true` with the event's `reference` for a verified successful payment; the premade code then trusts it without the API round-trip. `paid: true` from an unverified body is the one way to break this design — never do it.
46
46
  - Return **`null`** for events that aren't about a payment for one of this store's orders; the function answers 200 so the provider doesn't retry.
47
47
 
48
48
  Everything after `parseWebhook` — order lookup, the `order_key` match, idempotent confirmation, order progression — is premade either way.
49
49
 
50
+ ## Reference implementation — Stripe
51
+
52
+ **A worked example, not a bundled provider**: the template ships with no provider at all, and Stripe is one option among many (PayPal, Adyen, a local PSP — the four functions are the same shape against any API). Paste this in when Stripe is the provider the store actually chose; read it as a model of the four contracts when the provider is something else. Either way, only once §4.1's rule says cards are wanted at all.
53
+
54
+ Overwrite `base44/shared/commerce/card-payment.ts` with this **as one whole-file write** (never `find_replace` into the stubs — see *Implementation rules* above):
55
+
56
+ ```ts
57
+ // base44/shared/commerce/card-payment.ts — Stripe implementation
58
+ import Stripe from "npm:stripe@18";
59
+ import { HttpError } from "./auth.ts";
60
+
61
+ export interface CardPaymentPage {
62
+ url: string; // where the customer goes to pay
63
+ reference: string; // the provider's id for this payment, stored on the order
64
+ }
65
+
66
+ const stripe = () => {
67
+ const key = Deno.env.get("STRIPE_SECRET_KEY");
68
+ if (!key) {
69
+ throw new HttpError(503, "Card payments are not configured — the STRIPE_SECRET_KEY secret is missing.", "no_card_payment_provider");
70
+ }
71
+ return new Stripe(key);
72
+ };
73
+
74
+ // Stripe amounts are in minor units; these currencies have none.
75
+ const ZERO_DECIMAL = new Set(["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"]);
76
+ const minorUnits = (amount: number, currency: string) =>
77
+ Math.round(Number(amount) * (ZERO_DECIMAL.has(String(currency).toUpperCase()) ? 1 : 100));
78
+
79
+ export async function createCardPayment(
80
+ _sr: any,
81
+ order: any,
82
+ opts: { successUrl: string; cancelUrl: string; customerEmail?: string },
83
+ ): Promise<CardPaymentPage> {
84
+ // order_id + order_key in the metadata is how the premade payment-webhook
85
+ // names the order when Stripe's event arrives — keep it on both objects.
86
+ const metadata = { order_id: String(order.id), order_key: String(order.order_key) };
87
+ const session = await stripe().checkout.sessions.create({
88
+ mode: "payment",
89
+ line_items: [{
90
+ quantity: 1,
91
+ price_data: {
92
+ currency: String(order.currency || "USD").toLowerCase(),
93
+ product_data: { name: `Order #${order.order_number}` },
94
+ unit_amount: minorUnits(order.total, order.currency),
95
+ },
96
+ }],
97
+ customer_email: opts.customerEmail || undefined,
98
+ metadata,
99
+ payment_intent_data: { metadata },
100
+ success_url: opts.successUrl,
101
+ cancel_url: opts.cancelUrl,
102
+ });
103
+ if (!session.url) throw new HttpError(502, "Stripe did not return a payment page URL.", "payment_session_failed");
104
+ return { url: session.url, reference: session.id };
105
+ }
106
+
107
+ export async function checkCardPaymentPaid(_sr: any, order: any, reference: string): Promise<boolean> {
108
+ const session = await stripe().checkout.sessions.retrieve(reference);
109
+ // The payment must be for THIS order — stops a reference to some other
110
+ // (paid) session being replayed against a different order.
111
+ return session.payment_status === "paid" && session.metadata?.order_id === String(order.id);
112
+ }
113
+
114
+ export async function refundCardPayment(_sr: any, _order: any, opts: {
115
+ reference: string; amount: number; currency: string; reason?: string;
116
+ }): Promise<{ refund_id: string }> {
117
+ const session = await stripe().checkout.sessions.retrieve(opts.reference);
118
+ if (!session.payment_intent) {
119
+ throw new HttpError(409, "This payment has no charge to refund at Stripe.", "no_charge_to_refund");
120
+ }
121
+ const refund = await stripe().refunds.create({
122
+ payment_intent: String(session.payment_intent),
123
+ amount: minorUnits(opts.amount, opts.currency),
124
+ });
125
+ return { refund_id: refund.id };
126
+ }
127
+
128
+ /** What parseWebhook distills an event into — the premade webhook's contract. */
129
+ export interface CardWebhookEvent {
130
+ order_id: string;
131
+ order_key: string;
132
+ paid: boolean; // true only after signature verification — never from a raw body
133
+ reference?: string; // only if signature-verified; otherwise the order's stored reference is used
134
+ }
135
+
136
+ /**
137
+ * Stripe webhook events. The event body is never trusted: this only names
138
+ * the order (from the metadata createCardPayment attached) and returns
139
+ * paid: false, so the premade webhook verifies against Stripe's API through
140
+ * checkCardPaymentPaid — a forged call can never mark an order paid, and no
141
+ * signing secret is needed. (Optional fast path that skips the API
142
+ * round-trip: verify the signature instead — references/online-payments.md.)
143
+ */
144
+ export async function parseWebhook(_req: Request, payload: string): Promise<CardWebhookEvent | null> {
145
+ let event: any;
146
+ try { event = JSON.parse(payload); } catch { return null; }
147
+ const metadata = event?.data?.object?.metadata;
148
+ if (!metadata?.order_id || !metadata?.order_key) return null;
149
+ return { order_id: String(metadata.order_id), order_key: String(metadata.order_key), paid: false };
150
+ }
151
+ ```
152
+
153
+ Then the credential and the webhook endpoint:
154
+
155
+ 1. **Secret**: the user's Stripe **secret key**, stored as the `STRIPE_SECRET_KEY` app secret (backend env — never in code, never in an entity). Test keys (`sk_test_…`) work end to end.
156
+ 2. **Webhook endpoint**: register `https://<app-domain>/functions/commerce/payment-webhook` with Stripe for `checkout.session.completed`. There is **no signing secret to store** — events are nudges, verified against Stripe's API. One call with the same secret key (or the user does it in the Stripe dashboard):
157
+
158
+ ```js
159
+ await fetch("https://api.stripe.com/v1/webhook_endpoints", {
160
+ method: "POST",
161
+ headers: { Authorization: `Bearer ${Deno.env.get("STRIPE_SECRET_KEY")}`, "Content-Type": "application/x-www-form-urlencoded" },
162
+ body: new URLSearchParams({ url: "https://<app-domain>/functions/commerce/payment-webhook", "enabled_events[]": "checkout.session.completed" }),
163
+ });
164
+ ```
165
+
166
+ Finally **enable the `card` gateway** — it is seeded off, so none of the above is visible at checkout until `commerce/seed-store` runs with `{ payment_methods: ["offline", "card"] }` ([`post-installation.md` §4.2](../post-installation.md#42-wiring-a-provider--one-file)).
167
+
50
168
  ## Storefront requirements (unchanged by any of this)
51
169
 
52
170
  - Redirect to `payment.checkout_url` when `place-order` returns `payment.status === "requires_payment"`.
@@ -205,13 +205,15 @@ export default function PaymentsSettings() {
205
205
  </div>
206
206
 
207
207
  {/* The "card" option redirects to a provider-hosted payment page.
208
- Wiring a provider means implementing the four functions in
209
- shared/commerce/card-payment.ts (Stripe: paste-in in
210
- .agents/skills/commerce/post-installation.md §4; the payment
211
- webhook is premade). Until then, picking it at checkout answers
212
- 503 no_card_payment_provider. Deliberately not shown to the
213
- store operator it's developer guidance, not store
214
- configuration. */}
208
+ It ships disabled: no provider comes with the template, and
209
+ wiring one means implementing the four functions in
210
+ shared/commerce/card-payment.ts (rules, and a Stripe
211
+ implementation to paste, in
212
+ .agents/skills/commerce/references/online-payments.md; the
213
+ payment webhook is premade). Enabled without that, picking it
214
+ at checkout answers 503 no_card_payment_provider.
215
+ Deliberately not shown to the store operator — it's developer
216
+ guidance, not store configuration. */}
215
217
  </div>
216
218
  ))}
217
219
  </CardContent>