@base44/app-plugin-commerce 0.1.5 → 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.
- package/README.md +11 -11
- package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
- package/base44/entities/commerce.Cart.jsonc +1 -1
- package/base44/entities/commerce.Coupon.jsonc +5 -0
- package/base44/entities/commerce.Order.jsonc +6 -7
- package/base44/entities/commerce.OrderRefund.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
- package/base44/entities/commerce.Product.jsonc +6 -16
- package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
- package/base44/entities/commerce.ProductVariation.jsonc +1 -9
- package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
- package/base44/entities/commerce.Webhook.jsonc +1 -1
- package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
- package/base44/functions/commerce/admin-products/entry.ts +11 -17
- package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
- package/base44/functions/commerce/admin-reports/entry.ts +3 -3
- package/base44/functions/commerce/admin-tools/entry.ts +9 -36
- package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
- package/base44/functions/commerce/payments/entry.ts +46 -42
- package/base44/functions/commerce/seed-store/defaults.ts +35 -42
- package/base44/functions/commerce/seed-store/entry.ts +84 -31
- package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
- package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
- package/base44/shared/commerce/card-payment.ts +80 -0
- package/base44/shared/commerce/coupons.ts +16 -10
- package/base44/shared/commerce/emails.ts +30 -18
- package/base44/shared/commerce/money.ts +11 -24
- package/base44/shared/commerce/payments.ts +55 -286
- package/base44/shared/commerce/scan.ts +1 -1
- package/base44/shared/commerce/sequence.ts +1 -1
- package/base44/shared/commerce/settings.ts +4 -9
- package/base44/shared/commerce/shipping.ts +65 -133
- package/base44/shared/commerce/tax.ts +48 -96
- package/base44/shared/commerce/totals.ts +77 -91
- package/package.json +1 -1
- package/scripts/install.js +28 -7
- package/skills/commerce/SKILL.md +14 -14
- package/skills/commerce/docs/api-admin.md +23 -26
- package/skills/commerce/docs/api-storefront.md +67 -61
- package/skills/commerce/installation-guidelines.md +8 -8
- package/skills/commerce/post-installation.md +76 -41
- package/skills/commerce/references/admin-product-form.md +15 -12
- package/skills/commerce/references/emails.md +2 -2
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/online-payments.md +24 -191
- package/skills/commerce/references/product-render.md +18 -18
- package/skills/commerce/references/reviews.md +14 -8
- package/skills/commerce/references/storefront-product-page.md +1 -1
- package/src/commerce/admin/README.md +4 -5
- package/src/commerce/admin/bot/Markdown.jsx +1 -1
- package/src/commerce/admin/hooks/useMoney.js +13 -22
- package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
- package/src/commerce/admin/lib/constants.js +2 -29
- package/src/commerce/admin/lib/order-utils.js +1 -1
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
- package/src/commerce/admin/pages/products/Categories.jsx +147 -177
- package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
- package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
- package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
- package/src/commerce/admin/routes.jsx +6 -12
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +45 -49
- package/src/commerce/utils/variants.js +1 -1
- package/base44/entities/commerce.ShippingClass.jsonc +0 -30
- package/base44/entities/commerce.ShippingZone.jsonc +0 -41
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
- package/base44/entities/commerce.TaxClass.jsonc +0 -23
- package/base44/entities/commerce.TaxRate.jsonc +0 -68
- package/base44/shared/commerce/stripe.ts +0 -463
- package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
|
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
|
|
|
6
6
|
import { Textarea } from "@/components/ui/textarea";
|
|
7
7
|
import { Label } from "@/components/ui/label";
|
|
8
8
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
9
|
+
import { Switch } from "@/components/ui/switch";
|
|
9
10
|
import { Badge } from "@/components/ui/badge";
|
|
10
11
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
11
12
|
import { Separator } from "@/components/ui/separator";
|
|
@@ -29,6 +30,7 @@ import ConfirmDialog from "../../components/ConfirmDialog";
|
|
|
29
30
|
|
|
30
31
|
const EMPTY = {
|
|
31
32
|
code: "",
|
|
33
|
+
enabled: true,
|
|
32
34
|
discount_type: "fixed_cart",
|
|
33
35
|
amount: null,
|
|
34
36
|
description: "",
|
|
@@ -48,12 +50,6 @@ const EMPTY = {
|
|
|
48
50
|
limit_usage_to_x_items: null,
|
|
49
51
|
};
|
|
50
52
|
|
|
51
|
-
const TABS = [
|
|
52
|
-
{ id: "general", label: "General" },
|
|
53
|
-
{ id: "restriction", label: "Usage restriction" },
|
|
54
|
-
{ id: "limits", label: "Usage limits" },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
53
|
const randomCode = () =>
|
|
58
54
|
Array.from(crypto.getRandomValues(new Uint8Array(8)))
|
|
59
55
|
.map((b) => "abcdefghijklmnopqrstuvwxyz0123456789"[b % 36])
|
|
@@ -132,7 +128,6 @@ export default function CouponEditor() {
|
|
|
132
128
|
const [saving, setSaving] = useState(false);
|
|
133
129
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
134
130
|
const [deleting, setDeleting] = useState(false);
|
|
135
|
-
const [tab, setTab] = useState("general");
|
|
136
131
|
|
|
137
132
|
// Selected options with labels for the pickers.
|
|
138
133
|
const [productOpts, setProductOpts] = useState({ include: [], exclude: [] });
|
|
@@ -284,95 +279,80 @@ export default function CouponEditor() {
|
|
|
284
279
|
</CardContent>
|
|
285
280
|
</Card>
|
|
286
281
|
|
|
287
|
-
{/* Coupon data:
|
|
282
|
+
{/* Coupon data: stacked cards, like the product editor's sections */}
|
|
288
283
|
<Card>
|
|
289
|
-
<CardHeader className="pb-
|
|
290
|
-
<CardTitle className="text-base">
|
|
284
|
+
<CardHeader className="pb-2">
|
|
285
|
+
<CardTitle className="text-base">General</CardTitle>
|
|
291
286
|
</CardHeader>
|
|
292
|
-
<CardContent className="
|
|
293
|
-
<div className="
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
</
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
/>
|
|
355
|
-
<span>
|
|
356
|
-
Allow free shipping
|
|
357
|
-
<span className="block text-xs text-muted-foreground">
|
|
358
|
-
Grants access to free-shipping methods that require a coupon.
|
|
359
|
-
</span>
|
|
360
|
-
</span>
|
|
361
|
-
</label>
|
|
362
|
-
<div className="grid gap-1.5 sm:max-w-sm">
|
|
363
|
-
<Label>Coupon expiry date</Label>
|
|
364
|
-
<Input
|
|
365
|
-
type="date"
|
|
366
|
-
value={coupon.date_expires ? coupon.date_expires.slice(0, 10) : ""}
|
|
367
|
-
onChange={(e) => set("date_expires")(e.target.value || null)}
|
|
368
|
-
/>
|
|
369
|
-
</div>
|
|
370
|
-
</>
|
|
371
|
-
)}
|
|
287
|
+
<CardContent className="space-y-4">
|
|
288
|
+
<div className="grid gap-1.5 sm:max-w-sm">
|
|
289
|
+
<Label>Discount type</Label>
|
|
290
|
+
<Select value={coupon.discount_type} onValueChange={set("discount_type")}>
|
|
291
|
+
<SelectTrigger>
|
|
292
|
+
<SelectValue />
|
|
293
|
+
</SelectTrigger>
|
|
294
|
+
<SelectContent>
|
|
295
|
+
{COUPON_TYPES.map((t) => (
|
|
296
|
+
<SelectItem key={t.value} value={t.value}>
|
|
297
|
+
{t.label}
|
|
298
|
+
</SelectItem>
|
|
299
|
+
))}
|
|
300
|
+
</SelectContent>
|
|
301
|
+
</Select>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="grid gap-1.5 sm:max-w-sm">
|
|
304
|
+
<Label>Coupon amount {isPercent ? "(%)" : ""}</Label>
|
|
305
|
+
{isPercent ? (
|
|
306
|
+
<div className="relative">
|
|
307
|
+
<Input
|
|
308
|
+
type="number"
|
|
309
|
+
min="0"
|
|
310
|
+
max="100"
|
|
311
|
+
step="any"
|
|
312
|
+
value={coupon.amount ?? ""}
|
|
313
|
+
onChange={(e) =>
|
|
314
|
+
set("amount")(e.target.value === "" ? null : Number(e.target.value))
|
|
315
|
+
}
|
|
316
|
+
className="pr-8"
|
|
317
|
+
/>
|
|
318
|
+
<span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-sm text-muted-foreground">
|
|
319
|
+
%
|
|
320
|
+
</span>
|
|
321
|
+
</div>
|
|
322
|
+
) : (
|
|
323
|
+
<MoneyInput value={coupon.amount} onChange={set("amount")} />
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
<label className="flex items-start gap-2 text-sm">
|
|
327
|
+
<Checkbox
|
|
328
|
+
checked={!!coupon.free_shipping}
|
|
329
|
+
onCheckedChange={(v) => set("free_shipping")(!!v)}
|
|
330
|
+
className="mt-0.5"
|
|
331
|
+
/>
|
|
332
|
+
<span>
|
|
333
|
+
Allow free shipping
|
|
334
|
+
<span className="block text-xs text-muted-foreground">
|
|
335
|
+
Grants access to free-shipping methods that require a coupon.
|
|
336
|
+
</span>
|
|
337
|
+
</span>
|
|
338
|
+
</label>
|
|
339
|
+
<div className="grid gap-1.5 sm:max-w-sm">
|
|
340
|
+
<Label>Coupon expiry date</Label>
|
|
341
|
+
<Input
|
|
342
|
+
type="date"
|
|
343
|
+
value={coupon.date_expires ? coupon.date_expires.slice(0, 10) : ""}
|
|
344
|
+
onChange={(e) => set("date_expires")(e.target.value || null)}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
</CardContent>
|
|
348
|
+
</Card>
|
|
372
349
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
350
|
+
<Card>
|
|
351
|
+
<CardHeader className="pb-2">
|
|
352
|
+
<CardTitle className="text-base">Usage restriction</CardTitle>
|
|
353
|
+
</CardHeader>
|
|
354
|
+
<CardContent className="space-y-4">
|
|
355
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
376
356
|
<div className="grid gap-1.5">
|
|
377
357
|
<Label>Minimum spend ({money.code})</Label>
|
|
378
358
|
<MoneyInput
|
|
@@ -467,54 +447,53 @@ export default function CouponEditor() {
|
|
|
467
447
|
/>
|
|
468
448
|
</div>
|
|
469
449
|
</div>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
450
|
+
<div className="grid gap-1.5">
|
|
451
|
+
<Label>Allowed emails</Label>
|
|
452
|
+
<EmailChips
|
|
453
|
+
value={coupon.email_restrictions || []}
|
|
454
|
+
onChange={set("email_restrictions")}
|
|
455
|
+
/>
|
|
456
|
+
<p className="text-xs text-muted-foreground">
|
|
457
|
+
Only these billing emails can use the coupon. Wildcards like{" "}
|
|
458
|
+
<code>*@example.com</code> are allowed.
|
|
459
|
+
</p>
|
|
460
|
+
</div>
|
|
461
|
+
</CardContent>
|
|
462
|
+
</Card>
|
|
483
463
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
</div>
|
|
464
|
+
<Card>
|
|
465
|
+
<CardHeader className="pb-2">
|
|
466
|
+
<CardTitle className="text-base">Usage limits</CardTitle>
|
|
467
|
+
</CardHeader>
|
|
468
|
+
<CardContent className="max-w-sm space-y-4">
|
|
469
|
+
<div className="grid gap-1.5">
|
|
470
|
+
<Label>Usage limit per coupon</Label>
|
|
471
|
+
<NumberField value={coupon.usage_limit} onChange={set("usage_limit")} />
|
|
472
|
+
<p className="text-xs text-muted-foreground">
|
|
473
|
+
How many times the coupon can be used in total.
|
|
474
|
+
</p>
|
|
475
|
+
</div>
|
|
476
|
+
<div className="grid gap-1.5">
|
|
477
|
+
<Label>Limit usage to X items</Label>
|
|
478
|
+
<NumberField
|
|
479
|
+
value={coupon.limit_usage_to_x_items}
|
|
480
|
+
onChange={set("limit_usage_to_x_items")}
|
|
481
|
+
placeholder="Apply to all qualifying items"
|
|
482
|
+
/>
|
|
483
|
+
<p className="text-xs text-muted-foreground">
|
|
484
|
+
Maximum number of individual items the coupon applies to (product
|
|
485
|
+
discounts).
|
|
486
|
+
</p>
|
|
487
|
+
</div>
|
|
488
|
+
<div className="grid gap-1.5">
|
|
489
|
+
<Label>Usage limit per user</Label>
|
|
490
|
+
<NumberField
|
|
491
|
+
value={coupon.usage_limit_per_user}
|
|
492
|
+
onChange={set("usage_limit_per_user")}
|
|
493
|
+
/>
|
|
494
|
+
<p className="text-xs text-muted-foreground">
|
|
495
|
+
How many times a single customer (by billing email) can use it.
|
|
496
|
+
</p>
|
|
518
497
|
</div>
|
|
519
498
|
</CardContent>
|
|
520
499
|
</Card>
|
|
@@ -527,6 +506,18 @@ export default function CouponEditor() {
|
|
|
527
506
|
<CardTitle className="text-base">Publish</CardTitle>
|
|
528
507
|
</CardHeader>
|
|
529
508
|
<CardContent className="space-y-3">
|
|
509
|
+
<label className="flex items-center justify-between text-sm font-medium">
|
|
510
|
+
Enabled
|
|
511
|
+
<Switch
|
|
512
|
+
checked={coupon.enabled !== false}
|
|
513
|
+
onCheckedChange={(v) => set("enabled")(!!v)}
|
|
514
|
+
/>
|
|
515
|
+
</label>
|
|
516
|
+
{coupon.enabled === false && (
|
|
517
|
+
<p className="text-xs text-muted-foreground">
|
|
518
|
+
Disabled — customers can't apply this coupon, and carts holding it drop it.
|
|
519
|
+
</p>
|
|
520
|
+
)}
|
|
530
521
|
{!isNew && (
|
|
531
522
|
<p className="text-sm text-muted-foreground">
|
|
532
523
|
Used: <span className="font-medium text-foreground">{coupon.usage_count ?? 0}</span>{" "}
|
|
@@ -73,13 +73,20 @@ export default function CouponsList() {
|
|
|
73
73
|
label: "Code",
|
|
74
74
|
sortable: !debouncedQuery,
|
|
75
75
|
render: (c) => (
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
<span className="inline-flex items-center gap-2">
|
|
77
|
+
<Link
|
|
78
|
+
to={href(`coupons/${c.id}`)}
|
|
79
|
+
className="inline-block rounded bg-muted px-2 py-0.5 font-mono text-xs font-semibold uppercase tracking-wide hover:bg-muted/70"
|
|
80
|
+
onClick={(e) => e.stopPropagation()}
|
|
81
|
+
>
|
|
82
|
+
{c.code}
|
|
83
|
+
</Link>
|
|
84
|
+
{c.enabled === false && (
|
|
85
|
+
<span className="rounded border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
86
|
+
Disabled
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</span>
|
|
83
90
|
),
|
|
84
91
|
},
|
|
85
92
|
{
|
|
@@ -410,7 +410,7 @@ export default function OrderEditor() {
|
|
|
410
410
|
sku: variation?.sku || product.sku || "",
|
|
411
411
|
quantity,
|
|
412
412
|
price,
|
|
413
|
-
|
|
413
|
+
tax_group: product.tax_group || "Products",
|
|
414
414
|
subtotal: price * quantity,
|
|
415
415
|
subtotal_tax: 0,
|
|
416
416
|
total: price * quantity,
|
|
@@ -797,7 +797,7 @@ export default function OrderEditor() {
|
|
|
797
797
|
patchDraft({
|
|
798
798
|
fee_lines: [
|
|
799
799
|
...(draft.fee_lines || []),
|
|
800
|
-
{ line_id: uid(), name, total: amount, total_tax: 0, tax_status: "taxable",
|
|
800
|
+
{ line_id: uid(), name, total: amount, total_tax: 0, tax_status: "taxable", tax_group: "Products" },
|
|
801
801
|
],
|
|
802
802
|
})
|
|
803
803
|
}
|
|
@@ -812,7 +812,7 @@ export default function OrderEditor() {
|
|
|
812
812
|
patchDraft({
|
|
813
813
|
shipping_lines: [
|
|
814
814
|
...(draft.shipping_lines || []),
|
|
815
|
-
{ line_id: uid(), method_id: "
|
|
815
|
+
{ line_id: uid(), method_id: "rate", instance_id: "", method_title: name, total: amount, total_tax: 0, taxes: [] },
|
|
816
816
|
],
|
|
817
817
|
})
|
|
818
818
|
}
|
|
@@ -8,38 +8,35 @@ import { toast } from "sonner";
|
|
|
8
8
|
|
|
9
9
|
import { call } from "../../../lib/api";
|
|
10
10
|
import useMoney from "../../../hooks/useMoney";
|
|
11
|
-
import usePaymentProvider from "../../../hooks/usePaymentProvider";
|
|
12
11
|
import { formatDateTime } from "../../../lib/format";
|
|
13
12
|
|
|
13
|
+
/** The one gateway slug that pays online (see shared/commerce/payments.ts). */
|
|
14
|
+
const CARD_SLUG = "card";
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* Payment state for an order, plus the two things an operator wants when an
|
|
16
18
|
* order is unpaid: a **payment link** to send the customer, and a way to
|
|
17
19
|
* **re-check** whether the money arrived.
|
|
18
20
|
*
|
|
19
|
-
* Both come from `commerce/payments
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* hidden, so the operator can see the capability exists and what unlocks it.
|
|
21
|
+
* Both come from `commerce/payments`. When the store has no card payment
|
|
22
|
+
* provider implemented, the backend answers with a clear error — the buttons
|
|
23
|
+
* stay available so the operator learns what unlocks them.
|
|
23
24
|
*
|
|
24
25
|
* Props: { order, onChanged }
|
|
25
26
|
*/
|
|
26
27
|
export default function PaymentPanel({ order, onChanged }) {
|
|
27
28
|
const { format } = useMoney();
|
|
28
|
-
const provider = usePaymentProvider();
|
|
29
29
|
const [working, setWorking] = useState(""); // link | verify
|
|
30
30
|
const [link, setLink] = useState("");
|
|
31
31
|
const [copied, setCopied] = useState(false);
|
|
32
32
|
|
|
33
33
|
const paid = Boolean(order.date_paid) || ["processing", "completed", "refunded"].includes(order.status);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
const canAct = provider.connected && !paid;
|
|
41
|
-
const willSwitchMethod = canAct && known && !isOnline;
|
|
42
|
-
const disabledReason = !provider.connected ? "No payment provider connected" : undefined;
|
|
34
|
+
const isCard = order.payment_method === CARD_SLUG;
|
|
35
|
+
// An unpaid order can always be collected by card, whatever its current
|
|
36
|
+
// method — an admin order starts with no method at all, and "just send them
|
|
37
|
+
// a card link" is a normal thing to want for a manual one; the backend
|
|
38
|
+
// records the switch on the order.
|
|
39
|
+
const willSwitchMethod = !paid && !isCard;
|
|
43
40
|
|
|
44
41
|
const createLink = async () => {
|
|
45
42
|
setWorking("link");
|
|
@@ -117,22 +114,21 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
117
114
|
)}
|
|
118
115
|
</dl>
|
|
119
116
|
|
|
120
|
-
{
|
|
117
|
+
{!paid && !isCard && (
|
|
121
118
|
<p className="text-xs text-muted-foreground">
|
|
122
119
|
{order.payment_method
|
|
123
120
|
? `This order is set to be paid outside the store (${order.payment_method_title || order.payment_method}), so mark it paid by moving it to Processing once the money arrives.`
|
|
124
|
-
: "No payment method is set on this order. Take payment outside the store and move it to Processing, or
|
|
121
|
+
: "No payment method is set on this order. Take payment outside the store and move it to Processing, or send the customer a payment link."}
|
|
125
122
|
</p>
|
|
126
123
|
)}
|
|
127
124
|
|
|
128
|
-
{!paid && (
|
|
125
|
+
{!paid && (
|
|
129
126
|
<>
|
|
130
127
|
<div className="flex flex-wrap gap-2">
|
|
131
128
|
<Button
|
|
132
129
|
variant="outline"
|
|
133
130
|
size="sm"
|
|
134
|
-
disabled={
|
|
135
|
-
title={disabledReason}
|
|
131
|
+
disabled={Boolean(working)}
|
|
136
132
|
onClick={createLink}
|
|
137
133
|
>
|
|
138
134
|
{working === "link" ? (
|
|
@@ -145,8 +141,7 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
145
141
|
<Button
|
|
146
142
|
variant="outline"
|
|
147
143
|
size="sm"
|
|
148
|
-
disabled={
|
|
149
|
-
title={disabledReason}
|
|
144
|
+
disabled={Boolean(working)}
|
|
150
145
|
onClick={checkPayment}
|
|
151
146
|
>
|
|
152
147
|
{working === "verify" ? (
|
|
@@ -158,12 +153,6 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
158
153
|
</Button>
|
|
159
154
|
</div>
|
|
160
155
|
|
|
161
|
-
{!provider.connected && !provider.loading && (
|
|
162
|
-
<p className="text-xs text-muted-foreground">
|
|
163
|
-
No payment provider is connected, so this order can't be paid online yet.
|
|
164
|
-
</p>
|
|
165
|
-
)}
|
|
166
|
-
|
|
167
156
|
{willSwitchMethod && !link && (
|
|
168
157
|
<p className="text-xs text-muted-foreground">
|
|
169
158
|
{order.payment_method
|
|
@@ -16,7 +16,6 @@ import { toast } from "sonner";
|
|
|
16
16
|
|
|
17
17
|
import { call } from "../../../lib/api";
|
|
18
18
|
import useMoney from "../../../hooks/useMoney";
|
|
19
|
-
import usePaymentProvider from "../../../hooks/usePaymentProvider";
|
|
20
19
|
import { formatDate } from "../../../lib/format";
|
|
21
20
|
import { refundableQty, refundableAmount, round2 } from "../../../lib/order-utils";
|
|
22
21
|
|
|
@@ -33,15 +32,12 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
33
32
|
const [restock, setRestock] = useState(true);
|
|
34
33
|
const [viaGateway, setViaGateway] = useState(false);
|
|
35
34
|
const [busy, setBusy] = useState(false);
|
|
36
|
-
const provider = usePaymentProvider();
|
|
37
35
|
|
|
38
36
|
const maxRefund = refundableAmount(order);
|
|
39
37
|
// Refunding through the provider is only possible when the order was actually
|
|
40
|
-
// paid
|
|
41
|
-
// outside the store and has to go back the same way.
|
|
42
|
-
const canRefundOnline = Boolean(
|
|
43
|
-
provider.connected && order.payment_method === provider.gatewaySlug && order.transaction_id,
|
|
44
|
-
);
|
|
38
|
+
// paid by card and a payment reference is on record — otherwise the money was
|
|
39
|
+
// taken outside the store and has to go back the same way.
|
|
40
|
+
const canRefundOnline = Boolean(order.payment_method === "card" && order.transaction_id);
|
|
45
41
|
|
|
46
42
|
const unitNet = (line) => (line.quantity ? (line.total || 0) / line.quantity : 0);
|
|
47
43
|
|
|
@@ -182,15 +178,13 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
182
178
|
<Checkbox checked={restock} onCheckedChange={(v) => setRestock(Boolean(v))} />
|
|
183
179
|
Restock refunded items
|
|
184
180
|
</label>
|
|
185
|
-
{order.payment_method ===
|
|
181
|
+
{order.payment_method === "card" && (
|
|
186
182
|
<label
|
|
187
183
|
className={`flex items-center gap-2 text-sm ${canRefundOnline ? "" : "text-muted-foreground"}`}
|
|
188
184
|
title={
|
|
189
185
|
canRefundOnline
|
|
190
186
|
? undefined
|
|
191
|
-
:
|
|
192
|
-
? "This order has no online payment on record to refund"
|
|
193
|
-
: "No payment provider connected"
|
|
187
|
+
: "This order has no card payment on record to refund"
|
|
194
188
|
}
|
|
195
189
|
>
|
|
196
190
|
<Checkbox
|