@base44/app-plugin-commerce 0.1.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
- package/base44/entities/commerce.Cart.jsonc +73 -0
- package/base44/entities/commerce.Coupon.jsonc +113 -0
- package/base44/entities/commerce.Customer.jsonc +96 -0
- package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
- package/base44/entities/commerce.EmailLog.jsonc +43 -0
- package/base44/entities/commerce.Order.jsonc +287 -0
- package/base44/entities/commerce.OrderNote.jsonc +31 -0
- package/base44/entities/commerce.OrderRefund.jsonc +64 -0
- package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
- package/base44/entities/commerce.Product.jsonc +291 -0
- package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
- package/base44/entities/commerce.ProductCategory.jsonc +51 -0
- package/base44/entities/commerce.ProductReview.jsonc +48 -0
- package/base44/entities/commerce.ProductTag.jsonc +30 -0
- package/base44/entities/commerce.ProductVariation.jsonc +167 -0
- package/base44/entities/commerce.ShippingClass.jsonc +30 -0
- package/base44/entities/commerce.ShippingZone.jsonc +41 -0
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
- package/base44/entities/commerce.StoreSettings.jsonc +23 -0
- package/base44/entities/commerce.TaxClass.jsonc +23 -0
- package/base44/entities/commerce.TaxRate.jsonc +68 -0
- package/base44/entities/commerce.Webhook.jsonc +57 -0
- package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
- package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
- package/base44/functions/commerce/admin-customers/entry.ts +141 -0
- package/base44/functions/commerce/admin-orders/entry.ts +396 -0
- package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
- package/base44/functions/commerce/admin-products/entry.ts +506 -0
- package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
- package/base44/functions/commerce/admin-reports/entry.ts +283 -0
- package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
- package/base44/functions/commerce/admin-tools/entry.ts +261 -0
- package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
- package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
- package/base44/functions/commerce/payments/entry.ts +238 -0
- package/base44/functions/commerce/seed-store/defaults.ts +162 -0
- package/base44/functions/commerce/seed-store/entry.ts +310 -0
- package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
- package/base44/functions/commerce/storefront-account/entry.ts +207 -0
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
- package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
- package/base44/shared/commerce/auth.ts +60 -0
- package/base44/shared/commerce/coupons.ts +257 -0
- package/base44/shared/commerce/data/continents.ts +75 -0
- package/base44/shared/commerce/data/countries.ts +307 -0
- package/base44/shared/commerce/data/currencies.ts +46 -0
- package/base44/shared/commerce/email-templates.ts +240 -0
- package/base44/shared/commerce/emails.ts +225 -0
- package/base44/shared/commerce/money.ts +66 -0
- package/base44/shared/commerce/orders.ts +251 -0
- package/base44/shared/commerce/payments.ts +495 -0
- package/base44/shared/commerce/reviews.ts +36 -0
- package/base44/shared/commerce/scan.ts +57 -0
- package/base44/shared/commerce/sequence.ts +35 -0
- package/base44/shared/commerce/settings.ts +57 -0
- package/base44/shared/commerce/shipping.ts +215 -0
- package/base44/shared/commerce/stock.ts +227 -0
- package/base44/shared/commerce/stripe.ts +463 -0
- package/base44/shared/commerce/tax.ts +136 -0
- package/base44/shared/commerce/totals.ts +314 -0
- package/base44/shared/commerce/webhooks.ts +116 -0
- package/package.json +37 -0
- package/scripts/install.js +156 -0
- package/skills/commerce/SKILL.md +62 -0
- package/skills/commerce/docs/api-admin.md +186 -0
- package/skills/commerce/docs/api-storefront.md +408 -0
- package/skills/commerce/installation-guidelines.md +91 -0
- package/skills/commerce/post-installation.md +157 -0
- package/skills/commerce/references/emails.md +13 -0
- package/skills/commerce/references/guest-access-security.md +18 -0
- package/skills/commerce/references/limits-and-performance.md +16 -0
- package/skills/commerce/references/media-and-downloads.md +4 -0
- package/skills/commerce/references/online-payments.md +201 -0
- package/skills/commerce/references/product-render.md +87 -0
- package/skills/commerce/references/scheduled-work.md +19 -0
- package/skills/commerce/references/storefront-product-page.md +83 -0
- package/skills/commerce/references/webhooks.md +8 -0
- package/src/commerce/admin/README.md +107 -0
- package/src/commerce/admin/bot/Markdown.jsx +138 -0
- package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
- package/src/commerce/admin/bot/pipe-tables.js +116 -0
- package/src/commerce/admin/components/AddressForm.jsx +78 -0
- package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
- package/src/commerce/admin/components/CountrySelect.jsx +81 -0
- package/src/commerce/admin/components/DataTable.jsx +192 -0
- package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
- package/src/commerce/admin/components/EmptyState.jsx +17 -0
- package/src/commerce/admin/components/MediaUploader.jsx +116 -0
- package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
- package/src/commerce/admin/components/MoneyInput.jsx +50 -0
- package/src/commerce/admin/components/PageHeader.jsx +29 -0
- package/src/commerce/admin/components/RichTextarea.jsx +21 -0
- package/src/commerce/admin/components/SearchSelect.jsx +142 -0
- package/src/commerce/admin/components/StatusBadge.jsx +17 -0
- package/src/commerce/admin/context/BasePathContext.jsx +26 -0
- package/src/commerce/admin/context/SettingsContext.jsx +207 -0
- package/src/commerce/admin/hooks/useAsync.js +46 -0
- package/src/commerce/admin/hooks/useDebounce.js +11 -0
- package/src/commerce/admin/hooks/useMoney.js +52 -0
- package/src/commerce/admin/hooks/usePagedList.js +83 -0
- package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
- package/src/commerce/admin/hooks/useRealtime.js +129 -0
- package/src/commerce/admin/index.jsx +34 -0
- package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
- package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
- package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
- package/src/commerce/admin/layout/Sidebar.jsx +130 -0
- package/src/commerce/admin/layout/Topbar.jsx +94 -0
- package/src/commerce/admin/lib/api.js +55 -0
- package/src/commerce/admin/lib/constants.js +157 -0
- package/src/commerce/admin/lib/format.js +27 -0
- package/src/commerce/admin/lib/geo-data.js +125 -0
- package/src/commerce/admin/lib/order-utils.js +147 -0
- package/src/commerce/admin/lib/paths.js +35 -0
- package/src/commerce/admin/lib/product-utils.js +55 -0
- package/src/commerce/admin/pages/Dashboard.jsx +245 -0
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
- package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
- package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
- package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
- package/src/commerce/admin/pages/products/Categories.jsx +236 -0
- package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
- package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
- package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
- package/src/commerce/admin/pages/products/Tags.jsx +150 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
- package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
- package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
- package/src/commerce/admin/routes.jsx +151 -0
- package/src/commerce/utils/index.js +19 -0
- package/src/commerce/utils/shipping-promos.js +99 -0
- package/src/commerce/utils/variants.js +411 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commerce/payment-webhook — the payment provider's server-to-server callback.
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* **Trust model — the payload is never believed on its own.** Two paths:
|
|
14
|
+
*
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
41
|
+
import { getSettings } from "../../../shared/commerce/settings.ts";
|
|
42
|
+
import {
|
|
43
|
+
confirmOnlinePayment,
|
|
44
|
+
parsePaymentWebhook,
|
|
45
|
+
verifyPaymentWebhook,
|
|
46
|
+
} from "../../../shared/commerce/payments.ts";
|
|
47
|
+
|
|
48
|
+
/** Providers retry on non-2xx; answer 200 for anything we deliberately ignore. */
|
|
49
|
+
const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
|
|
50
|
+
const fail = (status: number, error: string, code: string) =>
|
|
51
|
+
Response.json({ success: false, error, code }, { status });
|
|
52
|
+
|
|
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
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Deno.serve(async (req: Request) => {
|
|
64
|
+
try {
|
|
65
|
+
const payload = await req.text();
|
|
66
|
+
const header = req.headers.get("stripe-signature") ?? req.headers.get("x-payment-signature") ?? "";
|
|
67
|
+
const secret = webhookSecret();
|
|
68
|
+
|
|
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" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const base44 = createClientFromRequest(req);
|
|
93
|
+
const sr = base44.asServiceRole;
|
|
94
|
+
|
|
95
|
+
let order: any = null;
|
|
96
|
+
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.
|
|
98
|
+
if (!order || !event.order_key || order.order_key !== event.order_key) {
|
|
99
|
+
console.error(`commerce/payment-webhook: no matching order for ${event.order_id}`);
|
|
100
|
+
return ok({ ignored: true, reason: "order_not_found" });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
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
|
+
}
|
|
122
|
+
|
|
123
|
+
return ok({
|
|
124
|
+
order_id: order.id,
|
|
125
|
+
paid: result.paid,
|
|
126
|
+
already_confirmed: result.already_confirmed,
|
|
127
|
+
status: result.order.status,
|
|
128
|
+
verified_signature: signatureVerified,
|
|
129
|
+
});
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// 500 makes the provider retry, which is what we want for a transient failure.
|
|
132
|
+
console.error("commerce/payment-webhook error:", e);
|
|
133
|
+
return fail(500, (e as Error)?.message ?? "Internal error", "internal_error");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commerce/payments — online payment for an order, provider-neutral.
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* Actions:
|
|
8
|
+
* status — is an online payment provider connected? (admin)
|
|
9
|
+
* create-link — hosted payment page for an unpaid order: the storefront's
|
|
10
|
+
* redirect target and the admin's "payment link" (admin, or the
|
|
11
|
+
* customer holding the order_key). Works whatever the order's
|
|
12
|
+
* current payment method is — asking for a link is the decision
|
|
13
|
+
* to collect online, so the order is switched onto the online
|
|
14
|
+
* gateway (noted in the order log). Refused only when the store
|
|
15
|
+
* has online payment switched off.
|
|
16
|
+
* complete-return — the `/order-received` flow in one call: confirm the payment,
|
|
17
|
+
* progress the order, and return the render-ready result plus a
|
|
18
|
+
* fresh payment link while unpaid. This is what the storefront's
|
|
19
|
+
* payment return page calls.
|
|
20
|
+
* verify — confirm payment after the customer returns from the provider,
|
|
21
|
+
* or re-check on demand (same access rules). Idempotent.
|
|
22
|
+
*
|
|
23
|
+
* Access: `order_id` + `order_key` authorizes the customer who owns the order
|
|
24
|
+
* (the same bearer rule as commerce/storefront-account); an admin may act on any
|
|
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
|
+
*/
|
|
30
|
+
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
31
|
+
import { HttpError, getCallerUser, isAdmin } from "../../../shared/commerce/auth.ts";
|
|
32
|
+
import { getSettings } from "../../../shared/commerce/settings.ts";
|
|
33
|
+
import { serializeOrderForCustomer } from "../../../shared/commerce/orders.ts";
|
|
34
|
+
import {
|
|
35
|
+
ACTIVE_PROVIDER,
|
|
36
|
+
confirmOnlinePayment,
|
|
37
|
+
isOnlineGateway,
|
|
38
|
+
isOrderPaid,
|
|
39
|
+
onlinePaymentStatus,
|
|
40
|
+
orderMeta,
|
|
41
|
+
paymentReturnState,
|
|
42
|
+
resolveReturnUrls,
|
|
43
|
+
SESSION_META_KEY,
|
|
44
|
+
startOnlinePayment,
|
|
45
|
+
} from "../../../shared/commerce/payments.ts";
|
|
46
|
+
|
|
47
|
+
function ok(data: unknown, status = 200): Response {
|
|
48
|
+
return Response.json({ success: true, data }, { status });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(e: unknown): Response {
|
|
52
|
+
if (e instanceof HttpError) {
|
|
53
|
+
return Response.json(
|
|
54
|
+
{ success: false, error: e.message, code: e.code ?? "error", ...(e.details ?? {}) },
|
|
55
|
+
{ status: e.status },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
console.error("commerce/payments error:", e);
|
|
59
|
+
return Response.json(
|
|
60
|
+
{ success: false, error: (e as Error)?.message ?? "Internal error", code: "internal_error" },
|
|
61
|
+
{ status: 500 },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Deno.serve(async (req: Request) => {
|
|
66
|
+
try {
|
|
67
|
+
const base44 = createClientFromRequest(req);
|
|
68
|
+
const sr = base44.asServiceRole;
|
|
69
|
+
const user = await getCallerUser(base44);
|
|
70
|
+
const admin = isAdmin(user);
|
|
71
|
+
const { action, ...payload } = await req.json();
|
|
72
|
+
|
|
73
|
+
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
|
+
case "create-link": {
|
|
80
|
+
const order = await authorizeOrder(sr, payload, admin);
|
|
81
|
+
if (isOrderPaid(order)) throw new HttpError(409, "This order is already paid.", "already_paid");
|
|
82
|
+
|
|
83
|
+
// 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
|
|
85
|
+
// order starts with none at all, and switching from a manual method is a
|
|
86
|
+
// 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
|
|
88
|
+
// turned the card gateway off has declined it, and this must respect that.
|
|
89
|
+
const gateway = await onlineGateway(sr);
|
|
90
|
+
if (!gateway || gateway.enabled === false) {
|
|
91
|
+
throw new HttpError(
|
|
92
|
+
400,
|
|
93
|
+
"Online payment is switched off for this store.",
|
|
94
|
+
"online_payments_disabled",
|
|
95
|
+
{ gateway_slug: ACTIVE_PROVIDER.gatewaySlug },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
// Record the switch, or the order would claim it's cash-on-delivery while
|
|
99
|
+
// the customer pays by card — and refunds key off `payment_method`.
|
|
100
|
+
if (!isOnlineGateway(order.payment_method)) {
|
|
101
|
+
const previous = order.payment_method_title || order.payment_method || "none";
|
|
102
|
+
await sr.entities["commerce.Order"].update(order.id, {
|
|
103
|
+
payment_method: gateway.slug,
|
|
104
|
+
payment_method_title: gateway.title ?? gateway.slug,
|
|
105
|
+
});
|
|
106
|
+
order.payment_method = gateway.slug;
|
|
107
|
+
order.payment_method_title = gateway.title ?? gateway.slug;
|
|
108
|
+
await sr.entities["commerce.OrderNote"].create({
|
|
109
|
+
order_id: order.id,
|
|
110
|
+
note: `Payment method set to ${order.payment_method_title} (was ${previous}) to send a payment link.`,
|
|
111
|
+
is_customer_note: false,
|
|
112
|
+
added_by: admin ? (user?.email ?? "admin") : "customer",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const settings = await getSettings(sr, "general");
|
|
117
|
+
const { successUrl, cancelUrl } = resolveReturnUrls({
|
|
118
|
+
order,
|
|
119
|
+
req,
|
|
120
|
+
successUrl: payload.success_url,
|
|
121
|
+
cancelUrl: payload.cancel_url,
|
|
122
|
+
returnUrl: payload.return_url,
|
|
123
|
+
storeUrl: settings.general?.store_url,
|
|
124
|
+
returnPath: settings.general?.order_received_path,
|
|
125
|
+
});
|
|
126
|
+
const link = await startOnlinePayment(sr, order, {
|
|
127
|
+
successUrl,
|
|
128
|
+
cancelUrl,
|
|
129
|
+
customerEmail: order.billing?.email || undefined,
|
|
130
|
+
});
|
|
131
|
+
return ok(link, 201);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The whole payment-return flow in one call, for the `/order-received`
|
|
136
|
+
* page a storefront has to implement: confirm with the provider, move the
|
|
137
|
+
* order on if the money landed, and hand back everything the page needs to
|
|
138
|
+
* render — including a fresh payment link when it is still unpaid, so
|
|
139
|
+
* "Pay now" needs no second round trip.
|
|
140
|
+
*
|
|
141
|
+
* Pass the query parameters the customer came back with. `payment` is only
|
|
142
|
+
* a hint: a confirmed payment always wins over it, so a hand-edited
|
|
143
|
+
* `?payment=success` cannot make this say paid.
|
|
144
|
+
*/
|
|
145
|
+
case "complete-return": {
|
|
146
|
+
const order = await authorizeOrder(sr, payload, admin);
|
|
147
|
+
const settings = await getSettings(sr);
|
|
148
|
+
const result = await confirmOnlinePayment(sr, order, {
|
|
149
|
+
sessionId: payload.session_id || orderMeta(order, SESSION_META_KEY),
|
|
150
|
+
settings,
|
|
151
|
+
actor: admin ? (user?.email ?? "admin") : "customer-return",
|
|
152
|
+
});
|
|
153
|
+
const state = paymentReturnState({ paid: result.paid, outcome: payload.payment });
|
|
154
|
+
|
|
155
|
+
// Offer a way to pay again while unpaid — best-effort: an unavailable
|
|
156
|
+
// provider or a store that switched card payment off must not break the
|
|
157
|
+
// page, it just means no "Pay now" button.
|
|
158
|
+
let paymentLink: { url: string; session_id: string } | null = null;
|
|
159
|
+
if (state !== "paid" && payload.with_payment_link !== false && isOnlineGateway(result.order.payment_method)) {
|
|
160
|
+
try {
|
|
161
|
+
const { successUrl, cancelUrl } = resolveReturnUrls({
|
|
162
|
+
order: result.order,
|
|
163
|
+
req,
|
|
164
|
+
successUrl: payload.success_url,
|
|
165
|
+
cancelUrl: payload.cancel_url,
|
|
166
|
+
returnUrl: payload.return_url,
|
|
167
|
+
storeUrl: settings.general?.store_url,
|
|
168
|
+
returnPath: settings.general?.order_received_path,
|
|
169
|
+
});
|
|
170
|
+
const link = await startOnlinePayment(sr, result.order, {
|
|
171
|
+
successUrl,
|
|
172
|
+
cancelUrl,
|
|
173
|
+
customerEmail: result.order.billing?.email || undefined,
|
|
174
|
+
});
|
|
175
|
+
paymentLink = { url: link.url, session_id: link.session_id };
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error("complete-return: could not mint a payment link:", e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return ok({
|
|
182
|
+
state, // paid | cancelled | unpaid
|
|
183
|
+
paid: result.paid,
|
|
184
|
+
already_confirmed: result.already_confirmed,
|
|
185
|
+
status: result.order.status,
|
|
186
|
+
order: admin ? result.order : serializeOrderForCustomer(result.order),
|
|
187
|
+
payment_link: paymentLink,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case "verify": {
|
|
192
|
+
const order = await authorizeOrder(sr, payload, admin);
|
|
193
|
+
const settings = await getSettings(sr);
|
|
194
|
+
const result = await confirmOnlinePayment(sr, order, {
|
|
195
|
+
sessionId: payload.session_id || orderMeta(order, SESSION_META_KEY),
|
|
196
|
+
settings,
|
|
197
|
+
actor: admin ? (user?.email ?? "admin") : "customer-return",
|
|
198
|
+
});
|
|
199
|
+
return ok({
|
|
200
|
+
paid: result.paid,
|
|
201
|
+
already_confirmed: result.already_confirmed,
|
|
202
|
+
status: result.order.status,
|
|
203
|
+
order: admin ? result.order : serializeOrderForCustomer(result.order),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
default:
|
|
208
|
+
throw new HttpError(400, `Unknown action: ${action}`, "unknown_action");
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return fail(e);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/** The `commerce.PaymentGateway` record backing online payment, if any. */
|
|
216
|
+
async function onlineGateway(sr: any): Promise<any | null> {
|
|
217
|
+
const hits = (await sr.entities["commerce.PaymentGateway"].filter(
|
|
218
|
+
{ slug: ACTIVE_PROVIDER.gatewaySlug },
|
|
219
|
+
undefined,
|
|
220
|
+
1,
|
|
221
|
+
)) ?? [];
|
|
222
|
+
return hits[0] ?? null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** order_key bearer, or an admin acting on any order. */
|
|
226
|
+
async function authorizeOrder(sr: any, payload: any, admin: boolean): Promise<any> {
|
|
227
|
+
const orderId = String(payload?.order_id ?? "");
|
|
228
|
+
if (!orderId) throw new HttpError(400, "order_id is required.", "invalid_payload");
|
|
229
|
+
let order: any = null;
|
|
230
|
+
try { order = await sr.entities["commerce.Order"].get(orderId); } catch { order = null; }
|
|
231
|
+
if (!order) throw new HttpError(404, "Order not found.", "order_not_found");
|
|
232
|
+
if (admin) return order;
|
|
233
|
+
|
|
234
|
+
const key = String(payload?.order_key ?? "");
|
|
235
|
+
if (!key) throw new HttpError(400, "order_key is required.", "order_key_required");
|
|
236
|
+
if (order.order_key !== key) throw new HttpError(404, "Order not found.", "order_not_found");
|
|
237
|
+
return order;
|
|
238
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Per-type email config keys. reset_password/new_account are handled by Base44 auth. */
|
|
8
|
+
const EMAIL_TYPE_DEFAULTS: Record<string, any> = {
|
|
9
|
+
new_order: { enabled: true, subject: "", heading: "", recipient: "" },
|
|
10
|
+
cancelled_order: { enabled: true, subject: "", heading: "", recipient: "" },
|
|
11
|
+
failed_order: { enabled: true, subject: "", heading: "", recipient: "" },
|
|
12
|
+
on_hold_order: { enabled: true, subject: "", heading: "" },
|
|
13
|
+
processing_order: { enabled: true, subject: "", heading: "" },
|
|
14
|
+
completed_order: { enabled: true, subject: "", heading: "" },
|
|
15
|
+
refunded_order: { enabled: true, subject: "", heading: "" },
|
|
16
|
+
partial_refund: { enabled: true, subject: "", heading: "" },
|
|
17
|
+
customer_invoice: { enabled: true, subject: "", heading: "" },
|
|
18
|
+
customer_note: { enabled: true, subject: "", heading: "" },
|
|
19
|
+
reset_password: { enabled: true, managed_by: "base44_auth" },
|
|
20
|
+
new_account: { enabled: true, managed_by: "base44_auth" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string, any> }> = [
|
|
24
|
+
{
|
|
25
|
+
group_id: "general",
|
|
26
|
+
values: {
|
|
27
|
+
store_name: "", // seed-store payload supplies it (the app/store name)
|
|
28
|
+
// Public URL of the shopfront (e.g. https://shop.example.com). Used to
|
|
29
|
+
// build the page a customer returns to after paying; when empty the
|
|
30
|
+
// origin of the calling app is used instead.
|
|
31
|
+
store_url: "",
|
|
32
|
+
// Storefront route customers return to after paying; a mismatch is a 404.
|
|
33
|
+
order_received_path: "/order-received",
|
|
34
|
+
address: { address_1: "", address_2: "", city: "", state: "", postcode: "", country: "US" },
|
|
35
|
+
enable_taxes: true,
|
|
36
|
+
enable_coupons: true,
|
|
37
|
+
calc_discounts_sequentially: false,
|
|
38
|
+
currency: "USD",
|
|
39
|
+
currency_position: "left", // left | right | left_space | right_space
|
|
40
|
+
thousand_sep: ",",
|
|
41
|
+
decimal_sep: ".",
|
|
42
|
+
num_decimals: 2,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
group_id: "products",
|
|
47
|
+
values: {
|
|
48
|
+
weight_unit: "kg", // kg | g | lbs | oz
|
|
49
|
+
dimension_unit: "cm", // m | cm | mm | in | yd
|
|
50
|
+
enable_reviews: true,
|
|
51
|
+
only_verified_reviews: false,
|
|
52
|
+
review_rating_required: true,
|
|
53
|
+
auto_approve_reviews: false,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
group_id: "inventory",
|
|
58
|
+
values: {
|
|
59
|
+
manage_stock: true,
|
|
60
|
+
hold_stock_minutes: 60,
|
|
61
|
+
notify_low_stock: true,
|
|
62
|
+
notify_out_of_stock: true,
|
|
63
|
+
notification_recipient: "",
|
|
64
|
+
low_stock_threshold: 2,
|
|
65
|
+
out_of_stock_threshold: 0,
|
|
66
|
+
hide_out_of_stock: false,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
group_id: "tax",
|
|
71
|
+
values: {
|
|
72
|
+
prices_include_tax: false,
|
|
73
|
+
tax_based_on: "shipping", // shipping | billing | base
|
|
74
|
+
shipping_tax_class: "inherit", // inherit | standard | <class slug>
|
|
75
|
+
display_prices_shop: "excl", // incl | excl
|
|
76
|
+
display_prices_cart: "excl",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
group_id: "shipping",
|
|
81
|
+
values: {
|
|
82
|
+
enable_shipping: true,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
group_id: "emails",
|
|
87
|
+
values: {
|
|
88
|
+
from_name: "", // blank: emails.ts inherits general.store_name
|
|
89
|
+
admin_recipients: [],
|
|
90
|
+
...EMAIL_TYPE_DEFAULTS,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export const GATEWAY_DEFAULTS = [
|
|
96
|
+
{
|
|
97
|
+
slug: "bacs",
|
|
98
|
+
title: "Direct bank transfer",
|
|
99
|
+
description: "Make your payment directly into our bank account. Please use your Order ID as the payment reference.",
|
|
100
|
+
enabled: true,
|
|
101
|
+
order: 0,
|
|
102
|
+
method_title: "Direct bank transfer",
|
|
103
|
+
method_description: "Take payments in person via BACS. Orders are set on-hold until payment clears.",
|
|
104
|
+
settings: { account_details: [] as any[] },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
slug: "cheque",
|
|
108
|
+
title: "Check payments",
|
|
109
|
+
description: "Please send a check to our store address.",
|
|
110
|
+
enabled: false,
|
|
111
|
+
order: 1,
|
|
112
|
+
method_title: "Check payments",
|
|
113
|
+
method_description: "Take payments by check. Orders are set on-hold until payment clears.",
|
|
114
|
+
settings: {},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
slug: "cod",
|
|
118
|
+
title: "Cash on delivery",
|
|
119
|
+
description: "Pay with cash upon delivery.",
|
|
120
|
+
enabled: true,
|
|
121
|
+
order: 2,
|
|
122
|
+
method_title: "Cash on delivery",
|
|
123
|
+
method_description: "Have your customers pay with cash (or by other means) upon delivery.",
|
|
124
|
+
settings: { enable_for_methods: [] as string[], enable_for_virtual: false },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
slug: "stripe",
|
|
128
|
+
title: "Credit card",
|
|
129
|
+
description: "Pay securely by credit card.",
|
|
130
|
+
// Enabled by default: online payment is how a store should take money. It is
|
|
131
|
+
// only *offered* to customers while a payment provider is connected — the
|
|
132
|
+
// storefront gateway list and the admin both check that live — so enabling it
|
|
133
|
+
// here cannot strand a shopper on an unusable option.
|
|
134
|
+
enabled: true,
|
|
135
|
+
order: 3,
|
|
136
|
+
method_title: "Online card payments",
|
|
137
|
+
method_description:
|
|
138
|
+
"Card payments through the connected payment provider (Stripe out of the box). Connect the provider's connector to start taking payments; customers only see this option once it is connected.",
|
|
139
|
+
settings: { connector: "stripe" },
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export const TAX_CLASS_DEFAULTS = [
|
|
144
|
+
{ slug: "standard", name: "Standard" },
|
|
145
|
+
{ slug: "reduced-rate", name: "Reduced rate" },
|
|
146
|
+
{ slug: "zero-rate", name: "Zero rate" },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
export const REST_OF_WORLD_ZONE = {
|
|
150
|
+
name: "Rest of the world",
|
|
151
|
+
order: 999,
|
|
152
|
+
locations: [] as any[],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Example method attached to the fallback zone so admins see the pattern. */
|
|
156
|
+
export const REST_OF_WORLD_EXAMPLE_METHOD = {
|
|
157
|
+
method_id: "flat_rate",
|
|
158
|
+
title: "Flat rate",
|
|
159
|
+
enabled: true,
|
|
160
|
+
order: 0,
|
|
161
|
+
settings: { cost: 0, tax_status: "taxable", class_costs: [], no_class_cost: 0, calculation_type: "class" },
|
|
162
|
+
};
|