@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 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 **exactly two files** — `base44/shared/commerce/card-payment.ts` and the payment webhook — and nothing else. Every other payment option is manual (on-hold + instructions) and needs no code; the admin can add more in Settings → Payments. See [`skills/commerce/references/online-payments.md`](./skills/commerce/references/online-payments.md).
11
+ - **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 functions for the shopfront you build: `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).
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 two payment files to wire one. 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.
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
@@ -84,7 +84,7 @@
84
84
  },
85
85
  "payment_method": {
86
86
  "type": "string",
87
- "description": "PaymentGateway slug (e.g. offline|stripe)"
87
+ "description": "PaymentGateway slug (e.g. offline|card)"
88
88
  },
89
89
  "payment_method_title": {
90
90
  "type": "string",
@@ -21,7 +21,7 @@
21
21
  "refunded_payment": {
22
22
  "type": "boolean",
23
23
  "default": false,
24
- "description": "True when a real gateway refund was made (Stripe wiring placeholder — see .agents/skills/commerce/references/online-payments.md)"
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 stripe (processed by commerce/payments + commerce/payment-webhook). Secrets are never stored here; use Base44 secrets.",
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 | stripe"
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}]}; stripe: {connector: \"stripe\"} — see .agents/skills/commerce/references/online-payments.md"
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 — file 2 of 2 to implement when wiring a payment
3
- * provider. File 1 is `base44/shared/commerce/card-payment.ts`.
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
- * This is the provider's server-to-server callback — the confirmation path
6
- * that covers the buyer who pays and closes the tab (the other path is the
7
- * customer returning to the storefront, which calls `commerce/payments`
8
- * `complete-return`; both are idempotent, whichever arrives second does
9
- * nothing).
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
- * Implement ONLY `parseWebhook` below. Everything after it is premade: the
12
- * order lookup, the order_key check, the idempotent confirmation and the
13
- * order progression (stock, emails, download permissions, store webhooks).
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 400 `webhook_not_implemented`.
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
- if (!event) {
61
- return fail(400, "This store's payment webhook is not implemented.", "webhook_not_implemented");
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(gw);
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 — file 1 of 2 to implement when wiring a payment provider
3
- * (Stripe, PayPal, Adyen, a local PSP…). File 2 is the webhook:
4
- * `base44/functions/commerce/payment-webhook/entry.ts`.
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
- * Everything around these three functions is already built: checkout creates
7
- * the order, prices it, holds stock and routes the customer to the URL you
8
- * return; the webhook and the return page confirm payment idempotently and
9
- * move the order to processing (stock, emails, download permissions, webhooks
10
- * all fire from there); admin refunds write the refund record and restock.
11
- * Implement ONLY the provider calls below — no other file needs to change.
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). If your
38
- * provider supports metadata, attach `order.id` and `order.order_key` so the
39
- * webhook can name the order.
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 two files an agent implements
5
- * when wiring a provider (see `.agents/skills/commerce/references/online-payments.md`):
6
- *
7
- * 1. `shared/commerce/card-payment.ts` — create a payment page, check it was
8
- * paid, refund it.
9
- * 2. `functions/commerce/payment-webhook/entry.ts` — parse the provider's
10
- * server-to-server event.
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. None of it needs touching to add a provider.
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.8",
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",
@@ -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 two-file model (card-payment.ts + the webhook);
100
- // the Stripe adapter and the provider-status hook are gone.
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
@@ -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 that make reading the full API docs unnecessary for the happy path. The full install-from-scratch steps are in [`.agents/skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
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) is premade; wiring a provider means implementing exactly two files (`shared/commerce/card-payment.ts` + the payment webhook) | [`references/online-payments.md`](./references/online-payments.md) |
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). Its `parseWebhook` is one of the two files to implement when wiring a provider; until then it answers `400 webhook_not_implemented`. See [`../references/online-payments.md`](../references/online-payments.md).
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 — two files to implement (see [`references/online-payments.md`](../references/online-payments.md)). Payment methods and the currency are admin-owned data — always render them from `get-store-info` rather than a hardcoded list, and format prices with `Intl.NumberFormat(undefined, { style: "currency", currency })`.
39
+ Two things that are **not** in the backend and are yours to build: the visitor UI itself, and 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. Wiring a payment provider means implementing two files; see [`../references/online-payments.md`](../references/online-payments.md).
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, read **[`SKILL.md` → *If you build a storefront, these four are not optional*](./SKILL.md)** before writing it, and [`docs/api-storefront.md`](./docs/api-storefront.md)'s *Required behaviors* table. 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.
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