@base44/app-plugin-commerce 0.1.8 → 0.1.12
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 +3 -3
- package/base44/entities/commerce.Order.jsonc +1 -1
- package/base44/entities/commerce.OrderRefund.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
- package/base44/functions/commerce/payment-webhook/entry.ts +28 -39
- package/base44/functions/commerce/seed-store/entry.ts +50 -2
- package/base44/shared/commerce/card-payment.ts +72 -12
- package/base44/shared/commerce/payments.ts +10 -8
- package/package.json +1 -1
- package/scripts/install.js +3 -2
- package/skills/commerce/SKILL.md +4 -3
- package/skills/commerce/docs/api-admin.md +2 -2
- package/skills/commerce/docs/api-storefront.md +2 -2
- package/skills/commerce/installation-guidelines.md +1 -1
- package/skills/commerce/post-installation.md +197 -50
- package/skills/commerce/references/guest-access-security.md +1 -1
- package/skills/commerce/references/online-payments.md +26 -8
- package/src/commerce/admin/hooks/useAsync.js +1 -1
- package/src/commerce/admin/hooks/usePagedList.js +1 -1
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +7 -6
- package/src/commerce/utils/index.js +5 -1
- package/src/commerce/utils/storefront.js +139 -0
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ 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) and refund records are all built. Wiring a provider (Stripe, PayPal, a local PSP…) means implementing **
|
|
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) §2.2. Every other payment option is manual (on-hold + instructions) and needs no code; the admin can add more in Settings → Payments. See [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md).
|
|
12
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
|
-
- **Storefront helpers** (`src/commerce/utils/`) — framework-free, dependency-free
|
|
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).
|
|
15
15
|
- **StoreAdmin agent + bot** — an AI copilot (`base44/agents/commerce/StoreAdmin.jsonc`, registered as `commerce/StoreAdmin`) with the `commerce/*` functions attached directly as tools (calls run as the chatting user → `requireAdmin()` still applies), variant-aware order editing, plus a chat panel in the admin sidebar with GFM markdown-table rendering.
|
|
16
16
|
- **Docs** — this README plus the commerce skill folder [`skills/commerce/`](./skills/commerce/), which holds [`SKILL.md`](./skills/commerce/SKILL.md) (the short map agents start from), [`installation-guidelines.md`](./skills/commerce/installation-guidelines.md), [`post-installation.md`](./skills/commerce/post-installation.md), per-topic guides in [`references/`](./skills/commerce/references/) and the API references in [`docs/`](./skills/commerce/docs/) — the whole folder is installed into the app at `.agents/skills/commerce/` so agents pick it up natively.
|
|
17
17
|
|
|
@@ -99,7 +99,7 @@ If you build on Base44's hosted platform, use the Base44 agent/MCP to write the
|
|
|
99
99
|
## What's NOT included
|
|
100
100
|
|
|
101
101
|
- **No visitor/storefront UI.** The storefront **API** is complete (`commerce/storefront-*` functions); building the shopfront is up to you — see [`skills/commerce/docs/api-storefront.md`](./skills/commerce/docs/api-storefront.md). What *does* ship for the storefront is **helper logic**: [`src/commerce/utils/`](./src/commerce/utils/) — framework-free variant-selection functions (map a Size/Color selection to a `ProductVariation` and back, per-option availability, variant price ranges) — plus [`skills/commerce/references/product-render.md`](./skills/commerce/references/product-render.md) (what to render in a grid vs. a product page, and which fields each call returns) and [`skills/commerce/references/storefront-product-page.md`](./skills/commerce/references/storefront-product-page.md), the variant rules that go with the helpers.
|
|
102
|
-
- **No payment provider** — the order side of card payments is premade (see above), but charging a card needs a provider: implement the
|
|
102
|
+
- **No payment provider** — the order side of card payments is premade (see above), but charging a card needs a provider: implement the four functions in `base44/shared/commerce/card-payment.ts` to wire one (Stripe: paste-in in `skills/commerce/post-installation.md` §2.2). Until then the card option answers "card payments are not available yet" at checkout (or switch it off in Settings → Payments), and the manual gateways (bank transfer, cash on delivery, anything the admin adds) carry checkout.
|
|
103
103
|
- **No scheduled workflows shipped.** Base44 *does* have a scheduler, but this template ships no workflow files — time-based jobs (stock-hold release, cart expiry, webhook-log pruning) run **opportunistically** where possible, and for the rest you (or the Base44 agent) create scheduled workflows that call `commerce/admin-tools`/`commerce/admin-orders` actions — see *Scheduled work* in [`skills/commerce/SKILL.md`](./skills/commerce/SKILL.md).
|
|
104
104
|
|
|
105
105
|
## Next steps
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"refunded_payment": {
|
|
22
22
|
"type": "boolean",
|
|
23
23
|
"default": false,
|
|
24
|
-
"description": "True when a real
|
|
24
|
+
"description": "True when a real refund was made at the payment provider (via shared/commerce/card-payment.ts refundCardPayment — see .agents/skills/commerce/references/online-payments.md)"
|
|
25
25
|
},
|
|
26
26
|
"restock_items": {
|
|
27
27
|
"type": "boolean",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commerce.PaymentGateway",
|
|
3
3
|
"type": "object",
|
|
4
|
-
"description": "Payment gateway configuration. Seeded: offline (settled outside the store) and
|
|
4
|
+
"description": "Payment gateway configuration. Seeded: offline (settled outside the store) and card (processed by commerce/payments + commerce/payment-webhook via shared/commerce/card-payment.ts). Secrets are never stored here; use Base44 secrets.",
|
|
5
5
|
"properties": {
|
|
6
6
|
"slug": {
|
|
7
7
|
"type": "string",
|
|
8
8
|
"minLength": 1,
|
|
9
|
-
"description": "Unique gateway id, e.g. offline |
|
|
9
|
+
"description": "Unique gateway id, e.g. offline | card"
|
|
10
10
|
},
|
|
11
11
|
"title": {
|
|
12
12
|
"type": "string",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"settings": {
|
|
37
37
|
"type": "object",
|
|
38
|
-
"description": "Per-gateway settings. offline: {account_details: [{account_name, account_number, bank_name, sort_code, iban, bic}]};
|
|
38
|
+
"description": "Per-gateway settings. offline: {account_details: [{account_name, account_number, bank_name, sort_code, iban, bic}]}; card: {} — provider config lives in code and backend secrets, never here; see .agents/skills/commerce/references/online-payments.md"
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"required": ["slug"],
|
|
@@ -1,66 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* commerce/payment-webhook —
|
|
3
|
-
* provider
|
|
2
|
+
* commerce/payment-webhook — the provider's server-to-server payment callback.
|
|
3
|
+
* PREMADE: wiring a payment provider does NOT mean editing this file. All
|
|
4
|
+
* provider-specific logic — including validating the incoming event — lives
|
|
5
|
+
* in `base44/shared/commerce/card-payment.ts` (`parseWebhook`), the one file
|
|
6
|
+
* to implement.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Trust model: an event body is never believed on its own. `parseWebhook`
|
|
9
|
+
* names the order (from the metadata `createCardPayment` attached) and only
|
|
10
|
+
* vouches `paid: true` after real signature verification; otherwise the event
|
|
11
|
+
* is a nudge and whether money arrived is asked of the provider itself
|
|
12
|
+
* through `checkCardPaymentPaid`. Either way a forged call can never mark an
|
|
13
|
+
* order paid, and the `order_key` match below stops an event being pointed
|
|
14
|
+
* at another order.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* This is one of the two idempotent confirmation paths — it covers the buyer
|
|
17
|
+
* who pays and closes the tab; the other is the customer returning to the
|
|
18
|
+
* storefront, which calls `commerce/payments` `complete-return`. Whichever
|
|
19
|
+
* runs second is a no-op. Register this function's URL with the provider
|
|
20
|
+
* (e.g. a Stripe webhook endpoint for `checkout.session.completed`).
|
|
14
21
|
*
|
|
15
22
|
* Unlike every other function here this takes the provider's **raw body**
|
|
16
23
|
* rather than an `{action, ...}` envelope, because webhook signatures are
|
|
17
24
|
* computed over those exact bytes.
|
|
18
25
|
*
|
|
19
|
-
* Until implemented, every request answers
|
|
26
|
+
* Until `parseWebhook` is implemented, every request answers
|
|
27
|
+
* 400 `webhook_not_implemented`.
|
|
20
28
|
*/
|
|
21
29
|
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
30
|
+
import { HttpError } from "../../../shared/commerce/auth.ts";
|
|
22
31
|
import { getSettings } from "../../../shared/commerce/settings.ts";
|
|
23
32
|
import { confirmCardPayment } from "../../../shared/commerce/payments.ts";
|
|
33
|
+
import { parseWebhook } from "../../../shared/commerce/card-payment.ts";
|
|
24
34
|
|
|
25
35
|
/** Providers retry on non-2xx; answer 200 for anything we deliberately ignore. */
|
|
26
36
|
const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
|
|
27
37
|
const fail = (status: number, error: string, code: string) =>
|
|
28
38
|
Response.json({ success: false, error, code }, { status });
|
|
29
39
|
|
|
30
|
-
interface CardWebhookEvent {
|
|
31
|
-
/** The order the event is about — read it from the event's metadata. */
|
|
32
|
-
order_id: string;
|
|
33
|
-
/** Must match the order's key; attach both to the payment when creating it. */
|
|
34
|
-
order_key: string;
|
|
35
|
-
/**
|
|
36
|
-
* Set true ONLY when you verified the request signature AND the event says
|
|
37
|
-
* the payment succeeded. When false, the event is treated as a nudge naming
|
|
38
|
-
* an order, and whether money arrived is asked of the provider through
|
|
39
|
-
* `checkCardPaymentPaid` — useful when no signing secret is available.
|
|
40
|
-
*/
|
|
41
|
-
paid: boolean;
|
|
42
|
-
/** The provider's payment id, if the event carries one. */
|
|
43
|
-
reference?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* ← IMPLEMENT THIS for your provider: verify the signature (over the raw
|
|
48
|
-
* `payload` bytes) and pull out the order reference. Return null for events
|
|
49
|
-
* that aren't about a payment for one of this store's orders.
|
|
50
|
-
*/
|
|
51
|
-
async function parseWebhook(_req: Request, _payload: string): Promise<CardWebhookEvent | null> {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
40
|
Deno.serve(async (req: Request) => {
|
|
56
41
|
try {
|
|
57
42
|
const payload = await req.text();
|
|
58
43
|
|
|
59
44
|
const event = await parseWebhook(req, payload);
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
// Not about one of this store's orders — acknowledge so the provider
|
|
46
|
+
// doesn't retry.
|
|
47
|
+
if (!event || !event.order_id) {
|
|
48
|
+
return ok({ ignored: true, reason: "no_order_reference" });
|
|
62
49
|
}
|
|
63
|
-
if (!event.order_id) return ok({ ignored: true, reason: "no_order_reference" });
|
|
64
50
|
|
|
65
51
|
const base44 = createClientFromRequest(req);
|
|
66
52
|
const sr = base44.asServiceRole;
|
|
@@ -89,6 +75,9 @@ Deno.serve(async (req: Request) => {
|
|
|
89
75
|
status: result.order.status,
|
|
90
76
|
});
|
|
91
77
|
} catch (e) {
|
|
78
|
+
if (e instanceof HttpError) {
|
|
79
|
+
return fail(e.status, e.message, e.code ?? "error");
|
|
80
|
+
}
|
|
92
81
|
// 500 makes the provider retry, which is what we want for a transient failure.
|
|
93
82
|
console.error("commerce/payment-webhook error:", e);
|
|
94
83
|
return fail(500, (e as Error)?.message ?? "Internal error", "internal_error");
|
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* best-effort rollback on mid-failure. See seed-catalog.ts for the pipeline.
|
|
16
16
|
*
|
|
17
17
|
* Body: { store_name?, currency?, weight_unit?, dimension_unit?,
|
|
18
|
-
* with_sample_data?, products?, coupons?, locations? }
|
|
18
|
+
* payment_methods?, with_sample_data?, products?, coupons?, locations? }
|
|
19
19
|
* — with_sample_data cannot be combined with the catalog keys.
|
|
20
20
|
*
|
|
21
|
+
* `payment_methods` (e.g. ["card"]) names the checkout methods the store
|
|
22
|
+
* offers: listed gateways are enabled, every other gateway row is disabled —
|
|
23
|
+
* the one call covers "card-only" stores with no entity editing. Explicit
|
|
24
|
+
* values always win, on first seed and re-runs alike (like `currency`).
|
|
25
|
+
*
|
|
21
26
|
* `store_name` is required on a first seed: a function's env is only
|
|
22
27
|
* BASE44_APP_ID, so it cannot read the app's name, and subjects need one.
|
|
23
28
|
* `currency` is an ISO code and `weight_unit`/`dimension_unit` are the
|
|
@@ -65,6 +70,30 @@ Deno.serve(async (req) => {
|
|
|
65
70
|
if (dimensionUnit && !DIMENSION_UNITS.includes(dimensionUnit)) {
|
|
66
71
|
unitErrors.push({ path: "dimension_unit", error: `must be one of: ${DIMENSION_UNITS.join(", ")}` });
|
|
67
72
|
}
|
|
73
|
+
let methodSlugs: string[] | null = null;
|
|
74
|
+
if (body.payment_methods !== undefined) {
|
|
75
|
+
const raw = body.payment_methods;
|
|
76
|
+
if (!Array.isArray(raw) || !raw.length || raw.some((s: unknown) => typeof s !== "string" || !String(s).trim())) {
|
|
77
|
+
unitErrors.push({ path: "payment_methods", error: `must be a non-empty array of gateway slugs, e.g. ["card"]` });
|
|
78
|
+
} else {
|
|
79
|
+
methodSlugs = [...new Set(raw.map((s: string) => s.trim().toLowerCase()))];
|
|
80
|
+
// Validate against the gateways that will exist after this call — the
|
|
81
|
+
// seeded defaults plus any rows already in the store ("stripe" rows are
|
|
82
|
+
// renamed to "card" below). Read-only, so failing here writes nothing.
|
|
83
|
+
const rows = (await sr.entities["commerce.PaymentGateway"].list(undefined, 100)) ?? [];
|
|
84
|
+
const known = new Set([
|
|
85
|
+
...GATEWAY_DEFAULTS.map((g) => g.slug),
|
|
86
|
+
...rows.map((r: any) => String(r.slug === "stripe" ? "card" : r.slug)),
|
|
87
|
+
]);
|
|
88
|
+
const unknown = methodSlugs.filter((s) => !known.has(s));
|
|
89
|
+
if (unknown.length) {
|
|
90
|
+
unitErrors.push({
|
|
91
|
+
path: "payment_methods",
|
|
92
|
+
error: `unknown gateway slug(s): ${unknown.join(", ")} — known: ${[...known].join(", ")}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
68
97
|
if (unitErrors.length) {
|
|
69
98
|
return fail(400, "Invalid catalog payload — nothing was written.", "invalid_payload", { errors: unitErrors });
|
|
70
99
|
}
|
|
@@ -159,10 +188,28 @@ Deno.serve(async (req) => {
|
|
|
159
188
|
for (const gw of GATEWAY_DEFAULTS) {
|
|
160
189
|
const hits = (await sr.entities["commerce.PaymentGateway"].filter({ slug: gw.slug }, undefined, 1)) ?? [];
|
|
161
190
|
if (hits.length) continue;
|
|
162
|
-
await sr.entities["commerce.PaymentGateway"].create(
|
|
191
|
+
await sr.entities["commerce.PaymentGateway"].create(
|
|
192
|
+
methodSlugs ? { ...gw, enabled: methodSlugs.includes(gw.slug) } : gw,
|
|
193
|
+
);
|
|
163
194
|
seeded.gateways++;
|
|
164
195
|
}
|
|
165
196
|
|
|
197
|
+
// Explicit payment_methods always win, like currency: converge EVERY
|
|
198
|
+
// gateway row's enabled flag to the requested set, so "card-only" is one
|
|
199
|
+
// seed call — no entity editing, on first seed and re-runs alike.
|
|
200
|
+
let paymentMethods: { enabled: string[]; disabled: string[] } | null = null;
|
|
201
|
+
if (methodSlugs) {
|
|
202
|
+
const rows = (await sr.entities["commerce.PaymentGateway"].list(undefined, 100)) ?? [];
|
|
203
|
+
paymentMethods = { enabled: [], disabled: [] };
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
const want = methodSlugs.includes(String(row.slug));
|
|
206
|
+
if (Boolean(row.enabled) !== want) {
|
|
207
|
+
await sr.entities["commerce.PaymentGateway"].update(row.id, { enabled: want });
|
|
208
|
+
}
|
|
209
|
+
(want ? paymentMethods.enabled : paymentMethods.disabled).push(String(row.slug));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
166
213
|
// Caller-supplied `locations` ARE the store's shipping story — seeding the
|
|
167
214
|
// free-shipping fallback next to them would leave a second no-region
|
|
168
215
|
// location the merchant never asked for (shadowed while the caller's has a
|
|
@@ -199,6 +246,7 @@ Deno.serve(async (req) => {
|
|
|
199
246
|
catalog: catalogResult,
|
|
200
247
|
store_name: { value: storeNameAction === "kept_existing" ? currentName : storeName, action: storeNameAction },
|
|
201
248
|
currency: currencyInfo ? { value: currencyInfo.code, action: currencyAction } : null,
|
|
249
|
+
payment_methods: paymentMethods,
|
|
202
250
|
});
|
|
203
251
|
} catch (e) {
|
|
204
252
|
if (e instanceof HttpError) return fail(e.status, e.message, e.code);
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Card payments —
|
|
3
|
-
* (Stripe, PayPal, Adyen, a local PSP…).
|
|
4
|
-
*
|
|
2
|
+
* Card payments — the ONE file to implement when wiring a payment provider
|
|
3
|
+
* (Stripe, PayPal, Adyen, a local PSP…). Implement the four functions below;
|
|
4
|
+
* no other file changes:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* createCardPayment → checkout + payment links (hosted payment page)
|
|
7
|
+
* checkCardPaymentPaid → confirmation (return page, webhook, admin "Check payment")
|
|
8
|
+
* refundCardPayment → admin refunds through the provider
|
|
9
|
+
* parseWebhook → validate the provider's server-to-server event
|
|
10
|
+
*
|
|
11
|
+
* Any provider works; a ready-made reference implementation (Stripe) is in
|
|
12
|
+
* `.agents/skills/commerce/post-installation.md` §2.2 to paste over this
|
|
13
|
+
* file — another provider implements the same functions against its own API.
|
|
14
|
+
*
|
|
15
|
+
* REPLACE THIS FILE WHOLE (one write of the full new content). Never
|
|
16
|
+
* find_replace into these stubs: a partial patch leaves the originals behind
|
|
17
|
+
* and every commerce function fails to deploy with duplicate-export bundle
|
|
18
|
+
* errors ("Multiple exports with the same name ...") — the fix is always to
|
|
19
|
+
* rewrite the file whole.
|
|
20
|
+
*
|
|
21
|
+
* Everything around these functions is already built: checkout creates the
|
|
22
|
+
* order, prices it, holds stock and routes the customer to the URL you
|
|
23
|
+
* return; the return page and the payment webhook confirm payment
|
|
24
|
+
* idempotently and move the order to processing (stock, emails, download
|
|
25
|
+
* permissions, webhooks all fire from there); admin refunds write the refund
|
|
26
|
+
* record and restock. `commerce/payment-webhook` (the function) is PREMADE —
|
|
27
|
+
* it calls `parseWebhook` from here and never trusts an event that this file
|
|
28
|
+
* hasn't vouched for.
|
|
12
29
|
*
|
|
13
30
|
* Until implemented, the Credit Card checkout option answers
|
|
14
31
|
* 503 `no_card_payment_provider`.
|
|
@@ -34,9 +51,9 @@ export interface CardPaymentPage {
|
|
|
34
51
|
* Create a payment page for `order.total` in `order.currency`.
|
|
35
52
|
*
|
|
36
53
|
* Send the customer back to `successUrl` / `cancelUrl` after they pay or give
|
|
37
|
-
* up (both already carry order_id/order_key for the return page).
|
|
38
|
-
*
|
|
39
|
-
* webhook
|
|
54
|
+
* up (both already carry order_id/order_key for the return page). Attach
|
|
55
|
+
* `order.id` and `order.order_key` to the payment's metadata — that is how
|
|
56
|
+
* the premade webhook names the order when the provider's event comes in.
|
|
40
57
|
*/
|
|
41
58
|
export async function createCardPayment(
|
|
42
59
|
_sr: any,
|
|
@@ -78,3 +95,46 @@ export async function refundCardPayment(_sr: any, _order: any, _opts: {
|
|
|
78
95
|
"card_refund_not_implemented",
|
|
79
96
|
);
|
|
80
97
|
}
|
|
98
|
+
|
|
99
|
+
/** What `parseWebhook` distills a provider's webhook event into. */
|
|
100
|
+
export interface CardWebhookEvent {
|
|
101
|
+
/** The order the event is about — read it from the event's metadata
|
|
102
|
+
* (`createCardPayment` attached it). */
|
|
103
|
+
order_id: string;
|
|
104
|
+
/** Must match the order's key; the premade webhook rejects the event
|
|
105
|
+
* otherwise, so a forged event can't be pointed at another order. */
|
|
106
|
+
order_key: string;
|
|
107
|
+
/**
|
|
108
|
+
* Set true ONLY when you verified the request signature over the raw
|
|
109
|
+
* payload bytes AND the event says the payment succeeded. When false, the
|
|
110
|
+
* event is treated as an unverified *nudge* naming an order: whether money
|
|
111
|
+
* actually arrived is then asked of the provider itself through
|
|
112
|
+
* `checkCardPaymentPaid`. `paid: false` therefore needs no signing secret
|
|
113
|
+
* and is already forgery-proof — never return true from an unverified body.
|
|
114
|
+
*/
|
|
115
|
+
paid: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The provider's payment id, ONLY if the event is signature-verified.
|
|
118
|
+
* Leave unset otherwise — the premade flow then verifies the reference
|
|
119
|
+
* stored on the order at checkout, so a forged event can't smuggle in a
|
|
120
|
+
* reference to some other (paid) payment.
|
|
121
|
+
*/
|
|
122
|
+
reference?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Validate an incoming provider webhook event (raw request + raw body — the
|
|
127
|
+
* exact bytes, for signature schemes) and name the order it is about. Called
|
|
128
|
+
* by the premade `commerce/payment-webhook` function on every delivery; the
|
|
129
|
+
* verification that money actually arrived happens after this, against the
|
|
130
|
+
* provider, per the `paid` contract above. Return null for events that
|
|
131
|
+
* aren't about a payment for one of this store's orders (answered 200 so the
|
|
132
|
+
* provider doesn't retry).
|
|
133
|
+
*/
|
|
134
|
+
export async function parseWebhook(_req: Request, _payload: string): Promise<CardWebhookEvent | null> {
|
|
135
|
+
throw new HttpError(
|
|
136
|
+
400,
|
|
137
|
+
"This store's payment webhook is not implemented.",
|
|
138
|
+
"webhook_not_implemented",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Card payment plumbing — the premade order-side half of online payments.
|
|
3
3
|
*
|
|
4
|
-
* The provider-specific half lives in exactly
|
|
5
|
-
* when wiring a provider
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The provider-specific half lives in exactly ONE file an agent implements
|
|
5
|
+
* when wiring a provider: `shared/commerce/card-payment.ts` — four functions
|
|
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` §2.2, and the
|
|
10
|
+
* provider-agnostic rules are in
|
|
11
|
+
* `.agents/skills/commerce/references/online-payments.md`.
|
|
11
12
|
*
|
|
12
13
|
* Everything here — return URLs, storing the payment reference on the order,
|
|
13
14
|
* idempotent confirmation that moves the order to processing, refund routing —
|
|
14
15
|
* is already wired into checkout, the payments function, the webhook and admin
|
|
15
|
-
* refunds.
|
|
16
|
+
* refunds. The payment webhook (`functions/commerce/payment-webhook`) is
|
|
17
|
+
* premade too. None of it needs touching to add a provider.
|
|
16
18
|
*/
|
|
17
19
|
import { HttpError } from "./auth.ts";
|
|
18
20
|
import { round2 } from "./money.ts";
|
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.12",
|
|
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/scripts/install.js
CHANGED
|
@@ -96,8 +96,9 @@
|
|
|
96
96
|
// remove the old copy so agents don't keep reading the stale one.
|
|
97
97
|
["skills", "commerce"],
|
|
98
98
|
[".agents", "skills", "commerce", "references", "stripe-payments.md"], // → references/online-payments.md
|
|
99
|
-
// Payments moved to the
|
|
100
|
-
// the Stripe adapter and the provider-status
|
|
99
|
+
// Payments moved to the card-payment.ts model (one file to implement;
|
|
100
|
+
// the webhook is premade). The old Stripe adapter and the provider-status
|
|
101
|
+
// hook are gone.
|
|
101
102
|
["base44", "shared", "commerce", "stripe.ts"],
|
|
102
103
|
["src", "commerce", "admin", "hooks", "usePaymentProvider.js"],
|
|
103
104
|
// Attributes and their values are edited in the Attributes section of the
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -10,10 +10,11 @@ Operational guidance for extending, operating and building on the Base44 Commerc
|
|
|
10
10
|
> **If you are a Base44 agent working inside the runtime, read this first:**
|
|
11
11
|
> - **Don't read the whole codebase up front.** Start with this file, then open only the files your current task touches plus the matching reference below, pulling in more as you go. Reading everything first just burns context.
|
|
12
12
|
> - **Don't weaken the admin gating.** The UI guard, entity RLS and function guards form three enforcement layers — see [`.agents/skills/commerce/post-installation.md`](./post-installation.md) — keep all of them intact when changing routes or schemas.
|
|
13
|
+
> - **Entity names are dotted — SDK access is bracket syntax only:** `base44.entities["commerce.PaymentGateway"]` (schema file: `base44/entities/commerce.PaymentGateway.jsonc`). `commerce__PaymentGateway` / `PaymentGateway` don't exist. Gateway on/off is seed data anyway: `commerce/seed-store` `payment_methods`.
|
|
13
14
|
|
|
14
15
|
## IMPORTANT — first-time installation
|
|
15
16
|
|
|
16
|
-
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**: 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 — §2) — and the **storefront quick start** (§3): logic-only chunks for product list → product page → cart → checkout
|
|
17
|
+
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 — §2) — **card payments in one file** (§2.2: a complete Stripe implementation to paste over `shared/commerce/card-payment.ts`; the payment webhook is premade), and the **storefront quick start** (§3): logic-only chunks for product list → product page → cart → checkout. 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).
|
|
17
18
|
|
|
18
19
|
## Working on the UI
|
|
19
20
|
|
|
@@ -34,7 +35,7 @@ Agents keep shipping storefronts that miss these, and each one breaks buying out
|
|
|
34
35
|
|
|
35
36
|
2. **Checkout must present shipping options and send a choice.** Call `set-shipping-address` **as soon as the customer provides an address** — it recalculates the options and cost, and fails with `400 shipping_not_available` when the store doesn't ship there. Then read `shipping_status` on the returned cart: `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **you must 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 the choice by looking it up in `available_shipping_methods` for its `title`/`cost`, never by rendering the id or dotting into it.)
|
|
36
37
|
|
|
37
|
-
3. **Handle card payments, and build `/order-received`.** Choosing the card gateway returns `payment.checkout_url` — redirect there (until a payment provider is implemented it answers `503 no_card_payment_provider`; offer the other methods). Every payment link comes back to `/order-received`, which **you must implement**: call `commerce/payments` `complete-return` with the query params and render its `state`. Without that page a customer pays into a 404 and the order is never marked paid. Also persist the `cart_token` from **every** cart response — `add-item` silently starts a fresh cart when the stored token is stale.
|
|
38
|
+
3. **Handle card payments, and build `/order-received`.** Choosing the card gateway returns `payment.checkout_url` — redirect there (until a payment provider is implemented — one file, Stripe paste-in in [`post-installation.md` §2.2](./post-installation.md#22-payments--one-file-any-provider-stripe-as-the-reference) — it answers `503 no_card_payment_provider`; offer the other methods). Every payment link comes back to `/order-received`, which **you must implement**: call `commerce/payments` `complete-return` with the query params and render its `state`. Without that page a customer pays into a 404 and the order is never marked paid. Also persist the `cart_token` from **every** cart response — `add-item` silently starts a fresh cart when the stored token is stale.
|
|
38
39
|
|
|
39
40
|
4. **Never advertise what isn't configured.** "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 `set-shipping-address`, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner.
|
|
40
41
|
|
|
@@ -55,7 +56,7 @@ Open the matching file under `.agents/skills/commerce/references/` only when a t
|
|
|
55
56
|
| 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) |
|
|
56
57
|
| 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) |
|
|
57
58
|
| 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) |
|
|
58
|
-
| Online payments | **any storefront or checkout work** — the order side (checkout, confirmation, payment links, refund records)
|
|
59
|
+
| 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` §2.2](./post-installation.md#22-payments--one-file-any-provider-stripe-as-the-reference)) | [`references/online-payments.md`](./references/online-payments.md) |
|
|
59
60
|
| Scheduled work | recurring maintenance — stock-hold release, abandoned-cart cleanup, webhook-log pruning, counter-drift repair | [`references/scheduled-work.md`](./references/scheduled-work.md) |
|
|
60
61
|
| Emails | transactional order emails, per-type overrides, deliverability, the email log | [`references/emails.md`](./references/emails.md) |
|
|
61
62
|
| Webhooks | outbound webhooks, HMAC signing, delivery log, auto-disable behavior | [`references/webhooks.md`](./references/webhooks.md) |
|
|
@@ -178,11 +178,11 @@ Actions: `create-link` · `complete-return` · `verify` — the admin side of on
|
|
|
178
178
|
- **`verify`** — `{ order_id }` → `{ paid, already_confirmed, status, order }`: re-asks the provider and moves the order to `processing` when the money is there. Idempotent.
|
|
179
179
|
- **`complete-return`** — `{ order_id, order_key, payment?, return_url? }` → `{ state, paid, already_confirmed, status, order, payment_link, payment_instructions }`: what the storefront's **mandatory** `/order-received` page calls — confirm, progress the order, and hand back a render-ready result plus a fresh payment link while unpaid (card) or the gateway's payment instructions (manual).
|
|
180
180
|
|
|
181
|
-
`commerce/payment-webhook` is the provider's server-to-server callback (raw body, no `action` envelope).
|
|
181
|
+
`commerce/payment-webhook` is the provider's server-to-server callback (raw body, no `action` envelope). The function is **premade** — it validates every event through `card-payment.ts`'s `parseWebhook` and never trusts an event body on its own: an unverified event only names an order, and payment is verified through `checkCardPaymentPaid` against the provider's API, so no signing secret is needed. Until `card-payment.ts` is implemented it answers `400 webhook_not_implemented`; events it can't tie to an order are acknowledged with `200 { ignored: true }` so providers don't retry. See [`../references/online-payments.md`](../references/online-payments.md).
|
|
182
182
|
|
|
183
183
|
## commerce/seed-store
|
|
184
184
|
|
|
185
|
-
Not action-routed. Body `{ store_name?, currency?, weight_unit?, dimension_unit?, with_sample_data?, products?, coupons?, locations? }`. **`store_name` is required** when the `emails` group doesn't exist yet (**400** `store_name_required` otherwise) — pass the app's name **as the platform shows it** — ask the user or read it from the dashboard. `base44/config.jsonc` → `name` is *not* authoritative: it can still say `New App` for an app the platform calls `Canvas`. A backend function can't read either, its environment being only `BASE44_APP_ID`. It lands in `emails.store_name` — one setting serving as both the store name in email subjects and the sender name on every transactional email; a nameless store renders subjects like `[]: New order #1002`, which is why seeding refuses one. Requires admin. Runs a **canary schema check** first — on any incompatibility returns **422** `{ success:false, code:"schema_incompatible", errors:[{ entity, error }] }` and writes nothing. Otherwise seeds defaults idempotently, then the catalog. On an already-seeded store a passed `store_name` fills a **blank** name and never overwrites one the merchant chose. **`currency`** (ISO code, validated against the shared currencies table) and **`weight_unit`/`dimension_unit`** set the `general` group; unknown values fail as **400** `invalid_payload`. Unlike `store_name` they **always win** — there is no blank state to distinguish a merchant's choice from the seeded default, so passing one on a re-run updates the store. Prices are formatted with `Intl.NumberFormat` — the currency is a value, there are no format settings.
|
|
185
|
+
Not action-routed. Body `{ store_name?, currency?, weight_unit?, dimension_unit?, payment_methods?, with_sample_data?, products?, coupons?, locations? }`. **`payment_methods`** (gateway slugs, e.g. `["card"]`) enables exactly the listed gateways and disables every other gateway row — the one-call way to honor "card-only"/"offline-only"; explicit values win on re-runs, unknown slugs fail as `400 invalid_payload`, and the response reports `payment_methods: { enabled, disabled }` (`null` when not passed). **`store_name` is required** when the `emails` group doesn't exist yet (**400** `store_name_required` otherwise) — pass the app's name **as the platform shows it** — ask the user or read it from the dashboard. `base44/config.jsonc` → `name` is *not* authoritative: it can still say `New App` for an app the platform calls `Canvas`. A backend function can't read either, its environment being only `BASE44_APP_ID`. It lands in `emails.store_name` — one setting serving as both the store name in email subjects and the sender name on every transactional email; a nameless store renders subjects like `[]: New order #1002`, which is why seeding refuses one. Requires admin. Runs a **canary schema check** first — on any incompatibility returns **422** `{ success:false, code:"schema_incompatible", errors:[{ entity, error }] }` and writes nothing. Otherwise seeds defaults idempotently, then the catalog. On an already-seeded store a passed `store_name` fills a **blank** name and never overwrites one the merchant chose. **`currency`** (ISO code, validated against the shared currencies table) and **`weight_unit`/`dimension_unit`** set the `general` group; unknown values fail as **400** `invalid_payload`. Unlike `store_name` they **always win** — there is no blank state to distinguish a merchant's choice from the seeded default, so passing one on a re-run updates the store. Prices are formatted with `Intl.NumberFormat` — the currency is a value, there are no format settings.
|
|
186
186
|
|
|
187
187
|
**`products`** is the one-call catalog bootstrap — the worked example and full semantics are in [`../post-installation.md` §2.1](../post-installation.md#21-the-products-payload). Each entry references categories/ribbons/attributes by **display name** (taxonomy is get-or-created: slugs/codes derived, existing records matched case-insensitively and reused, with the stored casing canonicalized into the product). `attributes: [{ name, options }]` (or `{ <name>: [options] }`) declares the variant axes; `variations: [{ options: { <name>: <option> }, ...overrides }]` lists the stocked combinations — omit it to auto-generate **all** combinations, each inheriting the product-level price/sale fields. A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without one it draws on the parent's pooled stock (`"parent"`). Variation SKUs are synthesized from the parent SKU when absent. Parent `price`/`regular_price`/`on_sale` are rolled up from the cheapest publishable variant, `stock_status` derived, category/ribbon counts maintained — same helpers as `admin-products` `save`, but **no `product.created` webhooks are dispatched** (bootstrap precedes webhook subscribers; use `admin-products` for webhook-visible creates). Payload problems fail before any write as **400** `invalid_payload` with `errors: [{ path, error }]`; an explicit variation SKU already in use is **409** `duplicate_sku`; a mid-write failure rolls back everything the call created (**500** `catalog_seed_failed`) without touching reused taxonomy. **Re-runs converge**: a product whose `sku` (or, without one, derived slug) already exists is skipped and reported, so retries never duplicate. Limits: ≤100 products, ≤50 variations per product, ≤500 variations per call, ≤50 coupons/locations. `coupons` is a thin passthrough (code lowercased, skip-if-exists); **`locations`** creates Shipping & Tax Locations (`{ name, countries?: ["IL"], shipping_rates: [{ name, cost, free_over? }], tax_groups?: [{ name, rates: [{ name, rate }] }], shipping_tax?: { type: "percent"|"fixed", value } }` — the default `Products` tax group is added when missing; skip-if-exists by name). Passing any `locations` suppresses the seeded "Rest of the world" fallback — yours become the store's only locations; without them the free-shipping fallback is seeded. `with_sample_data: true` seeds the template's demo catalog instead (only when the store has zero products) and cannot be combined with the catalog keys.
|
|
188
188
|
|
|
@@ -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 the payment provider integration — four functions in one file (Stripe = paste [`post-installation.md` §2.2](../post-installation.md#22-payments--one-file-any-provider-stripe-as-the-reference); 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 })`.
|
|
40
40
|
|
|
41
41
|
## Conventions
|
|
42
42
|
|
|
@@ -312,7 +312,7 @@ Separate function, same guest-bearer rule (`order_id` + `order_key`; an admin ma
|
|
|
312
312
|
|
|
313
313
|
> **You must build the `/order-received` page** — it is mandatory for payment links to work. No storefront UI ships with the template, and without that route a paying customer lands on a 404 *and* the order never gets marked paid. It only needs to call `complete-return` and render its three states: see [`../references/online-payments.md`](../references/online-payments.md).
|
|
314
314
|
|
|
315
|
-
Provider callbacks land on `commerce/payment-webhook` (server-to-server) — the second confirmation path, for buyers who pay and close the tab. Whichever path runs second is a no-op.
|
|
315
|
+
Provider callbacks land on `commerce/payment-webhook` (server-to-server) — the second confirmation path, for buyers who pay and close the tab. Whichever path runs second is a no-op. The webhook function is premade (it verifies through the provider's API, so no signing secret); wiring a payment provider means implementing four functions in one file — see [`../references/online-payments.md`](../references/online-payments.md).
|
|
316
316
|
|
|
317
317
|
### `confirm-payment`
|
|
318
318
|
Post-payment hook for an order on the card gateway. **Payload:** `{ order_id, order_key }`.
|
|
@@ -85,7 +85,7 @@ The template ships an AI copilot for store operators:
|
|
|
85
85
|
|
|
86
86
|
## 4. Building on it — the four storefront requirements
|
|
87
87
|
|
|
88
|
-
If your work includes a customer-facing shopfront,
|
|
88
|
+
If your work includes a customer-facing shopfront, the quick start in [`post-installation.md` §3](./post-installation.md#3-storefront-quick-start--logic-only) already walks all four requirements — you don't need any other reading. In short: attribute-level variant selectors resolved to a `variation_id`; shipping options presented and chosen; card payment redirect plus an `/order-received` page; and no offers the store isn't configured for. The API enforces the first three — a storefront that skips them cannot complete a purchase. (Day-2 deep dives: [`SKILL.md` → *these four are not optional*](./SKILL.md) and [`docs/api-storefront.md`](./docs/api-storefront.md)'s *Required behaviors* table.)
|
|
89
89
|
|
|
90
90
|
## 5. Next steps
|
|
91
91
|
|