vonpay-medusa 1.1.0 → 1.2.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.
@@ -626,605 +626,141 @@ const UpdateBanner = () => {
626
626
  ] }) });
627
627
  };
628
628
  adminSdk.defineWidgetConfig({ zone: "order.list.before" });
629
- const HEADLINE_MAX = 80;
630
- const PITCH_MAX = 300;
631
- const OFFERS_COPY = {
632
- title: "One-click offers",
633
- 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.",
634
- off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
635
- 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.",
636
- configEmpty: "No offers yet. Add the first one below.",
637
- adminEmpty: "No offers are shown to shoppers. Add one below.",
638
- importButton: "Import from medusa-config",
639
- add: "Add offer",
640
- name: "Name (only you see it)",
641
- variant: "Product variant id (variant_…)",
642
- shown: "Shown",
643
- up: "Move up",
644
- down: "Move down",
645
- remove: "Remove",
646
- removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
647
- missingVariant: "This product variant no longer exists — it is not offered.",
648
- loadFailed: "The offers could not be loaded. Reload the page to try again.",
649
- lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
650
- rules: "Who sees it",
651
- downsell: "If they say No thanks",
652
- downsellNone: "Nothing",
653
- 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.',
654
- downsellVariant: "Downsell product variant id (variant_…)",
655
- downsellRemove: "Remove downsell",
656
- downsellMissing: "This product variant no longer exists — it is not offered.",
657
- wording: "Wording",
658
- wordingDefault: "Standard",
659
- wordingPitchOnly: "Pitch only",
660
- wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
661
- 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.",
662
- headline: "Headline",
663
- headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
664
- pitch: "Pitch",
665
- pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
666
- wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
667
- wordingReset: "Use the standard wording",
668
- rulesEveryone: "Everyone",
669
- rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
670
- rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
671
- customers: "Customers",
672
- customersAll: "Everyone",
673
- customersGuests: "Guests only",
674
- customersSignedIn: "Signed-in customers only",
675
- products: "The shopper bought one of these products at checkout",
676
- collections: "The shopper bought a product from one of these collections at checkout",
677
- groups: "The signed-in customer is in one of these groups",
678
- paid: "The amount paid at checkout (tax and shipping included)",
679
- paidNote: "An order in a currency not listed here does not see this offer.",
680
- paidAtLeast: "At least",
681
- paidAtMost: "At most",
682
- search: "Search by name",
683
- addCurrency: "Add currency",
684
- removeCurrency: "Remove currency",
685
- 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.",
686
- replaceRules: "Replace the rules",
687
- save: "Save",
688
- cancel: "Cancel",
689
- saveFailed: "That change was not saved."
629
+ const EVENT_LABEL = {
630
+ "subscription.created": "Signed up",
631
+ "subscription.renewed": "Renewal paid",
632
+ "subscription.payment_failed": "Renewal declined",
633
+ "subscription.cancelled": "Cancelled",
634
+ "subscription.updated": "Changed"
690
635
  };
691
- function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
692
- const m = err?.message;
693
- if (typeof m !== "string") return fallback;
694
- const i = m.indexOf(": ");
695
- return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
696
- }
697
- function productLabel(o) {
698
- if (o.product_title === null) return o.variant_id;
699
- return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
700
- }
701
- function wordingSummary(copy) {
702
- if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
703
- if (!copy) return OFFERS_COPY.wordingDefault;
704
- return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
636
+ const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
637
+ const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
638
+ const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
639
+ const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
640
+ const SUBSCRIPTION_PAGE_SIZE = 20;
641
+ function useSubscriptionEvents(page) {
642
+ return reactQuery.useQuery({
643
+ queryKey: ["vonpay-subscription-events", page.limit, page.offset],
644
+ queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
645
+ placeholderData: reactQuery.keepPreviousData
646
+ });
705
647
  }
706
- function rulesSummary(rules) {
707
- if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
708
- if (!rules) return OFFERS_COPY.rulesEveryone;
709
- const parts = [];
710
- if (rules.customers === "guests") parts.push("Guests");
711
- if (rules.customers === "signed_in") parts.push("Signed-in");
712
- if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
713
- if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
714
- if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
715
- for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
716
- for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
717
- return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
648
+ const pageCountFor = (count) => {
649
+ const remainder = count % SUBSCRIPTION_PAGE_SIZE;
650
+ return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
651
+ };
652
+ const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
653
+ const VonpaySubscriptionsPage = () => {
654
+ const [pageIndex, setPageIndex] = react.useState(0);
655
+ const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
656
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
657
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
658
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
659
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
660
+ ] }),
661
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
662
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
663
+ data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
664
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
665
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
666
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
667
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
668
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
669
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
670
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
671
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
672
+ ] }) }),
673
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
674
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
675
+ eventLabel(e.type),
676
+ e.mode === "test" ? " (test)" : ""
677
+ ] }),
678
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
679
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
680
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
681
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
682
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
683
+ ] }, e.id)) })
684
+ ] }) }),
685
+ /* @__PURE__ */ jsxRuntime.jsx(
686
+ ui.Table.Pagination,
687
+ {
688
+ count: data.count,
689
+ pageSize: SUBSCRIPTION_PAGE_SIZE,
690
+ pageIndex,
691
+ pageCount: pageCountFor(data.count),
692
+ canPreviousPage: pageIndex > 0,
693
+ canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
694
+ previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
695
+ nextPage: () => setPageIndex((p) => p + 1)
696
+ }
697
+ )
698
+ ] })
699
+ ] })
700
+ ] });
701
+ };
702
+ const config$2 = adminSdk.defineRouteConfig({
703
+ label: "Vonpay subscriptions",
704
+ icon: icons.ArrowPath,
705
+ nested: "/orders"
706
+ });
707
+ function useSetupCheck() {
708
+ return reactQuery.useMutation({
709
+ mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
710
+ retry: false
711
+ });
718
712
  }
719
- function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
720
- const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
721
- const [problem, setProblem] = react.useState(null);
722
- const save = async (value) => {
723
- if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
724
- setProblem(null);
725
- const refused = await onSave(value);
726
- if (refused === null) onOpenChange(false);
727
- else setProblem(refused);
728
- };
729
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
730
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
731
- OFFERS_COPY.downsell,
732
- ": ",
733
- offer.name
734
- ] }) }),
735
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
736
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
713
+ const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
714
+ const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
715
+ const SetupCheckPanel = () => {
716
+ const check = useSetupCheck();
717
+ const items = check.data?.items ?? [];
718
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
719
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
737
720
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
738
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
739
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
721
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Vonpay setup" }),
722
+ /* @__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." })
740
723
  ] }),
741
- problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
724
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
742
725
  ] }),
743
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
744
- offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
745
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
746
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
726
+ 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." }) }),
727
+ check.data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
728
+ /* @__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.` }),
729
+ /* @__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: [
730
+ /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
731
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
732
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
733
+ i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix })
734
+ ] })
735
+ ] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
747
736
  ] })
748
- ] }) });
737
+ ] });
738
+ };
739
+ const relative = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
740
+ const UNITS = [
741
+ ["day", 864e5],
742
+ ["hour", 36e5],
743
+ ["minute", 6e4]
744
+ ];
745
+ function since(iso) {
746
+ if (!iso) return "Unknown";
747
+ const diff = Date.parse(iso) - Date.now();
748
+ for (const [unit, ms] of UNITS) {
749
+ if (Math.abs(diff) >= ms) return relative.format((diff - diff % ms) / ms, unit);
750
+ }
751
+ return "just now";
749
752
  }
750
- const SOURCES = {
751
- products: { path: "/admin/products", key: "products", label: "title" },
752
- collections: { path: "/admin/collections", key: "collections", label: "title" },
753
- groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
753
+ const STATUS_COLOR = {
754
+ open: "orange",
755
+ nothing_left: "orange",
756
+ refund_pending: "blue",
757
+ refund_unknown: "red",
758
+ refund_sending: "red",
759
+ resolved: "green"
754
760
  };
755
- async function fetchNamed(kind, query) {
756
- const s = SOURCES[kind];
757
- const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
758
- const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
759
- 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 }] : []);
760
- }
761
- function Picker({ kind, label, ids, onChange }) {
762
- const [q, setQ] = react.useState("");
763
- const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
764
- 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 });
765
- const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
766
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
767
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
768
- /* @__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: [
769
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
770
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
771
- ] }, id)) }),
772
- /* @__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) }),
773
- (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: [
774
- "+ ",
775
- n.label
776
- ] }, n.id))
777
- ] });
778
- }
779
- function rowsOf(rules) {
780
- const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
781
- return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
782
- }
783
- function amountText(text) {
784
- const t = text.trim().replace(",", ".");
785
- if (t === "") return void 0;
786
- return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
787
- }
788
- function rulesFromForm(f) {
789
- const out = {};
790
- if (f.customers !== "all") out.customers = f.customers;
791
- if (f.products.length) out.products = f.products;
792
- if (f.collections.length) out.collections = f.collections;
793
- if (f.groups.length) out.groups = f.groups;
794
- for (const r of f.paid) {
795
- const c = r.currency.trim().toLowerCase();
796
- if (!c && !r.min.trim() && !r.max.trim()) continue;
797
- if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
798
- const min = amountText(r.min);
799
- const max = amountText(r.max);
800
- if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
801
- if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
802
- if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
803
- if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
804
- if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
805
- }
806
- if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
807
- return Object.keys(out).length ? out : null;
808
- }
809
- function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
810
- const unreadable = offer.rules === "unreadable";
811
- const start = offer.rules === "unreadable" ? null : offer.rules;
812
- const [customers, setCustomers] = react.useState(start?.customers ?? "all");
813
- const [products, setProducts] = react.useState(start?.products ?? []);
814
- const [collections, setCollections] = react.useState(start?.collections ?? []);
815
- const [groups, setGroups] = react.useState(start?.groups ?? []);
816
- const [paid, setPaid] = react.useState(rowsOf(start));
817
- const [problem, setProblem] = react.useState(null);
818
- const save = async () => {
819
- const r = rulesFromForm({ customers, products, collections, groups, paid });
820
- if (typeof r === "string") return setProblem(r);
821
- setProblem(null);
822
- const refused = await onSave(r);
823
- if (refused === null) onOpenChange(false);
824
- else setProblem(refused);
825
- };
826
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
827
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
828
- OFFERS_COPY.rules,
829
- ": ",
830
- offer.name
831
- ] }) }),
832
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
833
- unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
834
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
835
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
836
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
837
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
838
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
839
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
840
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
841
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
842
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
843
- ] })
844
- ] })
845
- ] }),
846
- /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
847
- /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
848
- /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
849
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
850
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
851
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
852
- paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
853
- /* @__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)) }),
854
- /* @__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)) }),
855
- /* @__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)) }),
856
- /* @__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, {}) })
857
- ] }, i)),
858
- /* @__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 }) })
859
- ] }),
860
- problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
861
- ] }),
862
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
863
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
864
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
865
- ] })
866
- ] }) });
867
- }
868
- function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
869
- const start = offer.copy === "unreadable" ? null : offer.copy;
870
- const [headline, setHeadline] = react.useState(start?.headline ?? "");
871
- const [pitch, setPitch] = react.useState(start?.pitch ?? "");
872
- const [problem, setProblem] = react.useState(null);
873
- const save = async (copy) => {
874
- setProblem(null);
875
- const refused = await onSave(copy);
876
- if (refused === null) onOpenChange(false);
877
- else setProblem(refused);
878
- };
879
- const typed = () => {
880
- const h = headline.trim();
881
- const p = pitch.trim();
882
- return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
883
- };
884
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
885
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
886
- OFFERS_COPY.wording,
887
- ": ",
888
- offer.name
889
- ] }) }),
890
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
891
- offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
892
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
893
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
894
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
895
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
896
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
897
- ] }),
898
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
899
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
900
- /* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
901
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
902
- OFFERS_COPY.pitchHint,
903
- " (",
904
- pitch.length,
905
- "/",
906
- PITCH_MAX,
907
- ")"
908
- ] })
909
- ] }),
910
- offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
911
- problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
912
- ] }),
913
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
914
- offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
915
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
916
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
917
- ] })
918
- ] }) });
919
- }
920
- const KEY = "vonpay-offers";
921
- function useOffers() {
922
- return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
923
- }
924
- function useWrite(send) {
925
- const qc = reactQuery.useQueryClient();
926
- return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
927
- }
928
- const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
929
- const useUpdateOffer = () => useWrite((v) => {
930
- const { id, ...body } = v;
931
- return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
932
- });
933
- const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
934
- const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
935
- const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
936
- const VonpayOffersPage = () => {
937
- const { data, isLoading, isError, error: loadError } = useOffers();
938
- const create = useCreateOffer();
939
- const update = useUpdateOffer();
940
- const remove = useDeleteOffer();
941
- const reorder = useReorderOffers();
942
- const importConfig = useImportOffers();
943
- const prompt = ui.usePrompt();
944
- const [name, setName] = react.useState("");
945
- const [variantId, setVariantId] = react.useState("");
946
- const [error, setError] = react.useState(null);
947
- const [editing, setEditing] = react.useState(null);
948
- const [downsellFor, setDownsellFor] = react.useState(null);
949
- const [wordingFor, setWordingFor] = react.useState(null);
950
- const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
951
- const run = async (work) => {
952
- setError(null);
953
- try {
954
- await work();
955
- return true;
956
- } catch (err) {
957
- setError(errorSentence(err));
958
- return false;
959
- }
960
- };
961
- const onAdd = async (e) => {
962
- e.preventDefault();
963
- if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
964
- setName("");
965
- setVariantId("");
966
- }
967
- };
968
- const move = (offers, i, by) => {
969
- const ids = offers.map((o) => o.id);
970
- [ids[i], ids[i + by]] = [ids[i + by], ids[i]];
971
- void run(() => reorder.mutateAsync({ ids }));
972
- };
973
- const onRemove = async (o) => {
974
- if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
975
- };
976
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
977
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
978
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
979
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
980
- OFFERS_COPY.intro,
981
- " ",
982
- OFFERS_COPY.lag
983
- ] })
984
- ] }),
985
- 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: [
986
- !data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
987
- 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: [
988
- /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
989
- /* @__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)) }),
990
- /* @__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 }) })
991
- ] }),
992
- 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 }) }),
993
- 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: [
994
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
995
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
996
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
997
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
998
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
999
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
1000
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
1001
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
1002
- ] }) }),
1003
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
1004
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
1005
- /* @__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) }),
1006
- /* @__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) }) }) }),
1007
- /* @__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 }) }) }),
1008
- /* @__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) }) }) }),
1009
- /* @__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 })) }) }),
1010
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
1011
- /* @__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, {}) }),
1012
- /* @__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, {}) }),
1013
- /* @__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, {}) })
1014
- ] }) })
1015
- ] }, o.id)) })
1016
- ] }) }),
1017
- downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
1018
- OfferDownsellDrawer,
1019
- {
1020
- offer: downsellFor,
1021
- open: downsellFor !== null,
1022
- busy,
1023
- onOpenChange: (o) => !o && setDownsellFor(null),
1024
- onSave: async (downsell) => {
1025
- try {
1026
- await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
1027
- return null;
1028
- } catch (err) {
1029
- return errorSentence(err);
1030
- }
1031
- }
1032
- },
1033
- downsellFor.id
1034
- ),
1035
- wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
1036
- OfferWordingDrawer,
1037
- {
1038
- offer: wordingFor,
1039
- open: wordingFor !== null,
1040
- busy,
1041
- onOpenChange: (o) => !o && setWordingFor(null),
1042
- onSave: async (copy) => {
1043
- try {
1044
- await update.mutateAsync({ id: wordingFor.id, copy });
1045
- return null;
1046
- } catch (err) {
1047
- return errorSentence(err);
1048
- }
1049
- }
1050
- },
1051
- wordingFor.id
1052
- ),
1053
- editing && /* @__PURE__ */ jsxRuntime.jsx(
1054
- OfferRulesDrawer,
1055
- {
1056
- offer: editing,
1057
- open: editing !== null,
1058
- busy,
1059
- onOpenChange: (o) => !o && setEditing(null),
1060
- onSave: async (rules) => {
1061
- try {
1062
- await update.mutateAsync({ id: editing.id, rules });
1063
- return null;
1064
- } catch (err) {
1065
- return errorSentence(err);
1066
- }
1067
- }
1068
- },
1069
- editing.id
1070
- ),
1071
- /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
1072
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
1073
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
1074
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1075
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
1076
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
1077
- ] }),
1078
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1079
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
1080
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
1081
- ] })
1082
- ] }),
1083
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
1084
- ] })
1085
- ] })
1086
- ] });
1087
- };
1088
- const config$2 = adminSdk.defineRouteConfig({
1089
- label: "One-click offers",
1090
- icon: icons.Sparkles,
1091
- nested: "/orders"
1092
- });
1093
- const EVENT_LABEL = {
1094
- "subscription.created": "Signed up",
1095
- "subscription.renewed": "Renewal paid",
1096
- "subscription.payment_failed": "Renewal declined",
1097
- "subscription.cancelled": "Cancelled",
1098
- "subscription.updated": "Changed"
1099
- };
1100
- const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
1101
- const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
1102
- const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
1103
- const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
1104
- const SUBSCRIPTION_PAGE_SIZE = 20;
1105
- function useSubscriptionEvents(page) {
1106
- return reactQuery.useQuery({
1107
- queryKey: ["vonpay-subscription-events", page.limit, page.offset],
1108
- queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
1109
- placeholderData: reactQuery.keepPreviousData
1110
- });
1111
- }
1112
- const pageCountFor = (count) => {
1113
- const remainder = count % SUBSCRIPTION_PAGE_SIZE;
1114
- return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1115
- };
1116
- const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
1117
- const VonpaySubscriptionsPage = () => {
1118
- const [pageIndex, setPageIndex] = react.useState(0);
1119
- const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
1120
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1121
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1122
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
1123
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
1124
- ] }),
1125
- isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1126
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
1127
- data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1128
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1129
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1130
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
1131
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
1132
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
1133
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
1134
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
1135
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
1136
- ] }) }),
1137
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1138
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
1139
- eventLabel(e.type),
1140
- e.mode === "test" ? " (test)" : ""
1141
- ] }),
1142
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
1143
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
1144
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
1145
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
1146
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
1147
- ] }, e.id)) })
1148
- ] }) }),
1149
- /* @__PURE__ */ jsxRuntime.jsx(
1150
- ui.Table.Pagination,
1151
- {
1152
- count: data.count,
1153
- pageSize: SUBSCRIPTION_PAGE_SIZE,
1154
- pageIndex,
1155
- pageCount: pageCountFor(data.count),
1156
- canPreviousPage: pageIndex > 0,
1157
- canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
1158
- previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1159
- nextPage: () => setPageIndex((p) => p + 1)
1160
- }
1161
- )
1162
- ] })
1163
- ] })
1164
- ] });
1165
- };
1166
- const config$1 = adminSdk.defineRouteConfig({
1167
- label: "Vonpay subscriptions",
1168
- icon: icons.ArrowPath,
1169
- nested: "/orders"
1170
- });
1171
- function useSetupCheck() {
1172
- return reactQuery.useMutation({
1173
- mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
1174
- retry: false
1175
- });
1176
- }
1177
- const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
1178
- const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
1179
- const SetupCheckPanel = () => {
1180
- const check = useSetupCheck();
1181
- const items = check.data?.items ?? [];
1182
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1183
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
1184
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1185
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Vonpay setup" }),
1186
- /* @__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." })
1187
- ] }),
1188
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
1189
- ] }),
1190
- 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." }) }),
1191
- check.data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
1192
- /* @__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.` }),
1193
- /* @__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: [
1194
- /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
1195
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
1196
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
1197
- i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix })
1198
- ] })
1199
- ] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
1200
- ] })
1201
- ] });
1202
- };
1203
- const relative = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
1204
- const UNITS = [
1205
- ["day", 864e5],
1206
- ["hour", 36e5],
1207
- ["minute", 6e4]
1208
- ];
1209
- function since(iso) {
1210
- if (!iso) return "Unknown";
1211
- const diff = Date.parse(iso) - Date.now();
1212
- for (const [unit, ms] of UNITS) {
1213
- if (Math.abs(diff) >= ms) return relative.format((diff - diff % ms) / ms, unit);
1214
- }
1215
- return "just now";
1216
- }
1217
- const STATUS_COLOR = {
1218
- open: "orange",
1219
- nothing_left: "orange",
1220
- refund_pending: "blue",
1221
- refund_unknown: "red",
1222
- refund_sending: "red",
1223
- resolved: "green"
1224
- };
1225
- const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
1226
- function isKnownStatus(status) {
1227
- return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
761
+ const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
762
+ function isKnownStatus(status) {
763
+ return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
1228
764
  }
1229
765
  function statusLabel(status) {
1230
766
  return isKnownStatus(status) ? STATUS_LABEL[status] : UNKNOWN_STATUS_LABEL;
@@ -1347,254 +883,718 @@ const MarkResolvedAction = ({ row, onResult }) => {
1347
883
  /* @__PURE__ */ jsxRuntime.jsx(
1348
884
  ui.Prompt.Action,
1349
885
  {
1350
- disabled: mark.isPending || !matches,
1351
- onClick: (e) => {
1352
- e.preventDefault();
1353
- if (!matches) return;
1354
- mark.mutate({ row, confirm_vp_session_id: typed }, {
1355
- onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
1356
- onError: (e2) => {
1357
- const f = postFailure(e2);
1358
- onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
1359
- },
1360
- onSettled: () => setOpen(false)
1361
- });
886
+ disabled: mark.isPending || !matches,
887
+ onClick: (e) => {
888
+ e.preventDefault();
889
+ if (!matches) return;
890
+ mark.mutate({ row, confirm_vp_session_id: typed }, {
891
+ onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
892
+ onError: (e2) => {
893
+ const f = postFailure(e2);
894
+ onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
895
+ },
896
+ onSettled: () => setOpen(false)
897
+ });
898
+ },
899
+ children: "Mark resolved"
900
+ }
901
+ )
902
+ ] })
903
+ ] }) })
904
+ ] });
905
+ };
906
+ const WalletHoldAction = ({ row, kind, onResult }) => {
907
+ const [open, setOpen] = react.useState(false);
908
+ const release = useReleaseHold();
909
+ const complete = useCompleteHold();
910
+ const pending = release.isPending || complete.isPending;
911
+ 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" };
912
+ const fail = (e2) => {
913
+ const f = postFailure(e2);
914
+ onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
915
+ };
916
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
917
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: kind === "release" ? "secondary" : "primary", onClick: () => setOpen(true), children: text.button }),
918
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !pending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
919
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
920
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
921
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
922
+ ] }),
923
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
924
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: pending, children: "Cancel" }),
925
+ /* @__PURE__ */ jsxRuntime.jsx(
926
+ ui.Prompt.Action,
927
+ {
928
+ disabled: pending,
929
+ onClick: (e) => {
930
+ e.preventDefault();
931
+ if (kind === "release") release.mutate({ row }, { onSuccess: (o) => onResult(releaseOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
932
+ else complete.mutate({ row }, { onSuccess: (o) => onResult(completeOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
933
+ },
934
+ children: text.action
935
+ }
936
+ )
937
+ ] })
938
+ ] }) })
939
+ ] });
940
+ };
941
+ const HoldDrawer = ({ row, onClose }) => {
942
+ const copy = row ? reasonCopy(row.reason) : void 0;
943
+ const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
944
+ const showRefund = !!row && known && canRefund(row);
945
+ const showMark = !!row && known && canMarkResolved(row);
946
+ const showRelease = !!row && known && canRelease(row);
947
+ const showComplete = !!row && known && canComplete(row);
948
+ const showOpenOrder = !!row && known && canOpenOrder(row);
949
+ const steps = row ? resolutionSteps(known ? row : { ...row, reason: "unrecognised" }) : [];
950
+ const [result, setResult] = react.useState(null);
951
+ const close = () => {
952
+ setResult(null);
953
+ onClose();
954
+ };
955
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
956
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
957
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
958
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: copy?.what })
959
+ ] }),
960
+ row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
961
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
962
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Status" }),
963
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
964
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Held since" }),
965
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown" }),
966
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Where" }),
967
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: SOURCE_LABEL[row.source] ?? row.source }),
968
+ row.retry_until && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
969
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Retrying until" }),
970
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: new Date(row.retry_until).toLocaleString() })
971
+ ] }),
972
+ fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
973
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Evidence" }),
974
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "flex flex-col gap-y-1", children: [
975
+ /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT }),
976
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: FROM_BUYER_CHECKOUT_NOTE })
977
+ ] })
978
+ ] }),
979
+ row.order_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
980
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
981
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
982
+ ] }),
983
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
984
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
985
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
986
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
987
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Payment collection" }),
988
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.payment_collection_id, label: "payment collection id" }) }),
989
+ row.resolution && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
990
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: row.status === "resolved" ? "Resolved" : "Last action" }),
991
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
992
+ row.resolution.how ? `${RESOLVED_HOW_LABEL[row.resolution.how] ?? row.resolution.how} · ` : "",
993
+ new Date(row.resolution.at).toLocaleString(),
994
+ " · by ",
995
+ actorLabel(row.resolution)
996
+ ] }),
997
+ row.resolution.refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
998
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
999
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.resolution.refund_id, label: "Vonpay refund id" }) })
1000
+ ] }),
1001
+ row.resolution.prior && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1002
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Before that" }),
1003
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
1004
+ row.resolution.prior.how ? `${RESOLVED_HOW_LABEL[row.resolution.prior.how] ?? row.resolution.prior.how}` : statusLabel(row.resolution.prior.state),
1005
+ ` · ${new Date(row.resolution.prior.at).toLocaleString()} · by ${actorLabel(row.resolution.prior)}`
1006
+ ] })
1007
+ ] })
1008
+ ] })
1009
+ ] }),
1010
+ result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
1011
+ (showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
1012
+ 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" }) }),
1013
+ showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
1014
+ showRelease && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "release", onResult: setResult }),
1015
+ showRefund && /* @__PURE__ */ jsxRuntime.jsx(RefundAction, { row, onResult: setResult }),
1016
+ showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
1017
+ showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
1018
+ ] }),
1019
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
1020
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
1021
+ steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1022
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: step.title }),
1023
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: step.body }),
1024
+ step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
1025
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
1026
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
1027
+ ] })
1028
+ ] }, step.title)),
1029
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
1030
+ ] })
1031
+ ] })
1032
+ ] }) });
1033
+ };
1034
+ const FILTER_LABEL = { open: "Needs action", resolved: "Resolved", all: "All" };
1035
+ const HeldPaymentsPage = () => {
1036
+ const [pageIndex, setPageIndex] = react.useState(0);
1037
+ const [filter, setFilter] = react.useState("open");
1038
+ const [opened, setOpened] = react.useState(null);
1039
+ const { data, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
1040
+ const count = data?.count ?? 0;
1041
+ const remainder = count % HELD_PAGE_SIZE;
1042
+ const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1043
+ const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}`;
1044
+ const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
1045
+ const onRowKey = (row) => (e) => {
1046
+ if (e.key === "Enter" || e.key === " ") {
1047
+ e.preventDefault();
1048
+ setOpened(row);
1049
+ }
1050
+ };
1051
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
1052
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1053
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
1054
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1055
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Held payments" }),
1056
+ /* @__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." })
1057
+ ] }),
1058
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[180px]", children: /* @__PURE__ */ jsxRuntime.jsxs(
1059
+ ui.Select,
1060
+ {
1061
+ size: "small",
1062
+ value: filter,
1063
+ onValueChange: (v) => {
1064
+ setFilter(v);
1065
+ setPageIndex(0);
1362
1066
  },
1363
- children: "Mark resolved"
1067
+ children: [
1068
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1069
+ /* @__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)) })
1070
+ ]
1071
+ }
1072
+ ) })
1073
+ ] }),
1074
+ 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." }) }),
1075
+ isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
1076
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
1077
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
1078
+ ] }) : 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: [
1079
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1080
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1081
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reason" }),
1082
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
1083
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Held since" }),
1084
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Vonpay session" }),
1085
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Medusa payment session" }),
1086
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Where" })
1087
+ ] }) }),
1088
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.holds.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
1089
+ ui.Table.Row,
1090
+ {
1091
+ className: "cursor-pointer",
1092
+ tabIndex: 0,
1093
+ role: "button",
1094
+ "aria-label": `${reasonCopy(row.reason).label}, ${statusLabel(row.status)}, open details`,
1095
+ onClick: () => setOpened(row),
1096
+ onKeyDown: onRowKey(row),
1097
+ children: [
1098
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ReasonBadge, { reason: row.reason }) }),
1099
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
1100
+ /* @__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) }) }) }),
1101
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
1102
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
1103
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1104
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
1105
+ fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
1106
+ ] }) })
1107
+ ]
1108
+ },
1109
+ keyOf(row)
1110
+ )) })
1111
+ ] }) }),
1112
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
1113
+ ui.Table.Pagination,
1114
+ {
1115
+ count,
1116
+ pageSize: HELD_PAGE_SIZE,
1117
+ pageIndex,
1118
+ pageCount,
1119
+ canPreviousPage: pageIndex > 0,
1120
+ canNextPage: pageIndex + 1 < pageCount,
1121
+ previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1122
+ nextPage: () => setPageIndex((p) => p + 1)
1364
1123
  }
1365
1124
  )
1366
- ] })
1367
- ] }) })
1125
+ ] }),
1126
+ /* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, onClose: () => setOpened(null) })
1127
+ ] }),
1128
+ /* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
1368
1129
  ] });
1369
1130
  };
1370
- const WalletHoldAction = ({ row, kind, onResult }) => {
1371
- const [open, setOpen] = react.useState(false);
1372
- const release = useReleaseHold();
1373
- const complete = useCompleteHold();
1374
- const pending = release.isPending || complete.isPending;
1375
- 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" };
1376
- const fail = (e2) => {
1377
- const f = postFailure(e2);
1378
- onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
1131
+ const config$1 = adminSdk.defineRouteConfig({
1132
+ label: "Held payments",
1133
+ icon: icons.ExclamationCircle,
1134
+ nested: "/orders"
1135
+ });
1136
+ const HEADLINE_MAX = 80;
1137
+ const PITCH_MAX = 300;
1138
+ const OFFERS_COPY = {
1139
+ title: "One-click offers",
1140
+ 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.",
1141
+ off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
1142
+ 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.",
1143
+ configEmpty: "No offers yet. Add the first one below.",
1144
+ adminEmpty: "No offers are shown to shoppers. Add one below.",
1145
+ importButton: "Import from medusa-config",
1146
+ add: "Add offer",
1147
+ name: "Name (only you see it)",
1148
+ variant: "Product variant id (variant_…)",
1149
+ shown: "Shown",
1150
+ up: "Move up",
1151
+ down: "Move down",
1152
+ remove: "Remove",
1153
+ removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
1154
+ missingVariant: "This product variant no longer exists — it is not offered.",
1155
+ loadFailed: "The offers could not be loaded. Reload the page to try again.",
1156
+ lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
1157
+ rules: "Who sees it",
1158
+ downsell: "If they say No thanks",
1159
+ downsellNone: "Nothing",
1160
+ 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.',
1161
+ downsellVariant: "Downsell product variant id (variant_…)",
1162
+ downsellRemove: "Remove downsell",
1163
+ downsellMissing: "This product variant no longer exists — it is not offered.",
1164
+ wording: "Wording",
1165
+ wordingDefault: "Standard",
1166
+ wordingPitchOnly: "Pitch only",
1167
+ wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
1168
+ 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.",
1169
+ headline: "Headline",
1170
+ headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
1171
+ pitch: "Pitch",
1172
+ pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
1173
+ wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
1174
+ wordingReset: "Use the standard wording",
1175
+ rulesEveryone: "Everyone",
1176
+ rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
1177
+ rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
1178
+ customers: "Customers",
1179
+ customersAll: "Everyone",
1180
+ customersGuests: "Guests only",
1181
+ customersSignedIn: "Signed-in customers only",
1182
+ products: "The shopper bought one of these products at checkout",
1183
+ collections: "The shopper bought a product from one of these collections at checkout",
1184
+ groups: "The signed-in customer is in one of these groups",
1185
+ paid: "The amount paid at checkout (tax and shipping included)",
1186
+ paidNote: "An order in a currency not listed here does not see this offer.",
1187
+ paidAtLeast: "At least",
1188
+ paidAtMost: "At most",
1189
+ search: "Search by name",
1190
+ addCurrency: "Add currency",
1191
+ removeCurrency: "Remove currency",
1192
+ 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.",
1193
+ replaceRules: "Replace the rules",
1194
+ save: "Save",
1195
+ cancel: "Cancel",
1196
+ saveFailed: "That change was not saved."
1197
+ };
1198
+ function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
1199
+ const m = err?.message;
1200
+ if (typeof m !== "string") return fallback;
1201
+ const i = m.indexOf(": ");
1202
+ return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
1203
+ }
1204
+ function productLabel(o) {
1205
+ if (o.product_title === null) return o.variant_id;
1206
+ return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
1207
+ }
1208
+ function wordingSummary(copy) {
1209
+ if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
1210
+ if (!copy) return OFFERS_COPY.wordingDefault;
1211
+ return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
1212
+ }
1213
+ function rulesSummary(rules) {
1214
+ if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
1215
+ if (!rules) return OFFERS_COPY.rulesEveryone;
1216
+ const parts = [];
1217
+ if (rules.customers === "guests") parts.push("Guests");
1218
+ if (rules.customers === "signed_in") parts.push("Signed-in");
1219
+ if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
1220
+ if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
1221
+ if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
1222
+ for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
1223
+ for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
1224
+ return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
1225
+ }
1226
+ function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
1227
+ const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
1228
+ const [problem, setProblem] = react.useState(null);
1229
+ const save = async (value) => {
1230
+ if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
1231
+ setProblem(null);
1232
+ const refused = await onSave(value);
1233
+ if (refused === null) onOpenChange(false);
1234
+ else setProblem(refused);
1379
1235
  };
1380
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1381
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: kind === "release" ? "secondary" : "primary", onClick: () => setOpen(true), children: text.button }),
1382
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !pending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
1383
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
1384
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
1385
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
1236
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1237
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1238
+ OFFERS_COPY.downsell,
1239
+ ": ",
1240
+ offer.name
1241
+ ] }) }),
1242
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
1243
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
1244
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1245
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
1246
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
1386
1247
  ] }),
1387
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
1388
- /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: pending, children: "Cancel" }),
1389
- /* @__PURE__ */ jsxRuntime.jsx(
1390
- ui.Prompt.Action,
1391
- {
1392
- disabled: pending,
1393
- onClick: (e) => {
1394
- e.preventDefault();
1395
- if (kind === "release") release.mutate({ row }, { onSuccess: (o) => onResult(releaseOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
1396
- else complete.mutate({ row }, { onSuccess: (o) => onResult(completeOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
1397
- },
1398
- children: text.action
1399
- }
1400
- )
1401
- ] })
1402
- ] }) })
1248
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1251
+ offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
1252
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1253
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
1254
+ ] })
1255
+ ] }) });
1256
+ }
1257
+ const SOURCES = {
1258
+ products: { path: "/admin/products", key: "products", label: "title" },
1259
+ collections: { path: "/admin/collections", key: "collections", label: "title" },
1260
+ groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
1261
+ };
1262
+ async function fetchNamed(kind, query) {
1263
+ const s = SOURCES[kind];
1264
+ const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
1265
+ const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
1266
+ 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 }] : []);
1267
+ }
1268
+ function Picker({ kind, label, ids, onChange }) {
1269
+ const [q, setQ] = react.useState("");
1270
+ const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
1271
+ 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 });
1272
+ const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
1273
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
1274
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
1275
+ /* @__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: [
1276
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
1277
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
1278
+ ] }, id)) }),
1279
+ /* @__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) }),
1280
+ (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: [
1281
+ "+ ",
1282
+ n.label
1283
+ ] }, n.id))
1403
1284
  ] });
1404
- };
1405
- const HoldDrawer = ({ row, onClose }) => {
1406
- const copy = row ? reasonCopy(row.reason) : void 0;
1407
- const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
1408
- const showRefund = !!row && known && canRefund(row);
1409
- const showMark = !!row && known && canMarkResolved(row);
1410
- const showRelease = !!row && known && canRelease(row);
1411
- const showComplete = !!row && known && canComplete(row);
1412
- const showOpenOrder = !!row && known && canOpenOrder(row);
1413
- const steps = row ? resolutionSteps(known ? row : { ...row, reason: "unrecognised" }) : [];
1414
- const [result, setResult] = react.useState(null);
1415
- const close = () => {
1416
- setResult(null);
1417
- onClose();
1285
+ }
1286
+ function rowsOf(rules) {
1287
+ const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
1288
+ return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
1289
+ }
1290
+ function amountText(text) {
1291
+ const t = text.trim().replace(",", ".");
1292
+ if (t === "") return void 0;
1293
+ return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
1294
+ }
1295
+ function rulesFromForm(f) {
1296
+ const out = {};
1297
+ if (f.customers !== "all") out.customers = f.customers;
1298
+ if (f.products.length) out.products = f.products;
1299
+ if (f.collections.length) out.collections = f.collections;
1300
+ if (f.groups.length) out.groups = f.groups;
1301
+ for (const r of f.paid) {
1302
+ const c = r.currency.trim().toLowerCase();
1303
+ if (!c && !r.min.trim() && !r.max.trim()) continue;
1304
+ if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
1305
+ const min = amountText(r.min);
1306
+ const max = amountText(r.max);
1307
+ if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
1308
+ if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
1309
+ if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
1310
+ if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
1311
+ if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
1312
+ }
1313
+ if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
1314
+ return Object.keys(out).length ? out : null;
1315
+ }
1316
+ function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
1317
+ const unreadable = offer.rules === "unreadable";
1318
+ const start = offer.rules === "unreadable" ? null : offer.rules;
1319
+ const [customers, setCustomers] = react.useState(start?.customers ?? "all");
1320
+ const [products, setProducts] = react.useState(start?.products ?? []);
1321
+ const [collections, setCollections] = react.useState(start?.collections ?? []);
1322
+ const [groups, setGroups] = react.useState(start?.groups ?? []);
1323
+ const [paid, setPaid] = react.useState(rowsOf(start));
1324
+ const [problem, setProblem] = react.useState(null);
1325
+ const save = async () => {
1326
+ const r = rulesFromForm({ customers, products, collections, groups, paid });
1327
+ if (typeof r === "string") return setProblem(r);
1328
+ setProblem(null);
1329
+ const refused = await onSave(r);
1330
+ if (refused === null) onOpenChange(false);
1331
+ else setProblem(refused);
1418
1332
  };
1419
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
1420
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
1421
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
1422
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: copy?.what })
1423
- ] }),
1424
- row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
1425
- /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
1426
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Status" }),
1427
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
1428
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Held since" }),
1429
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown" }),
1430
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Where" }),
1431
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: SOURCE_LABEL[row.source] ?? row.source }),
1432
- row.retry_until && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1433
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Retrying until" }),
1434
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: new Date(row.retry_until).toLocaleString() })
1435
- ] }),
1436
- fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1437
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Evidence" }),
1438
- /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "flex flex-col gap-y-1", children: [
1439
- /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT }),
1440
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: FROM_BUYER_CHECKOUT_NOTE })
1441
- ] })
1442
- ] }),
1443
- row.order_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1444
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
1445
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
1446
- ] }),
1447
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
1448
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
1449
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
1450
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
1451
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Payment collection" }),
1452
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.payment_collection_id, label: "payment collection id" }) }),
1453
- row.resolution && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1454
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: row.status === "resolved" ? "Resolved" : "Last action" }),
1455
- /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
1456
- row.resolution.how ? `${RESOLVED_HOW_LABEL[row.resolution.how] ?? row.resolution.how} · ` : "",
1457
- new Date(row.resolution.at).toLocaleString(),
1458
- " · by ",
1459
- actorLabel(row.resolution)
1460
- ] }),
1461
- row.resolution.refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1462
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
1463
- /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.resolution.refund_id, label: "Vonpay refund id" }) })
1464
- ] }),
1465
- row.resolution.prior && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1466
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Before that" }),
1467
- /* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
1468
- row.resolution.prior.how ? `${RESOLVED_HOW_LABEL[row.resolution.prior.how] ?? row.resolution.prior.how}` : statusLabel(row.resolution.prior.state),
1469
- ` · ${new Date(row.resolution.prior.at).toLocaleString()} · by ${actorLabel(row.resolution.prior)}`
1470
- ] })
1333
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1334
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1335
+ OFFERS_COPY.rules,
1336
+ ": ",
1337
+ offer.name
1338
+ ] }) }),
1339
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
1340
+ unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
1341
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
1342
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1343
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
1344
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
1345
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1346
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
1347
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
1348
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
1349
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
1471
1350
  ] })
1472
1351
  ] })
1473
1352
  ] }),
1474
- result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
1475
- (showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
1476
- 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" }) }),
1477
- showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
1478
- showRelease && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "release", onResult: setResult }),
1479
- showRefund && /* @__PURE__ */ jsxRuntime.jsx(RefundAction, { row, onResult: setResult }),
1480
- showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
1481
- showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
1353
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
1354
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
1355
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
1356
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
1357
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
1358
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
1359
+ paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
1360
+ /* @__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)) }),
1361
+ /* @__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)) }),
1362
+ /* @__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)) }),
1363
+ /* @__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, {}) })
1364
+ ] }, i)),
1365
+ /* @__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 }) })
1482
1366
  ] }),
1483
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
1484
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
1485
- steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1486
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: step.title }),
1487
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: step.body }),
1488
- step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
1489
- /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
1490
- /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
1491
- ] })
1492
- ] }, step.title)),
1493
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
1494
- ] })
1367
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
1368
+ ] }),
1369
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1370
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1371
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
1495
1372
  ] })
1496
1373
  ] }) });
1497
- };
1498
- const FILTER_LABEL = { open: "Needs action", resolved: "Resolved", all: "All" };
1499
- const HeldPaymentsPage = () => {
1500
- const [pageIndex, setPageIndex] = react.useState(0);
1501
- const [filter, setFilter] = react.useState("open");
1502
- const [opened, setOpened] = react.useState(null);
1503
- const { data, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
1504
- const count = data?.count ?? 0;
1505
- const remainder = count % HELD_PAGE_SIZE;
1506
- const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1507
- const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}`;
1508
- const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
1509
- const onRowKey = (row) => (e) => {
1510
- if (e.key === "Enter" || e.key === " ") {
1511
- e.preventDefault();
1512
- setOpened(row);
1374
+ }
1375
+ function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
1376
+ const start = offer.copy === "unreadable" ? null : offer.copy;
1377
+ const [headline, setHeadline] = react.useState(start?.headline ?? "");
1378
+ const [pitch, setPitch] = react.useState(start?.pitch ?? "");
1379
+ const [problem, setProblem] = react.useState(null);
1380
+ const save = async (copy) => {
1381
+ setProblem(null);
1382
+ const refused = await onSave(copy);
1383
+ if (refused === null) onOpenChange(false);
1384
+ else setProblem(refused);
1385
+ };
1386
+ const typed = () => {
1387
+ const h = headline.trim();
1388
+ const p = pitch.trim();
1389
+ return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
1390
+ };
1391
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
1392
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
1393
+ OFFERS_COPY.wording,
1394
+ ": ",
1395
+ offer.name
1396
+ ] }) }),
1397
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
1398
+ offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
1399
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
1400
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1401
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
1402
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
1403
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
1404
+ ] }),
1405
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1406
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
1407
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
1408
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1409
+ OFFERS_COPY.pitchHint,
1410
+ " (",
1411
+ pitch.length,
1412
+ "/",
1413
+ PITCH_MAX,
1414
+ ")"
1415
+ ] })
1416
+ ] }),
1417
+ offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
1418
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
1419
+ ] }),
1420
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
1421
+ offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
1422
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
1423
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
1424
+ ] })
1425
+ ] }) });
1426
+ }
1427
+ const KEY = "vonpay-offers";
1428
+ function useOffers() {
1429
+ return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
1430
+ }
1431
+ function useWrite(send) {
1432
+ const qc = reactQuery.useQueryClient();
1433
+ return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
1434
+ }
1435
+ const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
1436
+ const useUpdateOffer = () => useWrite((v) => {
1437
+ const { id, ...body } = v;
1438
+ return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
1439
+ });
1440
+ const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
1441
+ const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
1442
+ const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
1443
+ const VonpayOffersPage = () => {
1444
+ const { data, isLoading, isError, error: loadError } = useOffers();
1445
+ const create = useCreateOffer();
1446
+ const update = useUpdateOffer();
1447
+ const remove = useDeleteOffer();
1448
+ const reorder = useReorderOffers();
1449
+ const importConfig = useImportOffers();
1450
+ const prompt = ui.usePrompt();
1451
+ const [name, setName] = react.useState("");
1452
+ const [variantId, setVariantId] = react.useState("");
1453
+ const [error, setError] = react.useState(null);
1454
+ const [editing, setEditing] = react.useState(null);
1455
+ const [downsellFor, setDownsellFor] = react.useState(null);
1456
+ const [wordingFor, setWordingFor] = react.useState(null);
1457
+ const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
1458
+ const run = async (work) => {
1459
+ setError(null);
1460
+ try {
1461
+ await work();
1462
+ return true;
1463
+ } catch (err) {
1464
+ setError(errorSentence(err));
1465
+ return false;
1513
1466
  }
1514
1467
  };
1515
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
1516
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1517
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
1518
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1519
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Held payments" }),
1520
- /* @__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." })
1521
- ] }),
1522
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[180px]", children: /* @__PURE__ */ jsxRuntime.jsxs(
1523
- ui.Select,
1524
- {
1525
- size: "small",
1526
- value: filter,
1527
- onValueChange: (v) => {
1528
- setFilter(v);
1529
- setPageIndex(0);
1530
- },
1531
- children: [
1532
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1533
- /* @__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)) })
1534
- ]
1535
- }
1536
- ) })
1468
+ const onAdd = async (e) => {
1469
+ e.preventDefault();
1470
+ if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
1471
+ setName("");
1472
+ setVariantId("");
1473
+ }
1474
+ };
1475
+ const move = (offers, i, by) => {
1476
+ const ids = offers.map((o) => o.id);
1477
+ [ids[i], ids[i + by]] = [ids[i + by], ids[i]];
1478
+ void run(() => reorder.mutateAsync({ ids }));
1479
+ };
1480
+ const onRemove = async (o) => {
1481
+ if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
1482
+ };
1483
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1484
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1485
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
1486
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1487
+ OFFERS_COPY.intro,
1488
+ " ",
1489
+ OFFERS_COPY.lag
1490
+ ] })
1491
+ ] }),
1492
+ 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: [
1493
+ !data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
1494
+ 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: [
1495
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
1496
+ /* @__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)) }),
1497
+ /* @__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 }) })
1537
1498
  ] }),
1538
- 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." }) }),
1539
- isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
1540
- /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
1541
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
1542
- ] }) : 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: [
1543
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1544
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1545
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reason" }),
1546
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
1547
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Held since" }),
1548
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Vonpay session" }),
1549
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Medusa payment session" }),
1550
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Where" })
1551
- ] }) }),
1552
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.holds.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
1553
- ui.Table.Row,
1554
- {
1555
- className: "cursor-pointer",
1556
- tabIndex: 0,
1557
- role: "button",
1558
- "aria-label": `${reasonCopy(row.reason).label}, ${statusLabel(row.status)}, open details`,
1559
- onClick: () => setOpened(row),
1560
- onKeyDown: onRowKey(row),
1561
- children: [
1562
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ReasonBadge, { reason: row.reason }) }),
1563
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
1564
- /* @__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) }) }) }),
1565
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
1566
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
1567
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1568
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
1569
- fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
1570
- ] }) })
1571
- ]
1572
- },
1573
- keyOf(row)
1574
- )) })
1499
+ 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 }) }),
1500
+ 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: [
1501
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1502
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
1503
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
1504
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
1505
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
1506
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
1507
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
1508
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
1575
1509
  ] }) }),
1576
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
1577
- ui.Table.Pagination,
1578
- {
1579
- count,
1580
- pageSize: HELD_PAGE_SIZE,
1581
- pageIndex,
1582
- pageCount,
1583
- canPreviousPage: pageIndex > 0,
1584
- canNextPage: pageIndex + 1 < pageCount,
1585
- previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1586
- nextPage: () => setPageIndex((p) => p + 1)
1510
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
1511
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
1512
+ /* @__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) }),
1513
+ /* @__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) }) }) }),
1514
+ /* @__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 }) }) }),
1515
+ /* @__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) }) }) }),
1516
+ /* @__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 })) }) }),
1517
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
1518
+ /* @__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, {}) }),
1519
+ /* @__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, {}) }),
1520
+ /* @__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, {}) })
1521
+ ] }) })
1522
+ ] }, o.id)) })
1523
+ ] }) }),
1524
+ downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
1525
+ OfferDownsellDrawer,
1526
+ {
1527
+ offer: downsellFor,
1528
+ open: downsellFor !== null,
1529
+ busy,
1530
+ onOpenChange: (o) => !o && setDownsellFor(null),
1531
+ onSave: async (downsell) => {
1532
+ try {
1533
+ await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
1534
+ return null;
1535
+ } catch (err) {
1536
+ return errorSentence(err);
1537
+ }
1587
1538
  }
1588
- )
1589
- ] }),
1590
- /* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, onClose: () => setOpened(null) })
1591
- ] }),
1592
- /* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
1539
+ },
1540
+ downsellFor.id
1541
+ ),
1542
+ wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
1543
+ OfferWordingDrawer,
1544
+ {
1545
+ offer: wordingFor,
1546
+ open: wordingFor !== null,
1547
+ busy,
1548
+ onOpenChange: (o) => !o && setWordingFor(null),
1549
+ onSave: async (copy) => {
1550
+ try {
1551
+ await update.mutateAsync({ id: wordingFor.id, copy });
1552
+ return null;
1553
+ } catch (err) {
1554
+ return errorSentence(err);
1555
+ }
1556
+ }
1557
+ },
1558
+ wordingFor.id
1559
+ ),
1560
+ editing && /* @__PURE__ */ jsxRuntime.jsx(
1561
+ OfferRulesDrawer,
1562
+ {
1563
+ offer: editing,
1564
+ open: editing !== null,
1565
+ busy,
1566
+ onOpenChange: (o) => !o && setEditing(null),
1567
+ onSave: async (rules) => {
1568
+ try {
1569
+ await update.mutateAsync({ id: editing.id, rules });
1570
+ return null;
1571
+ } catch (err) {
1572
+ return errorSentence(err);
1573
+ }
1574
+ }
1575
+ },
1576
+ editing.id
1577
+ ),
1578
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
1579
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
1580
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
1581
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1582
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
1583
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
1584
+ ] }),
1585
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1586
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
1587
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
1588
+ ] })
1589
+ ] }),
1590
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
1591
+ ] })
1592
+ ] })
1593
1593
  ] });
1594
1594
  };
1595
1595
  const config = adminSdk.defineRouteConfig({
1596
- label: "Held payments",
1597
- icon: icons.ExclamationCircle,
1596
+ label: "One-click offers",
1597
+ icon: icons.Sparkles,
1598
1598
  nested: "/orders"
1599
1599
  });
1600
1600
  const i18nTranslations0 = {};
@@ -1617,19 +1617,19 @@ const widgetModule = { widgets: [
1617
1617
  ] };
1618
1618
  const routeModule = {
1619
1619
  routes: [
1620
- {
1621
- Component: VonpayOffersPage,
1622
- path: "/vonpay-offers",
1623
- handle: { label: config$2.label, translationNs: config$2.translationNs }
1624
- },
1625
1620
  {
1626
1621
  Component: VonpaySubscriptionsPage,
1627
1622
  path: "/vonpay-subscriptions",
1628
- handle: { label: config$1.label, translationNs: config$1.translationNs }
1623
+ handle: { label: config$2.label, translationNs: config$2.translationNs }
1629
1624
  },
1630
1625
  {
1631
1626
  Component: HeldPaymentsPage,
1632
1627
  path: "/vonpay-holds",
1628
+ handle: { label: config$1.label, translationNs: config$1.translationNs }
1629
+ },
1630
+ {
1631
+ Component: VonpayOffersPage,
1632
+ path: "/vonpay-offers",
1633
1633
  handle: { label: config.label, translationNs: config.translationNs }
1634
1634
  }
1635
1635
  ]
@@ -1637,24 +1637,24 @@ const routeModule = {
1637
1637
  const menuItemModule = {
1638
1638
  menuItems: [
1639
1639
  {
1640
- label: config.label,
1641
- icon: config.icon,
1640
+ label: config$1.label,
1641
+ icon: config$1.icon,
1642
1642
  path: "/vonpay-holds",
1643
1643
  nested: "/orders",
1644
1644
  rank: void 0,
1645
1645
  translationNs: void 0
1646
1646
  },
1647
1647
  {
1648
- label: config$2.label,
1649
- icon: config$2.icon,
1648
+ label: config.label,
1649
+ icon: config.icon,
1650
1650
  path: "/vonpay-offers",
1651
1651
  nested: "/orders",
1652
1652
  rank: void 0,
1653
1653
  translationNs: void 0
1654
1654
  },
1655
1655
  {
1656
- label: config$1.label,
1657
- icon: config$1.icon,
1656
+ label: config$2.label,
1657
+ icon: config$2.icon,
1658
1658
  path: "/vonpay-subscriptions",
1659
1659
  nested: "/orders",
1660
1660
  rank: void 0,