@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 +2 -2
- package/base44/shared/commerce/card-payment.ts +7 -3
- package/base44/shared/commerce/payments.ts +4 -4
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +3 -3
- package/skills/commerce/docs/api-storefront.md +1 -1
- package/skills/commerce/post-installation.md +12 -119
- package/skills/commerce/references/online-payments.md +120 -2
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +9 -7
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
|
-
- **
|
|
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
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
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",
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -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;
|
|
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 (
|
|
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 |
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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",
|
|
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
|
-
|
|
438
|
+
Four steps, whichever provider it is:
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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>
|