@base44/app-plugin-commerce 0.1.6 → 0.1.7

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.
Files changed (95) hide show
  1. package/README.md +11 -11
  2. package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
  3. package/base44/entities/commerce.Cart.jsonc +1 -1
  4. package/base44/entities/commerce.Coupon.jsonc +5 -0
  5. package/base44/entities/commerce.Order.jsonc +6 -7
  6. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  7. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  8. package/base44/entities/commerce.Product.jsonc +6 -16
  9. package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
  10. package/base44/entities/commerce.ProductVariation.jsonc +1 -9
  11. package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
  12. package/base44/entities/commerce.Webhook.jsonc +1 -1
  13. package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
  14. package/base44/functions/commerce/admin-products/entry.ts +11 -17
  15. package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
  16. package/base44/functions/commerce/admin-reports/entry.ts +3 -3
  17. package/base44/functions/commerce/admin-tools/entry.ts +9 -36
  18. package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
  19. package/base44/functions/commerce/payments/entry.ts +46 -42
  20. package/base44/functions/commerce/seed-store/defaults.ts +35 -42
  21. package/base44/functions/commerce/seed-store/entry.ts +68 -46
  22. package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
  23. package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
  24. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
  25. package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
  26. package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
  27. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
  28. package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
  29. package/base44/shared/commerce/card-payment.ts +80 -0
  30. package/base44/shared/commerce/coupons.ts +16 -10
  31. package/base44/shared/commerce/emails.ts +30 -18
  32. package/base44/shared/commerce/money.ts +11 -24
  33. package/base44/shared/commerce/payments.ts +55 -286
  34. package/base44/shared/commerce/scan.ts +1 -1
  35. package/base44/shared/commerce/sequence.ts +1 -1
  36. package/base44/shared/commerce/settings.ts +4 -9
  37. package/base44/shared/commerce/shipping.ts +65 -133
  38. package/base44/shared/commerce/tax.ts +48 -96
  39. package/base44/shared/commerce/totals.ts +77 -91
  40. package/package.json +1 -1
  41. package/scripts/install.js +28 -7
  42. package/skills/commerce/SKILL.md +14 -14
  43. package/skills/commerce/docs/api-admin.md +23 -26
  44. package/skills/commerce/docs/api-storefront.md +67 -61
  45. package/skills/commerce/installation-guidelines.md +8 -8
  46. package/skills/commerce/post-installation.md +60 -39
  47. package/skills/commerce/references/admin-product-form.md +15 -12
  48. package/skills/commerce/references/emails.md +2 -2
  49. package/skills/commerce/references/guest-access-security.md +2 -2
  50. package/skills/commerce/references/online-payments.md +24 -166
  51. package/skills/commerce/references/product-render.md +18 -18
  52. package/skills/commerce/references/reviews.md +14 -8
  53. package/skills/commerce/references/storefront-product-page.md +1 -1
  54. package/src/commerce/admin/README.md +4 -5
  55. package/src/commerce/admin/bot/Markdown.jsx +1 -1
  56. package/src/commerce/admin/hooks/useMoney.js +13 -22
  57. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  58. package/src/commerce/admin/lib/constants.js +2 -29
  59. package/src/commerce/admin/lib/order-utils.js +1 -1
  60. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
  61. package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
  62. package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
  63. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
  64. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
  65. package/src/commerce/admin/pages/products/Categories.jsx +147 -177
  66. package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
  67. package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
  68. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
  69. package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
  70. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
  71. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
  72. package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
  73. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
  74. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
  75. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
  76. package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
  77. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
  78. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
  79. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
  80. package/src/commerce/admin/routes.jsx +6 -12
  81. package/src/commerce/utils/index.js +2 -2
  82. package/src/commerce/utils/shipping-promos.js +45 -49
  83. package/src/commerce/utils/variants.js +1 -1
  84. package/base44/entities/commerce.ShippingClass.jsonc +0 -30
  85. package/base44/entities/commerce.ShippingZone.jsonc +0 -41
  86. package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
  87. package/base44/entities/commerce.TaxClass.jsonc +0 -23
  88. package/base44/entities/commerce.TaxRate.jsonc +0 -68
  89. package/base44/shared/commerce/stripe.ts +0 -463
  90. package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
  91. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
  92. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
  93. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
  94. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
  95. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * commerce/admin-reports — on-demand report aggregation over orders.
3
3
  * Fine to ~10k orders; larger stores should materialize stats
4
- * (see skills/commerce/references/limits-and-performance.md).
4
+ * (see .agents/skills/commerce/references/limits-and-performance.md).
5
5
  *
6
6
  * Actions: summary | sales | top-sellers | stock |
7
7
  * orders-totals | products-totals | customers-totals | coupons-totals |
8
- * reviews-totals | categories-totals | tags-totals | attributes-totals
8
+ * reviews-totals | categories-totals | ribbons-totals | attributes-totals
9
9
  */
10
10
  import { createClientFromRequest } from "npm:@base44/sdk";
11
11
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
@@ -35,7 +35,7 @@ Deno.serve(async (req) => {
35
35
  case "coupons-totals": return ok(await groupCount(sr.entities["commerce.Coupon"], "discount_type"));
36
36
  case "reviews-totals": return ok(await groupCount(sr.entities["commerce.ProductReview"], "status"));
37
37
  case "categories-totals": return ok(await termTotals(sr.entities["commerce.ProductCategory"]));
38
- case "tags-totals": return ok(await termTotals(sr.entities["commerce.ProductTag"]));
38
+ case "ribbons-totals": return ok(await termTotals(sr.entities["commerce.ProductRibbon"]));
39
39
  case "attributes-totals": return ok(await attributesTotals(sr));
40
40
  default:
41
41
  return fail(400, `Unknown action: ${action}`, "unknown_action");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * commerce/admin-tools — maintenance and diagnostics (mini "system status").
3
3
  *
4
- * Actions: status | payment-connector-status | admin-email-recipients |
4
+ * Actions: status | admin-email-recipients |
5
5
  * recount-terms | recount-coupon-usage | recalculate-customer-stats-all |
6
6
  * prune-webhook-deliveries | clear-abandoned-carts |
7
7
  * regenerate-download-permissions
@@ -10,7 +10,6 @@ import { createClientFromRequest } from "npm:@base44/sdk";
10
10
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
11
11
  import { getSettings } from "../../../shared/commerce/settings.ts";
12
12
  import { adminUserEmails } from "../../../shared/commerce/emails.ts";
13
- import { onlinePaymentStatus } from "../../../shared/commerce/payments.ts";
14
13
  import { round2 } from "../../../shared/commerce/money.ts";
15
14
  import { grantDownloadPermissions, revokeDownloadPermissions } from "../../../shared/commerce/orders.ts";
16
15
  import { scanAll } from "../../../shared/commerce/scan.ts";
@@ -28,7 +27,6 @@ Deno.serve(async (req) => {
28
27
 
29
28
  switch (action) {
30
29
  case "status": return ok(await status(sr));
31
- case "payment-connector-status": return ok(await paymentConnectorStatus(sr, payload));
32
30
  case "admin-email-recipients": return ok(await adminEmailRecipients(sr));
33
31
  case "recount-terms": return ok(await recountTerms(sr));
34
32
  case "recount-coupon-usage": return ok(await recountCouponUsage(sr));
@@ -70,34 +68,12 @@ async function adminEmailRecipients(
70
68
  };
71
69
  }
72
70
 
73
- /**
74
- * Is a gateway's Base44 connector actually connected?
75
- *
76
- * The admin UI calls this instead of hardcoding a "not wired" notice, so the
77
- * warning disappears on its own once the connector is connected. Connectors are
78
- * service-role only, which is why this needs a round trip rather than a client
79
- * check. Any failure (SDK without `connectors`, connector never set up) is
80
- * reported as `connected: false` — the conservative answer.
81
- */
82
- /**
83
- * Is an online payment provider usable right now? Delegates to the payment
84
- * utility, so this answers for whichever provider is wired — the admin UI shows
85
- * "no payment provider connected" rather than naming one.
86
- */
87
- async function paymentConnectorStatus(sr: any, _payload: any): Promise<any> {
88
- const status = await onlinePaymentStatus(sr);
89
- return {
90
- ...status,
91
- connector: status.provider, // kept for callers written against the old shape
92
- };
93
- }
94
-
95
71
  /** Entity counts (capped at 1000 — shown as "1000+"), settings sanity, seeded flag. */
96
72
  async function status(sr: any): Promise<any> {
97
73
  const entities = [
98
- "commerce.Product", "commerce.ProductVariation", "commerce.ProductCategory", "commerce.ProductTag", "commerce.ProductAttribute",
74
+ "commerce.Product", "commerce.ProductVariation", "commerce.ProductCategory", "commerce.ProductRibbon", "commerce.ProductAttribute",
99
75
  "commerce.Order", "commerce.OrderRefund", "commerce.Coupon", "commerce.Customer", "commerce.ProductReview",
100
- "commerce.TaxRate", "commerce.ShippingZone", "commerce.PaymentGateway", "commerce.Webhook", "commerce.Cart",
76
+ "commerce.ShippingTaxLocation", "commerce.PaymentGateway", "commerce.Webhook", "commerce.Cart",
101
77
  ];
102
78
  const counts: Record<string, string | number> = {};
103
79
  for (const name of entities) {
@@ -119,23 +95,21 @@ async function status(sr: any): Promise<any> {
119
95
  // counts is keyed by the namespaced entity name — `counts.PaymentGateway`
120
96
  // is undefined, which silently reported both checks as false.
121
97
  has_payment_gateways: (counts["commerce.PaymentGateway"] as number) > 0,
122
- has_default_zone: (counts["commerce.ShippingZone"] as number) > 0,
98
+ has_default_location: (counts["commerce.ShippingTaxLocation"] as number) > 0,
123
99
  },
124
100
  };
125
101
  }
126
102
 
127
- /** Rebuild category/tag/shipping-class/attribute-term `count` fields from products. */
103
+ /** Rebuild category/ribbon/attribute-term `count` fields from products. */
128
104
  async function recountTerms(sr: any): Promise<any> {
129
105
  const products = await scanAll(sr.entities["commerce.Product"]);
130
106
  const catCounts: Record<string, number> = {};
131
- const tagCounts: Record<string, number> = {};
132
- const classCounts: Record<string, number> = {};
107
+ const ribbonCounts: Record<string, number> = {};
133
108
  const termCounts: Record<string, number> = {}; // `${attribute_id}::${option}`
134
109
 
135
110
  for (const p of products) {
136
111
  for (const c of p.category_ids ?? []) catCounts[c] = (catCounts[c] ?? 0) + 1;
137
- for (const t of p.tag_ids ?? []) tagCounts[t] = (tagCounts[t] ?? 0) + 1;
138
- if (p.shipping_class_id) classCounts[p.shipping_class_id] = (classCounts[p.shipping_class_id] ?? 0) + 1;
112
+ for (const t of p.ribbon_ids ?? []) ribbonCounts[t] = (ribbonCounts[t] ?? 0) + 1;
139
113
  for (const a of p.attributes ?? []) {
140
114
  if (!a.attribute_id) continue;
141
115
  for (const opt of a.options ?? []) {
@@ -158,8 +132,7 @@ async function recountTerms(sr: any): Promise<any> {
158
132
  };
159
133
 
160
134
  const categories = await updateCounts("commerce.ProductCategory", catCounts);
161
- const tags = await updateCounts("commerce.ProductTag", tagCounts);
162
- const classes = await updateCounts("commerce.ShippingClass", classCounts);
135
+ const ribbons = await updateCounts("commerce.ProductRibbon", ribbonCounts);
163
136
 
164
137
  let terms = 0;
165
138
  for (const term of await scanAll(sr.entities["commerce.ProductAttributeTerm"])) {
@@ -169,7 +142,7 @@ async function recountTerms(sr: any): Promise<any> {
169
142
  terms++;
170
143
  }
171
144
  }
172
- return { updated: { categories, tags, shipping_classes: classes, attribute_terms: terms } };
145
+ return { updated: { categories, ribbons, attribute_terms: terms } };
173
146
  }
174
147
 
175
148
  /** Rebuild coupon usage_count/used_by from orders where usage was counted. */
@@ -1,131 +1,92 @@
1
1
  /**
2
- * commerce/payment-webhook — the payment provider's server-to-server callback.
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`.
3
4
  *
4
- * The second of the two confirmation paths (the first is the customer returning
5
- * to the storefront, which calls `commerce/payments` `verify`). This one covers
6
- * the buyer who pays and closes the tab. Both are idempotent, so whichever
7
- * arrives second does nothing.
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
10
  *
9
- * Unlike every other function here this takes the provider's **raw body** rather
10
- * than an `{action, ...}` envelope, because the signature is computed over those
11
- * exact bytes.
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).
12
14
  *
13
- * **Trust model — the payload is never believed on its own.** Two paths:
15
+ * Unlike every other function here this takes the provider's **raw body**
16
+ * rather than an `{action, ...}` envelope, because webhook signatures are
17
+ * computed over those exact bytes.
14
18
  *
15
- * 1. **A signing secret is configured** (`PAYMENT_WEBHOOK_SECRET`, or
16
- * `STRIPE_WEBHOOK_SECRET`): the signature is verified here and the event's
17
- * "paid" is then trusted directly — one fewer round trip.
18
- * 2. **No secret available** — which is the case on Base44, where a function's
19
- * environment is fixed (`BASE44_APP_ID`, `STRIPE_PUBLISHABLE_KEY`,
20
- * `STRIPE_SECRET_KEY`) and apps cannot add their own: the body is treated as
21
- * nothing more than a *nudge naming an order*. Whether money arrived is then
22
- * asked of the provider directly, over our own authenticated API call.
23
- *
24
- * Either way the event must carry a matching `order_key`, and any session it
25
- * names is checked to have been opened for that order — so a real payment for
26
- * one order can't be pointed at another, and a forged webhook achieves nothing
27
- * beyond making us re-check an order.
28
- *
29
- * So this endpoint is useful with or without a secret, and safe either way.
30
- *
31
- * Setup: register this function's URL with the provider (Stripe: events
32
- * `checkout.session.completed`, `payment_intent.succeeded`). On the hosted Base44
33
- * platform, also configure the app's Stripe webhook secret in the app settings —
34
- * verified behavior: a request carrying a `stripe-signature` header is rejected
35
- * by the platform *before* any function runs unless that is set.
36
- *
37
- * Provider specifics (signature scheme, event shapes) live in
38
- * shared/commerce/payments.ts and its adapter — nothing here is Stripe-only.
19
+ * Until implemented, every request answers 400 `webhook_not_implemented`.
39
20
  */
40
21
  import { createClientFromRequest } from "npm:@base44/sdk";
41
22
  import { getSettings } from "../../../shared/commerce/settings.ts";
42
- import {
43
- confirmOnlinePayment,
44
- parsePaymentWebhook,
45
- verifyPaymentWebhook,
46
- } from "../../../shared/commerce/payments.ts";
23
+ import { confirmCardPayment } from "../../../shared/commerce/payments.ts";
47
24
 
48
25
  /** Providers retry on non-2xx; answer 200 for anything we deliberately ignore. */
49
26
  const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
50
27
  const fail = (status: number, error: string, code: string) =>
51
28
  Response.json({ success: false, error, code }, { status });
52
29
 
53
- function webhookSecret(): string {
54
- try {
55
- return String(
56
- Deno.env.get("PAYMENT_WEBHOOK_SECRET") ?? Deno.env.get("STRIPE_WEBHOOK_SECRET") ?? "",
57
- );
58
- } catch {
59
- return ""; // no env access → unsigned path (the payload is not trusted)
60
- }
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;
61
53
  }
62
54
 
63
55
  Deno.serve(async (req: Request) => {
64
56
  try {
65
57
  const payload = await req.text();
66
- const header = req.headers.get("stripe-signature") ?? req.headers.get("x-payment-signature") ?? "";
67
- const secret = webhookSecret();
68
58
 
69
- // With a secret we can prove the event came from the provider and take its
70
- // word. Without one we accept the request but believe nothing in it — the
71
- // payment is confirmed against the provider below.
72
- let signatureVerified = false;
73
- if (secret) {
74
- signatureVerified = await verifyPaymentWebhook({ payload, header, secret });
75
- if (!signatureVerified) return fail(400, "Invalid webhook signature.", "invalid_signature");
76
- }
77
-
78
- let body: any;
79
- try {
80
- body = JSON.parse(payload);
81
- } catch {
82
- return fail(400, "Malformed webhook body.", "invalid_body");
83
- }
84
- const event = parsePaymentWebhook(body);
85
- if (!event.order_id) return ok({ ignored: true, type: event.type, reason: "no_order_reference" });
86
- // An unsigned event claiming "paid" proves nothing, but it is still worth
87
- // re-checking the order it names; a signed one that doesn't claim paid isn't.
88
- if (signatureVerified && !event.paid) {
89
- return ok({ ignored: true, type: event.type, reason: "not_a_payment_success" });
59
+ const event = await parseWebhook(req, payload);
60
+ if (!event) {
61
+ return fail(400, "This store's payment webhook is not implemented.", "webhook_not_implemented");
90
62
  }
63
+ if (!event.order_id) return ok({ ignored: true, reason: "no_order_reference" });
91
64
 
92
65
  const base44 = createClientFromRequest(req);
93
66
  const sr = base44.asServiceRole;
94
67
 
95
68
  let order: any = null;
96
69
  try { order = await sr.entities["commerce.Order"].get(event.order_id); } catch { order = null; }
97
- // Required, not merely checked when offered: every session we open carries one.
70
+ // order_key is required, not merely checked when offered — it is what stops
71
+ // a forged event (or a payment for a cheap order) being pointed at another.
98
72
  if (!order || !event.order_key || order.order_key !== event.order_key) {
99
73
  console.error(`commerce/payment-webhook: no matching order for ${event.order_id}`);
100
74
  return ok({ ignored: true, reason: "order_not_found" });
101
75
  }
102
76
 
103
77
  const settings = await getSettings(sr);
104
- let result;
105
- try {
106
- result = await confirmOnlinePayment(sr, order, {
107
- sessionId: event.session_id,
108
- paymentReference: event.payment_reference,
109
- // Only a verified signature justifies skipping the provider round trip.
110
- trustedPaid: signatureVerified,
111
- settings,
112
- actor: "payment-webhook",
113
- });
114
- } catch (e) {
115
- // Forged or misrouted: answer 200 so the provider stops retrying.
116
- if ((e as any)?.code === "session_order_mismatch") {
117
- console.error(`commerce/payment-webhook: session ${event.session_id} is not for order ${order.id}`);
118
- return ok({ ignored: true, reason: "session_order_mismatch" });
119
- }
120
- throw e;
121
- }
78
+ const result = await confirmCardPayment(sr, order, {
79
+ reference: event.reference,
80
+ trustedPaid: event.paid,
81
+ settings,
82
+ actor: "payment-webhook",
83
+ });
122
84
 
123
85
  return ok({
124
86
  order_id: order.id,
125
87
  paid: result.paid,
126
88
  already_confirmed: result.already_confirmed,
127
89
  status: result.order.status,
128
- verified_signature: signatureVerified,
129
90
  });
130
91
  } catch (e) {
131
92
  // 500 makes the provider retry, which is what we want for a transient failure.
@@ -1,18 +1,18 @@
1
1
  /**
2
- * commerce/payments — online payment for an order, provider-neutral.
2
+ * commerce/payments — online payment for an order.
3
3
  *
4
- * All provider specifics live in shared/commerce/payments.ts (currently wired to
5
- * the Stripe adapter); this function is the API the storefront and the admin use.
4
+ * The provider specifics live in the two implementable files (see
5
+ * `.agents/skills/commerce/references/online-payments.md`); this function is
6
+ * the premade API the storefront and the admin use.
6
7
  *
7
8
  * Actions:
8
- * status — is an online payment provider connected? (admin)
9
9
  * create-link — hosted payment page for an unpaid order: the storefront's
10
10
  * redirect target and the admin's "payment link" (admin, or the
11
11
  * customer holding the order_key). Works whatever the order's
12
12
  * current payment method is — asking for a link is the decision
13
- * to collect online, so the order is switched onto the online
13
+ * to collect by card, so the order is switched onto the card
14
14
  * gateway (noted in the order log). Refused only when the store
15
- * has online payment switched off.
15
+ * has the card option switched off.
16
16
  * complete-return — the `/order-received` flow in one call: confirm the payment,
17
17
  * progress the order, and return the render-ready result plus a
18
18
  * fresh payment link while unpaid. This is what the storefront's
@@ -23,25 +23,19 @@
23
23
  * Access: `order_id` + `order_key` authorizes the customer who owns the order
24
24
  * (the same bearer rule as commerce/storefront-account); an admin may act on any
25
25
  * order without a key. Entity access is service-role throughout.
26
- * Authorization is not confirmation: a `session_id` passed to verify/
27
- * complete-return must be one the provider opened for this order
28
- * (409 `session_order_mismatch`).
29
26
  */
30
27
  import { createClientFromRequest } from "npm:@base44/sdk";
31
28
  import { HttpError, getCallerUser, isAdmin } from "../../../shared/commerce/auth.ts";
32
29
  import { getSettings } from "../../../shared/commerce/settings.ts";
33
30
  import { serializeOrderForCustomer } from "../../../shared/commerce/orders.ts";
34
31
  import {
35
- ACTIVE_PROVIDER,
36
- confirmOnlinePayment,
37
- isOnlineGateway,
32
+ CARD_GATEWAY_SLUG,
33
+ confirmCardPayment,
34
+ isCardGateway,
38
35
  isOrderPaid,
39
- onlinePaymentStatus,
40
- orderMeta,
41
36
  paymentReturnState,
42
37
  resolveReturnUrls,
43
- SESSION_META_KEY,
44
- startOnlinePayment,
38
+ startCardPayment,
45
39
  } from "../../../shared/commerce/payments.ts";
46
40
 
47
41
  function ok(data: unknown, status = 200): Response {
@@ -71,33 +65,28 @@ Deno.serve(async (req: Request) => {
71
65
  const { action, ...payload } = await req.json();
72
66
 
73
67
  switch (action) {
74
- case "status": {
75
- if (!admin) throw new HttpError(403, "Admin access required.", "forbidden");
76
- return ok(await onlinePaymentStatus(sr));
77
- }
78
-
79
68
  case "create-link": {
80
69
  const order = await authorizeOrder(sr, payload, admin);
81
70
  if (isOrderPaid(order)) throw new HttpError(409, "This order is already paid.", "already_paid");
82
71
 
83
72
  // Asking for a payment link *is* the decision to collect this order
84
- // online, so it works whatever the order's current method is — an admin
73
+ // by card, so it works whatever the order's current method is — an admin
85
74
  // order starts with none at all, and switching from a manual method is a
86
75
  // normal thing to want ("just send them a card link"). The one hard
87
- // requirement is that the store offers online payment: an operator who
76
+ // requirement is that the store offers card payment: an operator who
88
77
  // turned the card gateway off has declined it, and this must respect that.
89
- const gateway = await onlineGateway(sr);
78
+ const gateway = await cardGateway(sr);
90
79
  if (!gateway || gateway.enabled === false) {
91
80
  throw new HttpError(
92
81
  400,
93
- "Online payment is switched off for this store.",
94
- "online_payments_disabled",
95
- { gateway_slug: ACTIVE_PROVIDER.gatewaySlug },
82
+ "Card payment is switched off for this store.",
83
+ "card_payments_disabled",
84
+ { gateway_slug: CARD_GATEWAY_SLUG },
96
85
  );
97
86
  }
98
87
  // Record the switch, or the order would claim it's cash-on-delivery while
99
88
  // the customer pays by card — and refunds key off `payment_method`.
100
- if (!isOnlineGateway(order.payment_method)) {
89
+ if (!isCardGateway(order.payment_method)) {
101
90
  const previous = order.payment_method_title || order.payment_method || "none";
102
91
  await sr.entities["commerce.Order"].update(order.id, {
103
92
  payment_method: gateway.slug,
@@ -122,7 +111,7 @@ Deno.serve(async (req: Request) => {
122
111
  returnUrl: payload.return_url,
123
112
  returnPath: settings.general?.order_received_path,
124
113
  });
125
- const link = await startOnlinePayment(sr, order, {
114
+ const link = await startCardPayment(sr, order, {
126
115
  successUrl,
127
116
  cancelUrl,
128
117
  customerEmail: order.billing?.email || undefined,
@@ -132,7 +121,7 @@ Deno.serve(async (req: Request) => {
132
121
 
133
122
  /**
134
123
  * The whole payment-return flow in one call, for the `/order-received`
135
- * page a storefront has to implement: confirm with the provider, move the
124
+ * page a storefront has to implement: confirm the payment, move the
136
125
  * order on if the money landed, and hand back everything the page needs to
137
126
  * render — including a fresh payment link when it is still unpaid, so
138
127
  * "Pay now" needs no second round trip.
@@ -144,18 +133,17 @@ Deno.serve(async (req: Request) => {
144
133
  case "complete-return": {
145
134
  const order = await authorizeOrder(sr, payload, admin);
146
135
  const settings = await getSettings(sr);
147
- const result = await confirmOnlinePayment(sr, order, {
148
- sessionId: payload.session_id || orderMeta(order, SESSION_META_KEY),
136
+ const result = await confirmCardPayment(sr, order, {
149
137
  settings,
150
138
  actor: admin ? (user?.email ?? "admin") : "customer-return",
151
139
  });
152
140
  const state = paymentReturnState({ paid: result.paid, outcome: payload.payment });
153
141
 
154
- // Offer a way to pay again while unpaid — best-effort: an unavailable
142
+ // Offer a way to pay again while unpaid — best-effort: an unimplemented
155
143
  // provider or a store that switched card payment off must not break the
156
144
  // page, it just means no "Pay now" button.
157
- let paymentLink: { url: string; session_id: string } | null = null;
158
- if (state !== "paid" && payload.with_payment_link !== false && isOnlineGateway(result.order.payment_method)) {
145
+ let paymentLink: { url: string; reference: string } | null = null;
146
+ if (state !== "paid" && payload.with_payment_link !== false && isCardGateway(result.order.payment_method)) {
159
147
  try {
160
148
  const { successUrl, cancelUrl } = resolveReturnUrls({
161
149
  order: result.order,
@@ -165,17 +153,33 @@ Deno.serve(async (req: Request) => {
165
153
  returnUrl: payload.return_url,
166
154
  returnPath: settings.general?.order_received_path,
167
155
  });
168
- const link = await startOnlinePayment(sr, result.order, {
156
+ paymentLink = await startCardPayment(sr, result.order, {
169
157
  successUrl,
170
158
  cancelUrl,
171
159
  customerEmail: result.order.billing?.email || undefined,
172
160
  });
173
- paymentLink = { url: link.url, session_id: link.session_id };
174
161
  } catch (e) {
175
162
  console.error("complete-return: could not mint a payment link:", e);
176
163
  }
177
164
  }
178
165
 
166
+ // A manual order still needs its payment instructions after the
167
+ // customer lands back here — the place-order response that carried
168
+ // them is long gone, so the return page gets them again.
169
+ let paymentInstructions: any = null;
170
+ if (state !== "paid" && result.order.payment_method && !isCardGateway(result.order.payment_method)) {
171
+ const gw = (await sr.entities["commerce.PaymentGateway"].filter(
172
+ { slug: result.order.payment_method }, undefined, 1,
173
+ ))?.[0];
174
+ if (gw) {
175
+ paymentInstructions = {
176
+ type: gw.slug === "offline" ? "offline" : "manual",
177
+ description: gw.description ?? "",
178
+ account_details: gw.settings?.account_details ?? [],
179
+ };
180
+ }
181
+ }
182
+
179
183
  return ok({
180
184
  state, // paid | cancelled | unpaid
181
185
  paid: result.paid,
@@ -183,14 +187,14 @@ Deno.serve(async (req: Request) => {
183
187
  status: result.order.status,
184
188
  order: admin ? result.order : serializeOrderForCustomer(result.order),
185
189
  payment_link: paymentLink,
190
+ payment_instructions: paymentInstructions,
186
191
  });
187
192
  }
188
193
 
189
194
  case "verify": {
190
195
  const order = await authorizeOrder(sr, payload, admin);
191
196
  const settings = await getSettings(sr);
192
- const result = await confirmOnlinePayment(sr, order, {
193
- sessionId: payload.session_id || orderMeta(order, SESSION_META_KEY),
197
+ const result = await confirmCardPayment(sr, order, {
194
198
  settings,
195
199
  actor: admin ? (user?.email ?? "admin") : "customer-return",
196
200
  });
@@ -210,10 +214,10 @@ Deno.serve(async (req: Request) => {
210
214
  }
211
215
  });
212
216
 
213
- /** The `commerce.PaymentGateway` record backing online payment, if any. */
214
- async function onlineGateway(sr: any): Promise<any | null> {
217
+ /** The `commerce.PaymentGateway` record for the card option, if any. */
218
+ async function cardGateway(sr: any): Promise<any | null> {
215
219
  const hits = (await sr.entities["commerce.PaymentGateway"].filter(
216
- { slug: ACTIVE_PROVIDER.gatewaySlug },
220
+ { slug: CARD_GATEWAY_SLUG },
217
221
  undefined,
218
222
  1,
219
223
  )) ?? [];
@@ -1,7 +1,8 @@
1
1
  /**
2
- * commerce/seed-store default data: settings groups, payment gateways, tax classes and
3
- * the fallback shipping zone. All idempotent — the seeder checks for existing
4
- * records (by group_id / slug / name) before creating.
2
+ * commerce/seed-store default data: settings groups, payment gateways and the
3
+ * fallback Shipping & Tax Location (seeded only when the caller passes no
4
+ * `locations` of their own). All idempotent — the seeder checks for
5
+ * existing records (by group_id / slug / name) before creating.
5
6
  */
6
7
 
7
8
  /** Per-type email config keys. reset_password/new_account are handled by Base44 auth. */
@@ -9,6 +10,10 @@ const EMAIL_TYPE_DEFAULTS: Record<string, any> = {
9
10
  new_order: { enabled: true, subject: "", heading: "", recipient: "" },
10
11
  cancelled_order: { enabled: true, subject: "", heading: "", recipient: "" },
11
12
  failed_order: { enabled: true, subject: "", heading: "", recipient: "" },
13
+ // Stock alerts sit in the same Settings → Emails list; subject/body are
14
+ // generated, so only the switch and recipient override are configurable.
15
+ low_stock: { enabled: true, recipient: "" },
16
+ out_of_stock: { enabled: true, recipient: "" },
12
17
  on_hold_order: { enabled: true, subject: "", heading: "" },
13
18
  processing_order: { enabled: true, subject: "", heading: "" },
14
19
  completed_order: { enabled: true, subject: "", heading: "" },
@@ -26,21 +31,18 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
26
31
  values: {
27
32
  // Storefront route customers return to after paying; a mismatch is a 404.
28
33
  order_received_path: "/order-received",
34
+ // A value, not a format — prices are rendered with Intl.NumberFormat.
29
35
  currency: "USD",
30
- currency_position: "left", // left | right | left_space | right_space
31
- thousand_sep: ",",
32
- decimal_sep: ".",
33
- num_decimals: 2,
36
+ weight_unit: "kg", // kg | g | lbs | oz
37
+ dimension_unit: "cm", // m | cm | mm | in | yd
34
38
  },
35
39
  },
36
40
  {
37
41
  group_id: "products",
38
42
  values: {
39
- weight_unit: "kg", // kg | g | lbs | oz
40
- dimension_unit: "cm", // m | cm | mm | in | yd
41
- enable_reviews: true,
42
- only_verified_reviews: false,
43
- review_rating_required: true,
43
+ // The one review switch the server enforces; every other review policy
44
+ // (login-gated, verified-only, required rating) belongs to the storefront
45
+ // UI — see .agents/skills/commerce/references/reviews.md.
44
46
  auto_approve_reviews: false,
45
47
  },
46
48
  },
@@ -49,9 +51,6 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
49
51
  values: {
50
52
  manage_stock: true,
51
53
  hold_stock_minutes: 60,
52
- notify_low_stock: true,
53
- notify_out_of_stock: true,
54
- notification_recipient: "",
55
54
  low_stock_threshold: 2,
56
55
  out_of_stock_threshold: 0,
57
56
  hide_out_of_stock: false,
@@ -62,7 +61,6 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
62
61
  values: {
63
62
  prices_include_tax: false,
64
63
  tax_based_on: "shipping", // shipping | billing
65
- shipping_tax_class: "inherit", // inherit | standard | <class slug>
66
64
  display_prices_shop: "excl", // incl | excl
67
65
  display_prices_cart: "excl",
68
66
  },
@@ -98,39 +96,34 @@ export const GATEWAY_DEFAULTS = [
98
96
  settings: { account_details: [] as any[] },
99
97
  },
100
98
  {
101
- slug: "stripe",
99
+ slug: "card",
102
100
  title: "Credit card",
103
101
  description: "Pay securely by credit card.",
104
- // Enabled by default: online payment is how a store should take money. It is
105
- // only *offered* to customers while a payment provider is connected — the
106
- // storefront gateway list and the admin both check that live — so enabling it
107
- // here cannot strand a shopper on an unusable option.
102
+ // Enabled means offered: the storefront lists every enabled gateway. Until
103
+ // shared/commerce/card-payment.ts is implemented, picking this at checkout
104
+ // answers 503 no_card_payment_provider — switch it off to hide it instead.
108
105
  enabled: true,
109
106
  order: 1,
110
- method_title: "Online card payments (Stripe by default)",
111
- method_description:
112
- "Card payments through the connected payment provider (Stripe out of the box). Set the provider up in the app's integrations to start taking payments; customers only see this option once it is connected.",
113
- settings: { connector: "stripe" },
107
+ method_title: "Credit card",
108
+ method_description: "Card payment on a provider-hosted page.",
109
+ settings: {},
114
110
  },
115
111
  ];
116
112
 
117
- export const TAX_CLASS_DEFAULTS = [
118
- { slug: "standard", name: "Standard" },
119
- { slug: "reduced-rate", name: "Reduced rate" },
120
- { slug: "zero-rate", name: "Zero rate" },
121
- ];
122
-
123
- export const REST_OF_WORLD_ZONE = {
113
+ /**
114
+ * Fallback Shipping & Tax Location so a fresh store can sell anywhere: one
115
+ * free shipping rate, the default 'Products' tax group with no rates (no tax),
116
+ * no shipping tax. Admins refine it in Settings → Shipping & Tax.
117
+ */
118
+ export const REST_OF_WORLD_LOCATION = {
124
119
  name: "Rest of the world",
125
120
  order: 999,
126
- locations: [] as any[],
127
- };
128
-
129
- /** Example method attached to the fallback zone so admins see the pattern. */
130
- export const REST_OF_WORLD_EXAMPLE_METHOD = {
131
- method_id: "flat_rate",
132
- title: "Flat rate",
133
- enabled: true,
134
- order: 0,
135
- settings: { cost: 0, tax_status: "taxable", class_costs: [], no_class_cost: 0, calculation_type: "class" },
121
+ regions: [] as any[],
122
+ shipping_rates: [
123
+ { id: "rest-of-world-standard", name: "Standard delivery", cost: 0, free_over: null },
124
+ ],
125
+ tax_groups: [
126
+ { name: "Products", rates: [] as any[] },
127
+ ],
128
+ shipping_tax: null,
136
129
  };