@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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commerce/storefront-cart — public cart API (guest + member).
|
|
3
|
+
* Every action takes `cart_token` except `create`. Carts are bearer-token
|
|
4
|
+
* scoped (RLS blocks direct entity access); logged-in callers get their
|
|
5
|
+
* active carts merged by email.
|
|
6
|
+
*
|
|
7
|
+
* Actions: create | get | add-item | update-item | remove-item |
|
|
8
|
+
* apply-coupon | remove-coupon | set-shipping-address |
|
|
9
|
+
* choose-shipping-method | totals
|
|
10
|
+
* Every mutating action returns the fresh priced cart view.
|
|
11
|
+
*/
|
|
12
|
+
import { createClientFromRequest } from "npm:@base44/sdk";
|
|
13
|
+
import { HttpError, getCallerUser } from "../../../shared/commerce/auth.ts";
|
|
14
|
+
import { getSetting } from "../../../shared/commerce/settings.ts";
|
|
15
|
+
import { checkPurchasable } from "../../../shared/commerce/stock.ts";
|
|
16
|
+
import { validateCoupon } from "../../../shared/commerce/coupons.ts";
|
|
17
|
+
import { uuid } from "../../../shared/commerce/sequence.ts";
|
|
18
|
+
import {
|
|
19
|
+
cartExpiry,
|
|
20
|
+
couponCtxLines,
|
|
21
|
+
findCoupon,
|
|
22
|
+
loadCart,
|
|
23
|
+
loadPricingData,
|
|
24
|
+
priceCart,
|
|
25
|
+
} from "./cart-pricing.ts";
|
|
26
|
+
import { round2 } from "../../../shared/commerce/money.ts";
|
|
27
|
+
|
|
28
|
+
function ok(data: unknown, status = 200): Response {
|
|
29
|
+
return Response.json({ success: true, data }, { status });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fail(e: unknown): Response {
|
|
33
|
+
if (e instanceof HttpError) {
|
|
34
|
+
return Response.json(
|
|
35
|
+
{ success: false, error: e.message, code: e.code ?? "error", ...(e.details ?? {}) },
|
|
36
|
+
{ status: e.status },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
console.error("commerce/storefront-cart error:", e);
|
|
40
|
+
return Response.json(
|
|
41
|
+
{ success: false, error: (e as Error)?.message ?? "Internal error", code: "internal_error" },
|
|
42
|
+
{ status: 500 },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Persist a patch and refresh the rolling 48h expiry. */
|
|
47
|
+
async function touch(sr: any, cart: any, patch: Record<string, any> = {}): Promise<void> {
|
|
48
|
+
const full = { ...patch, expires_at: cartExpiry() };
|
|
49
|
+
await sr.entities["commerce.Cart"].update(cart.id, full);
|
|
50
|
+
Object.assign(cart, full);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function loadProductPair(sr: any, productId: string, variationId?: string): Promise<{ product: any; variation?: any }> {
|
|
54
|
+
let product: any = null;
|
|
55
|
+
try { product = productId ? await sr.entities["commerce.Product"].get(productId) : null; } catch { product = null; }
|
|
56
|
+
if (!product) throw new HttpError(404, "Product not found.", "product_not_found");
|
|
57
|
+
let variation: any = undefined;
|
|
58
|
+
if (variationId) {
|
|
59
|
+
try { variation = await sr.entities["commerce.ProductVariation"].get(variationId); } catch { variation = undefined; }
|
|
60
|
+
if (!variation || variation.product_id !== product.id) {
|
|
61
|
+
throw new HttpError(404, "Product option not found.", "variation_not_found");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { product, variation };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge the user's other active carts into `cart` (login-after-browsing flow),
|
|
69
|
+
* abandoning the sources. Quantities sum; sold-individually products cap at 1.
|
|
70
|
+
*/
|
|
71
|
+
async function mergeUserCarts(sr: any, cart: any, user: any): Promise<void> {
|
|
72
|
+
if (!user?.email) return;
|
|
73
|
+
const patch: Record<string, any> = {};
|
|
74
|
+
if (cart.customer_email !== user.email) patch.customer_email = user.email;
|
|
75
|
+
|
|
76
|
+
const others = (await sr.entities["commerce.Cart"].filter({ customer_email: user.email, status: "active" }, "-updated_date", 20)) ?? [];
|
|
77
|
+
let merged = false;
|
|
78
|
+
for (const other of others) {
|
|
79
|
+
if (other.id === cart.id || other.cart_token === cart.cart_token) continue;
|
|
80
|
+
for (const oi of other.items || []) {
|
|
81
|
+
const existing = (cart.items || []).find(
|
|
82
|
+
(i: any) => i.product_id === oi.product_id && (i.variation_id || "") === (oi.variation_id || ""),
|
|
83
|
+
);
|
|
84
|
+
if (existing) {
|
|
85
|
+
let qty = existing.quantity + oi.quantity;
|
|
86
|
+
try {
|
|
87
|
+
const { product } = await loadProductPair(sr, oi.product_id, undefined);
|
|
88
|
+
if (product.sold_individually) qty = 1;
|
|
89
|
+
} catch { /* keep summed qty; pricing revalidates */ }
|
|
90
|
+
existing.quantity = qty;
|
|
91
|
+
} else {
|
|
92
|
+
cart.items = [...(cart.items || []), oi];
|
|
93
|
+
}
|
|
94
|
+
merged = true;
|
|
95
|
+
}
|
|
96
|
+
for (const code of other.coupon_codes || []) {
|
|
97
|
+
if (!(cart.coupon_codes || []).includes(code)) {
|
|
98
|
+
cart.coupon_codes = [...(cart.coupon_codes || []), code];
|
|
99
|
+
merged = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try { await sr.entities["commerce.Cart"].update(other.id, { status: "abandoned" }); } catch { /* best-effort */ }
|
|
103
|
+
}
|
|
104
|
+
if (merged) {
|
|
105
|
+
patch.items = cart.items;
|
|
106
|
+
patch.coupon_codes = cart.coupon_codes ?? [];
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(patch).length) await touch(sr, cart, patch);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Deno.serve(async (req: Request) => {
|
|
112
|
+
try {
|
|
113
|
+
const base44 = createClientFromRequest(req);
|
|
114
|
+
const sr = base44.asServiceRole;
|
|
115
|
+
const { action, ...payload } = await req.json().catch(() => ({}));
|
|
116
|
+
if (!action) throw new HttpError(400, "action is required.", "action_required");
|
|
117
|
+
const user = await getCallerUser(base44);
|
|
118
|
+
|
|
119
|
+
switch (action) {
|
|
120
|
+
case "create": {
|
|
121
|
+
const cart = await sr.entities["commerce.Cart"].create({
|
|
122
|
+
cart_token: uuid(),
|
|
123
|
+
customer_email: user?.email ?? "",
|
|
124
|
+
items: [],
|
|
125
|
+
coupon_codes: [],
|
|
126
|
+
status: "active",
|
|
127
|
+
expires_at: cartExpiry(),
|
|
128
|
+
});
|
|
129
|
+
// optional initial items go through the same add path validations
|
|
130
|
+
for (const it of payload.items || []) {
|
|
131
|
+
await addItem(sr, cart, it);
|
|
132
|
+
}
|
|
133
|
+
if (user) await mergeUserCarts(sr, cart, user);
|
|
134
|
+
return ok((await priceCart(sr, cart)).view);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "get":
|
|
138
|
+
case "totals": {
|
|
139
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
140
|
+
if (user) await mergeUserCarts(sr, cart, user);
|
|
141
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "add-item": {
|
|
145
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
146
|
+
await addItem(sr, cart, payload);
|
|
147
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "update-item": {
|
|
151
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
152
|
+
const { item_key, quantity } = payload;
|
|
153
|
+
const items = [...(cart.items || [])];
|
|
154
|
+
const idx = items.findIndex((i: any) => i.item_key === item_key);
|
|
155
|
+
if (idx < 0) throw new HttpError(404, "Cart item not found.", "item_not_found");
|
|
156
|
+
const qty = Math.floor(Number(quantity));
|
|
157
|
+
if (!Number.isFinite(qty) || qty <= 0) {
|
|
158
|
+
items.splice(idx, 1);
|
|
159
|
+
} else {
|
|
160
|
+
const { product, variation } = await loadProductPair(sr, items[idx].product_id, items[idx].variation_id);
|
|
161
|
+
const pricing = await loadPricingData(sr);
|
|
162
|
+
const check = checkPurchasable(product, variation, qty, pricing.settings);
|
|
163
|
+
if (!check.ok) throw new HttpError(400, check.error ?? "Not available.", check.code);
|
|
164
|
+
items[idx] = { ...items[idx], quantity: qty };
|
|
165
|
+
}
|
|
166
|
+
await touch(sr, cart, { items });
|
|
167
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "remove-item": {
|
|
171
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
172
|
+
const items = (cart.items || []).filter((i: any) => i.item_key !== payload.item_key);
|
|
173
|
+
await touch(sr, cart, { items });
|
|
174
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case "apply-coupon": {
|
|
178
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
179
|
+
const pricingData = await loadPricingData(sr);
|
|
180
|
+
if (!getSetting(pricingData.settings, "general", "enable_coupons", true)) {
|
|
181
|
+
throw new HttpError(400, "Coupons are disabled for this store.", "coupons_disabled");
|
|
182
|
+
}
|
|
183
|
+
const code = String(payload.code || "").toLowerCase().trim();
|
|
184
|
+
if (!code) throw new HttpError(400, "Coupon code is required.", "code_required");
|
|
185
|
+
if ((cart.coupon_codes || []).includes(code)) {
|
|
186
|
+
throw new HttpError(400, "Coupon is already applied.", "already_applied");
|
|
187
|
+
}
|
|
188
|
+
const pre = await priceCart(sr, cart, { pricingData, customerEmail: user?.email });
|
|
189
|
+
const coupon = await findCoupon(sr, code);
|
|
190
|
+
const lines = couponCtxLines(pre.resolved);
|
|
191
|
+
const itemsSubtotal = round2(lines.reduce((a: number, l: any) => a + l.subtotal, 0));
|
|
192
|
+
const res = validateCoupon(coupon, {
|
|
193
|
+
lines,
|
|
194
|
+
itemsSubtotal,
|
|
195
|
+
customerEmail: cart.customer_email || user?.email,
|
|
196
|
+
appliedCoupons: pre.validCoupons,
|
|
197
|
+
});
|
|
198
|
+
if (!res.valid) throw new HttpError(400, res.error ?? "Invalid coupon.", res.code);
|
|
199
|
+
await touch(sr, cart, { coupon_codes: [...(cart.coupon_codes || []), code] });
|
|
200
|
+
return ok((await priceCart(sr, cart, { pricingData, customerEmail: user?.email })).view);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case "remove-coupon": {
|
|
204
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
205
|
+
const code = String(payload.code || "").toLowerCase().trim();
|
|
206
|
+
await touch(sr, cart, { coupon_codes: (cart.coupon_codes || []).filter((c: string) => c !== code) });
|
|
207
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "set-shipping-address": {
|
|
211
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
212
|
+
const a = payload.address || {};
|
|
213
|
+
const address = {
|
|
214
|
+
country: a.country ?? "",
|
|
215
|
+
state: a.state ?? "",
|
|
216
|
+
postcode: a.postcode ?? "",
|
|
217
|
+
city: a.city ?? "",
|
|
218
|
+
};
|
|
219
|
+
if (!address.country) throw new HttpError(400, "address.country is required.", "country_required");
|
|
220
|
+
await touch(sr, cart, { shipping_address: address });
|
|
221
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case "choose-shipping-method": {
|
|
225
|
+
const cart = await loadCart(sr, payload.cart_token);
|
|
226
|
+
const methodId = String(payload.method_id || "");
|
|
227
|
+
const pre = await priceCart(sr, cart, { customerEmail: user?.email });
|
|
228
|
+
if (!pre.view.available_shipping_methods.some((m: any) => m.id === methodId)) {
|
|
229
|
+
throw new HttpError(
|
|
230
|
+
400,
|
|
231
|
+
"This shipping method is not available for your address.",
|
|
232
|
+
"invalid_shipping_method",
|
|
233
|
+
{ available_shipping_methods: pre.view.available_shipping_methods },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
await touch(sr, cart, { chosen_shipping_method: methodId });
|
|
237
|
+
return ok((await priceCart(sr, cart, { customerEmail: user?.email })).view);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
default:
|
|
241
|
+
throw new HttpError(400, `Unknown action: ${action}`, "unknown_action");
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return fail(e);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/** Shared add-item validation path (used by `create` initial items too). */
|
|
249
|
+
async function addItem(sr: any, cart: any, payload: any): Promise<void> {
|
|
250
|
+
const { product_id, variation_id, attributes } = payload;
|
|
251
|
+
let quantity = Math.max(1, Math.floor(Number(payload.quantity) || 1));
|
|
252
|
+
const { product, variation } = await loadProductPair(sr, product_id, variation_id);
|
|
253
|
+
if (product.type === "variable" && !variation) {
|
|
254
|
+
throw new HttpError(400, "Please choose product options.", "variation_required");
|
|
255
|
+
}
|
|
256
|
+
if (product.sold_individually) quantity = 1;
|
|
257
|
+
|
|
258
|
+
const items = [...(cart.items || [])];
|
|
259
|
+
const existing = items.find(
|
|
260
|
+
(i: any) => i.product_id === product_id && (i.variation_id || "") === (variation_id || ""),
|
|
261
|
+
);
|
|
262
|
+
const newQty = product.sold_individually ? 1 : (existing ? existing.quantity + quantity : quantity);
|
|
263
|
+
|
|
264
|
+
const pricing = await loadPricingData(sr);
|
|
265
|
+
const check = checkPurchasable(product, variation, newQty, pricing.settings);
|
|
266
|
+
if (!check.ok) throw new HttpError(400, check.error ?? "Not available.", check.code);
|
|
267
|
+
|
|
268
|
+
if (existing) {
|
|
269
|
+
existing.quantity = newQty;
|
|
270
|
+
} else {
|
|
271
|
+
items.push({
|
|
272
|
+
item_key: uuid(),
|
|
273
|
+
product_id,
|
|
274
|
+
variation_id: variation_id ?? "",
|
|
275
|
+
quantity: newQty,
|
|
276
|
+
attributes: attributes ?? (variation?.attributes ?? []),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const expires_at = cartExpiry();
|
|
280
|
+
await sr.entities["commerce.Cart"].update(cart.id, { items, expires_at });
|
|
281
|
+
cart.items = items;
|
|
282
|
+
cart.expires_at = expires_at;
|
|
283
|
+
}
|