vonpay-medusa 1.2.1 → 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 +437 -129
- package/.medusa/server/src/admin/index.mjs +438 -130
- package/.medusa/server/src/api/admin/vonpay/holds/route.js +69 -3
- package/.medusa/server/src/api/admin/vonpay/offers/report/route.js +10 -0
- package/.medusa/server/src/api/admin/vonpay/subscriptions/route.js +15 -1
- package/.medusa/server/src/api/admin/vonpay/update/route.js +2 -1
- package/.medusa/server/src/api/admin/vonpay/variants/[id]/subscription/route.js +17 -0
- package/.medusa/server/src/api/middlewares.js +2 -0
- package/.medusa/server/src/api/store/vonpay/upsells/[order]/accept/route.js +11 -1
- package/.medusa/server/src/api/store/vonpay/upsells/[order]/shown/route.js +21 -0
- package/.medusa/server/src/lib/card-brand.js +4 -0
- package/.medusa/server/src/lib/errors.js +29 -1
- package/.medusa/server/src/lib/held-charge-copy.js +107 -2
- package/.medusa/server/src/lib/money.js +45 -0
- 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/Migration20261107202021.js +26 -0
- package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261109162717.js +15 -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/refund-watch.js +44 -0
- package/.medusa/server/src/modules/vonpay-jobs/models/renewal-period.js +48 -0
- package/.medusa/server/src/modules/vonpay-jobs/models/signup-watch.js +28 -0
- package/.medusa/server/src/modules/vonpay-jobs/service.js +202 -1
- package/.medusa/server/src/modules/vonpay-jobs/types.js +2 -0
- package/.medusa/server/src/modules/vonpay-subscriptions/migrations/Migration20261102180000.js +13 -0
- package/.medusa/server/src/modules/vonpay-subscriptions/models/subscription-event.js +4 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261102113326.js +20 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261102170444.js +14 -0
- package/.medusa/server/src/modules/vonpay-upsells/models/event.js +8 -2
- package/.medusa/server/src/modules/vonpay-upsells/service.js +138 -0
- package/.medusa/server/src/providers/vonpay/admin-holds.js +14 -6
- package/.medusa/server/src/providers/vonpay/charge-hold.js +87 -2
- package/.medusa/server/src/providers/vonpay/descriptor-rules.js +6 -1
- package/.medusa/server/src/providers/vonpay/follow-up.js +45 -0
- package/.medusa/server/src/providers/vonpay/held-list.js +223 -3
- package/.medusa/server/src/providers/vonpay/hold-actions.js +1 -0
- package/.medusa/server/src/providers/vonpay/job-beat.js +11 -2
- package/.medusa/server/src/providers/vonpay/options.js +8 -0
- package/.medusa/server/src/providers/vonpay/refund-verdict.js +15 -0
- package/.medusa/server/src/providers/vonpay/refund-watch.js +562 -0
- package/.medusa/server/src/providers/vonpay/renewal-check.js +14 -0
- 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 +387 -106
- package/.medusa/server/src/providers/vonpay/scan.js +13 -0
- package/.medusa/server/src/providers/vonpay/service.js +910 -51
- package/.medusa/server/src/providers/vonpay/session-data.js +245 -3
- package/.medusa/server/src/providers/vonpay/setup-check.js +260 -6
- package/.medusa/server/src/providers/vonpay/signup-api.js +176 -0
- package/.medusa/server/src/providers/vonpay/signup-watch.js +303 -0
- package/.medusa/server/src/providers/vonpay/signup.js +171 -0
- package/.medusa/server/src/providers/vonpay/subscription-cancel.js +168 -0
- package/.medusa/server/src/providers/vonpay/subscription-events.js +93 -4
- package/.medusa/server/src/providers/vonpay/subscription-products-admin.js +97 -0
- package/.medusa/server/src/providers/vonpay/upsell-accept.js +470 -40
- package/.medusa/server/src/providers/vonpay/upsell-cleanup.js +88 -5
- package/.medusa/server/src/providers/vonpay/upsell-report.js +489 -0
- package/.medusa/server/src/providers/vonpay/upsell-routes.js +9 -1
- package/.medusa/server/src/providers/vonpay/wallet-domains.js +13 -44
- package/.medusa/server/src/subscribers/vonpay-offer-refunds.js +9 -0
- package/README.md +441 -14
- package/bin/vonpay-medusa.cjs +26 -16
- package/package.json +4 -2
- package/storefront/src/copy.ts +15 -0
- package/storefront/src/index.ts +1 -1
- package/storefront/src/outcome.ts +37 -0
- package/storefront/src/reload-once.ts +13 -0
- package/storefront/src/server.ts +37 -6
- package/storefront/src/upsell.ts +43 -4
- package/storefront/src/vonpay-card-payment.tsx +38 -3
- package/storefront/src/vonpay-payment-view.tsx +43 -1
- package/storefront/src/vonpay-upsell-offer.tsx +95 -11
- package/storefront/src/vonpay-upsell-return.tsx +2 -4
|
@@ -13,6 +13,9 @@ const TIME_SLOT = "{time}";
|
|
|
13
13
|
function fillTime(text, at, format) {
|
|
14
14
|
return at && text.includes(TIME_SLOT) ? text.split(TIME_SLOT).join(format(at)) : text;
|
|
15
15
|
}
|
|
16
|
+
const SUBSCRIPTION_NOT_NAMED_TEXT = {
|
|
17
|
+
unknown: "Not known yet. In the Vonpay dashboard, find the subscription by this order's id."
|
|
18
|
+
};
|
|
16
19
|
const NOT_SAVED_TEXT = "That couldn't be saved. The list is read again: check the row as it stands now before you try again.";
|
|
17
20
|
const CHARGE_REASON_LABEL = {
|
|
18
21
|
renewal_outcome_unknown: "Renewal charge — result unknown",
|
|
@@ -31,8 +34,23 @@ const CHARGE_REASON_LABEL = {
|
|
|
31
34
|
upsell_refund_unclear: "Late offer charge — refund unclear",
|
|
32
35
|
upsell_refund_pending: "Late offer charge — refund in progress",
|
|
33
36
|
upsell_refunded_late: "Late offer charge refunded",
|
|
37
|
+
upsell_refund_requested: "Late offer charge — refund accepted, not finished",
|
|
34
38
|
upsell_hold_unsettled: "Offer payment held — not finished yet",
|
|
35
|
-
upsell_take_unconfirmed: "Offer payment — collection not confirmed"
|
|
39
|
+
upsell_take_unconfirmed: "Offer payment — collection not confirmed",
|
|
40
|
+
upsell_subscription_active: "Offer refund — cancel its subscription",
|
|
41
|
+
upsell_subscription_ended: "Offer refund — subscription ended",
|
|
42
|
+
refund_watch_canceled: "Refunded in Medusa — Vonpay did not complete it",
|
|
43
|
+
refund_watch_failed: "Refunded in Medusa — Vonpay's refund failed",
|
|
44
|
+
refund_watch_unclear: "Refunded in Medusa — Vonpay's refund failed, outcome unclear",
|
|
45
|
+
refund_watch_overdue: "Refunded in Medusa — Vonpay's refund not finished",
|
|
46
|
+
refund_watch_resolved: "Refunded in Medusa — Vonpay's refund settled by a person",
|
|
47
|
+
signup_watch_missing: "Subscription paid for — not started",
|
|
48
|
+
signup_watch_resolved: "Subscription paid for — settled by a person",
|
|
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"
|
|
36
54
|
};
|
|
37
55
|
const CHARGE_RESOLVED_HOW_LABEL = {
|
|
38
56
|
closed_blocking: "Closed by a person — no more renewal charges for this order",
|
|
@@ -75,6 +93,10 @@ const REASON_COPY = {
|
|
|
75
93
|
label: "Couldn't confirm amount",
|
|
76
94
|
what: "The cart total couldn't be read to confirm the charge, so no order was created."
|
|
77
95
|
},
|
|
96
|
+
signup_mismatch: {
|
|
97
|
+
label: "Subscription doesn't match the cart",
|
|
98
|
+
what: "The buyer paid, but the subscription this payment started doesn't match the items in the cart, so no order was created. A refund here doesn't cancel the subscription: cancel it in Vonpay too."
|
|
99
|
+
},
|
|
78
100
|
binding_mismatch: {
|
|
79
101
|
label: "Payment doesn't match checkout",
|
|
80
102
|
what: "The payment belongs to a different checkout than the one completing."
|
|
@@ -158,7 +180,10 @@ const SOURCE_LABEL = {
|
|
|
158
180
|
cancel: "Recorded when the order was cancelled",
|
|
159
181
|
renewal: "Recorded by a subscription renewal — check in Vonpay",
|
|
160
182
|
order: "Found on an order — its wallet payment wasn't collected",
|
|
161
|
-
upsell: "Recorded by an offer accepted after the order — check in Vonpay"
|
|
183
|
+
upsell: "Recorded by an offer accepted after the order — check in Vonpay",
|
|
184
|
+
refund: "Recorded when Vonpay did not complete a refund Medusa shows as done",
|
|
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"
|
|
162
187
|
};
|
|
163
188
|
const STATUS_LABEL = {
|
|
164
189
|
open: "Needs action",
|
|
@@ -212,9 +237,11 @@ const REFUNDABLE_REASONS = [
|
|
|
212
237
|
"paid_orphan_session",
|
|
213
238
|
"paid_after_auto_close",
|
|
214
239
|
"paid_no_order",
|
|
240
|
+
"signup_mismatch",
|
|
215
241
|
"cancel_refund_missing",
|
|
216
242
|
"cancel_refund_unconfirmed"
|
|
217
243
|
];
|
|
244
|
+
const SUBSCRIPTION_REFUND_NOTE = "If this payment started a Vonpay subscription, refunding it does not cancel the subscription: cancel it in your Vonpay dashboard.";
|
|
218
245
|
function isConfirmedRefund(row) {
|
|
219
246
|
return row.status === "resolved" && (row.resolution?.how === "refunded" || row.resolution?.how === "refunded_earlier");
|
|
220
247
|
}
|
|
@@ -222,7 +249,7 @@ function canRefund(row) {
|
|
|
222
249
|
return row.vp_session_id !== null && !isConfirmedRefund(row) && !isAutoClosed(row) && !isUnrecognisedRecord(row) && REFUNDABLE_REASONS.includes(row.reason);
|
|
223
250
|
}
|
|
224
251
|
function isChargeRow(row) {
|
|
225
|
-
return row.source === "renewal" || row.source === "upsell";
|
|
252
|
+
return row.source === "renewal" || row.source === "upsell" || row.source === "refund" || row.source === "signup" || row.source === "period";
|
|
226
253
|
}
|
|
227
254
|
function canRelease(row) {
|
|
228
255
|
return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && WALLET_REASONS.includes(row.reason);
|
|
@@ -235,6 +262,9 @@ function canMarkResolved(row) {
|
|
|
235
262
|
return row.vp_session_id !== null && row.status !== "resolved" && !isUnrecognisedRecord(row) && !WALLET_REASONS.includes(row.reason) && !TAKE_REASONS.includes(row.reason);
|
|
236
263
|
}
|
|
237
264
|
function locatorFor(row) {
|
|
265
|
+
if (row.source === "refund") return { source: "refund", watch_id: row.watch_id ?? "" };
|
|
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 ?? "" };
|
|
238
268
|
if (isChargeRow(row)) {
|
|
239
269
|
return {
|
|
240
270
|
source: row.source,
|
|
@@ -559,6 +589,7 @@ function useChargeAction() {
|
|
|
559
589
|
...locatorFor(v.row),
|
|
560
590
|
action: v.action,
|
|
561
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 } : {},
|
|
562
593
|
...v.action === "close_unreadable" && v.row.record_sha256 ? { confirm_record_sha256: v.row.record_sha256 } : {}
|
|
563
594
|
}
|
|
564
595
|
}),
|
|
@@ -596,11 +627,11 @@ const HeldPaymentsBanner = () => {
|
|
|
596
627
|
] }) });
|
|
597
628
|
};
|
|
598
629
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
599
|
-
const KEY$
|
|
630
|
+
const KEY$2 = "vonpay-ship-hold";
|
|
600
631
|
const SHIP_HOLD_REFRESH_MS = 3e4;
|
|
601
632
|
function useShipHold(orderId) {
|
|
602
633
|
return reactQuery.useQuery({
|
|
603
|
-
queryKey: [KEY$
|
|
634
|
+
queryKey: [KEY$2, orderId],
|
|
604
635
|
queryFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/offer-hold`),
|
|
605
636
|
enabled: !!orderId,
|
|
606
637
|
retry: false,
|
|
@@ -615,7 +646,7 @@ function useShipOverride(orderId) {
|
|
|
615
646
|
return reactQuery.useMutation({
|
|
616
647
|
mutationFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/ship-override`, { method: "POST", body: {} }),
|
|
617
648
|
retry: false,
|
|
618
|
-
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$
|
|
649
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$2, orderId] })
|
|
619
650
|
});
|
|
620
651
|
}
|
|
621
652
|
function clockTime(iso) {
|
|
@@ -644,8 +675,18 @@ function useUpdateNotice() {
|
|
|
644
675
|
retry: false
|
|
645
676
|
});
|
|
646
677
|
}
|
|
678
|
+
const CHANGELOG_URL = "https://docs.vonpay.com/connected-platforms/medusa-changelog";
|
|
647
679
|
const UpdateBanner = () => {
|
|
648
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
|
+
}
|
|
649
690
|
if (!data || data.status !== "update") return null;
|
|
650
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: [
|
|
651
692
|
data.security ? "Security fix: " : "",
|
|
@@ -664,6 +705,76 @@ const UpdateBanner = () => {
|
|
|
664
705
|
] }) });
|
|
665
706
|
};
|
|
666
707
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
708
|
+
const KEY$1 = "vonpay-variant-subscription";
|
|
709
|
+
function useVariantSubscription(variantId) {
|
|
710
|
+
return reactQuery.useQuery({
|
|
711
|
+
queryKey: [KEY$1, variantId],
|
|
712
|
+
queryFn: () => sdk.client.fetch(`/admin/vonpay/variants/${encodeURIComponent(variantId)}/subscription`),
|
|
713
|
+
enabled: !!variantId,
|
|
714
|
+
retry: false
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function useSetVariantSubscription(variantId) {
|
|
718
|
+
const qc = reactQuery.useQueryClient();
|
|
719
|
+
return reactQuery.useMutation({
|
|
720
|
+
mutationFn: (on) => sdk.client.fetch(`/admin/vonpay/variants/${encodeURIComponent(variantId)}/subscription`, { method: "POST", body: { on } }),
|
|
721
|
+
retry: false,
|
|
722
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$1, variantId] })
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
const VARIANT_SUBSCRIPTION_COPY = {
|
|
726
|
+
heading: "Vonpay subscription",
|
|
727
|
+
on: "Sold as a Vonpay subscription: buying it at checkout (or on an offer) starts a subscription that renews in Vonpay.",
|
|
728
|
+
off: "Sold as a one-time purchase.",
|
|
729
|
+
noProduct: "No Vonpay subscription product has this variant's id as its Store product number. Create one in your Vonpay dashboard (Subscriptions → Products) and type this id there:",
|
|
730
|
+
markedNoProduct: "Vonpay has no active subscription product for this variant, so its checkout is refused. Create or re-activate the product in your Vonpay dashboard with this id as its Store product number, or stop selling it as a subscription:",
|
|
731
|
+
turnOn: "Sell as a Vonpay subscription",
|
|
732
|
+
turnOff: "Stop selling as a subscription",
|
|
733
|
+
failed: "Couldn't change that. Reload and try again.",
|
|
734
|
+
unreadable: "Couldn't read Vonpay's subscription products. Reload to try again."
|
|
735
|
+
};
|
|
736
|
+
const VonpayVariantSubscription = ({ data: variant }) => {
|
|
737
|
+
const { data, isError } = useVariantSubscription(variant?.id);
|
|
738
|
+
const set = useSetVariantSubscription(variant?.id);
|
|
739
|
+
if (isError)
|
|
740
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "px-6 py-4", "data-testid": "vonpay-variant-subscription", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: VARIANT_SUBSCRIPTION_COPY.unreadable }) });
|
|
741
|
+
if (!data || !data.enabled) return null;
|
|
742
|
+
const product = data.products.find((p) => p.status === "active") ?? data.products[0];
|
|
743
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-3 px-6 py-4", "data-testid": "vonpay-variant-subscription", children: [
|
|
744
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
745
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: VARIANT_SUBSCRIPTION_COPY.heading }),
|
|
746
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: data.marked ? "green" : "grey", size: "2xsmall", children: data.marked ? "On" : "Off" })
|
|
747
|
+
] }),
|
|
748
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: data.marked ? VARIANT_SUBSCRIPTION_COPY.on : VARIANT_SUBSCRIPTION_COPY.off }),
|
|
749
|
+
product?.disclosure ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-variant-subscription-terms", children: product.disclosure }) : null,
|
|
750
|
+
!data.can_mark && !data.marked ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-variant-subscription-setup", children: [
|
|
751
|
+
VARIANT_SUBSCRIPTION_COPY.noProduct,
|
|
752
|
+
" ",
|
|
753
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: variant?.id })
|
|
754
|
+
] }) : null,
|
|
755
|
+
!data.can_mark && data.marked ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-error", "data-testid": "vonpay-variant-subscription-broken", children: [
|
|
756
|
+
VARIANT_SUBSCRIPTION_COPY.markedNoProduct,
|
|
757
|
+
" ",
|
|
758
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: variant?.id })
|
|
759
|
+
] }) : null,
|
|
760
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: data.marked ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: set.isPending, onClick: () => set.mutate(false), "data-testid": "vonpay-variant-subscription-off", children: VARIANT_SUBSCRIPTION_COPY.turnOff }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", disabled: !data.can_mark, isLoading: set.isPending, onClick: () => set.mutate(true), "data-testid": "vonpay-variant-subscription-on", children: VARIANT_SUBSCRIPTION_COPY.turnOn }) }),
|
|
761
|
+
set.isError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: VARIANT_SUBSCRIPTION_COPY.failed }) : null
|
|
762
|
+
] });
|
|
763
|
+
};
|
|
764
|
+
adminSdk.defineWidgetConfig({ zone: "product_variant.details.after" });
|
|
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;
|
|
667
778
|
function useSetupCheck() {
|
|
668
779
|
return reactQuery.useMutation({
|
|
669
780
|
mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
|
|
@@ -806,7 +917,8 @@ const RefundAction = ({ row, onResult }) => {
|
|
|
806
917
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !refund.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
807
918
|
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
808
919
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Refund this payment in full?" }),
|
|
809
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: description })
|
|
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 })
|
|
810
922
|
] }),
|
|
811
923
|
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
812
924
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: refund.isPending, children: "Cancel" }),
|
|
@@ -908,8 +1020,10 @@ const ChargeActions = ({ row, listAt, onResult }) => {
|
|
|
908
1020
|
const [asking, setAsking] = react.useState(null);
|
|
909
1021
|
const [typed, setTyped] = react.useState("");
|
|
910
1022
|
const [checked, setChecked] = react.useState(false);
|
|
1023
|
+
const [reason, setReason] = react.useState("");
|
|
911
1024
|
const act = useChargeAction();
|
|
912
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;
|
|
913
1027
|
const rowSays = JSON.stringify([row.held_at, row.actions ?? null, row.hint ?? null]);
|
|
914
1028
|
react.useEffect(() => setAnswer(null), [rowSays]);
|
|
915
1029
|
const actions = answer?.actions ?? row.actions ?? [];
|
|
@@ -937,12 +1051,12 @@ const ChargeActions = ({ row, listAt, onResult }) => {
|
|
|
937
1051
|
return () => clearTimeout(timer);
|
|
938
1052
|
}, [pastDue, listAt, qc]);
|
|
939
1053
|
const run = (a) => act.mutate(
|
|
940
|
-
{ row, action: a.id, ...a
|
|
1054
|
+
{ row, action: a.id, ...typedFor(a) !== null ? { typed_order_id: typed, checked_in_vonpay: checked } : {}, ...a.confirm_reason ? { reason } : {} },
|
|
941
1055
|
{
|
|
942
1056
|
onSuccess: (o) => {
|
|
943
1057
|
if (o.recorded === false || o.outcome === "not_saved") {
|
|
944
1058
|
setAnswer({ ...o, actions: [], hint: null, receivedAt: Date.now() });
|
|
945
|
-
onResult({ variant: "warning", text: NOT_SAVED_TEXT });
|
|
1059
|
+
onResult({ variant: "warning", text: o.outcome === "not_saved" && typeof o.text === "string" && o.text ? o.text : NOT_SAVED_TEXT });
|
|
946
1060
|
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
947
1061
|
return;
|
|
948
1062
|
}
|
|
@@ -956,8 +1070,10 @@ const ChargeActions = ({ row, listAt, onResult }) => {
|
|
|
956
1070
|
onSettled: () => setAsking(null)
|
|
957
1071
|
}
|
|
958
1072
|
);
|
|
959
|
-
const
|
|
960
|
-
const
|
|
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 ?? ""}`;
|
|
961
1077
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
962
1078
|
hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: fillTime(hint.text, hint.at, localTime) }),
|
|
963
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: [
|
|
@@ -976,6 +1092,7 @@ const ChargeActions = ({ row, listAt, onResult }) => {
|
|
|
976
1092
|
}
|
|
977
1093
|
setTyped("");
|
|
978
1094
|
setChecked(false);
|
|
1095
|
+
setReason("");
|
|
979
1096
|
setAsking(a);
|
|
980
1097
|
},
|
|
981
1098
|
children: a.label
|
|
@@ -994,10 +1111,12 @@ const ChargeActions = ({ row, listAt, onResult }) => {
|
|
|
994
1111
|
] }),
|
|
995
1112
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: asking?.confirm_text })
|
|
996
1113
|
] }),
|
|
997
|
-
|
|
1114
|
+
expected !== null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
|
|
998
1115
|
/* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
|
|
999
|
-
"Type the
|
|
1000
|
-
|
|
1116
|
+
"Type the ",
|
|
1117
|
+
expected.what,
|
|
1118
|
+
" to confirm: ",
|
|
1119
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: expected.value })
|
|
1001
1120
|
] }),
|
|
1002
1121
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) }),
|
|
1003
1122
|
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-x-2", children: [
|
|
@@ -1005,6 +1124,16 @@ const ChargeActions = ({ row, listAt, onResult }) => {
|
|
|
1005
1124
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small", children: "I checked this order in the Vonpay dashboard" })
|
|
1006
1125
|
] })
|
|
1007
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
|
+
] }),
|
|
1008
1137
|
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1009
1138
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: act.isPending, children: "Cancel" }),
|
|
1010
1139
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1103,6 +1232,14 @@ const HoldDrawer = ({ row, listAt, onClose }) => {
|
|
|
1103
1232
|
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay payment" }),
|
|
1104
1233
|
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_payment_intent_id, label: "Vonpay payment id" }) })
|
|
1105
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
|
+
] }),
|
|
1106
1243
|
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
|
|
1107
1244
|
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1108
1245
|
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
|
|
@@ -1140,7 +1277,7 @@ const HoldDrawer = ({ row, listAt, onClose }) => {
|
|
|
1140
1277
|
showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
|
|
1141
1278
|
showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
|
|
1142
1279
|
] }),
|
|
1143
|
-
row && isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ChargeActions, { row, listAt, onResult: setResult }, `${row.payment_collection_id}:${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}`),
|
|
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 ?? ""}`),
|
|
1144
1281
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
|
|
1145
1282
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
|
|
1146
1283
|
steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
@@ -1165,7 +1302,7 @@ const HeldPaymentsPage = () => {
|
|
|
1165
1302
|
const count = data?.count ?? 0;
|
|
1166
1303
|
const remainder = count % HELD_PAGE_SIZE;
|
|
1167
1304
|
const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
|
|
1168
|
-
const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}:${row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}`;
|
|
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}` : ""}`;
|
|
1169
1306
|
const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
|
|
1170
1307
|
const onRowKey = (row) => (e) => {
|
|
1171
1308
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -1222,7 +1359,10 @@ const HeldPaymentsPage = () => {
|
|
|
1222
1359
|
onClick: () => setOpened(row),
|
|
1223
1360
|
onKeyDown: onRowKey(row),
|
|
1224
1361
|
children: [
|
|
1225
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.
|
|
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
|
+
] }) }),
|
|
1226
1366
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
|
|
1227
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) }) }) }),
|
|
1228
1368
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
@@ -1350,6 +1490,63 @@ function rulesSummary(rules) {
|
|
|
1350
1490
|
for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
|
|
1351
1491
|
return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
|
|
1352
1492
|
}
|
|
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
|
+
}
|
|
1536
|
+
}
|
|
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
|
+
}
|
|
1353
1550
|
function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1354
1551
|
const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
|
|
1355
1552
|
const [problem, setProblem] = react.useState(null);
|
|
@@ -1381,6 +1578,114 @@ function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
|
1381
1578
|
] })
|
|
1382
1579
|
] }) });
|
|
1383
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) {
|
|
1593
|
+
const qc = reactQuery.useQueryClient();
|
|
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) })
|
|
1647
|
+
] }),
|
|
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) })
|
|
1651
|
+
] }),
|
|
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
|
+
] })
|
|
1687
|
+
] });
|
|
1688
|
+
}
|
|
1384
1689
|
const SOURCES = {
|
|
1385
1690
|
products: { path: "/admin/products", key: "products", label: "title" },
|
|
1386
1691
|
collections: { path: "/admin/collections", key: "collections", label: "title" },
|
|
@@ -1551,22 +1856,6 @@ function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
|
1551
1856
|
] })
|
|
1552
1857
|
] }) });
|
|
1553
1858
|
}
|
|
1554
|
-
const KEY = "vonpay-offers";
|
|
1555
|
-
function useOffers() {
|
|
1556
|
-
return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
|
|
1557
|
-
}
|
|
1558
|
-
function useWrite(send) {
|
|
1559
|
-
const qc = reactQuery.useQueryClient();
|
|
1560
|
-
return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
|
|
1561
|
-
}
|
|
1562
|
-
const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
|
|
1563
|
-
const useUpdateOffer = () => useWrite((v) => {
|
|
1564
|
-
const { id, ...body } = v;
|
|
1565
|
-
return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
|
|
1566
|
-
});
|
|
1567
|
-
const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
|
|
1568
|
-
const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
|
|
1569
|
-
const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
|
|
1570
1859
|
const VonpayOffersPage = () => {
|
|
1571
1860
|
const { data, isLoading, isError, error: loadError } = useOffers();
|
|
1572
1861
|
const create = useCreateOffer();
|
|
@@ -1616,106 +1905,113 @@ const VonpayOffersPage = () => {
|
|
|
1616
1905
|
OFFERS_COPY.lag
|
|
1617
1906
|
] })
|
|
1618
1907
|
] }),
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
1623
|
-
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-disc pl-6", children: data.config.map((c) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: productLabel(c) }) }, c.variant_id)) }),
|
|
1624
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: busy, onClick: () => void run(() => importConfig.mutateAsync(void 0)), children: OFFERS_COPY.importButton }) })
|
|
1625
|
-
] }),
|
|
1626
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-error", children: error }) }),
|
|
1627
|
-
data.offers.length === 0 ? (data.source === "admin" || data.config.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: data.source === "admin" ? OFFERS_COPY.adminEmpty : OFFERS_COPY.configEmpty }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1628
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1629
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
|
|
1630
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
|
|
1631
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
|
|
1632
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
|
|
1633
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
|
|
1634
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
|
|
1635
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
|
|
1636
|
-
] }) }),
|
|
1637
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
|
|
1638
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
|
|
1639
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.product_title === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: OFFERS_COPY.missingVariant }) : productLabel(o) }),
|
|
1640
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.rules}: ${o.name}`, disabled: busy, onClick: () => setEditing(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: o.rules === "unreadable" ? "text-ui-fg-error" : void 0, children: rulesSummary(o.rules) }) }) }),
|
|
1641
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.downsell}: ${o.name}`, disabled: busy, onClick: () => setDownsellFor(o), children: o.downsell_variant_id === null ? OFFERS_COPY.downsellNone : o.downsell_product_title === null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-error", children: OFFERS_COPY.downsellMissing }) : productLabel({ product_title: o.downsell_product_title, variant_title: o.downsell_variant_title, variant_id: o.downsell_variant_id }) }) }),
|
|
1642
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.wording}: ${o.name}`, disabled: busy, onClick: () => setWordingFor(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `max-w-[16rem] truncate ${o.copy === "unreadable" ? "text-ui-fg-error" : ""}`, children: wordingSummary(o.copy) }) }) }),
|
|
1643
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { "aria-label": `${OFFERS_COPY.shown}: ${o.name}`, checked: o.enabled, disabled: busy, onCheckedChange: (on) => void run(() => update.mutateAsync({ id: o.id, enabled: on })) }) }),
|
|
1644
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
|
|
1645
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.up}: ${o.name}`, disabled: busy || i === 0, onClick: () => move(data.offers, i, -1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpMini, {}) }),
|
|
1646
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.down}: ${o.name}`, disabled: busy || i === data.offers.length - 1, onClick: () => move(data.offers, i, 1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowDownMini, {}) }),
|
|
1647
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.remove}: ${o.name}`, disabled: busy, onClick: () => void onRemove(o), children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}) })
|
|
1648
|
-
] }) })
|
|
1649
|
-
] }, o.id)) })
|
|
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 })
|
|
1650
1912
|
] }) }),
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
{
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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 }) })
|
|
1920
|
+
] }),
|
|
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: [
|
|
1923
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { 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, {})
|
|
1931
|
+
] }) }),
|
|
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)) })
|
|
1945
|
+
] }) }),
|
|
1946
|
+
downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1947
|
+
OfferDownsellDrawer,
|
|
1948
|
+
{
|
|
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
|
+
}
|
|
1664
1960
|
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
+
}
|
|
1682
1978
|
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
+
}
|
|
1700
1996
|
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
+
] })
|
|
1711
2011
|
] }),
|
|
1712
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
] })
|
|
1716
|
-
] }),
|
|
1717
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
|
|
1718
|
-
] })
|
|
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
|
+
] }) })
|
|
1719
2015
|
] })
|
|
1720
2016
|
] });
|
|
1721
2017
|
};
|
|
@@ -1735,6 +2031,8 @@ const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
|
|
|
1735
2031
|
const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
|
|
1736
2032
|
const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
|
|
1737
2033
|
const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
|
|
2034
|
+
const CANCEL_IS_SEPARATE_NOTE = "Cancelling or refunding the order that started a subscription does not end the subscription: it keeps renewing until someone cancels it in your Vonpay dashboard.";
|
|
2035
|
+
const subscriptionOrderPath = (orderId) => `/orders/${encodeURIComponent(orderId)}`;
|
|
1738
2036
|
const SUBSCRIPTION_PAGE_SIZE = 20;
|
|
1739
2037
|
function useSubscriptionEvents(page) {
|
|
1740
2038
|
return reactQuery.useQuery({
|
|
@@ -1757,12 +2055,16 @@ const VonpaySubscriptionsPage = () => {
|
|
|
1757
2055
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
|
|
1758
2056
|
] }),
|
|
1759
2057
|
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1760
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2058
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
2059
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }),
|
|
2060
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-subscriptions-cancel-note", children: CANCEL_IS_SEPARATE_NOTE })
|
|
2061
|
+
] }),
|
|
1761
2062
|
data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1762
2063
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1763
2064
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1764
2065
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
|
|
1765
2066
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
|
|
2067
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Started by order" }),
|
|
1766
2068
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
|
|
1767
2069
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
|
|
1768
2070
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
|
|
@@ -1774,6 +2076,7 @@ const VonpaySubscriptionsPage = () => {
|
|
|
1774
2076
|
e.mode === "test" ? " (test)" : ""
|
|
1775
2077
|
] }),
|
|
1776
2078
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
|
|
2079
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", "data-testid": "vonpay-subscription-order", children: e.medusa_order_id ? /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: subscriptionOrderPath(e.medusa_order_id), children: e.medusa_order_id }) : "—" }),
|
|
1777
2080
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
|
|
1778
2081
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
|
|
1779
2082
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
|
|
@@ -1818,6 +2121,11 @@ const widgetModule = { widgets: [
|
|
|
1818
2121
|
Component: UpdateBanner,
|
|
1819
2122
|
zone: ["order.list.before"],
|
|
1820
2123
|
widgetId: "Widget-5ad3"
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
Component: VonpayVariantSubscription,
|
|
2127
|
+
zone: ["product_variant.details.after"],
|
|
2128
|
+
widgetId: "Widget-671a"
|
|
1821
2129
|
}
|
|
1822
2130
|
] };
|
|
1823
2131
|
const routeModule = {
|