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