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.
Files changed (64) hide show
  1. package/.medusa/server/src/admin/index.js +1263 -1003
  2. package/.medusa/server/src/admin/index.mjs +1265 -1005
  3. package/.medusa/server/src/api/admin/vonpay/holds/route.js +51 -3
  4. package/.medusa/server/src/api/admin/vonpay/offers/report/route.js +10 -0
  5. package/.medusa/server/src/api/admin/vonpay/subscriptions/route.js +15 -1
  6. package/.medusa/server/src/api/admin/vonpay/variants/[id]/subscription/route.js +17 -0
  7. package/.medusa/server/src/api/middlewares.js +2 -0
  8. package/.medusa/server/src/api/store/vonpay/upsells/[order]/accept/route.js +11 -1
  9. package/.medusa/server/src/api/store/vonpay/upsells/[order]/shown/route.js +21 -0
  10. package/.medusa/server/src/lib/errors.js +29 -1
  11. package/.medusa/server/src/lib/held-charge-copy.js +71 -2
  12. package/.medusa/server/src/lib/money.js +45 -0
  13. package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261107202021.js +26 -0
  14. package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261109162717.js +15 -0
  15. package/.medusa/server/src/modules/vonpay-jobs/models/refund-watch.js +44 -0
  16. package/.medusa/server/src/modules/vonpay-jobs/models/signup-watch.js +28 -0
  17. package/.medusa/server/src/modules/vonpay-jobs/service.js +57 -1
  18. package/.medusa/server/src/modules/vonpay-jobs/types.js +2 -0
  19. package/.medusa/server/src/modules/vonpay-subscriptions/migrations/Migration20261102180000.js +13 -0
  20. package/.medusa/server/src/modules/vonpay-subscriptions/models/subscription-event.js +4 -0
  21. package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261102113326.js +20 -0
  22. package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261102170444.js +14 -0
  23. package/.medusa/server/src/modules/vonpay-upsells/models/event.js +8 -2
  24. package/.medusa/server/src/modules/vonpay-upsells/service.js +138 -0
  25. package/.medusa/server/src/providers/vonpay/admin-holds.js +10 -6
  26. package/.medusa/server/src/providers/vonpay/charge-hold.js +59 -2
  27. package/.medusa/server/src/providers/vonpay/descriptor-rules.js +6 -1
  28. package/.medusa/server/src/providers/vonpay/follow-up.js +45 -0
  29. package/.medusa/server/src/providers/vonpay/held-list.js +176 -3
  30. package/.medusa/server/src/providers/vonpay/hold-actions.js +1 -0
  31. package/.medusa/server/src/providers/vonpay/job-beat.js +11 -2
  32. package/.medusa/server/src/providers/vonpay/options.js +8 -0
  33. package/.medusa/server/src/providers/vonpay/refund-verdict.js +15 -0
  34. package/.medusa/server/src/providers/vonpay/refund-watch.js +562 -0
  35. package/.medusa/server/src/providers/vonpay/renewal-check.js +14 -0
  36. package/.medusa/server/src/providers/vonpay/renewal.js +2 -1
  37. package/.medusa/server/src/providers/vonpay/scan.js +13 -0
  38. package/.medusa/server/src/providers/vonpay/service.js +840 -49
  39. package/.medusa/server/src/providers/vonpay/session-data.js +235 -3
  40. package/.medusa/server/src/providers/vonpay/setup-check.js +223 -3
  41. package/.medusa/server/src/providers/vonpay/signup-api.js +176 -0
  42. package/.medusa/server/src/providers/vonpay/signup-watch.js +303 -0
  43. package/.medusa/server/src/providers/vonpay/signup.js +171 -0
  44. package/.medusa/server/src/providers/vonpay/subscription-cancel.js +168 -0
  45. package/.medusa/server/src/providers/vonpay/subscription-events.js +93 -4
  46. package/.medusa/server/src/providers/vonpay/subscription-products-admin.js +97 -0
  47. package/.medusa/server/src/providers/vonpay/upsell-accept.js +470 -40
  48. package/.medusa/server/src/providers/vonpay/upsell-cleanup.js +88 -5
  49. package/.medusa/server/src/providers/vonpay/upsell-report.js +489 -0
  50. package/.medusa/server/src/providers/vonpay/upsell-routes.js +9 -1
  51. package/.medusa/server/src/subscribers/vonpay-offer-refunds.js +9 -0
  52. package/README.md +265 -11
  53. package/bin/vonpay-medusa.cjs +26 -16
  54. package/package.json +3 -1
  55. package/storefront/src/copy.ts +15 -0
  56. package/storefront/src/index.ts +1 -1
  57. package/storefront/src/outcome.ts +37 -0
  58. package/storefront/src/reload-once.ts +13 -0
  59. package/storefront/src/server.ts +37 -6
  60. package/storefront/src/upsell.ts +43 -4
  61. package/storefront/src/vonpay-card-payment.tsx +38 -3
  62. package/storefront/src/vonpay-payment-view.tsx +43 -1
  63. package/storefront/src/vonpay-upsell-offer.tsx +95 -11
  64. 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$1 = "vonpay-ship-hold";
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$1, orderId],
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$1, orderId] })
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
- function useSetupCheck() {
668
- return reactQuery.useMutation({
669
- mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
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 useMoveToHeld() {
699
+ function useSetVariantSubscription(variantId) {
674
700
  const qc = reactQuery.useQueryClient();
675
701
  return reactQuery.useMutation({
676
- 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 } }),
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: [HELD_LIST_KEY] })
704
+ onSettled: () => qc.invalidateQueries({ queryKey: [KEY$1, variantId] })
679
705
  });
680
706
  }
681
- const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
682
- const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
683
- function uniqueMoves(moves) {
684
- const seen = /* @__PURE__ */ new Set();
685
- return (moves ?? []).filter((m) => !seen.has(m.vp_session_id) && (seen.add(m.vp_session_id), true));
686
- }
687
- const MoveRow = ({ move }) => {
688
- const mutation = useMoveToHeld();
689
- const [result, setResult] = react.useState(null);
690
- const done = result !== null && result.variant !== "error";
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 SetupCheckPanel = () => {
710
- const check = useSetupCheck();
711
- const items = check.data?.items ?? [];
712
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
713
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
714
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
715
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Vonpay setup" }),
716
- /* @__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." })
717
- ] }),
718
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
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
- 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." }) }),
721
- check.data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
722
- /* @__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.` }),
723
- /* @__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: [
724
- /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
725
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
726
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
727
- i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix }),
728
- 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)) })
729
- ] })
730
- ] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
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
- const relative = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
735
- const UNITS = [
736
- ["day", 864e5],
737
- ["hour", 36e5],
738
- ["minute", 6e4]
739
- ];
740
- function since(iso) {
741
- if (!iso) return "Unknown";
742
- const diff = Date.parse(iso) - Date.now();
743
- for (const [unit, ms] of UNITS) {
744
- if (Math.abs(diff) >= ms) return relative.format((diff - diff % ms) / ms, unit);
745
- }
746
- return "just now";
747
- }
748
- const STATUS_COLOR = {
749
- open: "orange",
750
- nothing_left: "orange",
751
- refund_pending: "blue",
752
- refund_unknown: "red",
753
- refund_sending: "red",
754
- resolved: "green"
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
- const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
757
- function isKnownStatus(status) {
758
- return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
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 statusLabel(status) {
761
- return isKnownStatus(status) ? STATUS_LABEL[status] : UNKNOWN_STATUS_LABEL;
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 statusColor(status) {
764
- return isKnownStatus(status) && STATUS_COLOR[status] || "grey";
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 routeRefusal(err) {
767
- const status = err?.status;
768
- const message = err?.message;
769
- return typeof status === "number" && status >= 400 && status < 500 && typeof message === "string" && message !== "" ? message : null;
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
- function postFailure(err) {
772
- const refusal = routeRefusal(err);
773
- if (refusal) return { variant: "error", text: refusal };
774
- return {
775
- variant: "warning",
776
- 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."
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
- const Mono = ({ value, label }) => value ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-1", children: [
780
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small font-mono truncate max-w-[180px]", title: value, children: value }),
781
- /* @__PURE__ */ jsxRuntime.jsx(ui.Copy, { content: value, variant: "mini", "aria-label": `Copy ${label}`, onClick: (e) => e.stopPropagation() })
782
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: "Unreadable" });
783
- const ReasonBadge = ({ reason }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: reason === "unrecognised" ? "grey" : "orange", children: reasonCopy(reason).label });
784
- const RowStatus = ({ status }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: statusColor(status), children: statusLabel(status) });
785
- const RefundAction = ({ row, onResult }) => {
786
- const [open, setOpen] = react.useState(false);
787
- const preview = useRefundPreview(row, open);
788
- const refund = useRefundHold();
789
- const p = preview.data;
790
- const description = preview.isLoading ? "Checking the payment with Vonpay…" : preview.isError ? (
791
- routeRefusal(preview.error) ?? "Couldn't check the payment with Vonpay. Nothing was refunded."
792
- ) : p && p.refundable ? refundConfirmText(p) : outcomeCopy({ outcome: "refused", code: p?.code ?? null, recorded: true }).text;
793
- const confirm = () => {
794
- if (!p || !p.refundable) return;
795
- refund.mutate(
796
- { row, amount_minor: p.amount_minor, currency: p.currency },
797
- {
798
- onSuccess: (o) => onResult(outcomeCopy(o)),
799
- onError: (e) => onResult(postFailure(e)),
800
- onSettled: () => setOpen(false)
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.jsxs(jsxRuntime.Fragment, { children: [
805
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", onClick: () => setOpen(true), children: "Refund in full…" }),
806
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !refund.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
807
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
808
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Refund this payment in full?" }),
809
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: description })
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.jsxs(ui.Prompt.Footer, { children: [
812
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: refund.isPending, children: "Cancel" }),
813
- /* @__PURE__ */ jsxRuntime.jsx(
814
- ui.Prompt.Action,
815
- {
816
- disabled: !p || !p.refundable || refund.isPending,
817
- onClick: (e) => {
818
- e.preventDefault();
819
- confirm();
820
- },
821
- children: p && p.refundable ? refundActionLabel(p) : "Refund"
822
- }
823
- )
824
- ] })
825
- ] }) })
826
- ] });
827
- };
828
- const CheckWithVonpayAction = ({ row, onResult }) => {
829
- const reconcile = useReconcileHold();
830
- return /* @__PURE__ */ jsxRuntime.jsx(
831
- ui.Button,
832
- {
833
- size: "small",
834
- variant: "secondary",
835
- isLoading: reconcile.isPending,
836
- disabled: reconcile.isPending,
837
- onClick: () => reconcile.mutate(row, {
838
- onSuccess: (o) => onResult(reconcileCopy(o)),
839
- onError: (e) => onResult({ variant: "error", text: routeRefusal(e) ?? "Couldn't check this payment with Vonpay. Nothing was changed." })
840
- }),
841
- children: CHECK_WITH_VONPAY
842
- }
843
- );
844
- };
845
- const MarkResolvedAction = ({ row, onResult }) => {
846
- const [open, setOpen] = react.useState(false);
847
- const [typed, setTyped] = react.useState("");
848
- const mark = useMarkResolved();
849
- const matches = markConfirmMatches(typed, row);
850
- const inputId = `vonpay-mark-confirm-${row.vp_session_id ?? "unreadable"}`;
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
- ui.Button,
854
- {
855
- size: "small",
856
- variant: "secondary",
857
- onClick: () => {
858
- setTyped("");
859
- setOpen(true);
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
- const localTime = (iso) => new Date(iso).toLocaleString();
902
- const TIMER_MAX_MS = 2147483647;
903
- const REPOLL_MS = 15e3;
904
- const REPOLL_MAX = 8;
905
- const ChargeActions = ({ row, listAt, onResult }) => {
906
- const [answered, setAnswer] = react.useState(null);
907
- const answer = answered && !(listAt > answered.receivedAt) ? answered : null;
908
- const [asking, setAsking] = react.useState(null);
909
- const [typed, setTyped] = react.useState("");
910
- const [checked, setChecked] = react.useState(false);
911
- const act = useChargeAction();
912
- const qc = reactQuery.useQueryClient();
913
- const rowSays = JSON.stringify([row.held_at, row.actions ?? null, row.hint ?? null]);
914
- react.useEffect(() => setAnswer(null), [rowSays]);
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
- asking?.confirm === "type_order_id" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
998
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
999
- "Type the order id to confirm: ",
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.jsxs(ui.Prompt.Footer, { children: [
1009
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: act.isPending, children: "Cancel" }),
1010
- /* @__PURE__ */ jsxRuntime.jsx(
1011
- ui.Prompt.Action,
1012
- {
1013
- disabled: act.isPending || !typedOk,
1014
- onClick: (e) => {
1015
- e.preventDefault();
1016
- if (asking && typedOk) run(asking);
1017
- },
1018
- children: asking?.label.replace(/…$/, "")
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
- const WalletHoldAction = ({ row, kind, onResult }) => {
1026
- const [open, setOpen] = react.useState(false);
1027
- const release = useReleaseHold();
1028
- const complete = useCompleteHold();
1029
- const pending = release.isPending || complete.isPending;
1030
- 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" };
1031
- const fail = (e2) => {
1032
- const f = postFailure(e2);
1033
- onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
1034
- };
1035
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1036
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: kind === "release" ? "secondary" : "primary", onClick: () => setOpen(true), children: text.button }),
1037
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !pending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
1038
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
1039
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
1040
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
1041
- ] }),
1042
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
1043
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: pending, children: "Cancel" }),
1044
- /* @__PURE__ */ jsxRuntime.jsx(
1045
- ui.Prompt.Action,
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
- const HoldDrawer = ({ row, listAt, onClose }) => {
1061
- const copy = row ? reasonCopy(row.reason) : void 0;
1062
- const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
1063
- const showRefund = !!row && known && canRefund(row);
1064
- const showMark = !!row && known && canMarkResolved(row);
1065
- const showRelease = !!row && known && canRelease(row);
1066
- const showComplete = !!row && known && canComplete(row);
1067
- const showOpenOrder = !!row && known && canOpenOrder(row);
1068
- const steps = row ? resolutionSteps(known ? row : { ...row, reason: "unrecognised" }) : [];
1069
- const [result, setResult] = react.useState(null);
1070
- const close = () => {
1071
- setResult(null);
1072
- onClose();
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: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
1075
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
1076
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
1077
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: row && isChargeRow(row) ? void 0 : copy?.what })
1078
- ] }),
1079
- row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
1080
- /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
1081
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Status" }),
1082
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
1083
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Held since" }),
1084
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown" }),
1085
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Where" }),
1086
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: SOURCE_LABEL[row.source] ?? row.source }),
1087
- row.retry_until && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1088
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Retrying until" }),
1089
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: new Date(row.retry_until).toLocaleString() })
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
- result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
1134
- closedMoneyNote(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: closedMoneyNote(row) }),
1135
- (showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
1136
- 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" }) }),
1137
- showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
1138
- showRelease && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "release", onResult: setResult }),
1139
- showRefund && /* @__PURE__ */ jsxRuntime.jsx(RefundAction, { row, onResult: setResult }),
1140
- showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
1141
- showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
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
- 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 ?? ""}`),
1144
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
1145
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
1146
- steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1147
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: step.title }),
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
- const FILTER_LABEL = { open: "Needs action", resolved: "Resolved", all: "All" };
1160
- const HeldPaymentsPage = () => {
1161
- const [pageIndex, setPageIndex] = react.useState(0);
1162
- const [filter, setFilter] = react.useState("open");
1163
- const [opened, setOpened] = react.useState(null);
1164
- const { data, dataUpdatedAt, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
1165
- const count = data?.count ?? 0;
1166
- const remainder = count % HELD_PAGE_SIZE;
1167
- const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1168
- 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 ?? ""}`;
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
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
1177
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1178
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
1179
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
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.downsell,
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, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
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-downsell-variant", children: OFFERS_COPY.downsellVariant }),
1373
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
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
- problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
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.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
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(variant.trim()), children: OFFERS_COPY.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 SOURCES = {
1385
- products: { path: "/admin/products", key: "products", label: "title" },
1386
- collections: { path: "/admin/collections", key: "collections", label: "title" },
1387
- groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
1388
- };
1389
- async function fetchNamed(kind, query) {
1390
- const s = SOURCES[kind];
1391
- const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
1392
- const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
1393
- 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 }] : []);
1394
- }
1395
- function Picker({ kind, label, ids, onChange }) {
1396
- const [q, setQ] = react.useState("");
1397
- const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
1398
- 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 });
1399
- const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
1400
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
1401
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
1402
- /* @__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: [
1403
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
1404
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
1405
- ] }, id)) }),
1406
- /* @__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) }),
1407
- (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: [
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
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1461
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1462
- OFFERS_COPY.rules,
1463
- ": ",
1464
- offer.name
1465
- ] }) }),
1466
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
1467
- unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
1468
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
1469
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1470
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
1471
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
1472
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1473
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
1474
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
1475
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
1476
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
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(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
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.jsxs(ui.Drawer.Footer, { children: [
1497
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1498
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
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
- function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
1503
- const start = offer.copy === "unreadable" ? null : offer.copy;
1504
- const [headline, setHeadline] = react.useState(start?.headline ?? "");
1505
- const [pitch, setPitch] = react.useState(start?.pitch ?? "");
1506
- const [problem, setProblem] = react.useState(null);
1507
- const save = async (copy) => {
1508
- setProblem(null);
1509
- const refused = await onSave(copy);
1510
- if (refused === null) onOpenChange(false);
1511
- else setProblem(refused);
1512
- };
1513
- const typed = () => {
1514
- const h = headline.trim();
1515
- const p = pitch.trim();
1516
- return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
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
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1519
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1520
- OFFERS_COPY.wording,
1521
- ": ",
1522
- offer.name
1523
- ] }) }),
1524
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
1525
- offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
1526
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
1527
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1528
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
1529
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
1530
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
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("div", { className: "flex flex-col gap-y-1", children: [
1533
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
1534
- /* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
1535
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1536
- OFFERS_COPY.pitchHint,
1537
- " (",
1538
- pitch.length,
1539
- "/",
1540
- PITCH_MAX,
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
- offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
1545
- problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
1546
- ] }),
1547
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1548
- offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
1549
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1550
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
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 KEY = "vonpay-offers";
1555
- function useOffers() {
1556
- return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
1557
- }
1558
- function useWrite(send) {
1559
- const qc = reactQuery.useQueryClient();
1560
- return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
1561
- }
1562
- const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
1563
- const useUpdateOffer = () => useWrite((v) => {
1564
- const { id, ...body } = v;
1565
- return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
1566
- });
1567
- const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
1568
- const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
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
- const move = (offers, i, by) => {
1603
- const ids = offers.map((o) => o.id);
1604
- [ids[i], ids[i + by]] = [ids[i + by], ids[i]];
1605
- void run(() => reorder.mutateAsync({ ids }));
1606
- };
1607
- const onRemove = async (o) => {
1608
- if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
1609
- };
1610
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1611
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1612
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
1613
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1614
- OFFERS_COPY.intro,
1615
- " ",
1616
- OFFERS_COPY.lag
1617
- ] })
1618
- ] }),
1619
- 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: [
1620
- !data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
1621
- 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: [
1622
- /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
1623
- /* @__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)) }),
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
- 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 }) }),
1627
- 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: [
1628
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1629
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
1630
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
1631
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
1632
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
1633
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
1634
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
1635
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
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
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
1638
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
1639
- /* @__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) }),
1640
- /* @__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) }) }) }),
1641
- /* @__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 }) }) }),
1642
- /* @__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) }) }) }),
1643
- /* @__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 })) }) }),
1644
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
1645
- /* @__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, {}) }),
1646
- /* @__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, {}) }),
1647
- /* @__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, {}) })
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
- editing.id
1704
- ),
1705
- /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
1706
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
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: "One-click offers",
1724
- icon: icons.Sparkles,
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.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
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: HeldPaymentsPage,
1827
- path: "/vonpay-holds",
2086
+ Component: VonpayOffersPage,
2087
+ path: "/vonpay-offers",
1828
2088
  handle: { label: config$2.label, translationNs: config$2.translationNs }
1829
2089
  },
1830
2090
  {
1831
- Component: VonpayOffersPage,
1832
- path: "/vonpay-offers",
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$2.label,
1846
- icon: config$2.icon,
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$1.label,
1854
- icon: config$1.icon,
2113
+ label: config$2.label,
2114
+ icon: config$2.icon,
1855
2115
  path: "/vonpay-offers",
1856
2116
  nested: "/orders",
1857
2117
  rank: void 0,