vonpay-medusa 1.2.1 → 1.3.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/.medusa/server/src/admin/index.js +1263 -1003
- package/.medusa/server/src/admin/index.mjs +1265 -1005
- package/.medusa/server/src/api/admin/vonpay/holds/route.js +51 -3
- package/.medusa/server/src/api/admin/vonpay/offers/report/route.js +10 -0
- package/.medusa/server/src/api/admin/vonpay/subscriptions/route.js +15 -1
- package/.medusa/server/src/api/admin/vonpay/variants/[id]/subscription/route.js +17 -0
- package/.medusa/server/src/api/middlewares.js +2 -0
- package/.medusa/server/src/api/store/vonpay/upsells/[order]/accept/route.js +11 -1
- package/.medusa/server/src/api/store/vonpay/upsells/[order]/shown/route.js +21 -0
- package/.medusa/server/src/lib/errors.js +29 -1
- package/.medusa/server/src/lib/held-charge-copy.js +71 -2
- package/.medusa/server/src/lib/money.js +45 -0
- package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261107202021.js +26 -0
- package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261109162717.js +15 -0
- package/.medusa/server/src/modules/vonpay-jobs/models/refund-watch.js +44 -0
- package/.medusa/server/src/modules/vonpay-jobs/models/signup-watch.js +28 -0
- package/.medusa/server/src/modules/vonpay-jobs/service.js +57 -1
- package/.medusa/server/src/modules/vonpay-jobs/types.js +2 -0
- package/.medusa/server/src/modules/vonpay-subscriptions/migrations/Migration20261102180000.js +13 -0
- package/.medusa/server/src/modules/vonpay-subscriptions/models/subscription-event.js +4 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261102113326.js +20 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261102170444.js +14 -0
- package/.medusa/server/src/modules/vonpay-upsells/models/event.js +8 -2
- package/.medusa/server/src/modules/vonpay-upsells/service.js +138 -0
- package/.medusa/server/src/providers/vonpay/admin-holds.js +10 -6
- package/.medusa/server/src/providers/vonpay/charge-hold.js +59 -2
- package/.medusa/server/src/providers/vonpay/descriptor-rules.js +6 -1
- package/.medusa/server/src/providers/vonpay/follow-up.js +45 -0
- package/.medusa/server/src/providers/vonpay/held-list.js +176 -3
- package/.medusa/server/src/providers/vonpay/hold-actions.js +1 -0
- package/.medusa/server/src/providers/vonpay/job-beat.js +11 -2
- package/.medusa/server/src/providers/vonpay/options.js +8 -0
- package/.medusa/server/src/providers/vonpay/refund-verdict.js +15 -0
- package/.medusa/server/src/providers/vonpay/refund-watch.js +562 -0
- package/.medusa/server/src/providers/vonpay/renewal-check.js +14 -0
- package/.medusa/server/src/providers/vonpay/renewal.js +2 -1
- package/.medusa/server/src/providers/vonpay/scan.js +13 -0
- package/.medusa/server/src/providers/vonpay/service.js +840 -49
- package/.medusa/server/src/providers/vonpay/session-data.js +235 -3
- package/.medusa/server/src/providers/vonpay/setup-check.js +223 -3
- package/.medusa/server/src/providers/vonpay/signup-api.js +176 -0
- package/.medusa/server/src/providers/vonpay/signup-watch.js +303 -0
- package/.medusa/server/src/providers/vonpay/signup.js +171 -0
- package/.medusa/server/src/providers/vonpay/subscription-cancel.js +168 -0
- package/.medusa/server/src/providers/vonpay/subscription-events.js +93 -4
- package/.medusa/server/src/providers/vonpay/subscription-products-admin.js +97 -0
- package/.medusa/server/src/providers/vonpay/upsell-accept.js +470 -40
- package/.medusa/server/src/providers/vonpay/upsell-cleanup.js +88 -5
- package/.medusa/server/src/providers/vonpay/upsell-report.js +489 -0
- package/.medusa/server/src/providers/vonpay/upsell-routes.js +9 -1
- package/.medusa/server/src/subscribers/vonpay-offer-refunds.js +9 -0
- package/README.md +265 -11
- package/bin/vonpay-medusa.cjs +26 -16
- package/package.json +3 -1
- package/storefront/src/copy.ts +15 -0
- package/storefront/src/index.ts +1 -1
- package/storefront/src/outcome.ts +37 -0
- package/storefront/src/reload-once.ts +13 -0
- package/storefront/src/server.ts +37 -6
- package/storefront/src/upsell.ts +43 -4
- package/storefront/src/vonpay-card-payment.tsx +38 -3
- package/storefront/src/vonpay-payment-view.tsx +43 -1
- package/storefront/src/vonpay-upsell-offer.tsx +95 -11
- package/storefront/src/vonpay-upsell-return.tsx +2 -4
|
@@ -13,6 +13,9 @@ const TIME_SLOT = "{time}";
|
|
|
13
13
|
function fillTime(text, at, format) {
|
|
14
14
|
return at && text.includes(TIME_SLOT) ? text.split(TIME_SLOT).join(format(at)) : text;
|
|
15
15
|
}
|
|
16
|
+
const SUBSCRIPTION_NOT_NAMED_TEXT = {
|
|
17
|
+
unknown: "Not known yet. In the Vonpay dashboard, find the subscription by this order's id."
|
|
18
|
+
};
|
|
16
19
|
const NOT_SAVED_TEXT = "That couldn't be saved. The list is read again: check the row as it stands now before you try again.";
|
|
17
20
|
const CHARGE_REASON_LABEL = {
|
|
18
21
|
renewal_outcome_unknown: "Renewal charge — result unknown",
|
|
@@ -31,8 +34,18 @@ const CHARGE_REASON_LABEL = {
|
|
|
31
34
|
upsell_refund_unclear: "Late offer charge — refund unclear",
|
|
32
35
|
upsell_refund_pending: "Late offer charge — refund in progress",
|
|
33
36
|
upsell_refunded_late: "Late offer charge refunded",
|
|
37
|
+
upsell_refund_requested: "Late offer charge — refund accepted, not finished",
|
|
34
38
|
upsell_hold_unsettled: "Offer payment held — not finished yet",
|
|
35
|
-
upsell_take_unconfirmed: "Offer payment — collection not confirmed"
|
|
39
|
+
upsell_take_unconfirmed: "Offer payment — collection not confirmed",
|
|
40
|
+
upsell_subscription_active: "Offer refund — cancel its subscription",
|
|
41
|
+
upsell_subscription_ended: "Offer refund — subscription ended",
|
|
42
|
+
refund_watch_canceled: "Refunded in Medusa — Vonpay did not complete it",
|
|
43
|
+
refund_watch_failed: "Refunded in Medusa — Vonpay's refund failed",
|
|
44
|
+
refund_watch_unclear: "Refunded in Medusa — Vonpay's refund failed, outcome unclear",
|
|
45
|
+
refund_watch_overdue: "Refunded in Medusa — Vonpay's refund not finished",
|
|
46
|
+
refund_watch_resolved: "Refunded in Medusa — Vonpay's refund settled by a person",
|
|
47
|
+
signup_watch_missing: "Subscription paid for — not started",
|
|
48
|
+
signup_watch_resolved: "Subscription paid for — settled by a person"
|
|
36
49
|
};
|
|
37
50
|
const CHARGE_RESOLVED_HOW_LABEL = {
|
|
38
51
|
closed_blocking: "Closed by a person — no more renewal charges for this order",
|
|
@@ -75,6 +88,10 @@ const REASON_COPY = {
|
|
|
75
88
|
label: "Couldn't confirm amount",
|
|
76
89
|
what: "The cart total couldn't be read to confirm the charge, so no order was created."
|
|
77
90
|
},
|
|
91
|
+
signup_mismatch: {
|
|
92
|
+
label: "Subscription doesn't match the cart",
|
|
93
|
+
what: "The buyer paid, but the subscription this payment started doesn't match the items in the cart, so no order was created. A refund here doesn't cancel the subscription: cancel it in Vonpay too."
|
|
94
|
+
},
|
|
78
95
|
binding_mismatch: {
|
|
79
96
|
label: "Payment doesn't match checkout",
|
|
80
97
|
what: "The payment belongs to a different checkout than the one completing."
|
|
@@ -158,7 +175,9 @@ const SOURCE_LABEL = {
|
|
|
158
175
|
cancel: "Recorded when the order was cancelled",
|
|
159
176
|
renewal: "Recorded by a subscription renewal — check in Vonpay",
|
|
160
177
|
order: "Found on an order — its wallet payment wasn't collected",
|
|
161
|
-
upsell: "Recorded by an offer accepted after the order — check in Vonpay"
|
|
178
|
+
upsell: "Recorded by an offer accepted after the order — check in Vonpay",
|
|
179
|
+
refund: "Recorded when Vonpay did not complete a refund Medusa shows as done",
|
|
180
|
+
signup: "Recorded when Vonpay named no subscription for a subscription payment"
|
|
162
181
|
};
|
|
163
182
|
const STATUS_LABEL = {
|
|
164
183
|
open: "Needs action",
|
|
@@ -212,9 +231,11 @@ const REFUNDABLE_REASONS = [
|
|
|
212
231
|
"paid_orphan_session",
|
|
213
232
|
"paid_after_auto_close",
|
|
214
233
|
"paid_no_order",
|
|
234
|
+
"signup_mismatch",
|
|
215
235
|
"cancel_refund_missing",
|
|
216
236
|
"cancel_refund_unconfirmed"
|
|
217
237
|
];
|
|
238
|
+
const SUBSCRIPTION_REFUND_NOTE = "If this payment started a Vonpay subscription, refunding it does not cancel the subscription: cancel it in your Vonpay dashboard.";
|
|
218
239
|
function isConfirmedRefund(row) {
|
|
219
240
|
return row.status === "resolved" && (row.resolution?.how === "refunded" || row.resolution?.how === "refunded_earlier");
|
|
220
241
|
}
|
|
@@ -222,7 +243,7 @@ function canRefund(row) {
|
|
|
222
243
|
return row.vp_session_id !== null && !isConfirmedRefund(row) && !isAutoClosed(row) && !isUnrecognisedRecord(row) && REFUNDABLE_REASONS.includes(row.reason);
|
|
223
244
|
}
|
|
224
245
|
function isChargeRow(row) {
|
|
225
|
-
return row.source === "renewal" || row.source === "upsell";
|
|
246
|
+
return row.source === "renewal" || row.source === "upsell" || row.source === "refund" || row.source === "signup";
|
|
226
247
|
}
|
|
227
248
|
function canRelease(row) {
|
|
228
249
|
return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && WALLET_REASONS.includes(row.reason);
|
|
@@ -235,6 +256,8 @@ function canMarkResolved(row) {
|
|
|
235
256
|
return row.vp_session_id !== null && row.status !== "resolved" && !isUnrecognisedRecord(row) && !WALLET_REASONS.includes(row.reason) && !TAKE_REASONS.includes(row.reason);
|
|
236
257
|
}
|
|
237
258
|
function locatorFor(row) {
|
|
259
|
+
if (row.source === "refund") return { source: "refund", watch_id: row.watch_id ?? "" };
|
|
260
|
+
if (row.source === "signup") return { source: "signup", watch_id: row.watch_id ?? "" };
|
|
238
261
|
if (isChargeRow(row)) {
|
|
239
262
|
return {
|
|
240
263
|
source: row.source,
|
|
@@ -596,11 +619,11 @@ const HeldPaymentsBanner = () => {
|
|
|
596
619
|
] }) });
|
|
597
620
|
};
|
|
598
621
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
599
|
-
const KEY$
|
|
622
|
+
const KEY$2 = "vonpay-ship-hold";
|
|
600
623
|
const SHIP_HOLD_REFRESH_MS = 3e4;
|
|
601
624
|
function useShipHold(orderId) {
|
|
602
625
|
return reactQuery.useQuery({
|
|
603
|
-
queryKey: [KEY$
|
|
626
|
+
queryKey: [KEY$2, orderId],
|
|
604
627
|
queryFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/offer-hold`),
|
|
605
628
|
enabled: !!orderId,
|
|
606
629
|
retry: false,
|
|
@@ -615,7 +638,7 @@ function useShipOverride(orderId) {
|
|
|
615
638
|
return reactQuery.useMutation({
|
|
616
639
|
mutationFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/ship-override`, { method: "POST", body: {} }),
|
|
617
640
|
retry: false,
|
|
618
|
-
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$
|
|
641
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$2, orderId] })
|
|
619
642
|
});
|
|
620
643
|
}
|
|
621
644
|
function clockTime(iso) {
|
|
@@ -664,1064 +687,1289 @@ const UpdateBanner = () => {
|
|
|
664
687
|
] }) });
|
|
665
688
|
};
|
|
666
689
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
690
|
+
const KEY$1 = "vonpay-variant-subscription";
|
|
691
|
+
function useVariantSubscription(variantId) {
|
|
692
|
+
return reactQuery.useQuery({
|
|
693
|
+
queryKey: [KEY$1, variantId],
|
|
694
|
+
queryFn: () => sdk.client.fetch(`/admin/vonpay/variants/${encodeURIComponent(variantId)}/subscription`),
|
|
695
|
+
enabled: !!variantId,
|
|
670
696
|
retry: false
|
|
671
697
|
});
|
|
672
698
|
}
|
|
673
|
-
function
|
|
699
|
+
function useSetVariantSubscription(variantId) {
|
|
674
700
|
const qc = reactQuery.useQueryClient();
|
|
675
701
|
return reactQuery.useMutation({
|
|
676
|
-
mutationFn: (
|
|
702
|
+
mutationFn: (on) => sdk.client.fetch(`/admin/vonpay/variants/${encodeURIComponent(variantId)}/subscription`, { method: "POST", body: { on } }),
|
|
677
703
|
retry: false,
|
|
678
|
-
onSettled: () => qc.invalidateQueries({ queryKey: [
|
|
704
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$1, variantId] })
|
|
679
705
|
});
|
|
680
706
|
}
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-center sm:gap-x-3", children: [
|
|
692
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-mono", children: move.vp_session_id }),
|
|
693
|
-
!done && /* @__PURE__ */ jsxRuntime.jsx(
|
|
694
|
-
ui.Button,
|
|
695
|
-
{
|
|
696
|
-
size: "small",
|
|
697
|
-
variant: "secondary",
|
|
698
|
-
isLoading: mutation.isPending,
|
|
699
|
-
onClick: () => mutation.mutate(move, {
|
|
700
|
-
onSuccess: (o) => setResult(moveOutcomeCopy(o)),
|
|
701
|
-
onError: () => setResult({ variant: "error", text: "That couldn't be done. Nothing was changed — try again." })
|
|
702
|
-
}),
|
|
703
|
-
children: "Move to Held payments"
|
|
704
|
-
}
|
|
705
|
-
),
|
|
706
|
-
result && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: result.variant === "error" ? "text-ui-fg-error" : "text-ui-fg-subtle", children: result.text })
|
|
707
|
-
] });
|
|
707
|
+
const VARIANT_SUBSCRIPTION_COPY = {
|
|
708
|
+
heading: "Vonpay subscription",
|
|
709
|
+
on: "Sold as a Vonpay subscription: buying it at checkout (or on an offer) starts a subscription that renews in Vonpay.",
|
|
710
|
+
off: "Sold as a one-time purchase.",
|
|
711
|
+
noProduct: "No Vonpay subscription product has this variant's id as its Store product number. Create one in your Vonpay dashboard (Subscriptions → Products) and type this id there:",
|
|
712
|
+
markedNoProduct: "Vonpay has no active subscription product for this variant, so its checkout is refused. Create or re-activate the product in your Vonpay dashboard with this id as its Store product number, or stop selling it as a subscription:",
|
|
713
|
+
turnOn: "Sell as a Vonpay subscription",
|
|
714
|
+
turnOff: "Stop selling as a subscription",
|
|
715
|
+
failed: "Couldn't change that. Reload and try again.",
|
|
716
|
+
unreadable: "Couldn't read Vonpay's subscription products. Reload to try again."
|
|
708
717
|
};
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
/* @__PURE__ */ jsxRuntime.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
718
|
+
const VonpayVariantSubscription = ({ data: variant }) => {
|
|
719
|
+
const { data, isError } = useVariantSubscription(variant?.id);
|
|
720
|
+
const set = useSetVariantSubscription(variant?.id);
|
|
721
|
+
if (isError)
|
|
722
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "px-6 py-4", "data-testid": "vonpay-variant-subscription", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: VARIANT_SUBSCRIPTION_COPY.unreadable }) });
|
|
723
|
+
if (!data || !data.enabled) return null;
|
|
724
|
+
const product = data.products.find((p) => p.status === "active") ?? data.products[0];
|
|
725
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-3 px-6 py-4", "data-testid": "vonpay-variant-subscription", children: [
|
|
726
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
727
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: VARIANT_SUBSCRIPTION_COPY.heading }),
|
|
728
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: data.marked ? "green" : "grey", size: "2xsmall", children: data.marked ? "On" : "Off" })
|
|
719
729
|
] }),
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
] })
|
|
730
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: data.marked ? VARIANT_SUBSCRIPTION_COPY.on : VARIANT_SUBSCRIPTION_COPY.off }),
|
|
731
|
+
product?.disclosure ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-variant-subscription-terms", children: product.disclosure }) : null,
|
|
732
|
+
!data.can_mark && !data.marked ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-variant-subscription-setup", children: [
|
|
733
|
+
VARIANT_SUBSCRIPTION_COPY.noProduct,
|
|
734
|
+
" ",
|
|
735
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: variant?.id })
|
|
736
|
+
] }) : null,
|
|
737
|
+
!data.can_mark && data.marked ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-error", "data-testid": "vonpay-variant-subscription-broken", children: [
|
|
738
|
+
VARIANT_SUBSCRIPTION_COPY.markedNoProduct,
|
|
739
|
+
" ",
|
|
740
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: variant?.id })
|
|
741
|
+
] }) : null,
|
|
742
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: data.marked ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: set.isPending, onClick: () => set.mutate(false), "data-testid": "vonpay-variant-subscription-off", children: VARIANT_SUBSCRIPTION_COPY.turnOff }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", disabled: !data.can_mark, isLoading: set.isPending, onClick: () => set.mutate(true), "data-testid": "vonpay-variant-subscription-on", children: VARIANT_SUBSCRIPTION_COPY.turnOn }) }),
|
|
743
|
+
set.isError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: VARIANT_SUBSCRIPTION_COPY.failed }) : null
|
|
732
744
|
] });
|
|
733
745
|
};
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
746
|
+
adminSdk.defineWidgetConfig({ zone: "product_variant.details.after" });
|
|
747
|
+
const HEADLINE_MAX = 80;
|
|
748
|
+
const PITCH_MAX = 300;
|
|
749
|
+
const OFFERS_COPY = {
|
|
750
|
+
title: "One-click offers",
|
|
751
|
+
intro: "After paying, shoppers see these offers one at a time (up to three per order), in this order. Only switched-on offers are shown.",
|
|
752
|
+
off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
|
|
753
|
+
fromConfig: "Your offers come from medusa-config right now (upsells.variantIds). Import them to manage them here. Once an offer exists here, this list is used instead.",
|
|
754
|
+
configEmpty: "No offers yet. Add the first one below.",
|
|
755
|
+
adminEmpty: "No offers are shown to shoppers. Add one below.",
|
|
756
|
+
importButton: "Import from medusa-config",
|
|
757
|
+
add: "Add offer",
|
|
758
|
+
name: "Name (only you see it)",
|
|
759
|
+
variant: "Product variant id (variant_…)",
|
|
760
|
+
shown: "Shown",
|
|
761
|
+
up: "Move up",
|
|
762
|
+
down: "Move down",
|
|
763
|
+
remove: "Remove",
|
|
764
|
+
removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
|
|
765
|
+
missingVariant: "This product variant no longer exists — it is not offered.",
|
|
766
|
+
loadFailed: "The offers could not be loaded. Reload the page to try again.",
|
|
767
|
+
lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
|
|
768
|
+
rules: "Who sees it",
|
|
769
|
+
downsell: "If they say No thanks",
|
|
770
|
+
downsellNone: "Nothing",
|
|
771
|
+
downsellIntro: 'Show one other product after a shopper says No thanks to this offer — usually a smaller or cheaper option. It counts toward the three offers per order, and the same "Who sees it" applies.',
|
|
772
|
+
downsellVariant: "Downsell product variant id (variant_…)",
|
|
773
|
+
downsellRemove: "Remove downsell",
|
|
774
|
+
downsellMissing: "This product variant no longer exists — it is not offered.",
|
|
775
|
+
wording: "Wording",
|
|
776
|
+
wordingDefault: "Standard",
|
|
777
|
+
wordingPitchOnly: "Pitch only",
|
|
778
|
+
wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
|
|
779
|
+
wordingIntro: "What shoppers read on this offer. Leave a field empty to use the standard wording. The payment line, the buttons and every result message stay the same.",
|
|
780
|
+
headline: "Headline",
|
|
781
|
+
headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
|
|
782
|
+
pitch: "Pitch",
|
|
783
|
+
pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
|
|
784
|
+
wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
|
|
785
|
+
wordingReset: "Use the standard wording",
|
|
786
|
+
rulesEveryone: "Everyone",
|
|
787
|
+
rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
|
|
788
|
+
rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
|
|
789
|
+
customers: "Customers",
|
|
790
|
+
customersAll: "Everyone",
|
|
791
|
+
customersGuests: "Guests only",
|
|
792
|
+
customersSignedIn: "Signed-in customers only",
|
|
793
|
+
products: "The shopper bought one of these products at checkout",
|
|
794
|
+
collections: "The shopper bought a product from one of these collections at checkout",
|
|
795
|
+
groups: "The signed-in customer is in one of these groups",
|
|
796
|
+
paid: "The amount paid at checkout (tax and shipping included)",
|
|
797
|
+
paidNote: "An order in a currency not listed here does not see this offer.",
|
|
798
|
+
paidAtLeast: "At least",
|
|
799
|
+
paidAtMost: "At most",
|
|
800
|
+
search: "Search by name",
|
|
801
|
+
addCurrency: "Add currency",
|
|
802
|
+
removeCurrency: "Remove currency",
|
|
803
|
+
rulesUnreadableNotice: "The saved rules for this offer can't be read, so the offer is not shown. Saving replaces them with what you set here.",
|
|
804
|
+
replaceRules: "Replace the rules",
|
|
805
|
+
save: "Save",
|
|
806
|
+
cancel: "Cancel",
|
|
807
|
+
saveFailed: "That change was not saved."
|
|
755
808
|
};
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
809
|
+
function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
|
|
810
|
+
const m = err?.message;
|
|
811
|
+
if (typeof m !== "string") return fallback;
|
|
812
|
+
const i = m.indexOf(": ");
|
|
813
|
+
return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
|
|
759
814
|
}
|
|
760
|
-
function
|
|
761
|
-
|
|
815
|
+
function productLabel(o) {
|
|
816
|
+
if (o.product_title === null) return o.variant_id;
|
|
817
|
+
return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
|
|
762
818
|
}
|
|
763
|
-
function
|
|
764
|
-
|
|
819
|
+
function wordingSummary(copy) {
|
|
820
|
+
if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
|
|
821
|
+
if (!copy) return OFFERS_COPY.wordingDefault;
|
|
822
|
+
return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
|
|
765
823
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
824
|
+
function rulesSummary(rules) {
|
|
825
|
+
if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
|
|
826
|
+
if (!rules) return OFFERS_COPY.rulesEveryone;
|
|
827
|
+
const parts = [];
|
|
828
|
+
if (rules.customers === "guests") parts.push("Guests");
|
|
829
|
+
if (rules.customers === "signed_in") parts.push("Signed-in");
|
|
830
|
+
if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
|
|
831
|
+
if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
|
|
832
|
+
if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
|
|
833
|
+
for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
|
|
834
|
+
for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
|
|
835
|
+
return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
|
|
770
836
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
837
|
+
const REPORT_COPY = {
|
|
838
|
+
tabOffers: "Offers",
|
|
839
|
+
tabResults: "Results",
|
|
840
|
+
intro: "How shoppers answered each offer. An order counts as shown when the offer was offered to it: on the shopper's screen, or answered. Answers and money count when the offer has ended, about 2 hours after the order.",
|
|
841
|
+
from: "From",
|
|
842
|
+
to: "To",
|
|
843
|
+
live: "Live payments",
|
|
844
|
+
test: "Test payments",
|
|
845
|
+
offer: "Offer",
|
|
846
|
+
product: "Product",
|
|
847
|
+
shown: "Shown",
|
|
848
|
+
added: "Added",
|
|
849
|
+
noThanks: "No thanks",
|
|
850
|
+
declined: "Didn't go through",
|
|
851
|
+
takeRate: "Take rate",
|
|
852
|
+
charged: "Charged",
|
|
853
|
+
refunded: "Refunded",
|
|
854
|
+
net: "Net",
|
|
855
|
+
total: "All offers",
|
|
856
|
+
removed: "(removed)",
|
|
857
|
+
fromConfig: "From medusa-config",
|
|
858
|
+
downsell: (product) => `If they say No thanks: ${product}`,
|
|
859
|
+
shownNote: "Shown counts the orders the offer was offered to, once per order (and once more for its downsell): the offer was on the shopper's screen, or the shopper answered it. It is not a count of page views.",
|
|
860
|
+
takeRateNote: "Take rate is added ÷ shown.",
|
|
861
|
+
declinedNote: "Didn't go through: the payment was declined or not finished, or the item could not be added.",
|
|
862
|
+
chargedNote: "Charged is what shoppers paid for the offers, tax included. Refunded is what was refunded on those payments in Medusa since, by you or automatically, and never more than was charged. Refunds made outside Medusa, for example in the Vonpay dashboard, are not counted. Net is charged minus refunded.",
|
|
863
|
+
datesNote: "Each row counts on the date of its own moment: a view when the offer was shown, an answer when the shopper gave it, money when it was charged. A refund counts on the date of the sale it refunds, not the day it was made. Near the start or end of your dates, a view and its answer can fall on different sides.",
|
|
864
|
+
heldNote: "A payment on the Held payments page counts as it stood when it was held. For example, a late charge whose refund failed usually shows as charged (if it was held within moments of the charge, it may not show while held). If someone closes it there, it is left out of Results.",
|
|
865
|
+
viewsMissing: 'Some added items have no view counted, so their take rate is not shown. Your storefront may not count views yet: see "Offer results" in the vonpay-medusa README.',
|
|
866
|
+
empty: "No results for these dates yet.",
|
|
867
|
+
loadFailed: "The results could not be loaded. Try again.",
|
|
868
|
+
badRange: "The first date must be on or before the second, and at most 400 days apart.",
|
|
869
|
+
none: "—"
|
|
870
|
+
};
|
|
871
|
+
function moneyText(currency, amount, minor, locale) {
|
|
872
|
+
if (amount === null) return `${minor} ${currency}`;
|
|
873
|
+
const digits = amount.replace(/^-/, "");
|
|
874
|
+
const decimals = digits.includes(".") ? digits.length - digits.indexOf(".") - 1 : 0;
|
|
875
|
+
try {
|
|
876
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(amount);
|
|
877
|
+
} catch {
|
|
878
|
+
return `${amount} ${currency}`;
|
|
879
|
+
}
|
|
778
880
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
);
|
|
881
|
+
function moneyColumn(money, which, locale) {
|
|
882
|
+
if (money.length === 0) return REPORT_COPY.none;
|
|
883
|
+
return money.map((m) => moneyText(m.currency, m[which], m[`${which}_minor`], locale)).join(" · ");
|
|
884
|
+
}
|
|
885
|
+
function takeRateText(c, locale) {
|
|
886
|
+
if (c.shown <= 0 || c.added > c.shown) return null;
|
|
887
|
+
return new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }).format(c.added / c.shown);
|
|
888
|
+
}
|
|
889
|
+
function reportLineLabel(l) {
|
|
890
|
+
const product = productLabel(l);
|
|
891
|
+
const offer = l.offer_id === null ? REPORT_COPY.fromConfig : `${l.name ?? l.offer_id}${l.removed ? ` ${REPORT_COPY.removed}` : ""}`;
|
|
892
|
+
return { offer, product: l.step === "downsell" ? REPORT_COPY.downsell(product) : product };
|
|
893
|
+
}
|
|
894
|
+
function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
895
|
+
const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
|
|
896
|
+
const [problem, setProblem] = react.useState(null);
|
|
897
|
+
const save = async (value) => {
|
|
898
|
+
if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
|
|
899
|
+
setProblem(null);
|
|
900
|
+
const refused = await onSave(value);
|
|
901
|
+
if (refused === null) onOpenChange(false);
|
|
902
|
+
else setProblem(refused);
|
|
803
903
|
};
|
|
804
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
805
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
904
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
905
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
906
|
+
OFFERS_COPY.downsell,
|
|
907
|
+
": ",
|
|
908
|
+
offer.name
|
|
909
|
+
] }) }),
|
|
910
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
|
|
911
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
|
|
912
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
913
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
|
|
914
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
|
|
810
915
|
] }),
|
|
811
|
-
/* @__PURE__ */ jsxRuntime.
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
const
|
|
916
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
|
|
917
|
+
] }),
|
|
918
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
919
|
+
offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
|
|
920
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
921
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
|
|
922
|
+
] })
|
|
923
|
+
] }) });
|
|
924
|
+
}
|
|
925
|
+
const KEY = "vonpay-offers";
|
|
926
|
+
function useOffers() {
|
|
927
|
+
return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
|
|
928
|
+
}
|
|
929
|
+
function useOfferReport(q) {
|
|
930
|
+
return reactQuery.useQuery({
|
|
931
|
+
queryKey: [KEY, "report", q],
|
|
932
|
+
enabled: q !== null,
|
|
933
|
+
queryFn: () => sdk.client.fetch("/admin/vonpay/offers/report", { query: { from: q.from, to: q.to, ...q.mode ? { mode: q.mode } : {} } })
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function useWrite(send) {
|
|
937
|
+
const qc = reactQuery.useQueryClient();
|
|
938
|
+
return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
|
|
939
|
+
}
|
|
940
|
+
const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
|
|
941
|
+
const useUpdateOffer = () => useWrite((v) => {
|
|
942
|
+
const { id, ...body } = v;
|
|
943
|
+
return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
|
|
944
|
+
});
|
|
945
|
+
const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
|
|
946
|
+
const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
|
|
947
|
+
const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
|
|
948
|
+
const MAX_SPAN_DAYS = 400;
|
|
949
|
+
const DAY_MS = 24 * 60 * 6e4;
|
|
950
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
951
|
+
const dayText = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
952
|
+
function localMidnight(day, addDays = 0) {
|
|
953
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
|
|
954
|
+
if (!m) return null;
|
|
955
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]) + addDays);
|
|
956
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
957
|
+
}
|
|
958
|
+
function rangeOf(fromDay, toDay) {
|
|
959
|
+
const from = localMidnight(fromDay);
|
|
960
|
+
const to = localMidnight(toDay, 1);
|
|
961
|
+
if (!from || !to || !(from.getTime() < to.getTime()) || to.getTime() - from.getTime() > MAX_SPAN_DAYS * DAY_MS) return null;
|
|
962
|
+
return { from: from.toISOString(), to: to.toISOString() };
|
|
963
|
+
}
|
|
964
|
+
function CountCells({ c }) {
|
|
965
|
+
const rate = takeRateText(c);
|
|
851
966
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
852
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
},
|
|
861
|
-
children: "Mark resolved…"
|
|
862
|
-
}
|
|
863
|
-
),
|
|
864
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !mark.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
865
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
866
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Mark this payment resolved?" }),
|
|
867
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: "Do this only after you've refunded it (for example from the command line) or settled it with the buyer. Nothing is sent to Vonpay. The buyer can no longer complete an order with this payment. It leaves this list and stays under Resolved, with your name on it; pressing Refund later still works." })
|
|
868
|
-
] }),
|
|
869
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
|
|
870
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
|
|
871
|
-
"Type this payment's Vonpay session id to confirm: ",
|
|
872
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.vp_session_id })
|
|
873
|
-
] }),
|
|
874
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) })
|
|
875
|
-
] }),
|
|
876
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
877
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: mark.isPending, children: "Cancel" }),
|
|
878
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
879
|
-
ui.Prompt.Action,
|
|
880
|
-
{
|
|
881
|
-
disabled: mark.isPending || !matches,
|
|
882
|
-
onClick: (e) => {
|
|
883
|
-
e.preventDefault();
|
|
884
|
-
if (!matches) return;
|
|
885
|
-
mark.mutate({ row, confirm_vp_session_id: typed }, {
|
|
886
|
-
onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
|
|
887
|
-
onError: (e2) => {
|
|
888
|
-
const f = postFailure(e2);
|
|
889
|
-
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
|
|
890
|
-
},
|
|
891
|
-
onSettled: () => setOpen(false)
|
|
892
|
-
});
|
|
893
|
-
},
|
|
894
|
-
children: "Mark resolved"
|
|
895
|
-
}
|
|
896
|
-
)
|
|
897
|
-
] })
|
|
898
|
-
] }) })
|
|
967
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.shown }),
|
|
968
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.added }),
|
|
969
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.no_thanks }),
|
|
970
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.declined }),
|
|
971
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: rate ?? REPORT_COPY.none }),
|
|
972
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "charged") }),
|
|
973
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "refunded") }),
|
|
974
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "net") })
|
|
899
975
|
] });
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
const
|
|
906
|
-
const
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const actions = answer?.actions ?? row.actions ?? [];
|
|
916
|
-
const hint = answer ? answer.hint : row.hint ?? null;
|
|
917
|
-
const closedUntil = actions.filter((a) => !a.available && a.available_at).map((a) => Date.parse(a.available_at)).filter((t) => Number.isFinite(t));
|
|
918
|
-
const opensAt = closedUntil.filter((t) => t > Date.now());
|
|
919
|
-
const nextOpen = opensAt.length > 0 ? Math.min(...opensAt) : null;
|
|
920
|
-
react.useEffect(() => {
|
|
921
|
-
if (nextOpen === null) return;
|
|
922
|
-
const timer = setTimeout(() => {
|
|
923
|
-
setAnswer(null);
|
|
924
|
-
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
925
|
-
}, Math.min(TIMER_MAX_MS, Math.max(0, nextOpen - Date.now()) + 1e3));
|
|
926
|
-
return () => clearTimeout(timer);
|
|
927
|
-
}, [nextOpen, qc]);
|
|
928
|
-
const overdue = closedUntil.filter((t) => t <= Date.now());
|
|
929
|
-
const pastDue = overdue.length > 0 ? Math.min(...overdue) : null;
|
|
930
|
-
const repolls = react.useRef(0);
|
|
931
|
-
react.useEffect(() => {
|
|
932
|
-
if (pastDue === null || repolls.current >= REPOLL_MAX) return;
|
|
933
|
-
const timer = setTimeout(() => {
|
|
934
|
-
repolls.current += 1;
|
|
935
|
-
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
936
|
-
}, REPOLL_MS);
|
|
937
|
-
return () => clearTimeout(timer);
|
|
938
|
-
}, [pastDue, listAt, qc]);
|
|
939
|
-
const run = (a) => act.mutate(
|
|
940
|
-
{ row, action: a.id, ...a.confirm === "type_order_id" ? { typed_order_id: typed, checked_in_vonpay: checked } : {} },
|
|
941
|
-
{
|
|
942
|
-
onSuccess: (o) => {
|
|
943
|
-
if (o.recorded === false || o.outcome === "not_saved") {
|
|
944
|
-
setAnswer({ ...o, actions: [], hint: null, receivedAt: Date.now() });
|
|
945
|
-
onResult({ variant: "warning", text: NOT_SAVED_TEXT });
|
|
946
|
-
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
setAnswer({ ...o, receivedAt: Date.now() });
|
|
950
|
-
onResult({ variant: o.tone, text: fillTime(o.text, o.at, localTime) });
|
|
951
|
-
},
|
|
952
|
-
onError: (e) => {
|
|
953
|
-
const f = postFailure(e);
|
|
954
|
-
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
|
|
955
|
-
},
|
|
956
|
-
onSettled: () => setAsking(null)
|
|
957
|
-
}
|
|
958
|
-
);
|
|
959
|
-
const typedOk = !asking || asking.confirm !== "type_order_id" || typed.trim() === row.order_id && checked;
|
|
960
|
-
const inputId = `vonpay-charge-confirm-${row.payment_collection_id}-${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}`;
|
|
961
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
962
|
-
hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: fillTime(hint.text, hint.at, localTime) }),
|
|
963
|
-
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
964
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
965
|
-
ui.Button,
|
|
966
|
-
{
|
|
967
|
-
size: "small",
|
|
968
|
-
variant: a.id === "check" ? "primary" : "secondary",
|
|
969
|
-
disabled: !a.available || act.isPending,
|
|
970
|
-
isLoading: act.isPending && (asking?.id === a.id || a.id === "check" && !asking),
|
|
971
|
-
onClick: () => {
|
|
972
|
-
if (!a.available) return;
|
|
973
|
-
if (!a.confirm) {
|
|
974
|
-
run(a);
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
setTyped("");
|
|
978
|
-
setChecked(false);
|
|
979
|
-
setAsking(a);
|
|
980
|
-
},
|
|
981
|
-
children: a.label
|
|
982
|
-
}
|
|
983
|
-
),
|
|
984
|
-
!a.available && a.available_at && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
|
|
985
|
-
"Opens ",
|
|
986
|
-
localTime(a.available_at)
|
|
987
|
-
] })
|
|
988
|
-
] }, a.id)) }),
|
|
989
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open: asking !== null, onOpenChange: (o) => !act.isPending && !o && setAsking(null), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
990
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
991
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Title, { children: [
|
|
992
|
-
asking?.label.replace(/…$/, ""),
|
|
993
|
-
"?"
|
|
994
|
-
] }),
|
|
995
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: asking?.confirm_text })
|
|
976
|
+
}
|
|
977
|
+
function OfferReport({ today = /* @__PURE__ */ new Date() }) {
|
|
978
|
+
const [fromDay, setFromDay] = react.useState(() => dayText(new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29)));
|
|
979
|
+
const [toDay, setToDay] = react.useState(() => dayText(today));
|
|
980
|
+
const [mode, setMode] = react.useState(void 0);
|
|
981
|
+
const range = rangeOf(fromDay, toDay);
|
|
982
|
+
const { data, isLoading, isError } = useOfferReport(range ? { ...range, mode } : null);
|
|
983
|
+
const shownMode = mode ?? data?.mode;
|
|
984
|
+
const viewsMissing = (data?.lines ?? []).some((l) => l.added > l.shown);
|
|
985
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4 px-6 py-4", "data-testid": "vonpay-offer-report", children: [
|
|
986
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: REPORT_COPY.intro }),
|
|
987
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [
|
|
988
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
989
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-report-from", children: REPORT_COPY.from }),
|
|
990
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-report-from", type: "date", value: fromDay, onChange: (e) => setFromDay(e.target.value) })
|
|
996
991
|
] }),
|
|
997
|
-
|
|
998
|
-
/* @__PURE__ */ jsxRuntime.
|
|
999
|
-
|
|
1000
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.order_id })
|
|
1001
|
-
] }),
|
|
1002
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) }),
|
|
1003
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-x-2", children: [
|
|
1004
|
-
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked, onChange: (e) => setChecked(e.target.checked) }),
|
|
1005
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small", children: "I checked this order in the Vonpay dashboard" })
|
|
1006
|
-
] })
|
|
992
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
993
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-report-to", children: REPORT_COPY.to }),
|
|
994
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-report-to", type: "date", value: toDay, onChange: (e) => setToDay(e.target.value) })
|
|
1007
995
|
] }),
|
|
1008
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
996
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-x-2", role: "group", children: ["live", "test"].map((m) => /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: shownMode === m ? "primary" : "secondary", "aria-pressed": shownMode === m, onClick: () => setMode(m), children: m === "live" ? REPORT_COPY.live : REPORT_COPY.test }, m)) })
|
|
997
|
+
] }),
|
|
998
|
+
!range ? /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", "data-testid": "vonpay-report-bad-range", children: REPORT_COPY.badRange }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) : isError || !data ? /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-report-error", children: REPORT_COPY.loadFailed }) : data.lines.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { "data-testid": "vonpay-report-empty", children: REPORT_COPY.empty }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
999
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1000
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1001
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: REPORT_COPY.offer }),
|
|
1002
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: REPORT_COPY.product }),
|
|
1003
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.shown }),
|
|
1004
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.added }),
|
|
1005
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.noThanks }),
|
|
1006
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.declined }),
|
|
1007
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.takeRate }),
|
|
1008
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.charged }),
|
|
1009
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.refunded }),
|
|
1010
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.net })
|
|
1011
|
+
] }) }),
|
|
1012
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
|
|
1013
|
+
data.lines.map((l) => {
|
|
1014
|
+
const label = reportLineLabel(l);
|
|
1015
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-report-line", children: [
|
|
1016
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: l.step === "downsell" ? "" : label.offer }),
|
|
1017
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: label.product }),
|
|
1018
|
+
/* @__PURE__ */ jsxRuntime.jsx(CountCells, { c: l })
|
|
1019
|
+
] }, `${l.offer_id ?? ""}|${l.variant_id}|${l.step}`);
|
|
1020
|
+
}),
|
|
1021
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-report-total", children: [
|
|
1022
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: REPORT_COPY.total }) }),
|
|
1023
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
|
|
1024
|
+
/* @__PURE__ */ jsxRuntime.jsx(CountCells, { c: data.totals })
|
|
1025
|
+
] })
|
|
1026
|
+
] })
|
|
1027
|
+
] }) }),
|
|
1028
|
+
viewsMissing && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", "data-testid": "vonpay-report-views-missing", children: REPORT_COPY.viewsMissing }),
|
|
1029
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex list-disc flex-col gap-y-1 pl-6", children: [REPORT_COPY.shownNote, REPORT_COPY.takeRateNote, REPORT_COPY.declinedNote, REPORT_COPY.chargedNote, REPORT_COPY.datesNote, REPORT_COPY.heldNote].map((n) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: n }) }, n)) })
|
|
1030
|
+
] })
|
|
1023
1031
|
] });
|
|
1032
|
+
}
|
|
1033
|
+
const SOURCES = {
|
|
1034
|
+
products: { path: "/admin/products", key: "products", label: "title" },
|
|
1035
|
+
collections: { path: "/admin/collections", key: "collections", label: "title" },
|
|
1036
|
+
groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
|
|
1024
1037
|
};
|
|
1025
|
-
|
|
1026
|
-
const
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
disabled: pending,
|
|
1048
|
-
onClick: (e) => {
|
|
1049
|
-
e.preventDefault();
|
|
1050
|
-
if (kind === "release") release.mutate({ row }, { onSuccess: (o) => onResult(releaseOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
|
|
1051
|
-
else complete.mutate({ row }, { onSuccess: (o) => onResult(completeOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
|
|
1052
|
-
},
|
|
1053
|
-
children: text.action
|
|
1054
|
-
}
|
|
1055
|
-
)
|
|
1056
|
-
] })
|
|
1057
|
-
] }) })
|
|
1038
|
+
async function fetchNamed(kind, query) {
|
|
1039
|
+
const s = SOURCES[kind];
|
|
1040
|
+
const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
|
|
1041
|
+
const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
|
|
1042
|
+
return (body[s.key] ?? []).flatMap((r) => typeof r.id === "string" ? [{ id: r.id, label: typeof r[s.label] === "string" && plain(r[s.label]) || r.id }] : []);
|
|
1043
|
+
}
|
|
1044
|
+
function Picker({ kind, label, ids, onChange }) {
|
|
1045
|
+
const [q, setQ] = react.useState("");
|
|
1046
|
+
const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
|
|
1047
|
+
const found = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-search", kind, q], queryFn: () => fetchNamed(kind, { q, limit: 8 }), enabled: q.trim().length >= 2, placeholderData: reactQuery.keepPreviousData });
|
|
1048
|
+
const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
|
|
1049
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
|
|
1050
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
|
|
1051
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: ids.map((id) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "bg-ui-bg-subtle flex items-center gap-x-1 rounded-md px-2 py-1", children: [
|
|
1052
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
|
|
1053
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1054
|
+
] }, id)) }),
|
|
1055
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { placeholder: OFFERS_COPY.search, value: q, maxLength: 100, "aria-label": `${label}: ${OFFERS_COPY.search}`, onChange: (e) => setQ(e.target.value) }),
|
|
1056
|
+
(q.trim().length >= 2 ? found.data ?? [] : []).filter((n) => !ids.includes(n.id)).map((n) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "transparent", type: "button", onClick: () => onChange([...ids, n.id]), children: [
|
|
1057
|
+
"+ ",
|
|
1058
|
+
n.label
|
|
1059
|
+
] }, n.id))
|
|
1058
1060
|
] });
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1061
|
+
}
|
|
1062
|
+
function rowsOf(rules) {
|
|
1063
|
+
const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
|
|
1064
|
+
return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
|
|
1065
|
+
}
|
|
1066
|
+
function amountText(text) {
|
|
1067
|
+
const t = text.trim().replace(",", ".");
|
|
1068
|
+
if (t === "") return void 0;
|
|
1069
|
+
return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
|
|
1070
|
+
}
|
|
1071
|
+
function rulesFromForm(f) {
|
|
1072
|
+
const out = {};
|
|
1073
|
+
if (f.customers !== "all") out.customers = f.customers;
|
|
1074
|
+
if (f.products.length) out.products = f.products;
|
|
1075
|
+
if (f.collections.length) out.collections = f.collections;
|
|
1076
|
+
if (f.groups.length) out.groups = f.groups;
|
|
1077
|
+
for (const r of f.paid) {
|
|
1078
|
+
const c = r.currency.trim().toLowerCase();
|
|
1079
|
+
if (!c && !r.min.trim() && !r.max.trim()) continue;
|
|
1080
|
+
if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
|
|
1081
|
+
const min = amountText(r.min);
|
|
1082
|
+
const max = amountText(r.max);
|
|
1083
|
+
if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
|
|
1084
|
+
if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
|
|
1085
|
+
if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
|
|
1086
|
+
if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
|
|
1087
|
+
if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
|
|
1088
|
+
}
|
|
1089
|
+
if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
|
|
1090
|
+
return Object.keys(out).length ? out : null;
|
|
1091
|
+
}
|
|
1092
|
+
function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1093
|
+
const unreadable = offer.rules === "unreadable";
|
|
1094
|
+
const start = offer.rules === "unreadable" ? null : offer.rules;
|
|
1095
|
+
const [customers, setCustomers] = react.useState(start?.customers ?? "all");
|
|
1096
|
+
const [products, setProducts] = react.useState(start?.products ?? []);
|
|
1097
|
+
const [collections, setCollections] = react.useState(start?.collections ?? []);
|
|
1098
|
+
const [groups, setGroups] = react.useState(start?.groups ?? []);
|
|
1099
|
+
const [paid, setPaid] = react.useState(rowsOf(start));
|
|
1100
|
+
const [problem, setProblem] = react.useState(null);
|
|
1101
|
+
const save = async () => {
|
|
1102
|
+
const r = rulesFromForm({ customers, products, collections, groups, paid });
|
|
1103
|
+
if (typeof r === "string") return setProblem(r);
|
|
1104
|
+
setProblem(null);
|
|
1105
|
+
const refused = await onSave(r);
|
|
1106
|
+
if (refused === null) onOpenChange(false);
|
|
1107
|
+
else setProblem(refused);
|
|
1073
1108
|
};
|
|
1074
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open
|
|
1075
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1085
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1092
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Evidence" }),
|
|
1093
|
-
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "flex flex-col gap-y-1", children: [
|
|
1094
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT }),
|
|
1095
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: FROM_BUYER_CHECKOUT_NOTE })
|
|
1096
|
-
] })
|
|
1097
|
-
] }),
|
|
1098
|
-
row.order_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1099
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
|
|
1100
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
|
|
1101
|
-
] }),
|
|
1102
|
-
row.vp_payment_intent_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1103
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay payment" }),
|
|
1104
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_payment_intent_id, label: "Vonpay payment id" }) })
|
|
1105
|
-
] }),
|
|
1106
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
|
|
1107
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1108
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
|
|
1109
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1110
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Payment collection" }),
|
|
1111
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.payment_collection_id, label: "payment collection id" }) }),
|
|
1112
|
-
row.resolution && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1113
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: row.status === "resolved" ? "Resolved" : "Last action" }),
|
|
1114
|
-
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
|
|
1115
|
-
row.resolution.how ? `${RESOLVED_HOW_LABEL[row.resolution.how] ?? row.resolution.how} · ` : "",
|
|
1116
|
-
new Date(row.resolution.at).toLocaleString(),
|
|
1117
|
-
" · by ",
|
|
1118
|
-
actorLabel(row.resolution)
|
|
1119
|
-
] }),
|
|
1120
|
-
row.resolution.refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1121
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
|
|
1122
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.resolution.refund_id, label: "Vonpay refund id" }) })
|
|
1123
|
-
] }),
|
|
1124
|
-
row.resolution.prior && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1125
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Before that" }),
|
|
1126
|
-
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
|
|
1127
|
-
row.resolution.prior.how ? `${RESOLVED_HOW_LABEL[row.resolution.prior.how] ?? row.resolution.prior.how}` : statusLabel(row.resolution.prior.state),
|
|
1128
|
-
` · ${new Date(row.resolution.prior.at).toLocaleString()} · by ${actorLabel(row.resolution.prior)}`
|
|
1129
|
-
] })
|
|
1109
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1110
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1111
|
+
OFFERS_COPY.rules,
|
|
1112
|
+
": ",
|
|
1113
|
+
offer.name
|
|
1114
|
+
] }) }),
|
|
1115
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
|
|
1116
|
+
unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
|
|
1117
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
|
|
1118
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1119
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
|
|
1120
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
|
|
1121
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1122
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
|
|
1123
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
|
|
1124
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
|
|
1125
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
|
|
1130
1126
|
] })
|
|
1131
1127
|
] })
|
|
1132
1128
|
] }),
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1129
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
|
|
1130
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
|
|
1131
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
|
|
1132
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
|
|
1133
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
|
|
1134
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
|
|
1135
|
+
paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
|
|
1136
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": "Currency", placeholder: "EUR", value: r.currency, maxLength: 3, onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, currency: e.target.value } : x)) }),
|
|
1137
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtLeast} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtLeast, value: r.min, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, min: e.target.value } : x)) }),
|
|
1138
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtMost} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtMost, value: r.max, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, max: e.target.value } : x)) }),
|
|
1139
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `${OFFERS_COPY.removeCurrency} ${r.currency.toUpperCase()}`.trim(), onClick: () => setPaid(paid.filter((_, j) => j !== i)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1140
|
+
] }, i)),
|
|
1141
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", type: "button", onClick: () => setPaid([...paid, { currency: "", min: "", max: "" }]), children: OFFERS_COPY.addCurrency }) })
|
|
1142
1142
|
] }),
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: step.body }),
|
|
1149
|
-
step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
|
|
1150
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
|
|
1151
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
|
|
1152
|
-
] })
|
|
1153
|
-
] }, step.title)),
|
|
1154
|
-
!isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
|
|
1155
|
-
] })
|
|
1143
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
|
|
1144
|
+
] }),
|
|
1145
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1146
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1147
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
|
|
1156
1148
|
] })
|
|
1157
1149
|
] }) });
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1161
|
-
const [
|
|
1162
|
-
const [
|
|
1163
|
-
const [
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
|
|
1170
|
-
const onRowKey = (row) => (e) => {
|
|
1171
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
1172
|
-
e.preventDefault();
|
|
1173
|
-
setOpened(row);
|
|
1174
|
-
}
|
|
1150
|
+
}
|
|
1151
|
+
function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1152
|
+
const start = offer.copy === "unreadable" ? null : offer.copy;
|
|
1153
|
+
const [headline, setHeadline] = react.useState(start?.headline ?? "");
|
|
1154
|
+
const [pitch, setPitch] = react.useState(start?.pitch ?? "");
|
|
1155
|
+
const [problem, setProblem] = react.useState(null);
|
|
1156
|
+
const save = async (copy) => {
|
|
1157
|
+
setProblem(null);
|
|
1158
|
+
const refused = await onSave(copy);
|
|
1159
|
+
if (refused === null) onOpenChange(false);
|
|
1160
|
+
else setProblem(refused);
|
|
1175
1161
|
};
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Held payments" }),
|
|
1181
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Buyers who were charged through Vonpay but whose order couldn't be completed safely, cancelled orders whose refund Vonpay hasn't confirmed, and Apple Pay / Google Pay payments not yet collected for an order. Nothing here is refunded automatically." })
|
|
1182
|
-
] }),
|
|
1183
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[180px]", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1184
|
-
ui.Select,
|
|
1185
|
-
{
|
|
1186
|
-
size: "small",
|
|
1187
|
-
value: filter,
|
|
1188
|
-
onValueChange: (v) => {
|
|
1189
|
-
setFilter(v);
|
|
1190
|
-
setPageIndex(0);
|
|
1191
|
-
},
|
|
1192
|
-
children: [
|
|
1193
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1194
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: ["open", "resolved", "all"].map((f) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: f, children: FILTER_LABEL[f] }, f)) })
|
|
1195
|
-
]
|
|
1196
|
-
}
|
|
1197
|
-
) })
|
|
1198
|
-
] }),
|
|
1199
|
-
data && !data.scan_complete && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: "This list may be incomplete: the store has more records than one page load can check." }) }),
|
|
1200
|
-
data?.job?.filter((j) => j.banner).map((j) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: j.banner }) }, j.mode)),
|
|
1201
|
-
data && chargedUncheckedNote(data.charged_unchecked) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: chargedUncheckedNote(data.charged_unchecked) }) }),
|
|
1202
|
-
isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
|
|
1203
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
|
|
1204
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
|
|
1205
|
-
] }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-3 px-6 py-6", "aria-busy": "true", "aria-label": "Loading held payments", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-6 w-full" }, i)) }) : count === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-10", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: filter === "resolved" ? "Nothing resolved yet." : "No held payments. When a buyer is charged but the order can't be completed safely, it appears here." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1206
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1207
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1208
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reason" }),
|
|
1209
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
|
|
1210
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Held since" }),
|
|
1211
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Vonpay session" }),
|
|
1212
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Medusa payment session" }),
|
|
1213
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Where" })
|
|
1214
|
-
] }) }),
|
|
1215
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.holds.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1216
|
-
ui.Table.Row,
|
|
1217
|
-
{
|
|
1218
|
-
className: "cursor-pointer",
|
|
1219
|
-
tabIndex: 0,
|
|
1220
|
-
role: "button",
|
|
1221
|
-
"aria-label": `${reasonCopy(row.reason).label}, ${statusLabel(row.status)}, open details`,
|
|
1222
|
-
onClick: () => setOpened(row),
|
|
1223
|
-
onKeyDown: onRowKey(row),
|
|
1224
|
-
children: [
|
|
1225
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ReasonBadge, { reason: row.reason }) }),
|
|
1226
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
|
|
1227
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small whitespace-nowrap", children: since(row.held_at) }) }) }),
|
|
1228
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1229
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1230
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1231
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
|
|
1232
|
-
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
|
|
1233
|
-
] }) })
|
|
1234
|
-
]
|
|
1235
|
-
},
|
|
1236
|
-
keyOf(row)
|
|
1237
|
-
)) })
|
|
1238
|
-
] }) }),
|
|
1239
|
-
pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1240
|
-
ui.Table.Pagination,
|
|
1241
|
-
{
|
|
1242
|
-
count,
|
|
1243
|
-
pageSize: HELD_PAGE_SIZE,
|
|
1244
|
-
pageIndex,
|
|
1245
|
-
pageCount,
|
|
1246
|
-
canPreviousPage: pageIndex > 0,
|
|
1247
|
-
canNextPage: pageIndex + 1 < pageCount,
|
|
1248
|
-
previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
|
|
1249
|
-
nextPage: () => setPageIndex((p) => p + 1)
|
|
1250
|
-
}
|
|
1251
|
-
)
|
|
1252
|
-
] }),
|
|
1253
|
-
/* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, listAt: dataUpdatedAt, onClose: () => setOpened(null) })
|
|
1254
|
-
] }),
|
|
1255
|
-
/* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
|
|
1256
|
-
] });
|
|
1257
|
-
};
|
|
1258
|
-
const config$2 = adminSdk.defineRouteConfig({
|
|
1259
|
-
label: "Held payments",
|
|
1260
|
-
icon: icons.ExclamationCircle,
|
|
1261
|
-
nested: "/orders"
|
|
1262
|
-
});
|
|
1263
|
-
const HEADLINE_MAX = 80;
|
|
1264
|
-
const PITCH_MAX = 300;
|
|
1265
|
-
const OFFERS_COPY = {
|
|
1266
|
-
title: "One-click offers",
|
|
1267
|
-
intro: "After paying, shoppers see these offers one at a time (up to three per order), in this order. Only switched-on offers are shown.",
|
|
1268
|
-
off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
|
|
1269
|
-
fromConfig: "Your offers come from medusa-config right now (upsells.variantIds). Import them to manage them here. Once an offer exists here, this list is used instead.",
|
|
1270
|
-
configEmpty: "No offers yet. Add the first one below.",
|
|
1271
|
-
adminEmpty: "No offers are shown to shoppers. Add one below.",
|
|
1272
|
-
importButton: "Import from medusa-config",
|
|
1273
|
-
add: "Add offer",
|
|
1274
|
-
name: "Name (only you see it)",
|
|
1275
|
-
variant: "Product variant id (variant_…)",
|
|
1276
|
-
shown: "Shown",
|
|
1277
|
-
up: "Move up",
|
|
1278
|
-
down: "Move down",
|
|
1279
|
-
remove: "Remove",
|
|
1280
|
-
removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
|
|
1281
|
-
missingVariant: "This product variant no longer exists — it is not offered.",
|
|
1282
|
-
loadFailed: "The offers could not be loaded. Reload the page to try again.",
|
|
1283
|
-
lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
|
|
1284
|
-
rules: "Who sees it",
|
|
1285
|
-
downsell: "If they say No thanks",
|
|
1286
|
-
downsellNone: "Nothing",
|
|
1287
|
-
downsellIntro: 'Show one other product after a shopper says No thanks to this offer — usually a smaller or cheaper option. It counts toward the three offers per order, and the same "Who sees it" applies.',
|
|
1288
|
-
downsellVariant: "Downsell product variant id (variant_…)",
|
|
1289
|
-
downsellRemove: "Remove downsell",
|
|
1290
|
-
downsellMissing: "This product variant no longer exists — it is not offered.",
|
|
1291
|
-
wording: "Wording",
|
|
1292
|
-
wordingDefault: "Standard",
|
|
1293
|
-
wordingPitchOnly: "Pitch only",
|
|
1294
|
-
wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
|
|
1295
|
-
wordingIntro: "What shoppers read on this offer. Leave a field empty to use the standard wording. The payment line, the buttons and every result message stay the same.",
|
|
1296
|
-
headline: "Headline",
|
|
1297
|
-
headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
|
|
1298
|
-
pitch: "Pitch",
|
|
1299
|
-
pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
|
|
1300
|
-
wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
|
|
1301
|
-
wordingReset: "Use the standard wording",
|
|
1302
|
-
rulesEveryone: "Everyone",
|
|
1303
|
-
rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
|
|
1304
|
-
rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
|
|
1305
|
-
customers: "Customers",
|
|
1306
|
-
customersAll: "Everyone",
|
|
1307
|
-
customersGuests: "Guests only",
|
|
1308
|
-
customersSignedIn: "Signed-in customers only",
|
|
1309
|
-
products: "The shopper bought one of these products at checkout",
|
|
1310
|
-
collections: "The shopper bought a product from one of these collections at checkout",
|
|
1311
|
-
groups: "The signed-in customer is in one of these groups",
|
|
1312
|
-
paid: "The amount paid at checkout (tax and shipping included)",
|
|
1313
|
-
paidNote: "An order in a currency not listed here does not see this offer.",
|
|
1314
|
-
paidAtLeast: "At least",
|
|
1315
|
-
paidAtMost: "At most",
|
|
1316
|
-
search: "Search by name",
|
|
1317
|
-
addCurrency: "Add currency",
|
|
1318
|
-
removeCurrency: "Remove currency",
|
|
1319
|
-
rulesUnreadableNotice: "The saved rules for this offer can't be read, so the offer is not shown. Saving replaces them with what you set here.",
|
|
1320
|
-
replaceRules: "Replace the rules",
|
|
1321
|
-
save: "Save",
|
|
1322
|
-
cancel: "Cancel",
|
|
1323
|
-
saveFailed: "That change was not saved."
|
|
1324
|
-
};
|
|
1325
|
-
function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
|
|
1326
|
-
const m = err?.message;
|
|
1327
|
-
if (typeof m !== "string") return fallback;
|
|
1328
|
-
const i = m.indexOf(": ");
|
|
1329
|
-
return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
|
|
1330
|
-
}
|
|
1331
|
-
function productLabel(o) {
|
|
1332
|
-
if (o.product_title === null) return o.variant_id;
|
|
1333
|
-
return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
|
|
1334
|
-
}
|
|
1335
|
-
function wordingSummary(copy) {
|
|
1336
|
-
if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
|
|
1337
|
-
if (!copy) return OFFERS_COPY.wordingDefault;
|
|
1338
|
-
return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
|
|
1339
|
-
}
|
|
1340
|
-
function rulesSummary(rules) {
|
|
1341
|
-
if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
|
|
1342
|
-
if (!rules) return OFFERS_COPY.rulesEveryone;
|
|
1343
|
-
const parts = [];
|
|
1344
|
-
if (rules.customers === "guests") parts.push("Guests");
|
|
1345
|
-
if (rules.customers === "signed_in") parts.push("Signed-in");
|
|
1346
|
-
if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
|
|
1347
|
-
if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
|
|
1348
|
-
if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
|
|
1349
|
-
for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
|
|
1350
|
-
for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
|
|
1351
|
-
return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
|
|
1352
|
-
}
|
|
1353
|
-
function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1354
|
-
const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
|
|
1355
|
-
const [problem, setProblem] = react.useState(null);
|
|
1356
|
-
const save = async (value) => {
|
|
1357
|
-
if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
|
|
1358
|
-
setProblem(null);
|
|
1359
|
-
const refused = await onSave(value);
|
|
1360
|
-
if (refused === null) onOpenChange(false);
|
|
1361
|
-
else setProblem(refused);
|
|
1162
|
+
const typed = () => {
|
|
1163
|
+
const h = headline.trim();
|
|
1164
|
+
const p = pitch.trim();
|
|
1165
|
+
return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
|
|
1362
1166
|
};
|
|
1363
1167
|
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1364
1168
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1365
|
-
OFFERS_COPY.
|
|
1169
|
+
OFFERS_COPY.wording,
|
|
1366
1170
|
": ",
|
|
1367
1171
|
offer.name
|
|
1368
1172
|
] }) }),
|
|
1369
1173
|
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
|
|
1370
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
|
|
1174
|
+
offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
|
|
1175
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
|
|
1371
1176
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1372
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-
|
|
1373
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-
|
|
1177
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
|
|
1178
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
|
|
1179
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
|
|
1374
1180
|
] }),
|
|
1375
|
-
|
|
1181
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1182
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
|
|
1183
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
|
|
1184
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1185
|
+
OFFERS_COPY.pitchHint,
|
|
1186
|
+
" (",
|
|
1187
|
+
pitch.length,
|
|
1188
|
+
"/",
|
|
1189
|
+
PITCH_MAX,
|
|
1190
|
+
")"
|
|
1191
|
+
] })
|
|
1192
|
+
] }),
|
|
1193
|
+
offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
|
|
1194
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
|
|
1376
1195
|
] }),
|
|
1377
1196
|
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1378
|
-
offer.
|
|
1197
|
+
offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
|
|
1379
1198
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1380
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(
|
|
1199
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
|
|
1381
1200
|
] })
|
|
1382
1201
|
] }) });
|
|
1383
1202
|
}
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const [
|
|
1397
|
-
const
|
|
1398
|
-
const
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
"+ ",
|
|
1409
|
-
n.label
|
|
1410
|
-
] }, n.id))
|
|
1411
|
-
] });
|
|
1412
|
-
}
|
|
1413
|
-
function rowsOf(rules) {
|
|
1414
|
-
const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
|
|
1415
|
-
return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
|
|
1416
|
-
}
|
|
1417
|
-
function amountText(text) {
|
|
1418
|
-
const t = text.trim().replace(",", ".");
|
|
1419
|
-
if (t === "") return void 0;
|
|
1420
|
-
return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
|
|
1421
|
-
}
|
|
1422
|
-
function rulesFromForm(f) {
|
|
1423
|
-
const out = {};
|
|
1424
|
-
if (f.customers !== "all") out.customers = f.customers;
|
|
1425
|
-
if (f.products.length) out.products = f.products;
|
|
1426
|
-
if (f.collections.length) out.collections = f.collections;
|
|
1427
|
-
if (f.groups.length) out.groups = f.groups;
|
|
1428
|
-
for (const r of f.paid) {
|
|
1429
|
-
const c = r.currency.trim().toLowerCase();
|
|
1430
|
-
if (!c && !r.min.trim() && !r.max.trim()) continue;
|
|
1431
|
-
if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
|
|
1432
|
-
const min = amountText(r.min);
|
|
1433
|
-
const max = amountText(r.max);
|
|
1434
|
-
if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
|
|
1435
|
-
if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
|
|
1436
|
-
if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
|
|
1437
|
-
if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
|
|
1438
|
-
if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
|
|
1439
|
-
}
|
|
1440
|
-
if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
|
|
1441
|
-
return Object.keys(out).length ? out : null;
|
|
1442
|
-
}
|
|
1443
|
-
function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1444
|
-
const unreadable = offer.rules === "unreadable";
|
|
1445
|
-
const start = offer.rules === "unreadable" ? null : offer.rules;
|
|
1446
|
-
const [customers, setCustomers] = react.useState(start?.customers ?? "all");
|
|
1447
|
-
const [products, setProducts] = react.useState(start?.products ?? []);
|
|
1448
|
-
const [collections, setCollections] = react.useState(start?.collections ?? []);
|
|
1449
|
-
const [groups, setGroups] = react.useState(start?.groups ?? []);
|
|
1450
|
-
const [paid, setPaid] = react.useState(rowsOf(start));
|
|
1451
|
-
const [problem, setProblem] = react.useState(null);
|
|
1452
|
-
const save = async () => {
|
|
1453
|
-
const r = rulesFromForm({ customers, products, collections, groups, paid });
|
|
1454
|
-
if (typeof r === "string") return setProblem(r);
|
|
1455
|
-
setProblem(null);
|
|
1456
|
-
const refused = await onSave(r);
|
|
1457
|
-
if (refused === null) onOpenChange(false);
|
|
1458
|
-
else setProblem(refused);
|
|
1203
|
+
const VonpayOffersPage = () => {
|
|
1204
|
+
const { data, isLoading, isError, error: loadError } = useOffers();
|
|
1205
|
+
const create = useCreateOffer();
|
|
1206
|
+
const update = useUpdateOffer();
|
|
1207
|
+
const remove = useDeleteOffer();
|
|
1208
|
+
const reorder = useReorderOffers();
|
|
1209
|
+
const importConfig = useImportOffers();
|
|
1210
|
+
const prompt = ui.usePrompt();
|
|
1211
|
+
const [name, setName] = react.useState("");
|
|
1212
|
+
const [variantId, setVariantId] = react.useState("");
|
|
1213
|
+
const [error, setError] = react.useState(null);
|
|
1214
|
+
const [editing, setEditing] = react.useState(null);
|
|
1215
|
+
const [downsellFor, setDownsellFor] = react.useState(null);
|
|
1216
|
+
const [wordingFor, setWordingFor] = react.useState(null);
|
|
1217
|
+
const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
|
|
1218
|
+
const run = async (work) => {
|
|
1219
|
+
setError(null);
|
|
1220
|
+
try {
|
|
1221
|
+
await work();
|
|
1222
|
+
return true;
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
setError(errorSentence(err));
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1459
1227
|
};
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
"
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1228
|
+
const onAdd = async (e) => {
|
|
1229
|
+
e.preventDefault();
|
|
1230
|
+
if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
|
|
1231
|
+
setName("");
|
|
1232
|
+
setVariantId("");
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
const move = (offers, i, by) => {
|
|
1236
|
+
const ids = offers.map((o) => o.id);
|
|
1237
|
+
[ids[i], ids[i + by]] = [ids[i + by], ids[i]];
|
|
1238
|
+
void run(() => reorder.mutateAsync({ ids }));
|
|
1239
|
+
};
|
|
1240
|
+
const onRemove = async (o) => {
|
|
1241
|
+
if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
|
|
1242
|
+
};
|
|
1243
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
1245
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
|
|
1246
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1247
|
+
OFFERS_COPY.intro,
|
|
1248
|
+
" ",
|
|
1249
|
+
OFFERS_COPY.lag
|
|
1250
|
+
] })
|
|
1251
|
+
] }),
|
|
1252
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs, { defaultValue: "offers", className: "divide-y", children: [
|
|
1253
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs.List, { children: [
|
|
1254
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "offers", children: REPORT_COPY.tabOffers }),
|
|
1255
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "results", children: REPORT_COPY.tabResults })
|
|
1256
|
+
] }) }),
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "results", children: /* @__PURE__ */ jsxRuntime.jsx(OfferReport, {}) }),
|
|
1258
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "offers", className: "divide-y", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError || !data ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-load-error", children: errorSentence(loadError, OFFERS_COPY.loadFailed) }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1259
|
+
!data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
|
|
1260
|
+
data.source === "config" && data.config.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 py-4", "data-testid": "vonpay-offers-from-config", children: [
|
|
1261
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
|
|
1262
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-disc pl-6", children: data.config.map((c) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: productLabel(c) }) }, c.variant_id)) }),
|
|
1263
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: busy, onClick: () => void run(() => importConfig.mutateAsync(void 0)), children: OFFERS_COPY.importButton }) })
|
|
1264
|
+
] }),
|
|
1265
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-error", children: error }) }),
|
|
1266
|
+
data.offers.length === 0 ? (data.source === "admin" || data.config.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: data.source === "admin" ? OFFERS_COPY.adminEmpty : OFFERS_COPY.configEmpty }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1267
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1268
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
|
|
1269
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
|
|
1272
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
|
|
1273
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
|
|
1274
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
|
|
1275
|
+
] }) }),
|
|
1276
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
|
|
1277
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
|
|
1278
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.product_title === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: OFFERS_COPY.missingVariant }) : productLabel(o) }),
|
|
1279
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.rules}: ${o.name}`, disabled: busy, onClick: () => setEditing(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: o.rules === "unreadable" ? "text-ui-fg-error" : void 0, children: rulesSummary(o.rules) }) }) }),
|
|
1280
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.downsell}: ${o.name}`, disabled: busy, onClick: () => setDownsellFor(o), children: o.downsell_variant_id === null ? OFFERS_COPY.downsellNone : o.downsell_product_title === null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-error", children: OFFERS_COPY.downsellMissing }) : productLabel({ product_title: o.downsell_product_title, variant_title: o.downsell_variant_title, variant_id: o.downsell_variant_id }) }) }),
|
|
1281
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.wording}: ${o.name}`, disabled: busy, onClick: () => setWordingFor(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `max-w-[16rem] truncate ${o.copy === "unreadable" ? "text-ui-fg-error" : ""}`, children: wordingSummary(o.copy) }) }) }),
|
|
1282
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { "aria-label": `${OFFERS_COPY.shown}: ${o.name}`, checked: o.enabled, disabled: busy, onCheckedChange: (on) => void run(() => update.mutateAsync({ id: o.id, enabled: on })) }) }),
|
|
1283
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
|
|
1284
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.up}: ${o.name}`, disabled: busy || i === 0, onClick: () => move(data.offers, i, -1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpMini, {}) }),
|
|
1285
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.down}: ${o.name}`, disabled: busy || i === data.offers.length - 1, onClick: () => move(data.offers, i, 1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowDownMini, {}) }),
|
|
1286
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.remove}: ${o.name}`, disabled: busy, onClick: () => void onRemove(o), children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}) })
|
|
1287
|
+
] }) })
|
|
1288
|
+
] }, o.id)) })
|
|
1289
|
+
] }) }),
|
|
1290
|
+
downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1291
|
+
OfferDownsellDrawer,
|
|
1292
|
+
{
|
|
1293
|
+
offer: downsellFor,
|
|
1294
|
+
open: downsellFor !== null,
|
|
1295
|
+
busy,
|
|
1296
|
+
onOpenChange: (o) => !o && setDownsellFor(null),
|
|
1297
|
+
onSave: async (downsell) => {
|
|
1298
|
+
try {
|
|
1299
|
+
await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
|
|
1300
|
+
return null;
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
return errorSentence(err);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
downsellFor.id
|
|
1307
|
+
),
|
|
1308
|
+
wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1309
|
+
OfferWordingDrawer,
|
|
1310
|
+
{
|
|
1311
|
+
offer: wordingFor,
|
|
1312
|
+
open: wordingFor !== null,
|
|
1313
|
+
busy,
|
|
1314
|
+
onOpenChange: (o) => !o && setWordingFor(null),
|
|
1315
|
+
onSave: async (copy) => {
|
|
1316
|
+
try {
|
|
1317
|
+
await update.mutateAsync({ id: wordingFor.id, copy });
|
|
1318
|
+
return null;
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
return errorSentence(err);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
wordingFor.id
|
|
1325
|
+
),
|
|
1326
|
+
editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1327
|
+
OfferRulesDrawer,
|
|
1328
|
+
{
|
|
1329
|
+
offer: editing,
|
|
1330
|
+
open: editing !== null,
|
|
1331
|
+
busy,
|
|
1332
|
+
onOpenChange: (o) => !o && setEditing(null),
|
|
1333
|
+
onSave: async (rules) => {
|
|
1334
|
+
try {
|
|
1335
|
+
await update.mutateAsync({ id: editing.id, rules });
|
|
1336
|
+
return null;
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
return errorSentence(err);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
editing.id
|
|
1343
|
+
),
|
|
1344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
|
|
1345
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
|
|
1346
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
|
|
1347
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1348
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
|
|
1349
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
|
|
1350
|
+
] }),
|
|
1351
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1352
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
|
|
1353
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
|
|
1354
|
+
] })
|
|
1355
|
+
] }),
|
|
1356
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
|
|
1478
1357
|
] })
|
|
1358
|
+
] }) })
|
|
1359
|
+
] })
|
|
1360
|
+
] });
|
|
1361
|
+
};
|
|
1362
|
+
const config$2 = adminSdk.defineRouteConfig({
|
|
1363
|
+
label: "One-click offers",
|
|
1364
|
+
icon: icons.Sparkles,
|
|
1365
|
+
nested: "/orders"
|
|
1366
|
+
});
|
|
1367
|
+
function useSetupCheck() {
|
|
1368
|
+
return reactQuery.useMutation({
|
|
1369
|
+
mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
|
|
1370
|
+
retry: false
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
function useMoveToHeld() {
|
|
1374
|
+
const qc = reactQuery.useQueryClient();
|
|
1375
|
+
return reactQuery.useMutation({
|
|
1376
|
+
mutationFn: (m) => sdk.client.fetch("/admin/vonpay/setup-check/move-to-held", { method: "POST", body: { vp_session_id: m.vp_session_id, medusa_payment_session_id: m.medusa_payment_session_id } }),
|
|
1377
|
+
retry: false,
|
|
1378
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] })
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
|
|
1382
|
+
const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
|
|
1383
|
+
function uniqueMoves(moves) {
|
|
1384
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1385
|
+
return (moves ?? []).filter((m) => !seen.has(m.vp_session_id) && (seen.add(m.vp_session_id), true));
|
|
1386
|
+
}
|
|
1387
|
+
const MoveRow = ({ move }) => {
|
|
1388
|
+
const mutation = useMoveToHeld();
|
|
1389
|
+
const [result, setResult] = react.useState(null);
|
|
1390
|
+
const done = result !== null && result.variant !== "error";
|
|
1391
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-center sm:gap-x-3", children: [
|
|
1392
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-mono", children: move.vp_session_id }),
|
|
1393
|
+
!done && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1394
|
+
ui.Button,
|
|
1395
|
+
{
|
|
1396
|
+
size: "small",
|
|
1397
|
+
variant: "secondary",
|
|
1398
|
+
isLoading: mutation.isPending,
|
|
1399
|
+
onClick: () => mutation.mutate(move, {
|
|
1400
|
+
onSuccess: (o) => setResult(moveOutcomeCopy(o)),
|
|
1401
|
+
onError: () => setResult({ variant: "error", text: "That couldn't be done. Nothing was changed — try again." })
|
|
1402
|
+
}),
|
|
1403
|
+
children: "Move to Held payments"
|
|
1404
|
+
}
|
|
1405
|
+
),
|
|
1406
|
+
result && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: result.variant === "error" ? "text-ui-fg-error" : "text-ui-fg-subtle", children: result.text })
|
|
1407
|
+
] });
|
|
1408
|
+
};
|
|
1409
|
+
const SetupCheckPanel = () => {
|
|
1410
|
+
const check = useSetupCheck();
|
|
1411
|
+
const items = check.data?.items ?? [];
|
|
1412
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1413
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
|
|
1414
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1415
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Vonpay setup" }),
|
|
1416
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Checks your Vonpay key, your notification endpoint and the Medusa setup Vonpay needs. With test keys it also sends one test notification (it never creates a payment); that usually takes under 30 seconds." })
|
|
1479
1417
|
] }),
|
|
1480
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1481
|
-
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
|
|
1482
|
-
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
|
|
1483
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
|
|
1484
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
|
|
1485
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
|
|
1486
|
-
paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
|
|
1487
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": "Currency", placeholder: "EUR", value: r.currency, maxLength: 3, onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, currency: e.target.value } : x)) }),
|
|
1488
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtLeast} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtLeast, value: r.min, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, min: e.target.value } : x)) }),
|
|
1489
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtMost} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtMost, value: r.max, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, max: e.target.value } : x)) }),
|
|
1490
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `${OFFERS_COPY.removeCurrency} ${r.currency.toUpperCase()}`.trim(), onClick: () => setPaid(paid.filter((_, j) => j !== i)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1491
|
-
] }, i)),
|
|
1492
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", type: "button", onClick: () => setPaid([...paid, { currency: "", min: "", max: "" }]), children: OFFERS_COPY.addCurrency }) })
|
|
1493
|
-
] }),
|
|
1494
|
-
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
|
|
1418
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
|
|
1495
1419
|
] }),
|
|
1496
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1497
|
-
|
|
1498
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
1420
|
+
check.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The check could not run. Try again, or run npx vonpay-medusa-check on the server." }) }),
|
|
1421
|
+
check.data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
|
|
1422
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: !check.data.ok ? "error" : check.data.complete ? "success" : "warning", children: !check.data.ok ? "Something needs fixing — see the items marked Fix." : check.data.complete ? "No problems found." : `No failures, but ${check.data.unverified} ${check.data.unverified === 1 ? "check" : "checks"} could not run — see the items marked Not checked.` }),
|
|
1423
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-y-3", "aria-label": "Setup check results", children: items.map((i, n) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-start sm:gap-x-3", children: [
|
|
1424
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
|
|
1425
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
|
|
1426
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
|
|
1427
|
+
i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix }),
|
|
1428
|
+
uniqueMoves(i.moves).length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "mt-2 flex flex-col gap-y-2", "aria-label": "Paid checkouts with no order", children: uniqueMoves(i.moves).map((m) => /* @__PURE__ */ jsxRuntime.jsx(MoveRow, { move: m }, m.vp_session_id)) })
|
|
1429
|
+
] })
|
|
1430
|
+
] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
|
|
1499
1431
|
] })
|
|
1500
|
-
] })
|
|
1432
|
+
] });
|
|
1433
|
+
};
|
|
1434
|
+
const relative = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
1435
|
+
const UNITS = [
|
|
1436
|
+
["day", 864e5],
|
|
1437
|
+
["hour", 36e5],
|
|
1438
|
+
["minute", 6e4]
|
|
1439
|
+
];
|
|
1440
|
+
function since(iso) {
|
|
1441
|
+
if (!iso) return "Unknown";
|
|
1442
|
+
const diff = Date.parse(iso) - Date.now();
|
|
1443
|
+
for (const [unit, ms] of UNITS) {
|
|
1444
|
+
if (Math.abs(diff) >= ms) return relative.format((diff - diff % ms) / ms, unit);
|
|
1445
|
+
}
|
|
1446
|
+
return "just now";
|
|
1501
1447
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1448
|
+
const STATUS_COLOR = {
|
|
1449
|
+
open: "orange",
|
|
1450
|
+
nothing_left: "orange",
|
|
1451
|
+
refund_pending: "blue",
|
|
1452
|
+
refund_unknown: "red",
|
|
1453
|
+
refund_sending: "red",
|
|
1454
|
+
resolved: "green"
|
|
1455
|
+
};
|
|
1456
|
+
const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
|
|
1457
|
+
function isKnownStatus(status) {
|
|
1458
|
+
return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
|
|
1459
|
+
}
|
|
1460
|
+
function statusLabel(status) {
|
|
1461
|
+
return isKnownStatus(status) ? STATUS_LABEL[status] : UNKNOWN_STATUS_LABEL;
|
|
1462
|
+
}
|
|
1463
|
+
function statusColor(status) {
|
|
1464
|
+
return isKnownStatus(status) && STATUS_COLOR[status] || "grey";
|
|
1465
|
+
}
|
|
1466
|
+
function routeRefusal(err) {
|
|
1467
|
+
const status = err?.status;
|
|
1468
|
+
const message = err?.message;
|
|
1469
|
+
return typeof status === "number" && status >= 400 && status < 500 && typeof message === "string" && message !== "" ? message : null;
|
|
1470
|
+
}
|
|
1471
|
+
function postFailure(err) {
|
|
1472
|
+
const refusal = routeRefusal(err);
|
|
1473
|
+
if (refusal) return { variant: "error", text: refusal };
|
|
1474
|
+
return {
|
|
1475
|
+
variant: "warning",
|
|
1476
|
+
text: "The result is unknown. Reload the list before refunding another way — pressing Refund again is safe: it reuses the same key, so Vonpay can't refund twice."
|
|
1517
1477
|
};
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1478
|
+
}
|
|
1479
|
+
const Mono = ({ value, label }) => value ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-1", children: [
|
|
1480
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small font-mono truncate max-w-[180px]", title: value, children: value }),
|
|
1481
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Copy, { content: value, variant: "mini", "aria-label": `Copy ${label}`, onClick: (e) => e.stopPropagation() })
|
|
1482
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: "Unreadable" });
|
|
1483
|
+
const ReasonBadge = ({ reason }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: reason === "unrecognised" ? "grey" : "orange", children: reasonCopy(reason).label });
|
|
1484
|
+
const RowStatus = ({ status }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: statusColor(status), children: statusLabel(status) });
|
|
1485
|
+
const RefundAction = ({ row, onResult }) => {
|
|
1486
|
+
const [open, setOpen] = react.useState(false);
|
|
1487
|
+
const preview = useRefundPreview(row, open);
|
|
1488
|
+
const refund = useRefundHold();
|
|
1489
|
+
const p = preview.data;
|
|
1490
|
+
const description = preview.isLoading ? "Checking the payment with Vonpay…" : preview.isError ? (
|
|
1491
|
+
routeRefusal(preview.error) ?? "Couldn't check the payment with Vonpay. Nothing was refunded."
|
|
1492
|
+
) : p && p.refundable ? refundConfirmText(p) : outcomeCopy({ outcome: "refused", code: p?.code ?? null, recorded: true }).text;
|
|
1493
|
+
const confirm = () => {
|
|
1494
|
+
if (!p || !p.refundable) return;
|
|
1495
|
+
refund.mutate(
|
|
1496
|
+
{ row, amount_minor: p.amount_minor, currency: p.currency },
|
|
1497
|
+
{
|
|
1498
|
+
onSuccess: (o) => onResult(outcomeCopy(o)),
|
|
1499
|
+
onError: (e) => onResult(postFailure(e)),
|
|
1500
|
+
onSettled: () => setOpen(false)
|
|
1501
|
+
}
|
|
1502
|
+
);
|
|
1503
|
+
};
|
|
1504
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1505
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", onClick: () => setOpen(true), children: "Refund in full…" }),
|
|
1506
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !refund.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1507
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1508
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Refund this payment in full?" }),
|
|
1509
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: description }),
|
|
1510
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-refund-subscription-note", children: SUBSCRIPTION_REFUND_NOTE })
|
|
1531
1511
|
] }),
|
|
1532
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1533
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
1534
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1512
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1513
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: refund.isPending, children: "Cancel" }),
|
|
1514
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1515
|
+
ui.Prompt.Action,
|
|
1516
|
+
{
|
|
1517
|
+
disabled: !p || !p.refundable || refund.isPending,
|
|
1518
|
+
onClick: (e) => {
|
|
1519
|
+
e.preventDefault();
|
|
1520
|
+
confirm();
|
|
1521
|
+
},
|
|
1522
|
+
children: p && p.refundable ? refundActionLabel(p) : "Refund"
|
|
1523
|
+
}
|
|
1524
|
+
)
|
|
1525
|
+
] })
|
|
1526
|
+
] }) })
|
|
1527
|
+
] });
|
|
1528
|
+
};
|
|
1529
|
+
const CheckWithVonpayAction = ({ row, onResult }) => {
|
|
1530
|
+
const reconcile = useReconcileHold();
|
|
1531
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1532
|
+
ui.Button,
|
|
1533
|
+
{
|
|
1534
|
+
size: "small",
|
|
1535
|
+
variant: "secondary",
|
|
1536
|
+
isLoading: reconcile.isPending,
|
|
1537
|
+
disabled: reconcile.isPending,
|
|
1538
|
+
onClick: () => reconcile.mutate(row, {
|
|
1539
|
+
onSuccess: (o) => onResult(reconcileCopy(o)),
|
|
1540
|
+
onError: (e) => onResult({ variant: "error", text: routeRefusal(e) ?? "Couldn't check this payment with Vonpay. Nothing was changed." })
|
|
1541
|
+
}),
|
|
1542
|
+
children: CHECK_WITH_VONPAY
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
};
|
|
1546
|
+
const MarkResolvedAction = ({ row, onResult }) => {
|
|
1547
|
+
const [open, setOpen] = react.useState(false);
|
|
1548
|
+
const [typed, setTyped] = react.useState("");
|
|
1549
|
+
const mark = useMarkResolved();
|
|
1550
|
+
const matches = markConfirmMatches(typed, row);
|
|
1551
|
+
const inputId = `vonpay-mark-confirm-${row.vp_session_id ?? "unreadable"}`;
|
|
1552
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1553
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1554
|
+
ui.Button,
|
|
1555
|
+
{
|
|
1556
|
+
size: "small",
|
|
1557
|
+
variant: "secondary",
|
|
1558
|
+
onClick: () => {
|
|
1559
|
+
setTyped("");
|
|
1560
|
+
setOpen(true);
|
|
1561
|
+
},
|
|
1562
|
+
children: "Mark resolved…"
|
|
1563
|
+
}
|
|
1564
|
+
),
|
|
1565
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !mark.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1566
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1567
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Mark this payment resolved?" }),
|
|
1568
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: "Do this only after you've refunded it (for example from the command line) or settled it with the buyer. Nothing is sent to Vonpay. The buyer can no longer complete an order with this payment. It leaves this list and stays under Resolved, with your name on it; pressing Refund later still works." })
|
|
1569
|
+
] }),
|
|
1570
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
|
|
1571
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
|
|
1572
|
+
"Type this payment's Vonpay session id to confirm: ",
|
|
1573
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.vp_session_id })
|
|
1574
|
+
] }),
|
|
1575
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) })
|
|
1576
|
+
] }),
|
|
1577
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1578
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: mark.isPending, children: "Cancel" }),
|
|
1579
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1580
|
+
ui.Prompt.Action,
|
|
1581
|
+
{
|
|
1582
|
+
disabled: mark.isPending || !matches,
|
|
1583
|
+
onClick: (e) => {
|
|
1584
|
+
e.preventDefault();
|
|
1585
|
+
if (!matches) return;
|
|
1586
|
+
mark.mutate({ row, confirm_vp_session_id: typed }, {
|
|
1587
|
+
onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
|
|
1588
|
+
onError: (e2) => {
|
|
1589
|
+
const f = postFailure(e2);
|
|
1590
|
+
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
|
|
1591
|
+
},
|
|
1592
|
+
onSettled: () => setOpen(false)
|
|
1593
|
+
});
|
|
1594
|
+
},
|
|
1595
|
+
children: "Mark resolved"
|
|
1596
|
+
}
|
|
1597
|
+
)
|
|
1598
|
+
] })
|
|
1599
|
+
] }) })
|
|
1600
|
+
] });
|
|
1601
|
+
};
|
|
1602
|
+
const localTime = (iso) => new Date(iso).toLocaleString();
|
|
1603
|
+
const TIMER_MAX_MS = 2147483647;
|
|
1604
|
+
const REPOLL_MS = 15e3;
|
|
1605
|
+
const REPOLL_MAX = 8;
|
|
1606
|
+
const ChargeActions = ({ row, listAt, onResult }) => {
|
|
1607
|
+
const [answered, setAnswer] = react.useState(null);
|
|
1608
|
+
const answer = answered && !(listAt > answered.receivedAt) ? answered : null;
|
|
1609
|
+
const [asking, setAsking] = react.useState(null);
|
|
1610
|
+
const [typed, setTyped] = react.useState("");
|
|
1611
|
+
const [checked, setChecked] = react.useState(false);
|
|
1612
|
+
const act = useChargeAction();
|
|
1613
|
+
const qc = reactQuery.useQueryClient();
|
|
1614
|
+
const rowSays = JSON.stringify([row.held_at, row.actions ?? null, row.hint ?? null]);
|
|
1615
|
+
react.useEffect(() => setAnswer(null), [rowSays]);
|
|
1616
|
+
const actions = answer?.actions ?? row.actions ?? [];
|
|
1617
|
+
const hint = answer ? answer.hint : row.hint ?? null;
|
|
1618
|
+
const closedUntil = actions.filter((a) => !a.available && a.available_at).map((a) => Date.parse(a.available_at)).filter((t) => Number.isFinite(t));
|
|
1619
|
+
const opensAt = closedUntil.filter((t) => t > Date.now());
|
|
1620
|
+
const nextOpen = opensAt.length > 0 ? Math.min(...opensAt) : null;
|
|
1621
|
+
react.useEffect(() => {
|
|
1622
|
+
if (nextOpen === null) return;
|
|
1623
|
+
const timer = setTimeout(() => {
|
|
1624
|
+
setAnswer(null);
|
|
1625
|
+
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
1626
|
+
}, Math.min(TIMER_MAX_MS, Math.max(0, nextOpen - Date.now()) + 1e3));
|
|
1627
|
+
return () => clearTimeout(timer);
|
|
1628
|
+
}, [nextOpen, qc]);
|
|
1629
|
+
const overdue = closedUntil.filter((t) => t <= Date.now());
|
|
1630
|
+
const pastDue = overdue.length > 0 ? Math.min(...overdue) : null;
|
|
1631
|
+
const repolls = react.useRef(0);
|
|
1632
|
+
react.useEffect(() => {
|
|
1633
|
+
if (pastDue === null || repolls.current >= REPOLL_MAX) return;
|
|
1634
|
+
const timer = setTimeout(() => {
|
|
1635
|
+
repolls.current += 1;
|
|
1636
|
+
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
1637
|
+
}, REPOLL_MS);
|
|
1638
|
+
return () => clearTimeout(timer);
|
|
1639
|
+
}, [pastDue, listAt, qc]);
|
|
1640
|
+
const run = (a) => act.mutate(
|
|
1641
|
+
{ row, action: a.id, ...a.confirm === "type_order_id" ? { typed_order_id: typed, checked_in_vonpay: checked } : {} },
|
|
1642
|
+
{
|
|
1643
|
+
onSuccess: (o) => {
|
|
1644
|
+
if (o.recorded === false || o.outcome === "not_saved") {
|
|
1645
|
+
setAnswer({ ...o, actions: [], hint: null, receivedAt: Date.now() });
|
|
1646
|
+
onResult({ variant: "warning", text: o.outcome === "not_saved" && typeof o.text === "string" && o.text ? o.text : NOT_SAVED_TEXT });
|
|
1647
|
+
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
setAnswer({ ...o, receivedAt: Date.now() });
|
|
1651
|
+
onResult({ variant: o.tone, text: fillTime(o.text, o.at, localTime) });
|
|
1652
|
+
},
|
|
1653
|
+
onError: (e) => {
|
|
1654
|
+
const f = postFailure(e);
|
|
1655
|
+
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
|
|
1656
|
+
},
|
|
1657
|
+
onSettled: () => setAsking(null)
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
const typedOk = !asking || asking.confirm !== "type_order_id" || typed.trim() === row.order_id && checked;
|
|
1661
|
+
const inputId = `vonpay-charge-confirm-${row.payment_collection_id}-${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}-${row.watch_id ?? ""}`;
|
|
1662
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1663
|
+
hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: fillTime(hint.text, hint.at, localTime) }),
|
|
1664
|
+
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1665
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1666
|
+
ui.Button,
|
|
1667
|
+
{
|
|
1668
|
+
size: "small",
|
|
1669
|
+
variant: a.id === "check" ? "primary" : "secondary",
|
|
1670
|
+
disabled: !a.available || act.isPending,
|
|
1671
|
+
isLoading: act.isPending && (asking?.id === a.id || a.id === "check" && !asking),
|
|
1672
|
+
onClick: () => {
|
|
1673
|
+
if (!a.available) return;
|
|
1674
|
+
if (!a.confirm) {
|
|
1675
|
+
run(a);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
setTyped("");
|
|
1679
|
+
setChecked(false);
|
|
1680
|
+
setAsking(a);
|
|
1681
|
+
},
|
|
1682
|
+
children: a.label
|
|
1683
|
+
}
|
|
1684
|
+
),
|
|
1685
|
+
!a.available && a.available_at && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
|
|
1686
|
+
"Opens ",
|
|
1687
|
+
localTime(a.available_at)
|
|
1688
|
+
] })
|
|
1689
|
+
] }, a.id)) }),
|
|
1690
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open: asking !== null, onOpenChange: (o) => !act.isPending && !o && setAsking(null), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1691
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1692
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Title, { children: [
|
|
1693
|
+
asking?.label.replace(/…$/, ""),
|
|
1694
|
+
"?"
|
|
1695
|
+
] }),
|
|
1696
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: asking?.confirm_text })
|
|
1697
|
+
] }),
|
|
1698
|
+
asking?.confirm === "type_order_id" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
|
|
1699
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
|
|
1700
|
+
"Type the order id to confirm: ",
|
|
1701
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.order_id })
|
|
1702
|
+
] }),
|
|
1703
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) }),
|
|
1704
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-x-2", children: [
|
|
1705
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked, onChange: (e) => setChecked(e.target.checked) }),
|
|
1706
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small", children: "I checked this order in the Vonpay dashboard" })
|
|
1707
|
+
] })
|
|
1708
|
+
] }),
|
|
1709
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1710
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: act.isPending, children: "Cancel" }),
|
|
1711
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1712
|
+
ui.Prompt.Action,
|
|
1713
|
+
{
|
|
1714
|
+
disabled: act.isPending || !typedOk,
|
|
1715
|
+
onClick: (e) => {
|
|
1716
|
+
e.preventDefault();
|
|
1717
|
+
if (asking && typedOk) run(asking);
|
|
1718
|
+
},
|
|
1719
|
+
children: asking?.label.replace(/…$/, "")
|
|
1720
|
+
}
|
|
1721
|
+
)
|
|
1722
|
+
] })
|
|
1723
|
+
] }) })
|
|
1724
|
+
] });
|
|
1725
|
+
};
|
|
1726
|
+
const WalletHoldAction = ({ row, kind, onResult }) => {
|
|
1727
|
+
const [open, setOpen] = react.useState(false);
|
|
1728
|
+
const release = useReleaseHold();
|
|
1729
|
+
const complete = useCompleteHold();
|
|
1730
|
+
const pending = release.isPending || complete.isPending;
|
|
1731
|
+
const text = kind === "release" ? { button: "Release…", title: "Release this held payment?", body: "The hold is cancelled at Vonpay and drops off the buyer's card. Nothing was taken, so nothing is refunded. The buyer can't complete an order with this payment afterwards.", action: "Release" } : { button: "Complete the order…", title: "Create the order from this payment?", body: "Medusa completes the buyer's checkout with this held payment and creates the order; the payment is then taken with the order. Do this only if you intend to fulfil it.", action: "Complete the order" };
|
|
1732
|
+
const fail = (e2) => {
|
|
1733
|
+
const f = postFailure(e2);
|
|
1734
|
+
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
|
|
1735
|
+
};
|
|
1736
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1737
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: kind === "release" ? "secondary" : "primary", onClick: () => setOpen(true), children: text.button }),
|
|
1738
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !pending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1739
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1740
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
|
|
1741
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
|
|
1742
|
+
] }),
|
|
1743
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1744
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: pending, children: "Cancel" }),
|
|
1745
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1746
|
+
ui.Prompt.Action,
|
|
1747
|
+
{
|
|
1748
|
+
disabled: pending,
|
|
1749
|
+
onClick: (e) => {
|
|
1750
|
+
e.preventDefault();
|
|
1751
|
+
if (kind === "release") release.mutate({ row }, { onSuccess: (o) => onResult(releaseOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
|
|
1752
|
+
else complete.mutate({ row }, { onSuccess: (o) => onResult(completeOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
|
|
1753
|
+
},
|
|
1754
|
+
children: text.action
|
|
1755
|
+
}
|
|
1756
|
+
)
|
|
1757
|
+
] })
|
|
1758
|
+
] }) })
|
|
1759
|
+
] });
|
|
1760
|
+
};
|
|
1761
|
+
const HoldDrawer = ({ row, listAt, onClose }) => {
|
|
1762
|
+
const copy = row ? reasonCopy(row.reason) : void 0;
|
|
1763
|
+
const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
|
|
1764
|
+
const showRefund = !!row && known && canRefund(row);
|
|
1765
|
+
const showMark = !!row && known && canMarkResolved(row);
|
|
1766
|
+
const showRelease = !!row && known && canRelease(row);
|
|
1767
|
+
const showComplete = !!row && known && canComplete(row);
|
|
1768
|
+
const showOpenOrder = !!row && known && canOpenOrder(row);
|
|
1769
|
+
const steps = row ? resolutionSteps(known ? row : { ...row, reason: "unrecognised" }) : [];
|
|
1770
|
+
const [result, setResult] = react.useState(null);
|
|
1771
|
+
const close = () => {
|
|
1772
|
+
setResult(null);
|
|
1773
|
+
onClose();
|
|
1774
|
+
};
|
|
1775
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
1776
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
|
|
1777
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
|
|
1778
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: row && isChargeRow(row) ? void 0 : copy?.what })
|
|
1779
|
+
] }),
|
|
1780
|
+
row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
|
|
1781
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
|
|
1782
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Status" }),
|
|
1783
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
|
|
1784
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Held since" }),
|
|
1785
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown" }),
|
|
1786
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Where" }),
|
|
1787
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: SOURCE_LABEL[row.source] ?? row.source }),
|
|
1788
|
+
row.retry_until && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1789
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Retrying until" }),
|
|
1790
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: new Date(row.retry_until).toLocaleString() })
|
|
1791
|
+
] }),
|
|
1792
|
+
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1793
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Evidence" }),
|
|
1794
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "flex flex-col gap-y-1", children: [
|
|
1795
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT }),
|
|
1796
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: FROM_BUYER_CHECKOUT_NOTE })
|
|
1797
|
+
] })
|
|
1798
|
+
] }),
|
|
1799
|
+
row.order_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1800
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
|
|
1801
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
|
|
1802
|
+
] }),
|
|
1803
|
+
row.vp_payment_intent_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1804
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay payment" }),
|
|
1805
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_payment_intent_id, label: "Vonpay payment id" }) })
|
|
1806
|
+
] }),
|
|
1807
|
+
row.vp_refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1808
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
|
|
1809
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": "held-row-refund", children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_refund_id, label: "Vonpay refund id" }) })
|
|
1810
|
+
] }),
|
|
1811
|
+
row.subscription_id !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1812
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay subscription" }),
|
|
1813
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": "held-row-subscription", children: row.subscription_id ? /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.subscription_id, label: "Vonpay subscription id" }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: row.subscription_id_note ?? SUBSCRIPTION_NOT_NAMED_TEXT.unknown }) })
|
|
1814
|
+
] }),
|
|
1815
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
|
|
1816
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1817
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
|
|
1818
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1819
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Payment collection" }),
|
|
1820
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.payment_collection_id, label: "payment collection id" }) }),
|
|
1821
|
+
row.resolution && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1822
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: row.status === "resolved" ? "Resolved" : "Last action" }),
|
|
1823
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
|
|
1824
|
+
row.resolution.how ? `${RESOLVED_HOW_LABEL[row.resolution.how] ?? row.resolution.how} · ` : "",
|
|
1825
|
+
new Date(row.resolution.at).toLocaleString(),
|
|
1826
|
+
" · by ",
|
|
1827
|
+
actorLabel(row.resolution)
|
|
1828
|
+
] }),
|
|
1829
|
+
row.resolution.refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1830
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
|
|
1831
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.resolution.refund_id, label: "Vonpay refund id" }) })
|
|
1832
|
+
] }),
|
|
1833
|
+
row.resolution.prior && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1834
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Before that" }),
|
|
1835
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
|
|
1836
|
+
row.resolution.prior.how ? `${RESOLVED_HOW_LABEL[row.resolution.prior.how] ?? row.resolution.prior.how}` : statusLabel(row.resolution.prior.state),
|
|
1837
|
+
` · ${new Date(row.resolution.prior.at).toLocaleString()} · by ${actorLabel(row.resolution.prior)}`
|
|
1838
|
+
] })
|
|
1839
|
+
] })
|
|
1542
1840
|
] })
|
|
1543
1841
|
] }),
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1842
|
+
result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
|
|
1843
|
+
closedMoneyNote(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: closedMoneyNote(row) }),
|
|
1844
|
+
(showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
1845
|
+
showOpenOrder && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: orderPath(row.order_id), children: "Open order" }) }),
|
|
1846
|
+
showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
|
|
1847
|
+
showRelease && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "release", onResult: setResult }),
|
|
1848
|
+
showRefund && /* @__PURE__ */ jsxRuntime.jsx(RefundAction, { row, onResult: setResult }),
|
|
1849
|
+
showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
|
|
1850
|
+
showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
|
|
1851
|
+
] }),
|
|
1852
|
+
row && isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ChargeActions, { row, listAt, onResult: setResult }, `${row.payment_collection_id}:${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}:${row.watch_id ?? ""}`),
|
|
1853
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
|
|
1854
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
|
|
1855
|
+
steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1856
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: step.title }),
|
|
1857
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: step.body }),
|
|
1858
|
+
step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
|
|
1859
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
|
|
1860
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
|
|
1861
|
+
] })
|
|
1862
|
+
] }, step.title)),
|
|
1863
|
+
!isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
|
|
1864
|
+
] })
|
|
1551
1865
|
] })
|
|
1552
1866
|
] }) });
|
|
1553
|
-
}
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
|
|
1570
|
-
const VonpayOffersPage = () => {
|
|
1571
|
-
const { data, isLoading, isError, error: loadError } = useOffers();
|
|
1572
|
-
const create = useCreateOffer();
|
|
1573
|
-
const update = useUpdateOffer();
|
|
1574
|
-
const remove = useDeleteOffer();
|
|
1575
|
-
const reorder = useReorderOffers();
|
|
1576
|
-
const importConfig = useImportOffers();
|
|
1577
|
-
const prompt = ui.usePrompt();
|
|
1578
|
-
const [name, setName] = react.useState("");
|
|
1579
|
-
const [variantId, setVariantId] = react.useState("");
|
|
1580
|
-
const [error, setError] = react.useState(null);
|
|
1581
|
-
const [editing, setEditing] = react.useState(null);
|
|
1582
|
-
const [downsellFor, setDownsellFor] = react.useState(null);
|
|
1583
|
-
const [wordingFor, setWordingFor] = react.useState(null);
|
|
1584
|
-
const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
|
|
1585
|
-
const run = async (work) => {
|
|
1586
|
-
setError(null);
|
|
1587
|
-
try {
|
|
1588
|
-
await work();
|
|
1589
|
-
return true;
|
|
1590
|
-
} catch (err) {
|
|
1591
|
-
setError(errorSentence(err));
|
|
1592
|
-
return false;
|
|
1593
|
-
}
|
|
1594
|
-
};
|
|
1595
|
-
const onAdd = async (e) => {
|
|
1596
|
-
e.preventDefault();
|
|
1597
|
-
if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
|
|
1598
|
-
setName("");
|
|
1599
|
-
setVariantId("");
|
|
1867
|
+
};
|
|
1868
|
+
const FILTER_LABEL = { open: "Needs action", resolved: "Resolved", all: "All" };
|
|
1869
|
+
const HeldPaymentsPage = () => {
|
|
1870
|
+
const [pageIndex, setPageIndex] = react.useState(0);
|
|
1871
|
+
const [filter, setFilter] = react.useState("open");
|
|
1872
|
+
const [opened, setOpened] = react.useState(null);
|
|
1873
|
+
const { data, dataUpdatedAt, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
|
|
1874
|
+
const count = data?.count ?? 0;
|
|
1875
|
+
const remainder = count % HELD_PAGE_SIZE;
|
|
1876
|
+
const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
|
|
1877
|
+
const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}:${row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}${row.subscription_id !== void 0 ? `:subscription:${row.vp_payment_intent_id ?? ""}` : ""}${row.watch_id ? `:watch:${row.watch_id}` : ""}`;
|
|
1878
|
+
const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
|
|
1879
|
+
const onRowKey = (row) => (e) => {
|
|
1880
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1881
|
+
e.preventDefault();
|
|
1882
|
+
setOpened(row);
|
|
1600
1883
|
}
|
|
1601
1884
|
};
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: busy, onClick: () => void run(() => importConfig.mutateAsync(void 0)), children: OFFERS_COPY.importButton }) })
|
|
1885
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
|
|
1886
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1887
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
|
|
1888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1889
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Held payments" }),
|
|
1890
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Buyers who were charged through Vonpay but whose order couldn't be completed safely, cancelled orders whose refund Vonpay hasn't confirmed, and Apple Pay / Google Pay payments not yet collected for an order. Nothing here is refunded automatically." })
|
|
1891
|
+
] }),
|
|
1892
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[180px]", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1893
|
+
ui.Select,
|
|
1894
|
+
{
|
|
1895
|
+
size: "small",
|
|
1896
|
+
value: filter,
|
|
1897
|
+
onValueChange: (v) => {
|
|
1898
|
+
setFilter(v);
|
|
1899
|
+
setPageIndex(0);
|
|
1900
|
+
},
|
|
1901
|
+
children: [
|
|
1902
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1903
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: ["open", "resolved", "all"].map((f) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: f, children: FILTER_LABEL[f] }, f)) })
|
|
1904
|
+
]
|
|
1905
|
+
}
|
|
1906
|
+
) })
|
|
1625
1907
|
] }),
|
|
1626
|
-
|
|
1627
|
-
data
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.
|
|
1635
|
-
|
|
1908
|
+
data && !data.scan_complete && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: "This list may be incomplete: the store has more records than one page load can check." }) }),
|
|
1909
|
+
data?.job?.filter((j) => j.banner).map((j) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: j.banner }) }, j.mode)),
|
|
1910
|
+
data && chargedUncheckedNote(data.charged_unchecked) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: chargedUncheckedNote(data.charged_unchecked) }) }),
|
|
1911
|
+
isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
|
|
1912
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
|
|
1913
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
|
|
1914
|
+
] }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-3 px-6 py-6", "aria-busy": "true", "aria-label": "Loading held payments", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-6 w-full" }, i)) }) : count === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-10", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: filter === "resolved" ? "Nothing resolved yet." : "No held payments. When a buyer is charged but the order can't be completed safely, it appears here." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1915
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1916
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1917
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reason" }),
|
|
1918
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
|
|
1919
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Held since" }),
|
|
1920
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Vonpay session" }),
|
|
1921
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Medusa payment session" }),
|
|
1922
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Where" })
|
|
1923
|
+
] }) }),
|
|
1924
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.holds.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1925
|
+
ui.Table.Row,
|
|
1926
|
+
{
|
|
1927
|
+
className: "cursor-pointer",
|
|
1928
|
+
tabIndex: 0,
|
|
1929
|
+
role: "button",
|
|
1930
|
+
"aria-label": `${reasonCopy(row.reason).label}, ${statusLabel(row.status)}, open details`,
|
|
1931
|
+
onClick: () => setOpened(row),
|
|
1932
|
+
onKeyDown: onRowKey(row),
|
|
1933
|
+
children: [
|
|
1934
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1935
|
+
/* @__PURE__ */ jsxRuntime.jsx(ReasonBadge, { reason: row.reason }),
|
|
1936
|
+
row.subscription_id && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small font-mono text-ui-fg-subtle", "data-testid": "held-list-subscription", children: row.subscription_id })
|
|
1937
|
+
] }) }),
|
|
1938
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
|
|
1939
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small whitespace-nowrap", children: since(row.held_at) }) }) }),
|
|
1940
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1941
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1942
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1943
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
|
|
1944
|
+
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
|
|
1945
|
+
] }) })
|
|
1946
|
+
]
|
|
1947
|
+
},
|
|
1948
|
+
keyOf(row)
|
|
1949
|
+
)) })
|
|
1636
1950
|
] }) }),
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
] }) })
|
|
1649
|
-
] }, o.id)) })
|
|
1650
|
-
] }) }),
|
|
1651
|
-
downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1652
|
-
OfferDownsellDrawer,
|
|
1653
|
-
{
|
|
1654
|
-
offer: downsellFor,
|
|
1655
|
-
open: downsellFor !== null,
|
|
1656
|
-
busy,
|
|
1657
|
-
onOpenChange: (o) => !o && setDownsellFor(null),
|
|
1658
|
-
onSave: async (downsell) => {
|
|
1659
|
-
try {
|
|
1660
|
-
await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
|
|
1661
|
-
return null;
|
|
1662
|
-
} catch (err) {
|
|
1663
|
-
return errorSentence(err);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
},
|
|
1667
|
-
downsellFor.id
|
|
1668
|
-
),
|
|
1669
|
-
wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1670
|
-
OfferWordingDrawer,
|
|
1671
|
-
{
|
|
1672
|
-
offer: wordingFor,
|
|
1673
|
-
open: wordingFor !== null,
|
|
1674
|
-
busy,
|
|
1675
|
-
onOpenChange: (o) => !o && setWordingFor(null),
|
|
1676
|
-
onSave: async (copy) => {
|
|
1677
|
-
try {
|
|
1678
|
-
await update.mutateAsync({ id: wordingFor.id, copy });
|
|
1679
|
-
return null;
|
|
1680
|
-
} catch (err) {
|
|
1681
|
-
return errorSentence(err);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
},
|
|
1685
|
-
wordingFor.id
|
|
1686
|
-
),
|
|
1687
|
-
editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1688
|
-
OfferRulesDrawer,
|
|
1689
|
-
{
|
|
1690
|
-
offer: editing,
|
|
1691
|
-
open: editing !== null,
|
|
1692
|
-
busy,
|
|
1693
|
-
onOpenChange: (o) => !o && setEditing(null),
|
|
1694
|
-
onSave: async (rules) => {
|
|
1695
|
-
try {
|
|
1696
|
-
await update.mutateAsync({ id: editing.id, rules });
|
|
1697
|
-
return null;
|
|
1698
|
-
} catch (err) {
|
|
1699
|
-
return errorSentence(err);
|
|
1700
|
-
}
|
|
1951
|
+
pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1952
|
+
ui.Table.Pagination,
|
|
1953
|
+
{
|
|
1954
|
+
count,
|
|
1955
|
+
pageSize: HELD_PAGE_SIZE,
|
|
1956
|
+
pageIndex,
|
|
1957
|
+
pageCount,
|
|
1958
|
+
canPreviousPage: pageIndex > 0,
|
|
1959
|
+
canNextPage: pageIndex + 1 < pageCount,
|
|
1960
|
+
previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
|
|
1961
|
+
nextPage: () => setPageIndex((p) => p + 1)
|
|
1701
1962
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
)
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
|
|
1708
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1709
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
|
|
1710
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
|
|
1711
|
-
] }),
|
|
1712
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1713
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
|
|
1714
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
|
|
1715
|
-
] })
|
|
1716
|
-
] }),
|
|
1717
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
|
|
1718
|
-
] })
|
|
1719
|
-
] })
|
|
1963
|
+
)
|
|
1964
|
+
] }),
|
|
1965
|
+
/* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, listAt: dataUpdatedAt, onClose: () => setOpened(null) })
|
|
1966
|
+
] }),
|
|
1967
|
+
/* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
|
|
1720
1968
|
] });
|
|
1721
1969
|
};
|
|
1722
1970
|
const config$1 = adminSdk.defineRouteConfig({
|
|
1723
|
-
label: "
|
|
1724
|
-
icon: icons.
|
|
1971
|
+
label: "Held payments",
|
|
1972
|
+
icon: icons.ExclamationCircle,
|
|
1725
1973
|
nested: "/orders"
|
|
1726
1974
|
});
|
|
1727
1975
|
const EVENT_LABEL = {
|
|
@@ -1735,6 +1983,8 @@ const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
|
|
|
1735
1983
|
const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
|
|
1736
1984
|
const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
|
|
1737
1985
|
const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
|
|
1986
|
+
const CANCEL_IS_SEPARATE_NOTE = "Cancelling or refunding the order that started a subscription does not end the subscription: it keeps renewing until someone cancels it in your Vonpay dashboard.";
|
|
1987
|
+
const subscriptionOrderPath = (orderId) => `/orders/${encodeURIComponent(orderId)}`;
|
|
1738
1988
|
const SUBSCRIPTION_PAGE_SIZE = 20;
|
|
1739
1989
|
function useSubscriptionEvents(page) {
|
|
1740
1990
|
return reactQuery.useQuery({
|
|
@@ -1757,12 +2007,16 @@ const VonpaySubscriptionsPage = () => {
|
|
|
1757
2007
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
|
|
1758
2008
|
] }),
|
|
1759
2009
|
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1760
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2010
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
2011
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }),
|
|
2012
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-subscriptions-cancel-note", children: CANCEL_IS_SEPARATE_NOTE })
|
|
2013
|
+
] }),
|
|
1761
2014
|
data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1762
2015
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1763
2016
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1764
2017
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
|
|
1765
2018
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Started by order" }),
|
|
1766
2020
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
|
|
1767
2021
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
|
|
1768
2022
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
|
|
@@ -1774,6 +2028,7 @@ const VonpaySubscriptionsPage = () => {
|
|
|
1774
2028
|
e.mode === "test" ? " (test)" : ""
|
|
1775
2029
|
] }),
|
|
1776
2030
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
|
|
2031
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", "data-testid": "vonpay-subscription-order", children: e.medusa_order_id ? /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: subscriptionOrderPath(e.medusa_order_id), children: e.medusa_order_id }) : "—" }),
|
|
1777
2032
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
|
|
1778
2033
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
|
|
1779
2034
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
|
|
@@ -1818,18 +2073,23 @@ const widgetModule = { widgets: [
|
|
|
1818
2073
|
Component: UpdateBanner,
|
|
1819
2074
|
zone: ["order.list.before"],
|
|
1820
2075
|
widgetId: "Widget-5ad3"
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
Component: VonpayVariantSubscription,
|
|
2079
|
+
zone: ["product_variant.details.after"],
|
|
2080
|
+
widgetId: "Widget-671a"
|
|
1821
2081
|
}
|
|
1822
2082
|
] };
|
|
1823
2083
|
const routeModule = {
|
|
1824
2084
|
routes: [
|
|
1825
2085
|
{
|
|
1826
|
-
Component:
|
|
1827
|
-
path: "/vonpay-
|
|
2086
|
+
Component: VonpayOffersPage,
|
|
2087
|
+
path: "/vonpay-offers",
|
|
1828
2088
|
handle: { label: config$2.label, translationNs: config$2.translationNs }
|
|
1829
2089
|
},
|
|
1830
2090
|
{
|
|
1831
|
-
Component:
|
|
1832
|
-
path: "/vonpay-
|
|
2091
|
+
Component: HeldPaymentsPage,
|
|
2092
|
+
path: "/vonpay-holds",
|
|
1833
2093
|
handle: { label: config$1.label, translationNs: config$1.translationNs }
|
|
1834
2094
|
},
|
|
1835
2095
|
{
|
|
@@ -1842,16 +2102,16 @@ const routeModule = {
|
|
|
1842
2102
|
const menuItemModule = {
|
|
1843
2103
|
menuItems: [
|
|
1844
2104
|
{
|
|
1845
|
-
label: config$
|
|
1846
|
-
icon: config$
|
|
2105
|
+
label: config$1.label,
|
|
2106
|
+
icon: config$1.icon,
|
|
1847
2107
|
path: "/vonpay-holds",
|
|
1848
2108
|
nested: "/orders",
|
|
1849
2109
|
rank: void 0,
|
|
1850
2110
|
translationNs: void 0
|
|
1851
2111
|
},
|
|
1852
2112
|
{
|
|
1853
|
-
label: config$
|
|
1854
|
-
icon: config$
|
|
2113
|
+
label: config$2.label,
|
|
2114
|
+
icon: config$2.icon,
|
|
1855
2115
|
path: "/vonpay-offers",
|
|
1856
2116
|
nested: "/orders",
|
|
1857
2117
|
rank: void 0,
|