vonpay-medusa 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +566 -89
- package/.medusa/server/src/admin/index.mjs +568 -91
- package/.medusa/server/src/api/admin/vonpay/offers/[id]/route.js +20 -0
- package/.medusa/server/src/api/admin/vonpay/offers/import/route.js +12 -0
- package/.medusa/server/src/api/admin/vonpay/offers/reorder/route.js +11 -0
- package/.medusa/server/src/api/admin/vonpay/offers/route.js +18 -0
- package/.medusa/server/src/api/middlewares.js +4 -0
- package/.medusa/server/src/lib/errors.js +16 -1
- package/.medusa/server/src/lib/money.js +8 -0
- package/.medusa/server/src/modules/vonpay-upsells/index.js +11 -0
- package/.medusa/server/src/modules/vonpay-upsells/key.js +4 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261023131258.js +20 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261023142238.js +15 -0
- package/.medusa/server/src/modules/vonpay-upsells/models/event.js +25 -0
- package/.medusa/server/src/modules/vonpay-upsells/models/offer.js +23 -0
- package/.medusa/server/src/modules/vonpay-upsells/service.js +8 -0
- package/.medusa/server/src/providers/vonpay/options.js +11 -7
- package/.medusa/server/src/providers/vonpay/service.js +29 -2
- package/.medusa/server/src/providers/vonpay/session-data.js +11 -2
- package/.medusa/server/src/providers/vonpay/setup-check.js +69 -0
- package/.medusa/server/src/providers/vonpay/ship-hold.js +12 -6
- package/.medusa/server/src/providers/vonpay/upsell-accept.js +151 -24
- package/.medusa/server/src/providers/vonpay/upsell-copy.js +48 -0
- package/.medusa/server/src/providers/vonpay/upsell-offers-admin.js +275 -0
- package/.medusa/server/src/providers/vonpay/upsell-offers.js +136 -0
- package/.medusa/server/src/providers/vonpay/upsell-routes.js +5 -0
- package/.medusa/server/src/providers/vonpay/upsell-rules.js +106 -0
- package/README.md +78 -7
- package/bin/vonpay-medusa.cjs +15 -4
- package/package.json +2 -1
- package/storefront/src/context.tsx +2 -1
- package/storefront/src/csp.ts +1 -1
- package/storefront/src/index.ts +1 -1
- package/storefront/src/offer-pass.ts +22 -0
- package/storefront/src/offer-prepare.ts +21 -0
- package/storefront/src/server.ts +29 -1
- package/storefront/src/upsell.ts +45 -1
- package/storefront/src/vonpay-card-payment.tsx +12 -6
- package/storefront/src/vonpay-upsell-offer.tsx +60 -38
- package/storefront/src/vonpay-upsell-return.tsx +30 -28
- package/storefront/src/vora-csp.json +85 -5
|
@@ -558,11 +558,11 @@ const HeldPaymentsBanner = () => {
|
|
|
558
558
|
] }) });
|
|
559
559
|
};
|
|
560
560
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
561
|
-
const KEY = "vonpay-ship-hold";
|
|
561
|
+
const KEY$1 = "vonpay-ship-hold";
|
|
562
562
|
const SHIP_HOLD_REFRESH_MS = 3e4;
|
|
563
563
|
function useShipHold(orderId) {
|
|
564
564
|
return reactQuery.useQuery({
|
|
565
|
-
queryKey: [KEY, orderId],
|
|
565
|
+
queryKey: [KEY$1, orderId],
|
|
566
566
|
queryFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/offer-hold`),
|
|
567
567
|
enabled: !!orderId,
|
|
568
568
|
retry: false,
|
|
@@ -577,7 +577,7 @@ function useShipOverride(orderId) {
|
|
|
577
577
|
return reactQuery.useMutation({
|
|
578
578
|
mutationFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/ship-override`, { method: "POST", body: {} }),
|
|
579
579
|
retry: false,
|
|
580
|
-
onSettled: () => qc.invalidateQueries({ queryKey: [KEY, orderId] })
|
|
580
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$1, orderId] })
|
|
581
581
|
});
|
|
582
582
|
}
|
|
583
583
|
function clockTime(iso) {
|
|
@@ -626,6 +626,548 @@ 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."
|
|
690
|
+
};
|
|
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;
|
|
705
|
+
}
|
|
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;
|
|
718
|
+
}
|
|
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 }),
|
|
737
|
+
/* @__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) })
|
|
740
|
+
] }),
|
|
741
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
|
|
742
|
+
] }),
|
|
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 })
|
|
747
|
+
] })
|
|
748
|
+
] }) });
|
|
749
|
+
}
|
|
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" }
|
|
754
|
+
};
|
|
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
|
+
});
|
|
629
1171
|
function useSetupCheck() {
|
|
630
1172
|
return reactQuery.useMutation({
|
|
631
1173
|
mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
|
|
@@ -1050,89 +1592,11 @@ const HeldPaymentsPage = () => {
|
|
|
1050
1592
|
/* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
|
|
1051
1593
|
] });
|
|
1052
1594
|
};
|
|
1053
|
-
const config
|
|
1595
|
+
const config = adminSdk.defineRouteConfig({
|
|
1054
1596
|
label: "Held payments",
|
|
1055
1597
|
icon: icons.ExclamationCircle,
|
|
1056
1598
|
nested: "/orders"
|
|
1057
1599
|
});
|
|
1058
|
-
const EVENT_LABEL = {
|
|
1059
|
-
"subscription.created": "Signed up",
|
|
1060
|
-
"subscription.renewed": "Renewal paid",
|
|
1061
|
-
"subscription.payment_failed": "Renewal declined",
|
|
1062
|
-
"subscription.cancelled": "Cancelled",
|
|
1063
|
-
"subscription.updated": "Changed"
|
|
1064
|
-
};
|
|
1065
|
-
const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
|
|
1066
|
-
const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
|
|
1067
|
-
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.`;
|
|
1068
|
-
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.";
|
|
1069
|
-
const SUBSCRIPTION_PAGE_SIZE = 20;
|
|
1070
|
-
function useSubscriptionEvents(page) {
|
|
1071
|
-
return reactQuery.useQuery({
|
|
1072
|
-
queryKey: ["vonpay-subscription-events", page.limit, page.offset],
|
|
1073
|
-
queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
|
|
1074
|
-
placeholderData: reactQuery.keepPreviousData
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
const pageCountFor = (count) => {
|
|
1078
|
-
const remainder = count % SUBSCRIPTION_PAGE_SIZE;
|
|
1079
|
-
return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
|
|
1080
|
-
};
|
|
1081
|
-
const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
|
|
1082
|
-
const VonpaySubscriptionsPage = () => {
|
|
1083
|
-
const [pageIndex, setPageIndex] = react.useState(0);
|
|
1084
|
-
const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
|
|
1085
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1086
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
1087
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
|
|
1088
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
|
|
1089
|
-
] }),
|
|
1090
|
-
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: [
|
|
1091
|
-
/* @__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 }) }),
|
|
1092
|
-
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: [
|
|
1093
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1094
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1095
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
|
|
1096
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
|
|
1097
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
|
|
1098
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
|
|
1099
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
|
|
1100
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
|
|
1101
|
-
] }) }),
|
|
1102
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1103
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
|
|
1104
|
-
eventLabel(e.type),
|
|
1105
|
-
e.mode === "test" ? " (test)" : ""
|
|
1106
|
-
] }),
|
|
1107
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
|
|
1108
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
|
|
1109
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
|
|
1110
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
|
|
1111
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
|
|
1112
|
-
] }, e.id)) })
|
|
1113
|
-
] }) }),
|
|
1114
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1115
|
-
ui.Table.Pagination,
|
|
1116
|
-
{
|
|
1117
|
-
count: data.count,
|
|
1118
|
-
pageSize: SUBSCRIPTION_PAGE_SIZE,
|
|
1119
|
-
pageIndex,
|
|
1120
|
-
pageCount: pageCountFor(data.count),
|
|
1121
|
-
canPreviousPage: pageIndex > 0,
|
|
1122
|
-
canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
|
|
1123
|
-
previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
|
|
1124
|
-
nextPage: () => setPageIndex((p) => p + 1)
|
|
1125
|
-
}
|
|
1126
|
-
)
|
|
1127
|
-
] })
|
|
1128
|
-
] })
|
|
1129
|
-
] });
|
|
1130
|
-
};
|
|
1131
|
-
const config = adminSdk.defineRouteConfig({
|
|
1132
|
-
label: "Vonpay subscriptions",
|
|
1133
|
-
icon: icons.ArrowPath,
|
|
1134
|
-
nested: "/orders"
|
|
1135
|
-
});
|
|
1136
1600
|
const i18nTranslations0 = {};
|
|
1137
1601
|
const widgetModule = { widgets: [
|
|
1138
1602
|
{
|
|
@@ -1154,13 +1618,18 @@ const widgetModule = { widgets: [
|
|
|
1154
1618
|
const routeModule = {
|
|
1155
1619
|
routes: [
|
|
1156
1620
|
{
|
|
1157
|
-
Component:
|
|
1158
|
-
path: "/vonpay-
|
|
1159
|
-
handle: { label: config$
|
|
1621
|
+
Component: VonpayOffersPage,
|
|
1622
|
+
path: "/vonpay-offers",
|
|
1623
|
+
handle: { label: config$2.label, translationNs: config$2.translationNs }
|
|
1160
1624
|
},
|
|
1161
1625
|
{
|
|
1162
1626
|
Component: VonpaySubscriptionsPage,
|
|
1163
1627
|
path: "/vonpay-subscriptions",
|
|
1628
|
+
handle: { label: config$1.label, translationNs: config$1.translationNs }
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
Component: HeldPaymentsPage,
|
|
1632
|
+
path: "/vonpay-holds",
|
|
1164
1633
|
handle: { label: config.label, translationNs: config.translationNs }
|
|
1165
1634
|
}
|
|
1166
1635
|
]
|
|
@@ -1168,16 +1637,24 @@ const routeModule = {
|
|
|
1168
1637
|
const menuItemModule = {
|
|
1169
1638
|
menuItems: [
|
|
1170
1639
|
{
|
|
1171
|
-
label: config
|
|
1172
|
-
icon: config
|
|
1640
|
+
label: config.label,
|
|
1641
|
+
icon: config.icon,
|
|
1173
1642
|
path: "/vonpay-holds",
|
|
1174
1643
|
nested: "/orders",
|
|
1175
1644
|
rank: void 0,
|
|
1176
1645
|
translationNs: void 0
|
|
1177
1646
|
},
|
|
1178
1647
|
{
|
|
1179
|
-
label: config.label,
|
|
1180
|
-
icon: config.icon,
|
|
1648
|
+
label: config$2.label,
|
|
1649
|
+
icon: config$2.icon,
|
|
1650
|
+
path: "/vonpay-offers",
|
|
1651
|
+
nested: "/orders",
|
|
1652
|
+
rank: void 0,
|
|
1653
|
+
translationNs: void 0
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
label: config$1.label,
|
|
1657
|
+
icon: config$1.icon,
|
|
1181
1658
|
path: "/vonpay-subscriptions",
|
|
1182
1659
|
nested: "/orders",
|
|
1183
1660
|
rank: void 0,
|