vonpay-medusa 1.3.0 → 1.4.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 (26) hide show
  1. package/.medusa/server/src/admin/index.js +1217 -1169
  2. package/.medusa/server/src/admin/index.mjs +1219 -1171
  3. package/.medusa/server/src/api/admin/vonpay/holds/route.js +19 -1
  4. package/.medusa/server/src/api/admin/vonpay/update/route.js +2 -1
  5. package/.medusa/server/src/lib/card-brand.js +4 -0
  6. package/.medusa/server/src/lib/held-charge-copy.js +38 -2
  7. package/.medusa/server/src/lib/reopen-reason.js +26 -0
  8. package/.medusa/server/src/lib/store-metadata.js +53 -0
  9. package/.medusa/server/src/lib/update-check.js +237 -17
  10. package/.medusa/server/src/lib/update-timestamp-state.js +49 -0
  11. package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261111111134.js +18 -0
  12. package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261111164946.js +13 -0
  13. package/.medusa/server/src/modules/vonpay-jobs/models/renewal-period.js +48 -0
  14. package/.medusa/server/src/modules/vonpay-jobs/service.js +146 -1
  15. package/.medusa/server/src/providers/vonpay/admin-holds.js +4 -0
  16. package/.medusa/server/src/providers/vonpay/charge-hold.js +28 -0
  17. package/.medusa/server/src/providers/vonpay/held-list.js +48 -1
  18. package/.medusa/server/src/providers/vonpay/renewal-period.js +807 -0
  19. package/.medusa/server/src/providers/vonpay/renewal-recovery.js +2 -0
  20. package/.medusa/server/src/providers/vonpay/renewal.js +385 -105
  21. package/.medusa/server/src/providers/vonpay/service.js +70 -2
  22. package/.medusa/server/src/providers/vonpay/session-data.js +10 -0
  23. package/.medusa/server/src/providers/vonpay/setup-check.js +37 -3
  24. package/.medusa/server/src/providers/vonpay/wallet-domains.js +13 -44
  25. package/README.md +176 -3
  26. package/package.json +2 -2
@@ -45,7 +45,12 @@ const CHARGE_REASON_LABEL = {
45
45
  refund_watch_overdue: "Refunded in Medusa — Vonpay's refund not finished",
46
46
  refund_watch_resolved: "Refunded in Medusa — Vonpay's refund settled by a person",
47
47
  signup_watch_missing: "Subscription paid for — not started",
48
- signup_watch_resolved: "Subscription paid for — settled by a person"
48
+ signup_watch_resolved: "Subscription paid for — settled by a person",
49
+ renewal_period_paid_refused: "Renewal refused — this period is already paid",
50
+ renewal_period_declined: "Renewal declined — ReorderJS keeps asking",
51
+ renewal_period_stuck: "Earlier renewal charge not finished",
52
+ renewal_period_closed: "Renewal period kept blocked by a person",
53
+ renewal_period_reopened: "Renewal period reopened by a person"
49
54
  };
50
55
  const CHARGE_RESOLVED_HOW_LABEL = {
51
56
  closed_blocking: "Closed by a person — no more renewal charges for this order",
@@ -177,7 +182,8 @@ const SOURCE_LABEL = {
177
182
  order: "Found on an order — its wallet payment wasn't collected",
178
183
  upsell: "Recorded by an offer accepted after the order — check in Vonpay",
179
184
  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"
185
+ signup: "Recorded when Vonpay named no subscription for a subscription payment",
186
+ period: "Recorded by the plugin's one-charge-per-period record for subscription renewals"
181
187
  };
182
188
  const STATUS_LABEL = {
183
189
  open: "Needs action",
@@ -243,7 +249,7 @@ function canRefund(row) {
243
249
  return row.vp_session_id !== null && !isConfirmedRefund(row) && !isAutoClosed(row) && !isUnrecognisedRecord(row) && REFUNDABLE_REASONS.includes(row.reason);
244
250
  }
245
251
  function isChargeRow(row) {
246
- return row.source === "renewal" || row.source === "upsell" || row.source === "refund" || row.source === "signup";
252
+ return row.source === "renewal" || row.source === "upsell" || row.source === "refund" || row.source === "signup" || row.source === "period";
247
253
  }
248
254
  function canRelease(row) {
249
255
  return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && WALLET_REASONS.includes(row.reason);
@@ -258,6 +264,7 @@ function canMarkResolved(row) {
258
264
  function locatorFor(row) {
259
265
  if (row.source === "refund") return { source: "refund", watch_id: row.watch_id ?? "" };
260
266
  if (row.source === "signup") return { source: "signup", watch_id: row.watch_id ?? "" };
267
+ if (row.source === "period") return { source: "period", period_id: row.watch_id ?? "" };
261
268
  if (isChargeRow(row)) {
262
269
  return {
263
270
  source: row.source,
@@ -582,6 +589,7 @@ function useChargeAction() {
582
589
  ...locatorFor(v.row),
583
590
  action: v.action,
584
591
  ...v.typed_order_id !== void 0 ? { confirm_order_id: v.typed_order_id.trim(), confirm_checked_in_vonpay: v.checked_in_vonpay === true } : {},
592
+ ...v.reason !== void 0 ? { confirm_reason: v.reason } : {},
585
593
  ...v.action === "close_unreadable" && v.row.record_sha256 ? { confirm_record_sha256: v.row.record_sha256 } : {}
586
594
  }
587
595
  }),
@@ -667,8 +675,18 @@ function useUpdateNotice() {
667
675
  retry: false
668
676
  });
669
677
  }
678
+ const CHANGELOG_URL = "https://docs.vonpay.com/connected-platforms/medusa-changelog";
670
679
  const UpdateBanner = () => {
671
680
  const { data } = useUpdateNotice();
681
+ if (data?.status === "unavailable" && data.reason === "unconfirmed") {
682
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-update-unconfirmed", children: [
683
+ "Couldn't confirm plugin updates — check",
684
+ " ",
685
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: CHANGELOG_URL, target: "_blank", rel: "noreferrer", className: "text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: "docs.vonpay.com" }),
686
+ " ",
687
+ "for the latest version."
688
+ ] });
689
+ }
672
690
  if (!data || data.status !== "update") return null;
673
691
  return /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: data.security ? "error" : "info", "data-testid": "vonpay-update-banner", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
674
692
  data.security ? "Security fix: " : "",
@@ -744,1232 +762,1262 @@ const VonpayVariantSubscription = ({ data: variant }) => {
744
762
  ] });
745
763
  };
746
764
  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."
808
- };
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;
814
- }
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(" — ");
765
+ const REOPEN_REASON = Object.freeze({ min: 3, max: 200, cardDigits: 12 });
766
+ const INVISIBLE = /[\p{Cc}\p{Cf}\u2028\u2029]/gu;
767
+ const DIGIT_SEPARATORS = new RegExp("(?<=\\p{Nd})[^\\p{L}\\p{Nd}]+(?=\\p{Nd})", "gu");
768
+ const CARD_RUN = new RegExp(`\\p{Nd}{${REOPEN_REASON.cardDigits},}`, "u");
769
+ function reopenReasonOf(raw) {
770
+ if (typeof raw !== "string") return { ok: false, why: "reason_required" };
771
+ const text = raw.normalize("NFKC").replace(INVISIBLE, "").trim();
772
+ if (CARD_RUN.test(text.replace(DIGIT_SEPARATORS, ""))) return { ok: false, why: "reason_card_number" };
773
+ const length = reasonLength(text);
774
+ if (length < REOPEN_REASON.min || length > REOPEN_REASON.max) return { ok: false, why: "reason_required" };
775
+ return { ok: true, reason: text };
776
+ }
777
+ const reasonLength = (text) => [...text].length;
778
+ function useSetupCheck() {
779
+ return reactQuery.useMutation({
780
+ mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
781
+ retry: false
782
+ });
818
783
  }
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;
784
+ function useMoveToHeld() {
785
+ const qc = reactQuery.useQueryClient();
786
+ return reactQuery.useMutation({
787
+ 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 } }),
788
+ retry: false,
789
+ onSettled: () => qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] })
790
+ });
823
791
  }
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;
792
+ const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
793
+ const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
794
+ function uniqueMoves(moves) {
795
+ const seen = /* @__PURE__ */ new Set();
796
+ return (moves ?? []).filter((m) => !seen.has(m.vp_session_id) && (seen.add(m.vp_session_id), true));
836
797
  }
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: "—"
798
+ const MoveRow = ({ move }) => {
799
+ const mutation = useMoveToHeld();
800
+ const [result, setResult] = react.useState(null);
801
+ const done = result !== null && result.variant !== "error";
802
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-center sm:gap-x-3", children: [
803
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-mono", children: move.vp_session_id }),
804
+ !done && /* @__PURE__ */ jsxRuntime.jsx(
805
+ ui.Button,
806
+ {
807
+ size: "small",
808
+ variant: "secondary",
809
+ isLoading: mutation.isPending,
810
+ onClick: () => mutation.mutate(move, {
811
+ onSuccess: (o) => setResult(moveOutcomeCopy(o)),
812
+ onError: () => setResult({ variant: "error", text: "That couldn't be done. Nothing was changed — try again." })
813
+ }),
814
+ children: "Move to Held payments"
815
+ }
816
+ ),
817
+ result && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: result.variant === "error" ? "text-ui-fg-error" : "text-ui-fg-subtle", children: result.text })
818
+ ] });
870
819
  };
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}`;
820
+ const SetupCheckPanel = () => {
821
+ const check = useSetupCheck();
822
+ const items = check.data?.items ?? [];
823
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
824
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
825
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
826
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Vonpay setup" }),
827
+ /* @__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." })
828
+ ] }),
829
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
830
+ ] }),
831
+ 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." }) }),
832
+ check.data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
833
+ /* @__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.` }),
834
+ /* @__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: [
835
+ /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
836
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
837
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
838
+ i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix }),
839
+ 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)) })
840
+ ] })
841
+ ] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
842
+ ] })
843
+ ] });
844
+ };
845
+ const relative = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
846
+ const UNITS = [
847
+ ["day", 864e5],
848
+ ["hour", 36e5],
849
+ ["minute", 6e4]
850
+ ];
851
+ function since(iso) {
852
+ if (!iso) return "Unknown";
853
+ const diff = Date.parse(iso) - Date.now();
854
+ for (const [unit, ms] of UNITS) {
855
+ if (Math.abs(diff) >= ms) return relative.format((diff - diff % ms) / ms, unit);
879
856
  }
857
+ return "just now";
880
858
  }
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(" · ");
859
+ const STATUS_COLOR = {
860
+ open: "orange",
861
+ nothing_left: "orange",
862
+ refund_pending: "blue",
863
+ refund_unknown: "red",
864
+ refund_sending: "red",
865
+ resolved: "green"
866
+ };
867
+ const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
868
+ function isKnownStatus(status) {
869
+ return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
884
870
  }
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);
871
+ function statusLabel(status) {
872
+ return isKnownStatus(status) ? STATUS_LABEL[status] : UNKNOWN_STATUS_LABEL;
888
873
  }
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 };
874
+ function statusColor(status) {
875
+ return isKnownStatus(status) && STATUS_COLOR[status] || "grey";
893
876
  }
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);
877
+ function routeRefusal(err) {
878
+ const status = err?.status;
879
+ const message = err?.message;
880
+ return typeof status === "number" && status >= 400 && status < 500 && typeof message === "string" && message !== "" ? message : null;
881
+ }
882
+ function postFailure(err) {
883
+ const refusal = routeRefusal(err);
884
+ if (refusal) return { variant: "error", text: refusal };
885
+ return {
886
+ variant: "warning",
887
+ 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."
903
888
  };
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) })
915
- ] }),
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
889
  }
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);
890
+ const Mono = ({ value, label }) => value ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-1", children: [
891
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small font-mono truncate max-w-[180px]", title: value, children: value }),
892
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Copy, { content: value, variant: "mini", "aria-label": `Copy ${label}`, onClick: (e) => e.stopPropagation() })
893
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: "Unreadable" });
894
+ const ReasonBadge = ({ reason }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: reason === "unrecognised" ? "grey" : "orange", children: reasonCopy(reason).label });
895
+ const RowStatus = ({ status }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: statusColor(status), children: statusLabel(status) });
896
+ const RefundAction = ({ row, onResult }) => {
897
+ const [open, setOpen] = react.useState(false);
898
+ const preview = useRefundPreview(row, open);
899
+ const refund = useRefundHold();
900
+ const p = preview.data;
901
+ const description = preview.isLoading ? "Checking the payment with Vonpay…" : preview.isError ? (
902
+ routeRefusal(preview.error) ?? "Couldn't check the payment with Vonpay. Nothing was refunded."
903
+ ) : p && p.refundable ? refundConfirmText(p) : outcomeCopy({ outcome: "refused", code: p?.code ?? null, recorded: true }).text;
904
+ const confirm = () => {
905
+ if (!p || !p.refundable) return;
906
+ refund.mutate(
907
+ { row, amount_minor: p.amount_minor, currency: p.currency },
908
+ {
909
+ onSuccess: (o) => onResult(outcomeCopy(o)),
910
+ onError: (e) => onResult(postFailure(e)),
911
+ onSettled: () => setOpen(false)
912
+ }
913
+ );
914
+ };
966
915
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
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") })
916
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", onClick: () => setOpen(true), children: "Refund in full…" }),
917
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !refund.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
918
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
919
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Refund this payment in full?" }),
920
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: description }),
921
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-refund-subscription-note", children: SUBSCRIPTION_REFUND_NOTE })
922
+ ] }),
923
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
924
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: refund.isPending, children: "Cancel" }),
925
+ /* @__PURE__ */ jsxRuntime.jsx(
926
+ ui.Prompt.Action,
927
+ {
928
+ disabled: !p || !p.refundable || refund.isPending,
929
+ onClick: (e) => {
930
+ e.preventDefault();
931
+ confirm();
932
+ },
933
+ children: p && p.refundable ? refundActionLabel(p) : "Refund"
934
+ }
935
+ )
936
+ ] })
937
+ ] }) })
975
938
  ] });
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) })
939
+ };
940
+ const CheckWithVonpayAction = ({ row, onResult }) => {
941
+ const reconcile = useReconcileHold();
942
+ return /* @__PURE__ */ jsxRuntime.jsx(
943
+ ui.Button,
944
+ {
945
+ size: "small",
946
+ variant: "secondary",
947
+ isLoading: reconcile.isPending,
948
+ disabled: reconcile.isPending,
949
+ onClick: () => reconcile.mutate(row, {
950
+ onSuccess: (o) => onResult(reconcileCopy(o)),
951
+ onError: (e) => onResult({ variant: "error", text: routeRefusal(e) ?? "Couldn't check this payment with Vonpay. Nothing was changed." })
952
+ }),
953
+ children: CHECK_WITH_VONPAY
954
+ }
955
+ );
956
+ };
957
+ const MarkResolvedAction = ({ row, onResult }) => {
958
+ const [open, setOpen] = react.useState(false);
959
+ const [typed, setTyped] = react.useState("");
960
+ const mark = useMarkResolved();
961
+ const matches = markConfirmMatches(typed, row);
962
+ const inputId = `vonpay-mark-confirm-${row.vp_session_id ?? "unreadable"}`;
963
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
964
+ /* @__PURE__ */ jsxRuntime.jsx(
965
+ ui.Button,
966
+ {
967
+ size: "small",
968
+ variant: "secondary",
969
+ onClick: () => {
970
+ setTyped("");
971
+ setOpen(true);
972
+ },
973
+ children: "Mark resolved…"
974
+ }
975
+ ),
976
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !mark.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
977
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
978
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Mark this payment resolved?" }),
979
+ /* @__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." })
991
980
  ] }),
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) })
981
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
982
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
983
+ "Type this payment's Vonpay session id to confirm: ",
984
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.vp_session_id })
985
+ ] }),
986
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) })
995
987
  ] }),
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
- ] })
988
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
989
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: mark.isPending, children: "Cancel" }),
990
+ /* @__PURE__ */ jsxRuntime.jsx(
991
+ ui.Prompt.Action,
992
+ {
993
+ disabled: mark.isPending || !matches,
994
+ onClick: (e) => {
995
+ e.preventDefault();
996
+ if (!matches) return;
997
+ mark.mutate({ row, confirm_vp_session_id: typed }, {
998
+ onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
999
+ onError: (e2) => {
1000
+ const f = postFailure(e2);
1001
+ onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
1002
+ },
1003
+ onSettled: () => setOpen(false)
1004
+ });
1005
+ },
1006
+ children: "Mark resolved"
1007
+ }
1008
+ )
1009
+ ] })
1010
+ ] }) })
1031
1011
  ] });
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" }
1037
1012
  };
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))
1013
+ const localTime = (iso) => new Date(iso).toLocaleString();
1014
+ const TIMER_MAX_MS = 2147483647;
1015
+ const REPOLL_MS = 15e3;
1016
+ const REPOLL_MAX = 8;
1017
+ const ChargeActions = ({ row, listAt, onResult }) => {
1018
+ const [answered, setAnswer] = react.useState(null);
1019
+ const answer = answered && !(listAt > answered.receivedAt) ? answered : null;
1020
+ const [asking, setAsking] = react.useState(null);
1021
+ const [typed, setTyped] = react.useState("");
1022
+ const [checked, setChecked] = react.useState(false);
1023
+ const [reason, setReason] = react.useState("");
1024
+ const act = useChargeAction();
1025
+ const qc = reactQuery.useQueryClient();
1026
+ const typedFor = (a) => a.confirm === "type_order_id" ? { what: "order id", value: a.confirm_id ?? row.order_id } : a.confirm === "type_payment_id" ? { what: "Vonpay payment id", value: a.confirm_id ?? null } : null;
1027
+ const rowSays = JSON.stringify([row.held_at, row.actions ?? null, row.hint ?? null]);
1028
+ react.useEffect(() => setAnswer(null), [rowSays]);
1029
+ const actions = answer?.actions ?? row.actions ?? [];
1030
+ const hint = answer ? answer.hint : row.hint ?? null;
1031
+ const closedUntil = actions.filter((a) => !a.available && a.available_at).map((a) => Date.parse(a.available_at)).filter((t) => Number.isFinite(t));
1032
+ const opensAt = closedUntil.filter((t) => t > Date.now());
1033
+ const nextOpen = opensAt.length > 0 ? Math.min(...opensAt) : null;
1034
+ react.useEffect(() => {
1035
+ if (nextOpen === null) return;
1036
+ const timer = setTimeout(() => {
1037
+ setAnswer(null);
1038
+ void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
1039
+ }, Math.min(TIMER_MAX_MS, Math.max(0, nextOpen - Date.now()) + 1e3));
1040
+ return () => clearTimeout(timer);
1041
+ }, [nextOpen, qc]);
1042
+ const overdue = closedUntil.filter((t) => t <= Date.now());
1043
+ const pastDue = overdue.length > 0 ? Math.min(...overdue) : null;
1044
+ const repolls = react.useRef(0);
1045
+ react.useEffect(() => {
1046
+ if (pastDue === null || repolls.current >= REPOLL_MAX) return;
1047
+ const timer = setTimeout(() => {
1048
+ repolls.current += 1;
1049
+ void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
1050
+ }, REPOLL_MS);
1051
+ return () => clearTimeout(timer);
1052
+ }, [pastDue, listAt, qc]);
1053
+ const run = (a) => act.mutate(
1054
+ { row, action: a.id, ...typedFor(a) !== null ? { typed_order_id: typed, checked_in_vonpay: checked } : {}, ...a.confirm_reason ? { reason } : {} },
1055
+ {
1056
+ onSuccess: (o) => {
1057
+ if (o.recorded === false || o.outcome === "not_saved") {
1058
+ setAnswer({ ...o, actions: [], hint: null, receivedAt: Date.now() });
1059
+ onResult({ variant: "warning", text: o.outcome === "not_saved" && typeof o.text === "string" && o.text ? o.text : NOT_SAVED_TEXT });
1060
+ void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
1061
+ return;
1062
+ }
1063
+ setAnswer({ ...o, receivedAt: Date.now() });
1064
+ onResult({ variant: o.tone, text: fillTime(o.text, o.at, localTime) });
1065
+ },
1066
+ onError: (e) => {
1067
+ const f = postFailure(e);
1068
+ onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
1069
+ },
1070
+ onSettled: () => setAsking(null)
1071
+ }
1072
+ );
1073
+ const expected = asking ? typedFor(asking) : null;
1074
+ const reasonOk = !asking?.confirm_reason || reopenReasonOf(reason).ok;
1075
+ const typedOk = (!asking || expected === null || typed.trim() === expected.value && checked) && reasonOk;
1076
+ const inputId = `vonpay-charge-confirm-${row.payment_collection_id}-${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}-${row.watch_id ?? ""}`;
1077
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1078
+ hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: fillTime(hint.text, hint.at, localTime) }),
1079
+ 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: [
1080
+ /* @__PURE__ */ jsxRuntime.jsx(
1081
+ ui.Button,
1082
+ {
1083
+ size: "small",
1084
+ variant: a.id === "check" ? "primary" : "secondary",
1085
+ disabled: !a.available || act.isPending,
1086
+ isLoading: act.isPending && (asking?.id === a.id || a.id === "check" && !asking),
1087
+ onClick: () => {
1088
+ if (!a.available) return;
1089
+ if (!a.confirm) {
1090
+ run(a);
1091
+ return;
1092
+ }
1093
+ setTyped("");
1094
+ setChecked(false);
1095
+ setReason("");
1096
+ setAsking(a);
1097
+ },
1098
+ children: a.label
1099
+ }
1100
+ ),
1101
+ !a.available && a.available_at && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
1102
+ "Opens ",
1103
+ localTime(a.available_at)
1104
+ ] })
1105
+ ] }, a.id)) }),
1106
+ /* @__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: [
1107
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
1108
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Title, { children: [
1109
+ asking?.label.replace(/…$/, ""),
1110
+ "?"
1111
+ ] }),
1112
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: asking?.confirm_text })
1113
+ ] }),
1114
+ expected !== null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
1115
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
1116
+ "Type the ",
1117
+ expected.what,
1118
+ " to confirm: ",
1119
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: expected.value })
1120
+ ] }),
1121
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) }),
1122
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-x-2", children: [
1123
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked, onChange: (e) => setChecked(e.target.checked) }),
1124
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small", children: "I checked this order in the Vonpay dashboard" })
1125
+ ] })
1126
+ ] }),
1127
+ asking?.confirm_reason && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
1128
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: `${inputId}-reason`, size: "small", children: [
1129
+ "Reason (",
1130
+ REOPEN_REASON.min,
1131
+ " to ",
1132
+ REOPEN_REASON.max,
1133
+ " characters; never a card number — any 12 or more digits in a row are refused, a phone number too)"
1134
+ ] }),
1135
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: `${inputId}-reason`, size: "small", autoComplete: "off", maxLength: REOPEN_REASON.max, value: reason, onChange: (e) => setReason(e.target.value) })
1136
+ ] }),
1137
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
1138
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: act.isPending, children: "Cancel" }),
1139
+ /* @__PURE__ */ jsxRuntime.jsx(
1140
+ ui.Prompt.Action,
1141
+ {
1142
+ disabled: act.isPending || !typedOk,
1143
+ onClick: (e) => {
1144
+ e.preventDefault();
1145
+ if (asking && typedOk) run(asking);
1146
+ },
1147
+ children: asking?.label.replace(/…$/, "")
1148
+ }
1149
+ )
1150
+ ] })
1151
+ ] }) })
1060
1152
  ] });
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);
1153
+ };
1154
+ const WalletHoldAction = ({ row, kind, onResult }) => {
1155
+ const [open, setOpen] = react.useState(false);
1156
+ const release = useReleaseHold();
1157
+ const complete = useCompleteHold();
1158
+ const pending = release.isPending || complete.isPending;
1159
+ 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" };
1160
+ const fail = (e2) => {
1161
+ const f = postFailure(e2);
1162
+ onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
1108
1163
  };
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 })
1164
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1165
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: kind === "release" ? "secondary" : "primary", onClick: () => setOpen(true), children: text.button }),
1166
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !pending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
1167
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
1168
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
1169
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
1170
+ ] }),
1171
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
1172
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: pending, children: "Cancel" }),
1173
+ /* @__PURE__ */ jsxRuntime.jsx(
1174
+ ui.Prompt.Action,
1175
+ {
1176
+ disabled: pending,
1177
+ onClick: (e) => {
1178
+ e.preventDefault();
1179
+ if (kind === "release") release.mutate({ row }, { onSuccess: (o) => onResult(releaseOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
1180
+ else complete.mutate({ row }, { onSuccess: (o) => onResult(completeOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
1181
+ },
1182
+ children: text.action
1183
+ }
1184
+ )
1185
+ ] })
1186
+ ] }) })
1187
+ ] });
1188
+ };
1189
+ const HoldDrawer = ({ row, listAt, onClose }) => {
1190
+ const copy = row ? reasonCopy(row.reason) : void 0;
1191
+ const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
1192
+ const showRefund = !!row && known && canRefund(row);
1193
+ const showMark = !!row && known && canMarkResolved(row);
1194
+ const showRelease = !!row && known && canRelease(row);
1195
+ const showComplete = !!row && known && canComplete(row);
1196
+ const showOpenOrder = !!row && known && canOpenOrder(row);
1197
+ const steps = row ? resolutionSteps(known ? row : { ...row, reason: "unrecognised" }) : [];
1198
+ const [result, setResult] = react.useState(null);
1199
+ const close = () => {
1200
+ setResult(null);
1201
+ onClose();
1202
+ };
1203
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
1204
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
1205
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
1206
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: row && isChargeRow(row) ? void 0 : copy?.what })
1207
+ ] }),
1208
+ row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
1209
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
1210
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Status" }),
1211
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
1212
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Held since" }),
1213
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown" }),
1214
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Where" }),
1215
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: SOURCE_LABEL[row.source] ?? row.source }),
1216
+ row.retry_until && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1217
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Retrying until" }),
1218
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: new Date(row.retry_until).toLocaleString() })
1219
+ ] }),
1220
+ fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1221
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Evidence" }),
1222
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "flex flex-col gap-y-1", children: [
1223
+ /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT }),
1224
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: FROM_BUYER_CHECKOUT_NOTE })
1225
+ ] })
1226
+ ] }),
1227
+ row.order_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1228
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
1229
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
1230
+ ] }),
1231
+ row.vp_payment_intent_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1232
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay payment" }),
1233
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_payment_intent_id, label: "Vonpay payment id" }) })
1234
+ ] }),
1235
+ row.vp_refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1236
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
1237
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": "held-row-refund", children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_refund_id, label: "Vonpay refund id" }) })
1238
+ ] }),
1239
+ row.subscription_id !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1240
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay subscription" }),
1241
+ /* @__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 }) })
1242
+ ] }),
1243
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
1244
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
1245
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
1246
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
1247
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Payment collection" }),
1248
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.payment_collection_id, label: "payment collection id" }) }),
1249
+ row.resolution && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1250
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: row.status === "resolved" ? "Resolved" : "Last action" }),
1251
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
1252
+ row.resolution.how ? `${RESOLVED_HOW_LABEL[row.resolution.how] ?? row.resolution.how} · ` : "",
1253
+ new Date(row.resolution.at).toLocaleString(),
1254
+ " · by ",
1255
+ actorLabel(row.resolution)
1256
+ ] }),
1257
+ row.resolution.refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1258
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
1259
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.resolution.refund_id, label: "Vonpay refund id" }) })
1260
+ ] }),
1261
+ row.resolution.prior && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1262
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Before that" }),
1263
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
1264
+ row.resolution.prior.how ? `${RESOLVED_HOW_LABEL[row.resolution.prior.how] ?? row.resolution.prior.how}` : statusLabel(row.resolution.prior.state),
1265
+ ` · ${new Date(row.resolution.prior.at).toLocaleString()} · by ${actorLabel(row.resolution.prior)}`
1266
+ ] })
1126
1267
  ] })
1127
1268
  ] })
1128
1269
  ] }),
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
- ] }),
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 })
1148
- ] })
1149
- ] }) });
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);
1161
- };
1162
- const typed = () => {
1163
- const h = headline.trim();
1164
- const p = pitch.trim();
1165
- return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
1166
- };
1167
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1168
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1169
- OFFERS_COPY.wording,
1170
- ": ",
1171
- offer.name
1172
- ] }) }),
1173
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
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 }),
1176
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
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 })
1180
- ] }),
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
- ] })
1270
+ result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
1271
+ closedMoneyNote(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: closedMoneyNote(row) }),
1272
+ (showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
1273
+ 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" }) }),
1274
+ showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
1275
+ showRelease && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "release", onResult: setResult }),
1276
+ showRefund && /* @__PURE__ */ jsxRuntime.jsx(RefundAction, { row, onResult: setResult }),
1277
+ showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
1278
+ showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
1192
1279
  ] }),
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 })
1195
- ] }),
1196
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1197
- offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
1198
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1199
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
1280
+ 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 ?? ""}`),
1281
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
1282
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
1283
+ steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1284
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: step.title }),
1285
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: step.body }),
1286
+ step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
1287
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
1288
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
1289
+ ] })
1290
+ ] }, step.title)),
1291
+ !isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
1292
+ ] })
1200
1293
  ] })
1201
1294
  ] }) });
1202
- }
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
- }
1227
- };
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("");
1295
+ };
1296
+ const FILTER_LABEL = { open: "Needs action", resolved: "Resolved", all: "All" };
1297
+ const HeldPaymentsPage = () => {
1298
+ const [pageIndex, setPageIndex] = react.useState(0);
1299
+ const [filter, setFilter] = react.useState("open");
1300
+ const [opened, setOpened] = react.useState(null);
1301
+ const { data, dataUpdatedAt, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
1302
+ const count = data?.count ?? 0;
1303
+ const remainder = count % HELD_PAGE_SIZE;
1304
+ const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1305
+ 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}` : ""}`;
1306
+ const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
1307
+ const onRowKey = (row) => (e) => {
1308
+ if (e.key === "Enter" || e.key === " ") {
1309
+ e.preventDefault();
1310
+ setOpened(row);
1233
1311
  }
1234
1312
  };
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 }) })
1313
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
1314
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1315
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
1316
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1317
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Held payments" }),
1318
+ /* @__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." })
1264
1319
  ] }),
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: [
1320
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[180px]", children: /* @__PURE__ */ jsxRuntime.jsxs(
1321
+ ui.Select,
1322
+ {
1323
+ size: "small",
1324
+ value: filter,
1325
+ onValueChange: (v) => {
1326
+ setFilter(v);
1327
+ setPageIndex(0);
1328
+ },
1329
+ children: [
1330
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1331
+ /* @__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)) })
1332
+ ]
1333
+ }
1334
+ ) })
1335
+ ] }),
1336
+ 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." }) }),
1337
+ 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)),
1338
+ 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) }) }),
1339
+ isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
1340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
1341
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
1342
+ ] }) : 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: [
1343
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1267
1344
  /* @__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, {})
1345
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reason" }),
1346
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
1347
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Held since" }),
1348
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Vonpay session" }),
1349
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Medusa payment session" }),
1350
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Where" })
1275
1351
  ] }) }),
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)) })
1352
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.holds.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
1353
+ ui.Table.Row,
1354
+ {
1355
+ className: "cursor-pointer",
1356
+ tabIndex: 0,
1357
+ role: "button",
1358
+ "aria-label": `${reasonCopy(row.reason).label}, ${statusLabel(row.status)}, open details`,
1359
+ onClick: () => setOpened(row),
1360
+ onKeyDown: onRowKey(row),
1361
+ children: [
1362
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1363
+ /* @__PURE__ */ jsxRuntime.jsx(ReasonBadge, { reason: row.reason }),
1364
+ 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 })
1365
+ ] }) }),
1366
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
1367
+ /* @__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) }) }) }),
1368
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
1369
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
1370
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1371
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
1372
+ fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
1373
+ ] }) })
1374
+ ]
1375
+ },
1376
+ keyOf(row)
1377
+ )) })
1289
1378
  ] }) }),
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,
1379
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
1380
+ ui.Table.Pagination,
1328
1381
  {
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 }) })
1357
- ] })
1358
- ] }) })
1359
- ] })
1382
+ count,
1383
+ pageSize: HELD_PAGE_SIZE,
1384
+ pageIndex,
1385
+ pageCount,
1386
+ canPreviousPage: pageIndex > 0,
1387
+ canNextPage: pageIndex + 1 < pageCount,
1388
+ previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1389
+ nextPage: () => setPageIndex((p) => p + 1)
1390
+ }
1391
+ )
1392
+ ] }),
1393
+ /* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, listAt: dataUpdatedAt, onClose: () => setOpened(null) })
1394
+ ] }),
1395
+ /* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
1360
1396
  ] });
1361
1397
  };
1362
1398
  const config$2 = adminSdk.defineRouteConfig({
1363
- label: "One-click offers",
1364
- icon: icons.Sparkles,
1399
+ label: "Held payments",
1400
+ icon: icons.ExclamationCircle,
1365
1401
  nested: "/orders"
1366
1402
  });
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." })
1417
- ] }),
1418
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
1419
- ] }),
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}`)) })
1431
- ] })
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";
1447
- }
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"
1403
+ const HEADLINE_MAX = 80;
1404
+ const PITCH_MAX = 300;
1405
+ const OFFERS_COPY = {
1406
+ title: "One-click offers",
1407
+ 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.",
1408
+ off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
1409
+ 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.",
1410
+ configEmpty: "No offers yet. Add the first one below.",
1411
+ adminEmpty: "No offers are shown to shoppers. Add one below.",
1412
+ importButton: "Import from medusa-config",
1413
+ add: "Add offer",
1414
+ name: "Name (only you see it)",
1415
+ variant: "Product variant id (variant_…)",
1416
+ shown: "Shown",
1417
+ up: "Move up",
1418
+ down: "Move down",
1419
+ remove: "Remove",
1420
+ removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
1421
+ missingVariant: "This product variant no longer exists — it is not offered.",
1422
+ loadFailed: "The offers could not be loaded. Reload the page to try again.",
1423
+ lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
1424
+ rules: "Who sees it",
1425
+ downsell: "If they say No thanks",
1426
+ downsellNone: "Nothing",
1427
+ 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.',
1428
+ downsellVariant: "Downsell product variant id (variant_…)",
1429
+ downsellRemove: "Remove downsell",
1430
+ downsellMissing: "This product variant no longer exists — it is not offered.",
1431
+ wording: "Wording",
1432
+ wordingDefault: "Standard",
1433
+ wordingPitchOnly: "Pitch only",
1434
+ wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
1435
+ 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.",
1436
+ headline: "Headline",
1437
+ headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
1438
+ pitch: "Pitch",
1439
+ pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
1440
+ wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
1441
+ wordingReset: "Use the standard wording",
1442
+ rulesEveryone: "Everyone",
1443
+ rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
1444
+ rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
1445
+ customers: "Customers",
1446
+ customersAll: "Everyone",
1447
+ customersGuests: "Guests only",
1448
+ customersSignedIn: "Signed-in customers only",
1449
+ products: "The shopper bought one of these products at checkout",
1450
+ collections: "The shopper bought a product from one of these collections at checkout",
1451
+ groups: "The signed-in customer is in one of these groups",
1452
+ paid: "The amount paid at checkout (tax and shipping included)",
1453
+ paidNote: "An order in a currency not listed here does not see this offer.",
1454
+ paidAtLeast: "At least",
1455
+ paidAtMost: "At most",
1456
+ search: "Search by name",
1457
+ addCurrency: "Add currency",
1458
+ removeCurrency: "Remove currency",
1459
+ 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.",
1460
+ replaceRules: "Replace the rules",
1461
+ save: "Save",
1462
+ cancel: "Cancel",
1463
+ saveFailed: "That change was not saved."
1455
1464
  };
1456
- const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
1457
- function isKnownStatus(status) {
1458
- return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
1465
+ function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
1466
+ const m = err?.message;
1467
+ if (typeof m !== "string") return fallback;
1468
+ const i = m.indexOf(": ");
1469
+ return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
1459
1470
  }
1460
- function statusLabel(status) {
1461
- return isKnownStatus(status) ? STATUS_LABEL[status] : UNKNOWN_STATUS_LABEL;
1471
+ function productLabel(o) {
1472
+ if (o.product_title === null) return o.variant_id;
1473
+ return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
1462
1474
  }
1463
- function statusColor(status) {
1464
- return isKnownStatus(status) && STATUS_COLOR[status] || "grey";
1475
+ function wordingSummary(copy) {
1476
+ if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
1477
+ if (!copy) return OFFERS_COPY.wordingDefault;
1478
+ return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
1465
1479
  }
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;
1480
+ function rulesSummary(rules) {
1481
+ if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
1482
+ if (!rules) return OFFERS_COPY.rulesEveryone;
1483
+ const parts = [];
1484
+ if (rules.customers === "guests") parts.push("Guests");
1485
+ if (rules.customers === "signed_in") parts.push("Signed-in");
1486
+ if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
1487
+ if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
1488
+ if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
1489
+ for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
1490
+ for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
1491
+ return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
1470
1492
  }
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."
1477
- };
1493
+ const REPORT_COPY = {
1494
+ tabOffers: "Offers",
1495
+ tabResults: "Results",
1496
+ 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.",
1497
+ from: "From",
1498
+ to: "To",
1499
+ live: "Live payments",
1500
+ test: "Test payments",
1501
+ offer: "Offer",
1502
+ product: "Product",
1503
+ shown: "Shown",
1504
+ added: "Added",
1505
+ noThanks: "No thanks",
1506
+ declined: "Didn't go through",
1507
+ takeRate: "Take rate",
1508
+ charged: "Charged",
1509
+ refunded: "Refunded",
1510
+ net: "Net",
1511
+ total: "All offers",
1512
+ removed: "(removed)",
1513
+ fromConfig: "From medusa-config",
1514
+ downsell: (product) => `If they say No thanks: ${product}`,
1515
+ 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.",
1516
+ takeRateNote: "Take rate is added ÷ shown.",
1517
+ declinedNote: "Didn't go through: the payment was declined or not finished, or the item could not be added.",
1518
+ 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.",
1519
+ 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.",
1520
+ 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.",
1521
+ 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.',
1522
+ empty: "No results for these dates yet.",
1523
+ loadFailed: "The results could not be loaded. Try again.",
1524
+ badRange: "The first date must be on or before the second, and at most 400 days apart.",
1525
+ none: "—"
1526
+ };
1527
+ function moneyText(currency, amount, minor, locale) {
1528
+ if (amount === null) return `${minor} ${currency}`;
1529
+ const digits = amount.replace(/^-/, "");
1530
+ const decimals = digits.includes(".") ? digits.length - digits.indexOf(".") - 1 : 0;
1531
+ try {
1532
+ return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(amount);
1533
+ } catch {
1534
+ return `${amount} ${currency}`;
1535
+ }
1478
1536
  }
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
- );
1537
+ function moneyColumn(money, which, locale) {
1538
+ if (money.length === 0) return REPORT_COPY.none;
1539
+ return money.map((m) => moneyText(m.currency, m[which], m[`${which}_minor`], locale)).join(" · ");
1540
+ }
1541
+ function takeRateText(c, locale) {
1542
+ if (c.shown <= 0 || c.added > c.shown) return null;
1543
+ return new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }).format(c.added / c.shown);
1544
+ }
1545
+ function reportLineLabel(l) {
1546
+ const product = productLabel(l);
1547
+ const offer = l.offer_id === null ? REPORT_COPY.fromConfig : `${l.name ?? l.offer_id}${l.removed ? ` ${REPORT_COPY.removed}` : ""}`;
1548
+ return { offer, product: l.step === "downsell" ? REPORT_COPY.downsell(product) : product };
1549
+ }
1550
+ function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
1551
+ const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
1552
+ const [problem, setProblem] = react.useState(null);
1553
+ const save = async (value) => {
1554
+ if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
1555
+ setProblem(null);
1556
+ const refused = await onSave(value);
1557
+ if (refused === null) onOpenChange(false);
1558
+ else setProblem(refused);
1503
1559
  };
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 })
1511
- ] }),
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) })
1560
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1561
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1562
+ OFFERS_COPY.downsell,
1563
+ ": ",
1564
+ offer.name
1565
+ ] }) }),
1566
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
1567
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
1568
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1569
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
1570
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
1576
1571
  ] }),
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();
1572
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
1573
+ ] }),
1574
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1575
+ offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
1576
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1577
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
1578
+ ] })
1579
+ ] }) });
1580
+ }
1581
+ const KEY = "vonpay-offers";
1582
+ function useOffers() {
1583
+ return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
1584
+ }
1585
+ function useOfferReport(q) {
1586
+ return reactQuery.useQuery({
1587
+ queryKey: [KEY, "report", q],
1588
+ enabled: q !== null,
1589
+ queryFn: () => sdk.client.fetch("/admin/vonpay/offers/report", { query: { from: q.from, to: q.to, ...q.mode ? { mode: q.mode } : {} } })
1590
+ });
1591
+ }
1592
+ function useWrite(send) {
1613
1593
  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 })
1594
+ return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
1595
+ }
1596
+ const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
1597
+ const useUpdateOffer = () => useWrite((v) => {
1598
+ const { id, ...body } = v;
1599
+ return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
1600
+ });
1601
+ const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
1602
+ const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
1603
+ const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
1604
+ const MAX_SPAN_DAYS = 400;
1605
+ const DAY_MS = 24 * 60 * 6e4;
1606
+ const pad = (n) => String(n).padStart(2, "0");
1607
+ const dayText = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1608
+ function localMidnight(day, addDays = 0) {
1609
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
1610
+ if (!m) return null;
1611
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]) + addDays);
1612
+ return Number.isNaN(d.getTime()) ? null : d;
1613
+ }
1614
+ function rangeOf(fromDay, toDay) {
1615
+ const from = localMidnight(fromDay);
1616
+ const to = localMidnight(toDay, 1);
1617
+ if (!from || !to || !(from.getTime() < to.getTime()) || to.getTime() - from.getTime() > MAX_SPAN_DAYS * DAY_MS) return null;
1618
+ return { from: from.toISOString(), to: to.toISOString() };
1619
+ }
1620
+ function CountCells({ c }) {
1621
+ const rate = takeRateText(c);
1622
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1623
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.shown }),
1624
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.added }),
1625
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.no_thanks }),
1626
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.declined }),
1627
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: rate ?? REPORT_COPY.none }),
1628
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "charged") }),
1629
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "refunded") }),
1630
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "net") })
1631
+ ] });
1632
+ }
1633
+ function OfferReport({ today = /* @__PURE__ */ new Date() }) {
1634
+ const [fromDay, setFromDay] = react.useState(() => dayText(new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29)));
1635
+ const [toDay, setToDay] = react.useState(() => dayText(today));
1636
+ const [mode, setMode] = react.useState(void 0);
1637
+ const range = rangeOf(fromDay, toDay);
1638
+ const { data, isLoading, isError } = useOfferReport(range ? { ...range, mode } : null);
1639
+ const shownMode = mode ?? data?.mode;
1640
+ const viewsMissing = (data?.lines ?? []).some((l) => l.added > l.shown);
1641
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4 px-6 py-4", "data-testid": "vonpay-offer-report", children: [
1642
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: REPORT_COPY.intro }),
1643
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [
1644
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1645
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-report-from", children: REPORT_COPY.from }),
1646
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-report-from", type: "date", value: fromDay, onChange: (e) => setFromDay(e.target.value) })
1697
1647
  ] }),
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
- ] })
1648
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1649
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-report-to", children: REPORT_COPY.to }),
1650
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-report-to", type: "date", value: toDay, onChange: (e) => setToDay(e.target.value) })
1708
1651
  ] }),
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
- ] }) })
1652
+ /* @__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)) })
1653
+ ] }),
1654
+ !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: [
1655
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1656
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1657
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: REPORT_COPY.offer }),
1658
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: REPORT_COPY.product }),
1659
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.shown }),
1660
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.added }),
1661
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.noThanks }),
1662
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.declined }),
1663
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.takeRate }),
1664
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.charged }),
1665
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.refunded }),
1666
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.net })
1667
+ ] }) }),
1668
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
1669
+ data.lines.map((l) => {
1670
+ const label = reportLineLabel(l);
1671
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-report-line", children: [
1672
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: l.step === "downsell" ? "" : label.offer }),
1673
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: label.product }),
1674
+ /* @__PURE__ */ jsxRuntime.jsx(CountCells, { c: l })
1675
+ ] }, `${l.offer_id ?? ""}|${l.variant_id}|${l.step}`);
1676
+ }),
1677
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-report-total", children: [
1678
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: REPORT_COPY.total }) }),
1679
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
1680
+ /* @__PURE__ */ jsxRuntime.jsx(CountCells, { c: data.totals })
1681
+ ] })
1682
+ ] })
1683
+ ] }) }),
1684
+ viewsMissing && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", "data-testid": "vonpay-report-views-missing", children: REPORT_COPY.viewsMissing }),
1685
+ /* @__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)) })
1686
+ ] })
1724
1687
  ] });
1688
+ }
1689
+ const SOURCES = {
1690
+ products: { path: "/admin/products", key: "products", label: "title" },
1691
+ collections: { path: "/admin/collections", key: "collections", label: "title" },
1692
+ groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
1725
1693
  };
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
- ] }) })
1694
+ async function fetchNamed(kind, query) {
1695
+ const s = SOURCES[kind];
1696
+ const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
1697
+ const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
1698
+ 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 }] : []);
1699
+ }
1700
+ function Picker({ kind, label, ids, onChange }) {
1701
+ const [q, setQ] = react.useState("");
1702
+ const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
1703
+ 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 });
1704
+ const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
1705
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
1706
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
1707
+ /* @__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: [
1708
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
1709
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
1710
+ ] }, id)) }),
1711
+ /* @__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) }),
1712
+ (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: [
1713
+ "+ ",
1714
+ n.label
1715
+ ] }, n.id))
1759
1716
  ] });
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();
1717
+ }
1718
+ function rowsOf(rules) {
1719
+ const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
1720
+ return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
1721
+ }
1722
+ function amountText(text) {
1723
+ const t = text.trim().replace(",", ".");
1724
+ if (t === "") return void 0;
1725
+ return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
1726
+ }
1727
+ function rulesFromForm(f) {
1728
+ const out = {};
1729
+ if (f.customers !== "all") out.customers = f.customers;
1730
+ if (f.products.length) out.products = f.products;
1731
+ if (f.collections.length) out.collections = f.collections;
1732
+ if (f.groups.length) out.groups = f.groups;
1733
+ for (const r of f.paid) {
1734
+ const c = r.currency.trim().toLowerCase();
1735
+ if (!c && !r.min.trim() && !r.max.trim()) continue;
1736
+ if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
1737
+ const min = amountText(r.min);
1738
+ const max = amountText(r.max);
1739
+ if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
1740
+ if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
1741
+ if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
1742
+ if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
1743
+ if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
1744
+ }
1745
+ if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
1746
+ return Object.keys(out).length ? out : null;
1747
+ }
1748
+ function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
1749
+ const unreadable = offer.rules === "unreadable";
1750
+ const start = offer.rules === "unreadable" ? null : offer.rules;
1751
+ const [customers, setCustomers] = react.useState(start?.customers ?? "all");
1752
+ const [products, setProducts] = react.useState(start?.products ?? []);
1753
+ const [collections, setCollections] = react.useState(start?.collections ?? []);
1754
+ const [groups, setGroups] = react.useState(start?.groups ?? []);
1755
+ const [paid, setPaid] = react.useState(rowsOf(start));
1756
+ const [problem, setProblem] = react.useState(null);
1757
+ const save = async () => {
1758
+ const r = rulesFromForm({ customers, products, collections, groups, paid });
1759
+ if (typeof r === "string") return setProblem(r);
1760
+ setProblem(null);
1761
+ const refused = await onSave(r);
1762
+ if (refused === null) onOpenChange(false);
1763
+ else setProblem(refused);
1774
1764
  };
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
- ] })
1765
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1766
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1767
+ OFFERS_COPY.rules,
1768
+ ": ",
1769
+ offer.name
1770
+ ] }) }),
1771
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
1772
+ unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
1773
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
1774
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1775
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
1776
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
1777
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1778
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
1779
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
1780
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
1781
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
1839
1782
  ] })
1840
1783
  ] })
1841
1784
  ] }),
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 })
1785
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
1786
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
1787
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
1788
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
1789
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
1790
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
1791
+ paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
1792
+ /* @__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)) }),
1793
+ /* @__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)) }),
1794
+ /* @__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)) }),
1795
+ /* @__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, {}) })
1796
+ ] }, i)),
1797
+ /* @__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 }) })
1851
1798
  ] }),
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
- ] })
1799
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
1800
+ ] }),
1801
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1802
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1803
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
1865
1804
  ] })
1866
1805
  ] }) });
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);
1806
+ }
1807
+ function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
1808
+ const start = offer.copy === "unreadable" ? null : offer.copy;
1809
+ const [headline, setHeadline] = react.useState(start?.headline ?? "");
1810
+ const [pitch, setPitch] = react.useState(start?.pitch ?? "");
1811
+ const [problem, setProblem] = react.useState(null);
1812
+ const save = async (copy) => {
1813
+ setProblem(null);
1814
+ const refused = await onSave(copy);
1815
+ if (refused === null) onOpenChange(false);
1816
+ else setProblem(refused);
1817
+ };
1818
+ const typed = () => {
1819
+ const h = headline.trim();
1820
+ const p = pitch.trim();
1821
+ return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
1822
+ };
1823
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1824
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1825
+ OFFERS_COPY.wording,
1826
+ ": ",
1827
+ offer.name
1828
+ ] }) }),
1829
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
1830
+ offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
1831
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
1832
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1833
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
1834
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
1835
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
1836
+ ] }),
1837
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1838
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
1839
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
1840
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1841
+ OFFERS_COPY.pitchHint,
1842
+ " (",
1843
+ pitch.length,
1844
+ "/",
1845
+ PITCH_MAX,
1846
+ ")"
1847
+ ] })
1848
+ ] }),
1849
+ offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
1850
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
1851
+ ] }),
1852
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1853
+ offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
1854
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1855
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
1856
+ ] })
1857
+ ] }) });
1858
+ }
1859
+ const VonpayOffersPage = () => {
1860
+ const { data, isLoading, isError, error: loadError } = useOffers();
1861
+ const create = useCreateOffer();
1862
+ const update = useUpdateOffer();
1863
+ const remove = useDeleteOffer();
1864
+ const reorder = useReorderOffers();
1865
+ const importConfig = useImportOffers();
1866
+ const prompt = ui.usePrompt();
1867
+ const [name, setName] = react.useState("");
1868
+ const [variantId, setVariantId] = react.useState("");
1869
+ const [error, setError] = react.useState(null);
1870
+ const [editing, setEditing] = react.useState(null);
1871
+ const [downsellFor, setDownsellFor] = react.useState(null);
1872
+ const [wordingFor, setWordingFor] = react.useState(null);
1873
+ const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
1874
+ const run = async (work) => {
1875
+ setError(null);
1876
+ try {
1877
+ await work();
1878
+ return true;
1879
+ } catch (err) {
1880
+ setError(errorSentence(err));
1881
+ return false;
1883
1882
  }
1884
1883
  };
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." })
1884
+ const onAdd = async (e) => {
1885
+ e.preventDefault();
1886
+ if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
1887
+ setName("");
1888
+ setVariantId("");
1889
+ }
1890
+ };
1891
+ const move = (offers, i, by) => {
1892
+ const ids = offers.map((o) => o.id);
1893
+ [ids[i], ids[i + by]] = [ids[i + by], ids[i]];
1894
+ void run(() => reorder.mutateAsync({ ids }));
1895
+ };
1896
+ const onRemove = async (o) => {
1897
+ if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
1898
+ };
1899
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1900
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1901
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
1902
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1903
+ OFFERS_COPY.intro,
1904
+ " ",
1905
+ OFFERS_COPY.lag
1906
+ ] })
1907
+ ] }),
1908
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs, { defaultValue: "offers", className: "divide-y", children: [
1909
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs.List, { children: [
1910
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "offers", children: REPORT_COPY.tabOffers }),
1911
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "results", children: REPORT_COPY.tabResults })
1912
+ ] }) }),
1913
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "results", children: /* @__PURE__ */ jsxRuntime.jsx(OfferReport, {}) }),
1914
+ /* @__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: [
1915
+ !data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
1916
+ 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: [
1917
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
1918
+ /* @__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)) }),
1919
+ /* @__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 }) })
1891
1920
  ] }),
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
- ) })
1907
- ] }),
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: [
1921
+ 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 }) }),
1922
+ 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: [
1916
1923
  /* @__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" })
1924
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
1925
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
1926
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
1927
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
1928
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
1929
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
1930
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
1923
1931
  ] }) }),
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
- )) })
1932
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
1933
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
1934
+ /* @__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) }),
1935
+ /* @__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) }) }) }),
1936
+ /* @__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 }) }) }),
1937
+ /* @__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) }) }) }),
1938
+ /* @__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 })) }) }),
1939
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
1940
+ /* @__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, {}) }),
1941
+ /* @__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, {}) }),
1942
+ /* @__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, {}) })
1943
+ ] }) })
1944
+ ] }, o.id)) })
1950
1945
  ] }) }),
1951
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
1952
- ui.Table.Pagination,
1946
+ downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
1947
+ OfferDownsellDrawer,
1953
1948
  {
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)
1962
- }
1963
- )
1964
- ] }),
1965
- /* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, listAt: dataUpdatedAt, onClose: () => setOpened(null) })
1966
- ] }),
1967
- /* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
1949
+ offer: downsellFor,
1950
+ open: downsellFor !== null,
1951
+ busy,
1952
+ onOpenChange: (o) => !o && setDownsellFor(null),
1953
+ onSave: async (downsell) => {
1954
+ try {
1955
+ await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
1956
+ return null;
1957
+ } catch (err) {
1958
+ return errorSentence(err);
1959
+ }
1960
+ }
1961
+ },
1962
+ downsellFor.id
1963
+ ),
1964
+ wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
1965
+ OfferWordingDrawer,
1966
+ {
1967
+ offer: wordingFor,
1968
+ open: wordingFor !== null,
1969
+ busy,
1970
+ onOpenChange: (o) => !o && setWordingFor(null),
1971
+ onSave: async (copy) => {
1972
+ try {
1973
+ await update.mutateAsync({ id: wordingFor.id, copy });
1974
+ return null;
1975
+ } catch (err) {
1976
+ return errorSentence(err);
1977
+ }
1978
+ }
1979
+ },
1980
+ wordingFor.id
1981
+ ),
1982
+ editing && /* @__PURE__ */ jsxRuntime.jsx(
1983
+ OfferRulesDrawer,
1984
+ {
1985
+ offer: editing,
1986
+ open: editing !== null,
1987
+ busy,
1988
+ onOpenChange: (o) => !o && setEditing(null),
1989
+ onSave: async (rules) => {
1990
+ try {
1991
+ await update.mutateAsync({ id: editing.id, rules });
1992
+ return null;
1993
+ } catch (err) {
1994
+ return errorSentence(err);
1995
+ }
1996
+ }
1997
+ },
1998
+ editing.id
1999
+ ),
2000
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
2001
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
2002
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
2003
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
2004
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
2005
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
2006
+ ] }),
2007
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
2008
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
2009
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
2010
+ ] })
2011
+ ] }),
2012
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
2013
+ ] })
2014
+ ] }) })
2015
+ ] })
1968
2016
  ] });
1969
2017
  };
1970
2018
  const config$1 = adminSdk.defineRouteConfig({
1971
- label: "Held payments",
1972
- icon: icons.ExclamationCircle,
2019
+ label: "One-click offers",
2020
+ icon: icons.Sparkles,
1973
2021
  nested: "/orders"
1974
2022
  });
1975
2023
  const EVENT_LABEL = {
@@ -2083,13 +2131,13 @@ const widgetModule = { widgets: [
2083
2131
  const routeModule = {
2084
2132
  routes: [
2085
2133
  {
2086
- Component: VonpayOffersPage,
2087
- path: "/vonpay-offers",
2134
+ Component: HeldPaymentsPage,
2135
+ path: "/vonpay-holds",
2088
2136
  handle: { label: config$2.label, translationNs: config$2.translationNs }
2089
2137
  },
2090
2138
  {
2091
- Component: HeldPaymentsPage,
2092
- path: "/vonpay-holds",
2139
+ Component: VonpayOffersPage,
2140
+ path: "/vonpay-offers",
2093
2141
  handle: { label: config$1.label, translationNs: config$1.translationNs }
2094
2142
  },
2095
2143
  {
@@ -2102,16 +2150,16 @@ const routeModule = {
2102
2150
  const menuItemModule = {
2103
2151
  menuItems: [
2104
2152
  {
2105
- label: config$1.label,
2106
- icon: config$1.icon,
2153
+ label: config$2.label,
2154
+ icon: config$2.icon,
2107
2155
  path: "/vonpay-holds",
2108
2156
  nested: "/orders",
2109
2157
  rank: void 0,
2110
2158
  translationNs: void 0
2111
2159
  },
2112
2160
  {
2113
- label: config$2.label,
2114
- icon: config$2.icon,
2161
+ label: config$1.label,
2162
+ icon: config$1.icon,
2115
2163
  path: "/vonpay-offers",
2116
2164
  nested: "/orders",
2117
2165
  rank: void 0,