@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
@@ -5,7 +5,6 @@ import { Input } from "@/components/ui/input";
5
5
  import { Label } from "@/components/ui/label";
6
6
  import { Textarea } from "@/components/ui/textarea";
7
7
  import { Switch } from "@/components/ui/switch";
8
- import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
9
8
  import {
10
9
  Dialog,
11
10
  DialogContent,
@@ -13,20 +12,26 @@ import {
13
12
  DialogHeader,
14
13
  DialogTitle,
15
14
  } from "@/components/ui/dialog";
16
- import { ArrowDown, ArrowUp, Info, Loader2, Plus, Settings2, Trash2 } from "lucide-react";
15
+ import { ArrowDown, ArrowUp, Loader2, Plus, Settings2, Trash2 } from "lucide-react";
17
16
  import { toast } from "sonner";
18
17
  import { base44 } from "../../lib/api";
19
18
  import useAsync from "../../hooks/useAsync";
20
- import usePaymentProvider from "../../hooks/usePaymentProvider";
21
19
 
22
20
  const emptyBank = () => ({ account_name: "", account_number: "", bank_name: "", sort_code: "", iban: "", bic: "" });
23
21
 
22
+ /** The two seeded options; everything else is operator-added and deletable. */
23
+ const BUILT_IN_SLUGS = new Set(["offline", "card"]);
24
+
25
+ function slugify(title, taken) {
26
+ const base = String(title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "payment-method";
27
+ let slug = base;
28
+ for (let n = 2; taken.has(slug); n += 1) slug = `${base}-${n}`;
29
+ return slug;
30
+ }
31
+
24
32
  export default function PaymentsSettings() {
25
33
  const gateways = useAsync(() => base44.entities["commerce.PaymentGateway"].list("order", 100), []);
26
- // Whether the store can take card payments is a live fact about the connected
27
- // payment provider, not a constant — the notice below follows it.
28
- const provider = usePaymentProvider();
29
- const [dialog, setDialog] = useState(null); // { gateway (local copy) }
34
+ const [dialog, setDialog] = useState(null); // { gateway (local copy), isNew }
30
35
  const [saving, setSaving] = useState(false);
31
36
  const [busy, setBusy] = useState(false);
32
37
 
@@ -60,16 +65,54 @@ export default function PaymentsSettings() {
60
65
  }
61
66
  };
62
67
 
68
+ const remove = async (g) => {
69
+ if (!window.confirm(`Delete the "${g.title || g.slug}" payment option?`)) return;
70
+ setBusy(true);
71
+ try {
72
+ await base44.entities["commerce.PaymentGateway"].delete(g.id);
73
+ toast.success("Payment method deleted");
74
+ gateways.refetch();
75
+ } catch (err) {
76
+ toast.error(err.message || "Failed to delete");
77
+ } finally {
78
+ setBusy(false);
79
+ }
80
+ };
81
+
82
+ const openNew = () =>
83
+ setDialog({
84
+ isNew: true,
85
+ gateway: { title: "", description: "", settings: { account_details: [] } },
86
+ });
87
+
63
88
  const saveGateway = async () => {
89
+ const g = dialog.gateway;
90
+ if (!String(g.title || "").trim()) {
91
+ toast.error("Give the payment method a title");
92
+ return;
93
+ }
64
94
  setSaving(true);
65
95
  try {
66
- const g = dialog.gateway;
67
- await base44.entities["commerce.PaymentGateway"].update(g.id, {
68
- title: g.title,
69
- description: g.description,
70
- settings: g.settings || {},
71
- });
72
- toast.success("Payment method saved");
96
+ if (dialog.isNew) {
97
+ await base44.entities["commerce.PaymentGateway"].create({
98
+ slug: slugify(g.title, new Set(rows.map((r) => r.slug))),
99
+ title: g.title,
100
+ description: g.description || "",
101
+ enabled: true,
102
+ order: rows.length,
103
+ method_title: g.title,
104
+ method_description: g.description || "",
105
+ settings: g.settings || {},
106
+ });
107
+ toast.success("Payment method added");
108
+ } else {
109
+ await base44.entities["commerce.PaymentGateway"].update(g.id, {
110
+ title: g.title,
111
+ description: g.description,
112
+ settings: g.settings || {},
113
+ });
114
+ toast.success("Payment method saved");
115
+ }
73
116
  setDialog(null);
74
117
  gateways.refetch();
75
118
  } catch (err) {
@@ -90,118 +133,86 @@ export default function PaymentsSettings() {
90
133
  return (
91
134
  <div className="max-w-3xl">
92
135
  <Card>
93
- <CardHeader className="pb-3">
94
- <CardTitle>Checkout options</CardTitle>
95
- <CardDescription>
96
- Which payment options customers can pick at checkout, in this order, and the wording they
97
- see. This is the storefront's menu only — switching an option on or off, or renaming it,
98
- does not connect, disconnect or configure any payment provider.
99
- </CardDescription>
136
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0 pb-3">
137
+ <div className="space-y-1.5">
138
+ <CardTitle>Checkout options</CardTitle>
139
+ <CardDescription>
140
+ Which payment options customers can pick at checkout, in this order, and the wording
141
+ they see. Every option except Credit card settles outside the store: the order goes
142
+ on-hold with the option's description as instructions, and you move it on once paid.
143
+ </CardDescription>
144
+ </div>
145
+ <Button size="sm" onClick={openNew}>
146
+ <Plus className="mr-2 h-4 w-4" />
147
+ Add payment method
148
+ </Button>
100
149
  </CardHeader>
101
150
  <CardContent className="divide-y border-t p-0">
102
- {rows.map((g, i) => {
103
- const isOnline = g.slug === provider.gatewaySlug;
104
- return (
105
- <div key={g.id} className="space-y-3 px-6 py-4">
106
- <div className="flex items-start gap-4">
107
- <div className="flex flex-col pt-0.5">
108
- <Button
109
- variant="ghost"
110
- size="icon"
111
- className="h-6 w-6"
112
- disabled={i === 0 || busy}
113
- onClick={() => move(i, -1)}
114
- >
115
- <ArrowUp className="h-3.5 w-3.5" />
116
- </Button>
117
- <Button
118
- variant="ghost"
119
- size="icon"
120
- className="h-6 w-6"
121
- disabled={i === rows.length - 1 || busy}
122
- onClick={() => move(i, 1)}
123
- >
124
- <ArrowDown className="h-3.5 w-3.5" />
125
- </Button>
126
- </div>
151
+ {rows.map((g, i) => (
152
+ <div key={g.id} className="space-y-2 px-6 py-4">
153
+ <div className="flex items-start gap-4">
154
+ <div className="flex flex-col pt-0.5">
155
+ <Button
156
+ variant="ghost"
157
+ size="icon"
158
+ className="h-6 w-6"
159
+ disabled={i === 0 || busy}
160
+ onClick={() => move(i, -1)}
161
+ >
162
+ <ArrowUp className="h-3.5 w-3.5" />
163
+ </Button>
164
+ <Button
165
+ variant="ghost"
166
+ size="icon"
167
+ className="h-6 w-6"
168
+ disabled={i === rows.length - 1 || busy}
169
+ onClick={() => move(i, 1)}
170
+ >
171
+ <ArrowDown className="h-3.5 w-3.5" />
172
+ </Button>
173
+ </div>
127
174
 
128
- <div className="min-w-0 flex-1">
129
- <div className="font-medium">{g.method_title || g.title || g.slug}</div>
130
- <div className="text-sm text-muted-foreground">
131
- {g.method_description || g.description}
132
- </div>
175
+ <div className="min-w-0 flex-1">
176
+ <div className="font-medium">{g.method_title || g.title || g.slug}</div>
177
+ <div className="text-sm text-muted-foreground">
178
+ {g.method_description || g.description}
133
179
  </div>
180
+ </div>
134
181
 
135
- <div className="flex shrink-0 items-center gap-2 pt-0.5">
136
- <Switch
137
- checked={!!g.enabled}
138
- disabled={busy}
139
- title={
140
- isOnline && !provider.connected
141
- ? "Stays hidden from customers until a payment provider is connected"
142
- : undefined
143
- }
144
- onCheckedChange={(v) => toggle(g, v)}
145
- />
146
- {/* Every option's customer-facing wording is editable, the card
147
- one included — and a button on every row keeps the toggles
148
- in one column. */}
182
+ <div className="flex shrink-0 items-center gap-2 pt-0.5">
183
+ <Switch checked={!!g.enabled} disabled={busy} onCheckedChange={(v) => toggle(g, v)} />
184
+ <Button
185
+ variant="outline"
186
+ size="sm"
187
+ onClick={() => setDialog({ gateway: { ...g, settings: { ...(g.settings || {}) } } })}
188
+ >
189
+ <Settings2 className="mr-2 h-4 w-4" />
190
+ Edit
191
+ </Button>
192
+ {!BUILT_IN_SLUGS.has(g.slug) && (
149
193
  <Button
150
- variant="outline"
151
- size="sm"
152
- onClick={() =>
153
- setDialog({
154
- gateway: {
155
- ...g,
156
- settings: {
157
- ...(g.settings || {}),
158
- account_details:
159
- g.slug === "offline"
160
- ? g.settings?.account_details || []
161
- : g.settings?.account_details,
162
- },
163
- },
164
- })
165
- }
194
+ variant="ghost"
195
+ size="icon"
196
+ className="h-8 w-8 text-muted-foreground"
197
+ disabled={busy}
198
+ title="Delete"
199
+ onClick={() => remove(g)}
166
200
  >
167
- <Settings2 className="mr-2 h-4 w-4" />
168
- Labels
201
+ <Trash2 className="h-3.5 w-3.5" />
169
202
  </Button>
170
- </div>
203
+ )}
171
204
  </div>
172
-
173
- {/* Full width, below the row: inside it the tall notice drags the
174
- arrows and the switch down to the vertical middle. */}
175
- {isOnline && !provider.loading && (
176
- <Alert>
177
- <Info className="h-4 w-4" />
178
- {provider.connected ? (
179
- <>
180
- <AlertTitle>Payment provider connected</AlertTitle>
181
- <AlertDescription>
182
- Card payments are live: customers see this option at checkout, orders are
183
- confirmed automatically once paid, and refunds can be sent back through the
184
- provider{provider.providerLabel ? ` (${provider.providerLabel})` : ""}.
185
- </AlertDescription>
186
- </>
187
- ) : (
188
- <>
189
- <AlertTitle>No payment provider connected</AlertTitle>
190
- <AlertDescription>
191
- Card payments are ready to go but need a provider: set{" "}
192
- {provider.providerLabel || "a payment provider"} up in this app's
193
- integrations, and this option starts appearing at checkout — no code to
194
- write. Until then it stays hidden from customers, so nobody can pick it and
195
- pay nothing. Prefer a different provider? See{" "}
196
- <code className="text-xs">skills/commerce/references/online-payments.md</code>.
197
- </AlertDescription>
198
- </>
199
- )}
200
- </Alert>
201
- )}
202
205
  </div>
203
- );
204
- })}
206
+
207
+ {/* The "card" option redirects to a provider-hosted payment page.
208
+ Wiring a provider means implementing the two payment files —
209
+ shared/commerce/card-payment.ts + functions/commerce/payment-webhook,
210
+ see .agents/skills/commerce/references/online-payments.md. Until
211
+ then, picking it at checkout answers 503 no_card_payment_provider.
212
+ Deliberately not shown to the store operator — it's developer
213
+ guidance, not store configuration. */}
214
+ </div>
215
+ ))}
205
216
  </CardContent>
206
217
  </Card>
207
218
 
@@ -209,7 +220,9 @@ export default function PaymentsSettings() {
209
220
  <Dialog open onOpenChange={(o) => !o && setDialog(null)}>
210
221
  <DialogContent className="max-h-[85vh] overflow-y-auto">
211
222
  <DialogHeader>
212
- <DialogTitle>{dialog.gateway.method_title || dialog.gateway.title}</DialogTitle>
223
+ <DialogTitle>
224
+ {dialog.isNew ? "Add payment method" : dialog.gateway.method_title || dialog.gateway.title}
225
+ </DialogTitle>
213
226
  </DialogHeader>
214
227
  <div className="space-y-4">
215
228
  <div className="space-y-1.5">
@@ -227,9 +240,14 @@ export default function PaymentsSettings() {
227
240
  value={dialog.gateway.description || ""}
228
241
  onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, description: e.target.value } })}
229
242
  />
243
+ {dialog.gateway.slug !== "card" && (
244
+ <p className="text-xs text-muted-foreground">
245
+ Also shown as the payment instructions after the order is placed.
246
+ </p>
247
+ )}
230
248
  </div>
231
249
 
232
- {dialog.gateway.slug === "offline" && (
250
+ {dialog.gateway.slug !== "card" && (
233
251
  <BankAccountsEditor
234
252
  accounts={dialog.gateway.settings?.account_details || []}
235
253
  onChange={(account_details) =>
@@ -242,7 +260,7 @@ export default function PaymentsSettings() {
242
260
  <Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button>
243
261
  <Button onClick={saveGateway} disabled={saving}>
244
262
  {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
245
- Save
263
+ {dialog.isNew ? "Add" : "Save"}
246
264
  </Button>
247
265
  </DialogFooter>
248
266
  </DialogContent>
@@ -5,10 +5,8 @@ import PageHeader from "../../components/PageHeader";
5
5
 
6
6
  const TABS = [
7
7
  { seg: "general", label: "General" },
8
- { seg: "products", label: "Products" },
9
8
  { seg: "inventory", label: "Inventory" },
10
- { seg: "tax", label: "Tax" },
11
- { seg: "shipping", label: "Shipping" },
9
+ { seg: "shipping-tax", label: "Shipping & Tax" },
12
10
  { seg: "payments", label: "Payments" },
13
11
  { seg: "emails", label: "Emails" },
14
12
  { seg: "webhooks", label: "Webhooks" },
@@ -20,7 +18,7 @@ export default function SettingsLayout() {
20
18
  const navigate = useNavigate();
21
19
  const location = useLocation();
22
20
 
23
- // Current group segment: ".../settings/<seg>[/...]" (zone editor keeps "shipping" active).
21
+ // Current group segment: ".../settings/<seg>[/...]" (location editor keeps "shipping-tax" active).
24
22
  const afterSettings = location.pathname.split("/settings/")[1] || "general";
25
23
  const current = afterSettings.split("/")[0] || "general";
26
24
 
@@ -0,0 +1,191 @@
1
+ import React, { useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Card, CardContent } from "@/components/ui/card";
5
+ import { Label } from "@/components/ui/label";
6
+ import { Switch } from "@/components/ui/switch";
7
+ import { Copy, Globe, MapPin, Pencil, Plus, Trash2 } from "lucide-react";
8
+ import { toast } from "sonner";
9
+
10
+ import { base44 } from "../../lib/api";
11
+ import useAsync from "../../hooks/useAsync";
12
+ import { useAdminHref } from "../../context/BasePathContext";
13
+ import { useSettings } from "../../context/SettingsContext";
14
+ import DataTable from "../../components/DataTable";
15
+ import ConfirmDialog from "../../components/ConfirmDialog";
16
+ import useMoney from "../../hooks/useMoney";
17
+ import { COUNTRIES, CONTINENTS } from "../../lib/geo-data";
18
+
19
+ function regionLabel(region) {
20
+ if (region.type === "continent") return CONTINENTS.find((c) => c.code === region.code)?.name || region.code;
21
+ if (region.type === "state") return region.code;
22
+ return COUNTRIES.find((c) => c.code === region.code)?.name || region.code;
23
+ }
24
+
25
+ /**
26
+ * Shipping & Tax Locations — the one screen for where the store sells and what
27
+ * it charges there: each location carries its shipping rates, tax groups and
28
+ * shipping tax. Rows edit, clone or delete; the editor is its own page.
29
+ */
30
+ export default function ShippingTaxSettings() {
31
+ const href = useAdminHref();
32
+ const navigate = useNavigate();
33
+ const money = useMoney();
34
+ const settings = useSettings();
35
+ const { data: locations, loading, refetch } = useAsync(
36
+ () => base44.entities["commerce.ShippingTaxLocation"].list("order", 500),
37
+ []
38
+ );
39
+ const [confirmDelete, setConfirmDelete] = useState(null);
40
+ const [deleting, setDeleting] = useState(false);
41
+
42
+ const shippingEnabled = settings?.get("shipping", "enable_shipping", true) !== false;
43
+ const setShippingEnabled = async (v) => {
44
+ try {
45
+ await settings.saveGroup("shipping", { ...(settings.settings?.shipping || {}), enable_shipping: v });
46
+ toast.success(v ? "Shipping enabled" : "Shipping disabled");
47
+ } catch (e) {
48
+ toast.error(e.message || "Failed to save");
49
+ }
50
+ };
51
+
52
+ const clone = async (row) => {
53
+ try {
54
+ const { id: _id, created_date: _cd, updated_date: _ud, created_by: _cb, ...fields } = row;
55
+ await base44.entities["commerce.ShippingTaxLocation"].create({
56
+ ...fields,
57
+ name: `${row.name} (copy)`,
58
+ // fresh rate ids — carts/orders reference rates by id, so a clone must not share them
59
+ shipping_rates: (row.shipping_rates || []).map((r) => ({ ...r, id: crypto.randomUUID() })),
60
+ });
61
+ toast.success("Location cloned");
62
+ refetch();
63
+ } catch (e) {
64
+ toast.error(e.message || "Failed to clone location");
65
+ }
66
+ };
67
+
68
+ const doDelete = async () => {
69
+ setDeleting(true);
70
+ try {
71
+ await base44.entities["commerce.ShippingTaxLocation"].delete(confirmDelete.id);
72
+ toast.success("Location deleted");
73
+ setConfirmDelete(null);
74
+ refetch();
75
+ } catch (e) {
76
+ toast.error(e.message || "Failed to delete location");
77
+ } finally {
78
+ setDeleting(false);
79
+ }
80
+ };
81
+
82
+ const columns = [
83
+ {
84
+ key: "name",
85
+ label: "Location",
86
+ render: (row) => (
87
+ <div className="flex items-center gap-2">
88
+ {(row.regions || []).length ? (
89
+ <MapPin className="h-4 w-4 text-muted-foreground" />
90
+ ) : (
91
+ <Globe className="h-4 w-4 text-muted-foreground" />
92
+ )}
93
+ <span className="font-medium">{row.name}</span>
94
+ </div>
95
+ ),
96
+ },
97
+ {
98
+ key: "regions",
99
+ label: "Regions",
100
+ className: "hidden md:table-cell text-muted-foreground",
101
+ render: (row) => {
102
+ const regions = row.regions || [];
103
+ if (!regions.length) return "Everywhere else (fallback)";
104
+ const names = regions.slice(0, 3).map(regionLabel).join(", ");
105
+ return regions.length > 3 ? `${names} +${regions.length - 3}` : names;
106
+ },
107
+ },
108
+ {
109
+ key: "shipping",
110
+ label: "Shipping rates",
111
+ className: "hidden lg:table-cell text-muted-foreground",
112
+ render: (row) => {
113
+ const rates = row.shipping_rates || [];
114
+ if (!rates.length) return "none — nothing ships here";
115
+ return rates.map((r) => `${r.name || "Rate"} ${money.format(r.cost || 0)}`).join(" · ");
116
+ },
117
+ },
118
+ {
119
+ key: "tax",
120
+ label: "Tax",
121
+ className: "hidden lg:table-cell text-muted-foreground",
122
+ render: (row) => {
123
+ const groups = (row.tax_groups || []).filter((g) => (g.rates || []).some((r) => Number(r.rate) > 0));
124
+ const parts = groups.map(
125
+ (g) => `${g.name} ${(g.rates || []).reduce((a, r) => a + (Number(r.rate) || 0), 0)}%`
126
+ );
127
+ if (row.shipping_tax?.value > 0) {
128
+ parts.push(
129
+ row.shipping_tax.type === "fixed"
130
+ ? `Shipping ${money.format(row.shipping_tax.value)}`
131
+ : `Shipping ${row.shipping_tax.value}%`
132
+ );
133
+ }
134
+ return parts.length ? parts.join(" · ") : "no tax";
135
+ },
136
+ },
137
+ ];
138
+
139
+ return (
140
+ <div className="max-w-4xl space-y-4">
141
+ <Card>
142
+ <CardContent className="flex items-center justify-between gap-4 py-4">
143
+ <div>
144
+ <Label>Charge shipping</Label>
145
+ <p className="text-xs text-muted-foreground">
146
+ Off means carts never carry a shipping step — for fully virtual stores.
147
+ </p>
148
+ </div>
149
+ <Switch checked={shippingEnabled} onCheckedChange={setShippingEnabled} />
150
+ </CardContent>
151
+ </Card>
152
+
153
+ <div className="flex items-center justify-between">
154
+ <p className="text-sm text-muted-foreground">
155
+ A location covers the regions it names; the location with no regions catches everyone else.
156
+ Each carries its own shipping rates and tax.
157
+ </p>
158
+ <Button onClick={() => navigate(href("settings/shipping-tax/new"))}>
159
+ <Plus className="mr-2 h-4 w-4" />
160
+ Add location
161
+ </Button>
162
+ </div>
163
+
164
+ <DataTable
165
+ columns={columns}
166
+ rows={locations || []}
167
+ loading={loading}
168
+ rowActions={(row) => [
169
+ { label: "Edit", icon: Pencil, onClick: () => navigate(href(`settings/shipping-tax/${row.id}`)) },
170
+ { label: "Clone", icon: Copy, onClick: () => clone(row) },
171
+ { label: "Delete", icon: Trash2, destructive: true, onClick: () => setConfirmDelete(row) },
172
+ ]}
173
+ empty={{
174
+ icon: Globe,
175
+ title: "No locations yet",
176
+ description: "Add a location to define where you ship and what tax applies.",
177
+ }}
178
+ />
179
+
180
+ <ConfirmDialog
181
+ open={Boolean(confirmDelete)}
182
+ onOpenChange={(o) => !o && setConfirmDelete(null)}
183
+ title={`Delete "${confirmDelete?.name}"?`}
184
+ description="Orders already placed keep their charges; carts shipping to this location lose their options."
185
+ confirmLabel="Delete"
186
+ onConfirm={doDelete}
187
+ loading={deleting}
188
+ />
189
+ </div>
190
+ );
191
+ }
@@ -19,11 +19,9 @@ import CustomerEditor from "./pages/customers/CustomerEditor";
19
19
  import Reports from "./pages/reports/Reports";
20
20
  import SettingsLayout from "./pages/settings/SettingsLayout";
21
21
  import GeneralSettings from "./pages/settings/GeneralSettings";
22
- import ProductsSettings from "./pages/settings/ProductsSettings";
23
22
  import InventorySettings from "./pages/settings/InventorySettings";
24
- import TaxSettings from "./pages/settings/TaxSettings";
25
- import ShippingSettings from "./pages/settings/ShippingSettings";
26
- import ShippingZoneEditor from "./pages/settings/ShippingZoneEditor";
23
+ import ShippingTaxSettings from "./pages/settings/ShippingTaxSettings";
24
+ import LocationEditor from "./pages/settings/LocationEditor";
27
25
  import PaymentsSettings from "./pages/settings/PaymentsSettings";
28
26
  import EmailsSettings from "./pages/settings/EmailsSettings";
29
27
  import Webhooks from "./pages/status/Webhooks";
@@ -52,11 +50,9 @@ export const ADMIN_ROUTES = [
52
50
  { path: "reports", label: "Reports" },
53
51
  { path: "settings", label: "Settings" },
54
52
  { path: "settings/general", label: "General" },
55
- { path: "settings/products", label: "Products" },
56
53
  { path: "settings/inventory", label: "Inventory" },
57
- { path: "settings/tax", label: "Tax" },
58
- { path: "settings/shipping", label: "Shipping" },
59
- { path: "settings/shipping/zones/:zoneId", label: "Shipping zone" },
54
+ { path: "settings/shipping-tax", label: "Shipping & Tax" },
55
+ { path: "settings/shipping-tax/:locationId", label: "Location" },
60
56
  { path: "settings/payments", label: "Payments" },
61
57
  { path: "settings/emails", label: "Emails" },
62
58
  { path: "settings/webhooks", label: "Webhooks" },
@@ -124,11 +120,9 @@ export default function AdminRoutes() {
124
120
  <Route path="settings" element={<SettingsLayout />}>
125
121
  <Route index element={<Navigate to="general" replace />} />
126
122
  <Route path="general" element={<GeneralSettings />} />
127
- <Route path="products" element={<ProductsSettings />} />
128
123
  <Route path="inventory" element={<InventorySettings />} />
129
- <Route path="tax" element={<TaxSettings />} />
130
- <Route path="shipping" element={<ShippingSettings />} />
131
- <Route path="shipping/zones/:zoneId" element={<ShippingZoneEditor />} />
124
+ <Route path="shipping-tax" element={<ShippingTaxSettings />} />
125
+ <Route path="shipping-tax/:locationId" element={<LocationEditor />} />
132
126
  <Route path="payments" element={<PaymentsSettings />} />
133
127
  <Route path="emails" element={<EmailsSettings />} />
134
128
  <Route path="webhooks" element={<Webhooks />} />
@@ -10,10 +10,10 @@
10
10
  *
11
11
  * - `variants.js` — variant selection: map attribute selections (Size, Color)
12
12
  * to a `ProductVariation` and back, per-option availability, variant price
13
- * ranges. See `skills/commerce/references/storefront-product-page.md`.
13
+ * ranges. See `.agents/skills/commerce/references/storefront-product-page.md`.
14
14
  * - `shipping-promos.js` — read the store's real free-shipping configuration so
15
15
  * "Free shipping over €150" copy states a configured rule, not an invented
16
- * number. See `skills/commerce/docs/api-storefront.md`.
16
+ * number. See `.agents/skills/commerce/docs/api-storefront.md`.
17
17
  */
18
18
  export * from "./variants.js";
19
19
  export * from "./shipping-promos.js";