vonpay-medusa 1.2.0 → 1.3.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 +1449 -984
- package/.medusa/server/src/admin/index.mjs +1452 -987
- package/.medusa/server/src/api/admin/vonpay/holds/resolve/route.js +6 -0
- package/.medusa/server/src/api/admin/vonpay/holds/route.js +52 -3
- package/.medusa/server/src/api/admin/vonpay/offers/report/route.js +10 -0
- package/.medusa/server/src/api/admin/vonpay/setup-check/move-to-held/route.js +14 -0
- package/.medusa/server/src/api/admin/vonpay/subscriptions/route.js +15 -1
- package/.medusa/server/src/api/admin/vonpay/variants/[id]/subscription/route.js +17 -0
- package/.medusa/server/src/api/middlewares.js +3 -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/errors.js +38 -2
- package/.medusa/server/src/lib/held-charge-copy.js +245 -0
- package/.medusa/server/src/lib/money.js +45 -0
- package/.medusa/server/src/modules/vonpay-jobs/index.js +11 -0
- package/.medusa/server/src/modules/vonpay-jobs/key.js +4 -0
- package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261105120000.js +15 -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/models/job-beat.js +20 -0
- package/.medusa/server/src/modules/vonpay-jobs/models/refund-watch.js +44 -0
- package/.medusa/server/src/modules/vonpay-jobs/models/signup-watch.js +28 -0
- package/.medusa/server/src/modules/vonpay-jobs/service.js +63 -0
- 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 +54 -44
- package/.medusa/server/src/providers/vonpay/charge-hold.js +181 -0
- package/.medusa/server/src/providers/vonpay/deadline.js +26 -0
- package/.medusa/server/src/providers/vonpay/descriptor-rules.js +6 -1
- package/.medusa/server/src/providers/vonpay/descriptor.js +4 -4
- package/.medusa/server/src/providers/vonpay/follow-up.js +45 -0
- package/.medusa/server/src/providers/vonpay/held-list.js +342 -27
- package/.medusa/server/src/providers/vonpay/hold-actions.js +1 -0
- package/.medusa/server/src/providers/vonpay/job-beat.js +66 -0
- package/.medusa/server/src/providers/vonpay/move-to-held.js +95 -0
- package/.medusa/server/src/providers/vonpay/options.js +19 -1
- package/.medusa/server/src/providers/vonpay/reading.js +25 -2
- package/.medusa/server/src/providers/vonpay/recovery.js +44 -11
- package/.medusa/server/src/providers/vonpay/refund-after-order.js +35 -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 +449 -0
- package/.medusa/server/src/providers/vonpay/renewal-recovery.js +425 -25
- package/.medusa/server/src/providers/vonpay/renewal.js +544 -40
- package/.medusa/server/src/providers/vonpay/scan.js +13 -0
- package/.medusa/server/src/providers/vonpay/service.js +1373 -134
- package/.medusa/server/src/providers/vonpay/session-data.js +476 -10
- package/.medusa/server/src/providers/vonpay/setup-check.js +727 -7
- 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 +567 -73
- 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/scripts/vonpay-check.js +23 -1
- package/.medusa/server/src/subscribers/vonpay-offer-refunds.js +9 -0
- package/.medusa/server/src/subscribers/vonpay-order-placed.js +2 -0
- package/README.md +484 -20
- package/bin/vonpay-medusa-check.cjs +23 -3
- package/bin/vonpay-medusa.cjs +244 -9
- package/package.json +6 -3
- package/storefront/src/cache-tag.ts +7 -0
- package/storefront/src/cart-fields.ts +1 -0
- package/storefront/src/checkout-steps.ts +27 -0
- package/storefront/src/context.tsx +1 -1
- package/storefront/src/copy.ts +15 -0
- package/storefront/src/csp.ts +1 -1
- package/storefront/src/index.ts +3 -1
- package/storefront/src/offer-prepare.ts +3 -2
- package/storefront/src/outcome.ts +37 -0
- package/storefront/src/payment-step.ts +99 -0
- package/storefront/src/reload-once.ts +13 -0
- package/storefront/src/server.ts +86 -18
- package/storefront/src/shared.ts +1 -0
- package/storefront/src/upsell.ts +43 -4
- package/storefront/src/vonpay-card-payment.tsx +51 -6
- 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
- package/storefront/src/vora-csp.json +17 -3
|
@@ -9,6 +9,60 @@ const icons = require("@medusajs/icons");
|
|
|
9
9
|
const react = require("react");
|
|
10
10
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
11
11
|
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
12
|
+
const TIME_SLOT = "{time}";
|
|
13
|
+
function fillTime(text, at, format) {
|
|
14
|
+
return at && text.includes(TIME_SLOT) ? text.split(TIME_SLOT).join(format(at)) : text;
|
|
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
|
+
};
|
|
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.";
|
|
20
|
+
const CHARGE_REASON_LABEL = {
|
|
21
|
+
renewal_outcome_unknown: "Renewal charge — result unknown",
|
|
22
|
+
renewal_not_settled: "Renewal charge not completed",
|
|
23
|
+
renewal_charged_unrecorded: "Renewal charged, order unpaid",
|
|
24
|
+
renewal_paid_twice: "Renewal charged — order also paid another way",
|
|
25
|
+
renewal_buyer_mismatch: "Renewal charged to a different customer",
|
|
26
|
+
upsell_outcome_unknown: "Offer charge — result unknown",
|
|
27
|
+
upsell_not_settled: "Offer charge not completed",
|
|
28
|
+
upsell_paid_not_added: "Offer paid, item not added",
|
|
29
|
+
upsell_paid_unsettled: "Offer paid — check the order",
|
|
30
|
+
upsell_paid_unrecorded: "Offer paid, payment not recorded",
|
|
31
|
+
upsell_hold_not_released: "Offer payment held — release not confirmed",
|
|
32
|
+
upsell_bank_check_open: "Offer bank check still open",
|
|
33
|
+
upsell_refund_failed: "Late offer charge — refund failed",
|
|
34
|
+
upsell_refund_unclear: "Late offer charge — refund unclear",
|
|
35
|
+
upsell_refund_pending: "Late offer charge — refund in progress",
|
|
36
|
+
upsell_refunded_late: "Late offer charge refunded",
|
|
37
|
+
upsell_refund_requested: "Late offer charge — refund accepted, not finished",
|
|
38
|
+
upsell_hold_unsettled: "Offer payment held — not finished yet",
|
|
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
|
+
};
|
|
50
|
+
const CHARGE_RESOLVED_HOW_LABEL = {
|
|
51
|
+
closed_blocking: "Closed by a person — no more renewal charges for this order",
|
|
52
|
+
freed: "Freed — later renewals of this order may charge the customer",
|
|
53
|
+
closed_no_change: "Closed by a person — no change to money"
|
|
54
|
+
};
|
|
55
|
+
function chargeHandStep(paymentIntentId) {
|
|
56
|
+
return { title: "Check this order in Vonpay", body: `Find ${paymentIntentId ? `payment ${paymentIntentId}` : "any payment"} in the Vonpay dashboard by the order id, and settle it there if it needs it.` };
|
|
57
|
+
}
|
|
58
|
+
const OFFER_CLOSED_MONEY_TEXT = {
|
|
59
|
+
upsell_paid_not_added: "The buyer paid for this offer. Closing this row did not refund it or add the item: that was yours to settle in Vonpay or on the order.",
|
|
60
|
+
upsell_paid_unsettled: "The buyer paid for this offer. Closing this row did not refund it or add the item: that was yours to settle in Vonpay or on the order.",
|
|
61
|
+
upsell_paid_unrecorded: "The buyer paid, and the item is on the order. Closing this row did not mark that balance paid in Medusa: that was yours to do.",
|
|
62
|
+
upsell_refund_failed: "The buyer was charged and the item was not added. Closing this row sent no refund: that was yours to send in Vonpay.",
|
|
63
|
+
upsell_refund_unclear: "The buyer was charged and the item was not added. Closing this row sent no refund: that was yours to send in Vonpay."
|
|
64
|
+
};
|
|
65
|
+
const OFFER_CLOSED_MONEY_DEFAULT = "Closing this row moved no money. What Vonpay holds for this payment is as it was.";
|
|
12
66
|
const REASON_COPY = {
|
|
13
67
|
deleted_while_charged: {
|
|
14
68
|
label: "Charged, then cart changed",
|
|
@@ -34,6 +88,10 @@ const REASON_COPY = {
|
|
|
34
88
|
label: "Couldn't confirm amount",
|
|
35
89
|
what: "The cart total couldn't be read to confirm the charge, so no order was created."
|
|
36
90
|
},
|
|
91
|
+
signup_mismatch: {
|
|
92
|
+
label: "Subscription doesn't match the cart",
|
|
93
|
+
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."
|
|
94
|
+
},
|
|
37
95
|
binding_mismatch: {
|
|
38
96
|
label: "Payment doesn't match checkout",
|
|
39
97
|
what: "The payment belongs to a different checkout than the one completing."
|
|
@@ -82,78 +140,7 @@ const REASON_COPY = {
|
|
|
82
140
|
label: "Paid, order not created",
|
|
83
141
|
what: "The buyer paid, but Medusa did not create the order, even after the plugin retried it automatically. Check the cart and the payment in Vonpay, then complete the order with the buyer or refund it."
|
|
84
142
|
},
|
|
85
|
-
|
|
86
|
-
label: "Renewal charge — result unknown",
|
|
87
|
-
what: "A subscription renewal charged the customer's saved card, but Vonpay's answer could not be read, so the customer may have been charged. No further renewal charge is sent for this order. Check the payment in Vonpay before charging or refunding anything."
|
|
88
|
-
},
|
|
89
|
-
renewal_not_settled: {
|
|
90
|
-
label: "Renewal charge not completed",
|
|
91
|
-
what: "Vonpay answered the subscription renewal charge, but the payment had not completed. No further renewal charge is sent for this order. Check the payment in Vonpay."
|
|
92
|
-
},
|
|
93
|
-
renewal_charged_unrecorded: {
|
|
94
|
-
label: "Renewal charged, order unpaid",
|
|
95
|
-
what: "Vonpay charged the customer's saved card for this subscription renewal, but Medusa's records do not show the payment, even after the plugin retried it automatically (or Medusa recorded it and then cancelled it). The customer has paid; the order shows unpaid. No further renewal charge is sent for this order. Find the payment in Vonpay by the order id, then mark the order paid in Medusa — do not charge again."
|
|
96
|
-
},
|
|
97
|
-
renewal_paid_twice: {
|
|
98
|
-
label: "Renewal charged — order also paid another way",
|
|
99
|
-
what: "Vonpay charged the customer's saved card for this subscription renewal, and the order was also marked paid another way. The customer may have paid twice. Check the payment in Vonpay and the order in Medusa, then refund the extra payment. No further renewal charge is sent for this order."
|
|
100
|
-
},
|
|
101
|
-
renewal_buyer_mismatch: {
|
|
102
|
-
label: "Renewal charged to a different customer",
|
|
103
|
-
what: "Vonpay recorded the subscription renewal charge against a different customer than the order's. No further renewal charge is sent for this order. Check the payment in Vonpay."
|
|
104
|
-
},
|
|
105
|
-
upsell_outcome_unknown: {
|
|
106
|
-
label: "Offer charge — result unknown",
|
|
107
|
-
what: "The buyer accepted an offer after their order and the plugin charged their card for it, but Vonpay's answer could not be read, so the buyer may have been charged. The item was NOT added to the order, and the plugin will not try that offer again. Check the payment in Vonpay by the order id: if it was charged, add the item to the order (or refund it in Vonpay); if not, cancel the open order edit in Medusa."
|
|
108
|
-
},
|
|
109
|
-
upsell_not_settled: {
|
|
110
|
-
label: "Offer charge not completed",
|
|
111
|
-
what: "The buyer accepted an offer after their order, and Vonpay answered the charge, but the payment had not completed. The item was NOT added to the order. Check the payment in Vonpay by the order id, then cancel the open order edit in Medusa if nothing was charged."
|
|
112
|
-
},
|
|
113
|
-
upsell_paid_not_added: {
|
|
114
|
-
label: "Offer paid, item not added",
|
|
115
|
-
what: "The buyer accepted an offer after their order and paid for it, but the item was NOT added to the order, even after the plugin retried. The buyer has paid. Either add the item to the order in Medusa, or refund the charge in Vonpay — not both, and do not charge again."
|
|
116
|
-
},
|
|
117
|
-
upsell_paid_unsettled: {
|
|
118
|
-
label: "Offer paid — check the order",
|
|
119
|
-
what: "The buyer accepted an offer after their order and paid for it, but the plugin could not finish recording it. Open the order first. If the item is on it, mark that balance paid in Medusa (do NOT refund — the item ships). If the item is not on it, either add it or refund the charge in Vonpay — not both. Never charge again."
|
|
120
|
-
},
|
|
121
|
-
upsell_paid_unrecorded: {
|
|
122
|
-
label: "Offer paid, payment not recorded",
|
|
123
|
-
what: "The buyer accepted an offer after their order and paid for it, and the item IS on the order, but Medusa did not record the payment, even after the plugin retried — so the order shows an unpaid balance. The buyer has paid. Find the payment in Vonpay by the order id and mark that balance paid in Medusa. Do NOT refund it (the item ships) and do not charge again."
|
|
124
|
-
},
|
|
125
|
-
upsell_hold_not_released: {
|
|
126
|
-
label: "Offer payment held — release not confirmed",
|
|
127
|
-
what: "The buyer approved an offer after their order with Apple Pay or Google Pay, which only held the money. The item could not be added, so the plugin released the hold, but Vonpay has not confirmed the release. Nothing was taken and the item was NOT added. The plugin keeps releasing it on every run. If this row stays, find the payment in Vonpay by the order id and void it there. Do not take it."
|
|
128
|
-
},
|
|
129
|
-
upsell_bank_check_open: {
|
|
130
|
-
label: "Offer bank check still open",
|
|
131
|
-
what: "The offer ended while the buyer's bank check was still open, so nothing was added and the order change was undone. If the bank approves it later, the plugin refunds it automatically. Nothing to do unless this stays for a few days; then check the payment in Vonpay by the order id."
|
|
132
|
-
},
|
|
133
|
-
upsell_refund_failed: {
|
|
134
|
-
label: "Late offer charge — refund failed",
|
|
135
|
-
what: "The buyer was charged for an offer whose item was NOT added to the order — the bank approved it after the offer had ended, the item could not be added, or an earlier declined attempt was approved after all — so the plugin tried to refund that charge in full, but the refund did not go through (refused, or not settled within 48 hours). Refund the payment in Vonpay by the order id. Do not add the item."
|
|
136
|
-
},
|
|
137
|
-
upsell_refund_unclear: {
|
|
138
|
-
label: "Late offer charge — refund unclear",
|
|
139
|
-
what: "The buyer was charged for an offer whose item was NOT added to the order, and the plugin's automatic refund got an unclear answer: Vonpay reported an error, then recorded that refund as failed, but the money may have gone back anyway. The plugin will not send another refund. First check the payment in Vonpay by the order id: refund it there ONLY if no refund went through. Do not add the item."
|
|
140
|
-
},
|
|
141
|
-
upsell_refund_pending: {
|
|
142
|
-
label: "Late offer charge — refund in progress",
|
|
143
|
-
what: "The buyer's bank approved an offer after the offer had ended, so nothing was added and the plugin is refunding the charge in full. It keeps retrying. If this row stays for a day, refund the payment in Vonpay by the order id."
|
|
144
|
-
},
|
|
145
|
-
upsell_refunded_late: {
|
|
146
|
-
label: "Late offer charge refunded",
|
|
147
|
-
what: "The buyer's bank approved an offer after the offer had ended, so nothing was added and the plugin refunded the charge in full. Nothing to do."
|
|
148
|
-
},
|
|
149
|
-
upsell_hold_unsettled: {
|
|
150
|
-
label: "Offer payment held — not finished yet",
|
|
151
|
-
what: "The buyer approved an offer after their order with Apple Pay or Google Pay, which only held the money, and the plugin has not finished it yet. Nothing was taken. The plugin keeps working on it: it adds the item and takes the money, or releases the hold if the item cannot be added. Do not take or void it yourself unless this row stays for more than a day; then check the payment in Vonpay by the order id."
|
|
152
|
-
},
|
|
153
|
-
upsell_take_unconfirmed: {
|
|
154
|
-
label: "Offer payment — collection not confirmed",
|
|
155
|
-
what: "The buyer approved an offer after their order with Apple Pay or Google Pay, and the plugin asked Vonpay to take the held money, but Vonpay has not confirmed whether it was taken. The plugin re-checks it on every run: if it was taken, the plugin adds the item; if it was released, it closes the open order edit. Check the payment in Vonpay by the order id. Do not take it again."
|
|
156
|
-
},
|
|
143
|
+
...Object.fromEntries(Object.entries(CHARGE_REASON_LABEL).map(([reason, label]) => [reason, { label }])),
|
|
157
144
|
cancel_refund_missing: {
|
|
158
145
|
label: "Cancelled, refund failed",
|
|
159
146
|
what: "The order was cancelled, but Medusa's refund to the buyer did not go through. The buyer may still be charged."
|
|
@@ -188,7 +175,9 @@ const SOURCE_LABEL = {
|
|
|
188
175
|
cancel: "Recorded when the order was cancelled",
|
|
189
176
|
renewal: "Recorded by a subscription renewal — check in Vonpay",
|
|
190
177
|
order: "Found on an order — its wallet payment wasn't collected",
|
|
191
|
-
upsell: "Recorded by an offer accepted after the order — check in Vonpay"
|
|
178
|
+
upsell: "Recorded by an offer accepted after the order — check in Vonpay",
|
|
179
|
+
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"
|
|
192
181
|
};
|
|
193
182
|
const STATUS_LABEL = {
|
|
194
183
|
open: "Needs action",
|
|
@@ -207,6 +196,7 @@ const RESOLVED_HOW_LABEL = {
|
|
|
207
196
|
refunded_earlier: "Already refunded with this payment's key (for example from the command line)",
|
|
208
197
|
nothing_left: "Vonpay showed nothing left to refund — confirm in the Vonpay dashboard that the buyer got it back",
|
|
209
198
|
marked: "Marked resolved",
|
|
199
|
+
...CHARGE_RESOLVED_HOW_LABEL,
|
|
210
200
|
auto_closed: "Closed automatically — Vonpay confirmed nothing was charged and the payment can no longer be used",
|
|
211
201
|
paid_after_auto_close: "Re-opened automatically — money arrived after the automatic close",
|
|
212
202
|
paid_after_resolution: "Re-opened automatically — Vonpay reported this payment as paid after it was marked resolved",
|
|
@@ -241,15 +231,20 @@ const REFUNDABLE_REASONS = [
|
|
|
241
231
|
"paid_orphan_session",
|
|
242
232
|
"paid_after_auto_close",
|
|
243
233
|
"paid_no_order",
|
|
234
|
+
"signup_mismatch",
|
|
244
235
|
"cancel_refund_missing",
|
|
245
236
|
"cancel_refund_unconfirmed"
|
|
246
237
|
];
|
|
238
|
+
const SUBSCRIPTION_REFUND_NOTE = "If this payment started a Vonpay subscription, refunding it does not cancel the subscription: cancel it in your Vonpay dashboard.";
|
|
247
239
|
function isConfirmedRefund(row) {
|
|
248
240
|
return row.status === "resolved" && (row.resolution?.how === "refunded" || row.resolution?.how === "refunded_earlier");
|
|
249
241
|
}
|
|
250
242
|
function canRefund(row) {
|
|
251
243
|
return row.vp_session_id !== null && !isConfirmedRefund(row) && !isAutoClosed(row) && !isUnrecognisedRecord(row) && REFUNDABLE_REASONS.includes(row.reason);
|
|
252
244
|
}
|
|
245
|
+
function isChargeRow(row) {
|
|
246
|
+
return row.source === "renewal" || row.source === "upsell" || row.source === "refund" || row.source === "signup";
|
|
247
|
+
}
|
|
253
248
|
function canRelease(row) {
|
|
254
249
|
return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && WALLET_REASONS.includes(row.reason);
|
|
255
250
|
}
|
|
@@ -257,9 +252,22 @@ function canComplete(row) {
|
|
|
257
252
|
return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && row.reason === "wallet_held_no_order";
|
|
258
253
|
}
|
|
259
254
|
function canMarkResolved(row) {
|
|
255
|
+
if (isChargeRow(row)) return false;
|
|
260
256
|
return row.vp_session_id !== null && row.status !== "resolved" && !isUnrecognisedRecord(row) && !WALLET_REASONS.includes(row.reason) && !TAKE_REASONS.includes(row.reason);
|
|
261
257
|
}
|
|
262
258
|
function locatorFor(row) {
|
|
259
|
+
if (row.source === "refund") return { source: "refund", watch_id: row.watch_id ?? "" };
|
|
260
|
+
if (row.source === "signup") return { source: "signup", watch_id: row.watch_id ?? "" };
|
|
261
|
+
if (isChargeRow(row)) {
|
|
262
|
+
return {
|
|
263
|
+
source: row.source,
|
|
264
|
+
payment_collection_id: row.payment_collection_id,
|
|
265
|
+
...row.order_id ? { order_id: row.order_id } : {},
|
|
266
|
+
...row.medusa_payment_session_id ? { medusa_payment_session_id: row.medusa_payment_session_id } : {},
|
|
267
|
+
...row.acceptance_id ? { acceptance_id: row.acceptance_id } : {},
|
|
268
|
+
...row.prior_intent_id ? { prior_intent_id: row.prior_intent_id } : {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
263
271
|
return {
|
|
264
272
|
source: row.source,
|
|
265
273
|
vp_session_id: row.vp_session_id ?? "",
|
|
@@ -269,6 +277,9 @@ function locatorFor(row) {
|
|
|
269
277
|
};
|
|
270
278
|
}
|
|
271
279
|
function resolutionSteps(row) {
|
|
280
|
+
if (isChargeRow(row)) {
|
|
281
|
+
return [chargeHandStep(row.vp_payment_intent_id)];
|
|
282
|
+
}
|
|
272
283
|
if (!row.vp_session_id || row.reason === "unrecognised") {
|
|
273
284
|
return [{ title: "Check this payment in Vonpay", body: "Find the payment in the Vonpay dashboard before refunding or fulfilling anything." }];
|
|
274
285
|
}
|
|
@@ -348,6 +359,14 @@ function reconcileCopy(o) {
|
|
|
348
359
|
function markConfirmMatches(typed, row) {
|
|
349
360
|
return row.vp_session_id !== null && typed.trim() === row.vp_session_id;
|
|
350
361
|
}
|
|
362
|
+
function closedMoneyNote(row) {
|
|
363
|
+
if (row.status !== "resolved" || row.resolution?.how !== "closed_no_change") return null;
|
|
364
|
+
return OFFER_CLOSED_MONEY_TEXT[row.reason] ?? OFFER_CLOSED_MONEY_DEFAULT;
|
|
365
|
+
}
|
|
366
|
+
function chargedUncheckedNote(count) {
|
|
367
|
+
if (!(count > 0)) return null;
|
|
368
|
+
return `${count} paid ${count === 1 ? "renewal was" : "renewals were"} not checked against Medusa on this load, because a load checks a limited number or Medusa did not answer. One that Medusa has no record of may be missing from this list. Reload to check the next ones.`;
|
|
369
|
+
}
|
|
351
370
|
const REFUSAL_TEXT = {
|
|
352
371
|
hold_release_not_eligible: "Only a held wallet payment can be released here. Nothing was changed.",
|
|
353
372
|
hold_release_payment_active: "This held payment belongs to an order. Take it or cancel it from the order. Nothing was changed.",
|
|
@@ -369,7 +388,7 @@ const REFUSAL_TEXT = {
|
|
|
369
388
|
hold_refund_auto_closed: "This payment was closed automatically because Vonpay confirmed nothing was charged. There's nothing to refund.",
|
|
370
389
|
hold_refund_not_eligible: "This held payment has no single charge that can be refunded from here. Nothing was refunded — check it in Vonpay.",
|
|
371
390
|
hold_record_unrecognised: "This payment's record in the list isn't one the plugin recognises. Nothing was changed — check it in Vonpay.",
|
|
372
|
-
refund_refused: "Vonpay refused the refund. Nothing was refunded.",
|
|
391
|
+
refund_refused: "Vonpay refused the refund. Nothing was refunded. If the buyer already has the money back (a dispute, or a refund made in the Vonpay dashboard), close this row with Mark resolved.",
|
|
373
392
|
refund_exceeds_remaining: "Vonpay shows nothing left to refund on this payment. Nothing new was refunded.",
|
|
374
393
|
hold_provider_unavailable: "The Vonpay payment provider couldn't be reached from the admin. Nothing was changed — try again.",
|
|
375
394
|
hold_provider_ambiguous: "More than one Vonpay provider could own this payment. Nothing was changed — check the store's payment setup.",
|
|
@@ -377,7 +396,7 @@ const REFUSAL_TEXT = {
|
|
|
377
396
|
hold_refund_key_conflict: "A refund with this payment's key was already made with different details (for example a partial amount from the command line). Nothing new was refunded — check it in Vonpay.",
|
|
378
397
|
refund_declined: "The payment provider declined the refund. No money moved.",
|
|
379
398
|
refund_not_completed: "Vonpay recorded this refund as failed. No money moved.",
|
|
380
|
-
hold_refund_not_refundable_now: "Vonpay says nothing can be refunded on this payment right now (not settled yet, disputed, or already refunded). Nothing was refunded.",
|
|
399
|
+
hold_refund_not_refundable_now: "Vonpay says nothing can be refunded on this payment right now (not settled yet, disputed, or already refunded). Nothing was refunded. Once the buyer has the money back (a dispute, or a refund made in the Vonpay dashboard), close this row with Mark resolved; if it has not settled yet, try again later.",
|
|
381
400
|
hold_checkout_busy: "The buyer's checkout for this payment is in progress right now. Nothing was changed — try again in a moment.",
|
|
382
401
|
hold_checkout_lock_unavailable: "This payment's checkout couldn't be locked, so nothing was changed. Check the store's locking setup, or refund from the command line.",
|
|
383
402
|
hold_recheck_failed: "The order couldn't be re-checked just before acting. Nothing was changed — try again.",
|
|
@@ -387,6 +406,14 @@ const REFUSAL_TEXT = {
|
|
|
387
406
|
hold_refund_payment_active: "This payment now belongs to an order that isn't cancelled. Nothing was refunded — refund it from the order.",
|
|
388
407
|
hold_resolve_wallet_held: "Use Release for a held wallet payment. It checks Vonpay first, then releases the hold, lists it for a refund, or clears it with its order. Nothing was changed.",
|
|
389
408
|
hold_resolve_payment_active: "This payment now belongs to an order that isn't cancelled, so it isn't held. Nothing was changed.",
|
|
409
|
+
move_request_invalid: "That request was missing the payment's ids. Run the check again. Nothing was changed.",
|
|
410
|
+
hold_resolve_confirm_mismatch: "To mark a payment resolved, type its Vonpay session id exactly as the row shows it. Nothing was changed.",
|
|
411
|
+
move_not_listed: "This payment isn't one the check lists any more (it may have become an order, or already be on Held payments). Run the check again. Nothing was changed.",
|
|
412
|
+
move_not_paid: "Vonpay doesn't show money taken or held on this payment, so there is nothing to move. Nothing was changed.",
|
|
413
|
+
move_refunded: "Vonpay shows this payment refunded in full (or a full refund still in progress), so there is nothing to move. Nothing was changed.",
|
|
414
|
+
move_not_this_checkout: "Vonpay's record of this payment names a different checkout. Check it in the Vonpay dashboard. Nothing was changed.",
|
|
415
|
+
move_lock_not_shared: "The command can't move a payment on a store without a shared lock (Redis or Postgres locking). Use the Move to Held payments button in the admin instead. Nothing was changed.",
|
|
416
|
+
move_vonpay_unavailable: "Vonpay couldn't be reached to check this payment again. Nothing was changed — try again shortly.",
|
|
390
417
|
hold_not_found: "This held payment is no longer on the list. It may already have been completed, released or refunded. Reload the list and check the order."
|
|
391
418
|
};
|
|
392
419
|
function outcomeCopy(o) {
|
|
@@ -456,6 +483,23 @@ function completeOutcomeCopy(o) {
|
|
|
456
483
|
return UNKNOWN_OUTCOME;
|
|
457
484
|
}
|
|
458
485
|
}
|
|
486
|
+
function moveOutcomeCopy(o) {
|
|
487
|
+
switch (o.outcome) {
|
|
488
|
+
case "moved":
|
|
489
|
+
return {
|
|
490
|
+
variant: "success",
|
|
491
|
+
text: o.reason === "wallet_held_no_order" ? "Moved to Held payments. Release the held payment or complete the order there." : "Moved to Held payments. Refund it or mark it resolved there."
|
|
492
|
+
};
|
|
493
|
+
case "already_held":
|
|
494
|
+
return { variant: "success", text: "It is already on Held payments. Nothing was changed." };
|
|
495
|
+
case "ordered":
|
|
496
|
+
return { variant: "warning", text: "An order holds this payment now, so it was not moved. Nothing was changed." };
|
|
497
|
+
case "refused":
|
|
498
|
+
return { variant: "error", text: o.code && REFUSAL_TEXT[o.code] || "That couldn't be done. Nothing was changed." };
|
|
499
|
+
default:
|
|
500
|
+
return UNKNOWN_OUTCOME;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
459
503
|
function resolveOutcomeCopy(o) {
|
|
460
504
|
if (o.outcome === "refused") return { variant: "error", text: o.code && REFUSAL_TEXT[o.code] || "That couldn't be done. Nothing was changed." };
|
|
461
505
|
if (!o.recorded) return { variant: "warning", text: "Couldn't save that. Try again." };
|
|
@@ -466,6 +510,7 @@ const authType = typeof __AUTH_TYPE__ !== "undefined" && __AUTH_TYPE__ ? __AUTH_
|
|
|
466
510
|
const sdk = new Medusa__default.default({ baseUrl: backendUrl, auth: { type: authType } });
|
|
467
511
|
const HELD_PAGE_SIZE = 20;
|
|
468
512
|
const LIST_KEY = "vonpay-held-payments";
|
|
513
|
+
const HELD_LIST_KEY = LIST_KEY;
|
|
469
514
|
function useHeldPayments(page) {
|
|
470
515
|
const status = page.status ?? "open";
|
|
471
516
|
return reactQuery.useQuery({
|
|
@@ -528,6 +573,22 @@ function useMarkResolved() {
|
|
|
528
573
|
onSettled: () => qc.invalidateQueries({ queryKey: [LIST_KEY] })
|
|
529
574
|
});
|
|
530
575
|
}
|
|
576
|
+
function useChargeAction() {
|
|
577
|
+
const qc = reactQuery.useQueryClient();
|
|
578
|
+
return reactQuery.useMutation({
|
|
579
|
+
mutationFn: (v) => sdk.client.fetch("/admin/vonpay/holds/resolve", {
|
|
580
|
+
method: "POST",
|
|
581
|
+
body: {
|
|
582
|
+
...locatorFor(v.row),
|
|
583
|
+
action: v.action,
|
|
584
|
+
...v.typed_order_id !== void 0 ? { confirm_order_id: v.typed_order_id.trim(), confirm_checked_in_vonpay: v.checked_in_vonpay === true } : {},
|
|
585
|
+
...v.action === "close_unreadable" && v.row.record_sha256 ? { confirm_record_sha256: v.row.record_sha256 } : {}
|
|
586
|
+
}
|
|
587
|
+
}),
|
|
588
|
+
retry: false,
|
|
589
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [LIST_KEY] })
|
|
590
|
+
});
|
|
591
|
+
}
|
|
531
592
|
function useReconcileHold() {
|
|
532
593
|
const qc = reactQuery.useQueryClient();
|
|
533
594
|
return reactQuery.useMutation({
|
|
@@ -558,11 +619,11 @@ const HeldPaymentsBanner = () => {
|
|
|
558
619
|
] }) });
|
|
559
620
|
};
|
|
560
621
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
561
|
-
const KEY$
|
|
622
|
+
const KEY$2 = "vonpay-ship-hold";
|
|
562
623
|
const SHIP_HOLD_REFRESH_MS = 3e4;
|
|
563
624
|
function useShipHold(orderId) {
|
|
564
625
|
return reactQuery.useQuery({
|
|
565
|
-
queryKey: [KEY$
|
|
626
|
+
queryKey: [KEY$2, orderId],
|
|
566
627
|
queryFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/offer-hold`),
|
|
567
628
|
enabled: !!orderId,
|
|
568
629
|
retry: false,
|
|
@@ -577,7 +638,7 @@ function useShipOverride(orderId) {
|
|
|
577
638
|
return reactQuery.useMutation({
|
|
578
639
|
mutationFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/ship-override`, { method: "POST", body: {} }),
|
|
579
640
|
retry: false,
|
|
580
|
-
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$
|
|
641
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$2, orderId] })
|
|
581
642
|
});
|
|
582
643
|
}
|
|
583
644
|
function clockTime(iso) {
|
|
@@ -626,975 +687,1374 @@ const UpdateBanner = () => {
|
|
|
626
687
|
] }) });
|
|
627
688
|
};
|
|
628
689
|
adminSdk.defineWidgetConfig({ zone: "order.list.before" });
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
"subscription.renewed": "Renewal paid",
|
|
632
|
-
"subscription.payment_failed": "Renewal declined",
|
|
633
|
-
"subscription.cancelled": "Cancelled",
|
|
634
|
-
"subscription.updated": "Changed"
|
|
635
|
-
};
|
|
636
|
-
const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
|
|
637
|
-
const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
|
|
638
|
-
const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
|
|
639
|
-
const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
|
|
640
|
-
const SUBSCRIPTION_PAGE_SIZE = 20;
|
|
641
|
-
function useSubscriptionEvents(page) {
|
|
690
|
+
const KEY$1 = "vonpay-variant-subscription";
|
|
691
|
+
function useVariantSubscription(variantId) {
|
|
642
692
|
return reactQuery.useQuery({
|
|
643
|
-
queryKey: [
|
|
644
|
-
queryFn: () => sdk.client.fetch(
|
|
645
|
-
|
|
693
|
+
queryKey: [KEY$1, variantId],
|
|
694
|
+
queryFn: () => sdk.client.fetch(`/admin/vonpay/variants/${encodeURIComponent(variantId)}/subscription`),
|
|
695
|
+
enabled: !!variantId,
|
|
696
|
+
retry: false
|
|
646
697
|
});
|
|
647
698
|
}
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
|
|
651
|
-
};
|
|
652
|
-
const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
|
|
653
|
-
const VonpaySubscriptionsPage = () => {
|
|
654
|
-
const [pageIndex, setPageIndex] = react.useState(0);
|
|
655
|
-
const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
|
|
656
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
657
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
658
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
|
|
659
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
|
|
660
|
-
] }),
|
|
661
|
-
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
662
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
|
|
663
|
-
data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
664
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
665
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
666
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
|
|
667
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
|
|
668
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
|
|
669
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
|
|
670
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
|
|
671
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
|
|
672
|
-
] }) }),
|
|
673
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
674
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
|
|
675
|
-
eventLabel(e.type),
|
|
676
|
-
e.mode === "test" ? " (test)" : ""
|
|
677
|
-
] }),
|
|
678
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
|
|
679
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
|
|
680
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
|
|
681
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
|
|
682
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
|
|
683
|
-
] }, e.id)) })
|
|
684
|
-
] }) }),
|
|
685
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
686
|
-
ui.Table.Pagination,
|
|
687
|
-
{
|
|
688
|
-
count: data.count,
|
|
689
|
-
pageSize: SUBSCRIPTION_PAGE_SIZE,
|
|
690
|
-
pageIndex,
|
|
691
|
-
pageCount: pageCountFor(data.count),
|
|
692
|
-
canPreviousPage: pageIndex > 0,
|
|
693
|
-
canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
|
|
694
|
-
previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
|
|
695
|
-
nextPage: () => setPageIndex((p) => p + 1)
|
|
696
|
-
}
|
|
697
|
-
)
|
|
698
|
-
] })
|
|
699
|
-
] })
|
|
700
|
-
] });
|
|
701
|
-
};
|
|
702
|
-
const config$2 = adminSdk.defineRouteConfig({
|
|
703
|
-
label: "Vonpay subscriptions",
|
|
704
|
-
icon: icons.ArrowPath,
|
|
705
|
-
nested: "/orders"
|
|
706
|
-
});
|
|
707
|
-
function useSetupCheck() {
|
|
699
|
+
function useSetVariantSubscription(variantId) {
|
|
700
|
+
const qc = reactQuery.useQueryClient();
|
|
708
701
|
return reactQuery.useMutation({
|
|
709
|
-
mutationFn: () => sdk.client.fetch(
|
|
710
|
-
retry: false
|
|
702
|
+
mutationFn: (on) => sdk.client.fetch(`/admin/vonpay/variants/${encodeURIComponent(variantId)}/subscription`, { method: "POST", body: { on } }),
|
|
703
|
+
retry: false,
|
|
704
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [KEY$1, variantId] })
|
|
711
705
|
});
|
|
712
706
|
}
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
707
|
+
const VARIANT_SUBSCRIPTION_COPY = {
|
|
708
|
+
heading: "Vonpay subscription",
|
|
709
|
+
on: "Sold as a Vonpay subscription: buying it at checkout (or on an offer) starts a subscription that renews in Vonpay.",
|
|
710
|
+
off: "Sold as a one-time purchase.",
|
|
711
|
+
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:",
|
|
712
|
+
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:",
|
|
713
|
+
turnOn: "Sell as a Vonpay subscription",
|
|
714
|
+
turnOff: "Stop selling as a subscription",
|
|
715
|
+
failed: "Couldn't change that. Reload and try again.",
|
|
716
|
+
unreadable: "Couldn't read Vonpay's subscription products. Reload to try again."
|
|
717
|
+
};
|
|
718
|
+
const VonpayVariantSubscription = ({ data: variant }) => {
|
|
719
|
+
const { data, isError } = useVariantSubscription(variant?.id);
|
|
720
|
+
const set = useSetVariantSubscription(variant?.id);
|
|
721
|
+
if (isError)
|
|
722
|
+
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 }) });
|
|
723
|
+
if (!data || !data.enabled) return null;
|
|
724
|
+
const product = data.products.find((p) => p.status === "active") ?? data.products[0];
|
|
725
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-3 px-6 py-4", "data-testid": "vonpay-variant-subscription", children: [
|
|
726
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
727
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: VARIANT_SUBSCRIPTION_COPY.heading }),
|
|
728
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: data.marked ? "green" : "grey", size: "2xsmall", children: data.marked ? "On" : "Off" })
|
|
725
729
|
] }),
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
730
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: data.marked ? VARIANT_SUBSCRIPTION_COPY.on : VARIANT_SUBSCRIPTION_COPY.off }),
|
|
731
|
+
product?.disclosure ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-variant-subscription-terms", children: product.disclosure }) : null,
|
|
732
|
+
!data.can_mark && !data.marked ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-variant-subscription-setup", children: [
|
|
733
|
+
VARIANT_SUBSCRIPTION_COPY.noProduct,
|
|
734
|
+
" ",
|
|
735
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: variant?.id })
|
|
736
|
+
] }) : null,
|
|
737
|
+
!data.can_mark && data.marked ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-error", "data-testid": "vonpay-variant-subscription-broken", children: [
|
|
738
|
+
VARIANT_SUBSCRIPTION_COPY.markedNoProduct,
|
|
739
|
+
" ",
|
|
740
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: variant?.id })
|
|
741
|
+
] }) : null,
|
|
742
|
+
/* @__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 }) }),
|
|
743
|
+
set.isError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: VARIANT_SUBSCRIPTION_COPY.failed }) : null
|
|
737
744
|
] });
|
|
738
745
|
};
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
746
|
+
adminSdk.defineWidgetConfig({ zone: "product_variant.details.after" });
|
|
747
|
+
const HEADLINE_MAX = 80;
|
|
748
|
+
const PITCH_MAX = 300;
|
|
749
|
+
const OFFERS_COPY = {
|
|
750
|
+
title: "One-click offers",
|
|
751
|
+
intro: "After paying, shoppers see these offers one at a time (up to three per order), in this order. Only switched-on offers are shown.",
|
|
752
|
+
off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
|
|
753
|
+
fromConfig: "Your offers come from medusa-config right now (upsells.variantIds). Import them to manage them here. Once an offer exists here, this list is used instead.",
|
|
754
|
+
configEmpty: "No offers yet. Add the first one below.",
|
|
755
|
+
adminEmpty: "No offers are shown to shoppers. Add one below.",
|
|
756
|
+
importButton: "Import from medusa-config",
|
|
757
|
+
add: "Add offer",
|
|
758
|
+
name: "Name (only you see it)",
|
|
759
|
+
variant: "Product variant id (variant_…)",
|
|
760
|
+
shown: "Shown",
|
|
761
|
+
up: "Move up",
|
|
762
|
+
down: "Move down",
|
|
763
|
+
remove: "Remove",
|
|
764
|
+
removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
|
|
765
|
+
missingVariant: "This product variant no longer exists — it is not offered.",
|
|
766
|
+
loadFailed: "The offers could not be loaded. Reload the page to try again.",
|
|
767
|
+
lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
|
|
768
|
+
rules: "Who sees it",
|
|
769
|
+
downsell: "If they say No thanks",
|
|
770
|
+
downsellNone: "Nothing",
|
|
771
|
+
downsellIntro: 'Show one other product after a shopper says No thanks to this offer — usually a smaller or cheaper option. It counts toward the three offers per order, and the same "Who sees it" applies.',
|
|
772
|
+
downsellVariant: "Downsell product variant id (variant_…)",
|
|
773
|
+
downsellRemove: "Remove downsell",
|
|
774
|
+
downsellMissing: "This product variant no longer exists — it is not offered.",
|
|
775
|
+
wording: "Wording",
|
|
776
|
+
wordingDefault: "Standard",
|
|
777
|
+
wordingPitchOnly: "Pitch only",
|
|
778
|
+
wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
|
|
779
|
+
wordingIntro: "What shoppers read on this offer. Leave a field empty to use the standard wording. The payment line, the buttons and every result message stay the same.",
|
|
780
|
+
headline: "Headline",
|
|
781
|
+
headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
|
|
782
|
+
pitch: "Pitch",
|
|
783
|
+
pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
|
|
784
|
+
wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
|
|
785
|
+
wordingReset: "Use the standard wording",
|
|
786
|
+
rulesEveryone: "Everyone",
|
|
787
|
+
rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
|
|
788
|
+
rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
|
|
789
|
+
customers: "Customers",
|
|
790
|
+
customersAll: "Everyone",
|
|
791
|
+
customersGuests: "Guests only",
|
|
792
|
+
customersSignedIn: "Signed-in customers only",
|
|
793
|
+
products: "The shopper bought one of these products at checkout",
|
|
794
|
+
collections: "The shopper bought a product from one of these collections at checkout",
|
|
795
|
+
groups: "The signed-in customer is in one of these groups",
|
|
796
|
+
paid: "The amount paid at checkout (tax and shipping included)",
|
|
797
|
+
paidNote: "An order in a currency not listed here does not see this offer.",
|
|
798
|
+
paidAtLeast: "At least",
|
|
799
|
+
paidAtMost: "At most",
|
|
800
|
+
search: "Search by name",
|
|
801
|
+
addCurrency: "Add currency",
|
|
802
|
+
removeCurrency: "Remove currency",
|
|
803
|
+
rulesUnreadableNotice: "The saved rules for this offer can't be read, so the offer is not shown. Saving replaces them with what you set here.",
|
|
804
|
+
replaceRules: "Replace the rules",
|
|
805
|
+
save: "Save",
|
|
806
|
+
cancel: "Cancel",
|
|
807
|
+
saveFailed: "That change was not saved."
|
|
760
808
|
};
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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;
|
|
764
814
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
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(" — ");
|
|
767
818
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
819
|
+
function wordingSummary(copy) {
|
|
820
|
+
if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
|
|
821
|
+
if (!copy) return OFFERS_COPY.wordingDefault;
|
|
822
|
+
return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
|
|
770
823
|
}
|
|
771
|
-
function
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
824
|
+
function rulesSummary(rules) {
|
|
825
|
+
if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
|
|
826
|
+
if (!rules) return OFFERS_COPY.rulesEveryone;
|
|
827
|
+
const parts = [];
|
|
828
|
+
if (rules.customers === "guests") parts.push("Guests");
|
|
829
|
+
if (rules.customers === "signed_in") parts.push("Signed-in");
|
|
830
|
+
if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
|
|
831
|
+
if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
|
|
832
|
+
if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
|
|
833
|
+
for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
|
|
834
|
+
for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
|
|
835
|
+
return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
|
|
775
836
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
837
|
+
const REPORT_COPY = {
|
|
838
|
+
tabOffers: "Offers",
|
|
839
|
+
tabResults: "Results",
|
|
840
|
+
intro: "How shoppers answered each offer. An order counts as shown when the offer was offered to it: on the shopper's screen, or answered. Answers and money count when the offer has ended, about 2 hours after the order.",
|
|
841
|
+
from: "From",
|
|
842
|
+
to: "To",
|
|
843
|
+
live: "Live payments",
|
|
844
|
+
test: "Test payments",
|
|
845
|
+
offer: "Offer",
|
|
846
|
+
product: "Product",
|
|
847
|
+
shown: "Shown",
|
|
848
|
+
added: "Added",
|
|
849
|
+
noThanks: "No thanks",
|
|
850
|
+
declined: "Didn't go through",
|
|
851
|
+
takeRate: "Take rate",
|
|
852
|
+
charged: "Charged",
|
|
853
|
+
refunded: "Refunded",
|
|
854
|
+
net: "Net",
|
|
855
|
+
total: "All offers",
|
|
856
|
+
removed: "(removed)",
|
|
857
|
+
fromConfig: "From medusa-config",
|
|
858
|
+
downsell: (product) => `If they say No thanks: ${product}`,
|
|
859
|
+
shownNote: "Shown counts the orders the offer was offered to, once per order (and once more for its downsell): the offer was on the shopper's screen, or the shopper answered it. It is not a count of page views.",
|
|
860
|
+
takeRateNote: "Take rate is added ÷ shown.",
|
|
861
|
+
declinedNote: "Didn't go through: the payment was declined or not finished, or the item could not be added.",
|
|
862
|
+
chargedNote: "Charged is what shoppers paid for the offers, tax included. Refunded is what was refunded on those payments in Medusa since, by you or automatically, and never more than was charged. Refunds made outside Medusa, for example in the Vonpay dashboard, are not counted. Net is charged minus refunded.",
|
|
863
|
+
datesNote: "Each row counts on the date of its own moment: a view when the offer was shown, an answer when the shopper gave it, money when it was charged. A refund counts on the date of the sale it refunds, not the day it was made. Near the start or end of your dates, a view and its answer can fall on different sides.",
|
|
864
|
+
heldNote: "A payment on the Held payments page counts as it stood when it was held. For example, a late charge whose refund failed usually shows as charged (if it was held within moments of the charge, it may not show while held). If someone closes it there, it is left out of Results.",
|
|
865
|
+
viewsMissing: 'Some added items have no view counted, so their take rate is not shown. Your storefront may not count views yet: see "Offer results" in the vonpay-medusa README.',
|
|
866
|
+
empty: "No results for these dates yet.",
|
|
867
|
+
loadFailed: "The results could not be loaded. Try again.",
|
|
868
|
+
badRange: "The first date must be on or before the second, and at most 400 days apart.",
|
|
869
|
+
none: "—"
|
|
870
|
+
};
|
|
871
|
+
function moneyText(currency, amount, minor, locale) {
|
|
872
|
+
if (amount === null) return `${minor} ${currency}`;
|
|
873
|
+
const digits = amount.replace(/^-/, "");
|
|
874
|
+
const decimals = digits.includes(".") ? digits.length - digits.indexOf(".") - 1 : 0;
|
|
875
|
+
try {
|
|
876
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(amount);
|
|
877
|
+
} catch {
|
|
878
|
+
return `${amount} ${currency}`;
|
|
879
|
+
}
|
|
783
880
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
);
|
|
881
|
+
function moneyColumn(money, which, locale) {
|
|
882
|
+
if (money.length === 0) return REPORT_COPY.none;
|
|
883
|
+
return money.map((m) => moneyText(m.currency, m[which], m[`${which}_minor`], locale)).join(" · ");
|
|
884
|
+
}
|
|
885
|
+
function takeRateText(c, locale) {
|
|
886
|
+
if (c.shown <= 0 || c.added > c.shown) return null;
|
|
887
|
+
return new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }).format(c.added / c.shown);
|
|
888
|
+
}
|
|
889
|
+
function reportLineLabel(l) {
|
|
890
|
+
const product = productLabel(l);
|
|
891
|
+
const offer = l.offer_id === null ? REPORT_COPY.fromConfig : `${l.name ?? l.offer_id}${l.removed ? ` ${REPORT_COPY.removed}` : ""}`;
|
|
892
|
+
return { offer, product: l.step === "downsell" ? REPORT_COPY.downsell(product) : product };
|
|
893
|
+
}
|
|
894
|
+
function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
895
|
+
const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
|
|
896
|
+
const [problem, setProblem] = react.useState(null);
|
|
897
|
+
const save = async (value) => {
|
|
898
|
+
if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
|
|
899
|
+
setProblem(null);
|
|
900
|
+
const refused = await onSave(value);
|
|
901
|
+
if (refused === null) onOpenChange(false);
|
|
902
|
+
else setProblem(refused);
|
|
808
903
|
};
|
|
809
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
810
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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) })
|
|
815
915
|
] }),
|
|
816
|
-
/* @__PURE__ */ jsxRuntime.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
const
|
|
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
|
+
}
|
|
958
|
+
function rangeOf(fromDay, toDay) {
|
|
959
|
+
const from = localMidnight(fromDay);
|
|
960
|
+
const to = localMidnight(toDay, 1);
|
|
961
|
+
if (!from || !to || !(from.getTime() < to.getTime()) || to.getTime() - from.getTime() > MAX_SPAN_DAYS * DAY_MS) return null;
|
|
962
|
+
return { from: from.toISOString(), to: to.toISOString() };
|
|
963
|
+
}
|
|
964
|
+
function CountCells({ c }) {
|
|
965
|
+
const rate = takeRateText(c);
|
|
856
966
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
857
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
},
|
|
866
|
-
children: "Mark resolved…"
|
|
867
|
-
}
|
|
868
|
-
),
|
|
869
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !mark.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
870
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
871
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Mark this payment resolved?" }),
|
|
872
|
-
/* @__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." })
|
|
873
|
-
] }),
|
|
874
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
|
|
875
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
|
|
876
|
-
"Type this payment's Vonpay session id to confirm: ",
|
|
877
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.vp_session_id })
|
|
878
|
-
] }),
|
|
879
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) })
|
|
880
|
-
] }),
|
|
881
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
882
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: mark.isPending, children: "Cancel" }),
|
|
883
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
884
|
-
ui.Prompt.Action,
|
|
885
|
-
{
|
|
886
|
-
disabled: mark.isPending || !matches,
|
|
887
|
-
onClick: (e) => {
|
|
888
|
-
e.preventDefault();
|
|
889
|
-
if (!matches) return;
|
|
890
|
-
mark.mutate({ row, confirm_vp_session_id: typed }, {
|
|
891
|
-
onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
|
|
892
|
-
onError: (e2) => {
|
|
893
|
-
const f = postFailure(e2);
|
|
894
|
-
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
|
|
895
|
-
},
|
|
896
|
-
onSettled: () => setOpen(false)
|
|
897
|
-
});
|
|
898
|
-
},
|
|
899
|
-
children: "Mark resolved"
|
|
900
|
-
}
|
|
901
|
-
)
|
|
902
|
-
] })
|
|
903
|
-
] }) })
|
|
967
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.shown }),
|
|
968
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.added }),
|
|
969
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.no_thanks }),
|
|
970
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: c.declined }),
|
|
971
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: rate ?? REPORT_COPY.none }),
|
|
972
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "charged") }),
|
|
973
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "refunded") }),
|
|
974
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: moneyColumn(c.money, "net") })
|
|
904
975
|
] });
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const [
|
|
908
|
-
const
|
|
909
|
-
const
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
|
|
921
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
|
|
976
|
+
}
|
|
977
|
+
function OfferReport({ today = /* @__PURE__ */ new Date() }) {
|
|
978
|
+
const [fromDay, setFromDay] = react.useState(() => dayText(new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29)));
|
|
979
|
+
const [toDay, setToDay] = react.useState(() => dayText(today));
|
|
980
|
+
const [mode, setMode] = react.useState(void 0);
|
|
981
|
+
const range = rangeOf(fromDay, toDay);
|
|
982
|
+
const { data, isLoading, isError } = useOfferReport(range ? { ...range, mode } : null);
|
|
983
|
+
const shownMode = mode ?? data?.mode;
|
|
984
|
+
const viewsMissing = (data?.lines ?? []).some((l) => l.added > l.shown);
|
|
985
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4 px-6 py-4", "data-testid": "vonpay-offer-report", children: [
|
|
986
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: REPORT_COPY.intro }),
|
|
987
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [
|
|
988
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
989
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-report-from", children: REPORT_COPY.from }),
|
|
990
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-report-from", type: "date", value: fromDay, onChange: (e) => setFromDay(e.target.value) })
|
|
922
991
|
] }),
|
|
923
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
924
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
925
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
992
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
993
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-report-to", children: REPORT_COPY.to }),
|
|
994
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-report-to", type: "date", value: toDay, onChange: (e) => setToDay(e.target.value) })
|
|
995
|
+
] }),
|
|
996
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-x-2", role: "group", children: ["live", "test"].map((m) => /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: shownMode === m ? "primary" : "secondary", "aria-pressed": shownMode === m, onClick: () => setMode(m), children: m === "live" ? REPORT_COPY.live : REPORT_COPY.test }, m)) })
|
|
997
|
+
] }),
|
|
998
|
+
!range ? /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", "data-testid": "vonpay-report-bad-range", children: REPORT_COPY.badRange }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) : isError || !data ? /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-report-error", children: REPORT_COPY.loadFailed }) : data.lines.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { "data-testid": "vonpay-report-empty", children: REPORT_COPY.empty }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
999
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1000
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1001
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: REPORT_COPY.offer }),
|
|
1002
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: REPORT_COPY.product }),
|
|
1003
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.shown }),
|
|
1004
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.added }),
|
|
1005
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.noThanks }),
|
|
1006
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.declined }),
|
|
1007
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.takeRate }),
|
|
1008
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.charged }),
|
|
1009
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.refunded }),
|
|
1010
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: REPORT_COPY.net })
|
|
1011
|
+
] }) }),
|
|
1012
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
|
|
1013
|
+
data.lines.map((l) => {
|
|
1014
|
+
const label = reportLineLabel(l);
|
|
1015
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-report-line", children: [
|
|
1016
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: l.step === "downsell" ? "" : label.offer }),
|
|
1017
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: label.product }),
|
|
1018
|
+
/* @__PURE__ */ jsxRuntime.jsx(CountCells, { c: l })
|
|
1019
|
+
] }, `${l.offer_id ?? ""}|${l.variant_id}|${l.step}`);
|
|
1020
|
+
}),
|
|
1021
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-report-total", children: [
|
|
1022
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: REPORT_COPY.total }) }),
|
|
1023
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
|
|
1024
|
+
/* @__PURE__ */ jsxRuntime.jsx(CountCells, { c: data.totals })
|
|
1025
|
+
] })
|
|
1026
|
+
] })
|
|
1027
|
+
] }) }),
|
|
1028
|
+
viewsMissing && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", "data-testid": "vonpay-report-views-missing", children: REPORT_COPY.viewsMissing }),
|
|
1029
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex list-disc flex-col gap-y-1 pl-6", children: [REPORT_COPY.shownNote, REPORT_COPY.takeRateNote, REPORT_COPY.declinedNote, REPORT_COPY.chargedNote, REPORT_COPY.datesNote, REPORT_COPY.heldNote].map((n) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: n }) }, n)) })
|
|
1030
|
+
] })
|
|
939
1031
|
] });
|
|
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" }
|
|
940
1037
|
};
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
const
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
const [
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
]
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1038
|
+
async function fetchNamed(kind, query) {
|
|
1039
|
+
const s = SOURCES[kind];
|
|
1040
|
+
const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
|
|
1041
|
+
const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
|
|
1042
|
+
return (body[s.key] ?? []).flatMap((r) => typeof r.id === "string" ? [{ id: r.id, label: typeof r[s.label] === "string" && plain(r[s.label]) || r.id }] : []);
|
|
1043
|
+
}
|
|
1044
|
+
function Picker({ kind, label, ids, onChange }) {
|
|
1045
|
+
const [q, setQ] = react.useState("");
|
|
1046
|
+
const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
|
|
1047
|
+
const found = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-search", kind, q], queryFn: () => fetchNamed(kind, { q, limit: 8 }), enabled: q.trim().length >= 2, placeholderData: reactQuery.keepPreviousData });
|
|
1048
|
+
const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
|
|
1049
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
|
|
1050
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
|
|
1051
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: ids.map((id) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "bg-ui-bg-subtle flex items-center gap-x-1 rounded-md px-2 py-1", children: [
|
|
1052
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
|
|
1053
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1054
|
+
] }, id)) }),
|
|
1055
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { placeholder: OFFERS_COPY.search, value: q, maxLength: 100, "aria-label": `${label}: ${OFFERS_COPY.search}`, onChange: (e) => setQ(e.target.value) }),
|
|
1056
|
+
(q.trim().length >= 2 ? found.data ?? [] : []).filter((n) => !ids.includes(n.id)).map((n) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "transparent", type: "button", onClick: () => onChange([...ids, n.id]), children: [
|
|
1057
|
+
"+ ",
|
|
1058
|
+
n.label
|
|
1059
|
+
] }, n.id))
|
|
1060
|
+
] });
|
|
1061
|
+
}
|
|
1062
|
+
function rowsOf(rules) {
|
|
1063
|
+
const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
|
|
1064
|
+
return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
|
|
1065
|
+
}
|
|
1066
|
+
function amountText(text) {
|
|
1067
|
+
const t = text.trim().replace(",", ".");
|
|
1068
|
+
if (t === "") return void 0;
|
|
1069
|
+
return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
|
|
1070
|
+
}
|
|
1071
|
+
function rulesFromForm(f) {
|
|
1072
|
+
const out = {};
|
|
1073
|
+
if (f.customers !== "all") out.customers = f.customers;
|
|
1074
|
+
if (f.products.length) out.products = f.products;
|
|
1075
|
+
if (f.collections.length) out.collections = f.collections;
|
|
1076
|
+
if (f.groups.length) out.groups = f.groups;
|
|
1077
|
+
for (const r of f.paid) {
|
|
1078
|
+
const c = r.currency.trim().toLowerCase();
|
|
1079
|
+
if (!c && !r.min.trim() && !r.max.trim()) continue;
|
|
1080
|
+
if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
|
|
1081
|
+
const min = amountText(r.min);
|
|
1082
|
+
const max = amountText(r.max);
|
|
1083
|
+
if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
|
|
1084
|
+
if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
|
|
1085
|
+
if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
|
|
1086
|
+
if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
|
|
1087
|
+
if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
|
|
1088
|
+
}
|
|
1089
|
+
if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
|
|
1090
|
+
return Object.keys(out).length ? out : null;
|
|
1091
|
+
}
|
|
1092
|
+
function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1093
|
+
const unreadable = offer.rules === "unreadable";
|
|
1094
|
+
const start = offer.rules === "unreadable" ? null : offer.rules;
|
|
1095
|
+
const [customers, setCustomers] = react.useState(start?.customers ?? "all");
|
|
1096
|
+
const [products, setProducts] = react.useState(start?.products ?? []);
|
|
1097
|
+
const [collections, setCollections] = react.useState(start?.collections ?? []);
|
|
1098
|
+
const [groups, setGroups] = react.useState(start?.groups ?? []);
|
|
1099
|
+
const [paid, setPaid] = react.useState(rowsOf(start));
|
|
1100
|
+
const [problem, setProblem] = react.useState(null);
|
|
1101
|
+
const save = async () => {
|
|
1102
|
+
const r = rulesFromForm({ customers, products, collections, groups, paid });
|
|
1103
|
+
if (typeof r === "string") return setProblem(r);
|
|
1104
|
+
setProblem(null);
|
|
1105
|
+
const refused = await onSave(r);
|
|
1106
|
+
if (refused === null) onOpenChange(false);
|
|
1107
|
+
else setProblem(refused);
|
|
1108
|
+
};
|
|
1109
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1110
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1111
|
+
OFFERS_COPY.rules,
|
|
1112
|
+
": ",
|
|
1113
|
+
offer.name
|
|
1114
|
+
] }) }),
|
|
1115
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
|
|
1116
|
+
unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
|
|
1117
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
|
|
1118
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1119
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
|
|
1120
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
|
|
1121
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1122
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
|
|
1123
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
|
|
1124
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
|
|
1125
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
|
|
1007
1126
|
] })
|
|
1008
1127
|
] })
|
|
1009
1128
|
] }),
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1129
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
|
|
1130
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
|
|
1131
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
|
|
1132
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
|
|
1133
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
|
|
1134
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
|
|
1135
|
+
paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
|
|
1136
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": "Currency", placeholder: "EUR", value: r.currency, maxLength: 3, onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, currency: e.target.value } : x)) }),
|
|
1137
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtLeast} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtLeast, value: r.min, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, min: e.target.value } : x)) }),
|
|
1138
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtMost} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtMost, value: r.max, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, max: e.target.value } : x)) }),
|
|
1139
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `${OFFERS_COPY.removeCurrency} ${r.currency.toUpperCase()}`.trim(), onClick: () => setPaid(paid.filter((_, j) => j !== i)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1140
|
+
] }, i)),
|
|
1141
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", type: "button", onClick: () => setPaid([...paid, { currency: "", min: "", max: "" }]), children: OFFERS_COPY.addCurrency }) })
|
|
1018
1142
|
] }),
|
|
1019
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
|
|
1025
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
|
|
1026
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
|
|
1027
|
-
] })
|
|
1028
|
-
] }, step.title)),
|
|
1029
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
|
|
1030
|
-
] })
|
|
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 })
|
|
1031
1148
|
] })
|
|
1032
1149
|
] }) });
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const
|
|
1036
|
-
const [
|
|
1037
|
-
const [
|
|
1038
|
-
const [
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
+
] })
|
|
1192
|
+
] }),
|
|
1193
|
+
offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
|
|
1194
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
|
|
1195
|
+
] }),
|
|
1196
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1197
|
+
offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
|
|
1198
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1199
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
|
|
1200
|
+
] })
|
|
1201
|
+
] }) });
|
|
1202
|
+
}
|
|
1203
|
+
const VonpayOffersPage = () => {
|
|
1204
|
+
const { data, isLoading, isError, error: loadError } = useOffers();
|
|
1205
|
+
const create = useCreateOffer();
|
|
1206
|
+
const update = useUpdateOffer();
|
|
1207
|
+
const remove = useDeleteOffer();
|
|
1208
|
+
const reorder = useReorderOffers();
|
|
1209
|
+
const importConfig = useImportOffers();
|
|
1210
|
+
const prompt = ui.usePrompt();
|
|
1211
|
+
const [name, setName] = react.useState("");
|
|
1212
|
+
const [variantId, setVariantId] = react.useState("");
|
|
1213
|
+
const [error, setError] = react.useState(null);
|
|
1214
|
+
const [editing, setEditing] = react.useState(null);
|
|
1215
|
+
const [downsellFor, setDownsellFor] = react.useState(null);
|
|
1216
|
+
const [wordingFor, setWordingFor] = react.useState(null);
|
|
1217
|
+
const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
|
|
1218
|
+
const run = async (work) => {
|
|
1219
|
+
setError(null);
|
|
1220
|
+
try {
|
|
1221
|
+
await work();
|
|
1222
|
+
return true;
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
setError(errorSentence(err));
|
|
1225
|
+
return false;
|
|
1049
1226
|
}
|
|
1050
1227
|
};
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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("");
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
const move = (offers, i, by) => {
|
|
1236
|
+
const ids = offers.map((o) => o.id);
|
|
1237
|
+
[ids[i], ids[i + by]] = [ids[i + by], ids[i]];
|
|
1238
|
+
void run(() => reorder.mutateAsync({ ids }));
|
|
1239
|
+
};
|
|
1240
|
+
const onRemove = async (o) => {
|
|
1241
|
+
if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
|
|
1242
|
+
};
|
|
1243
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
1245
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
|
|
1246
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1247
|
+
OFFERS_COPY.intro,
|
|
1248
|
+
" ",
|
|
1249
|
+
OFFERS_COPY.lag
|
|
1250
|
+
] })
|
|
1251
|
+
] }),
|
|
1252
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs, { defaultValue: "offers", className: "divide-y", children: [
|
|
1253
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs.List, { children: [
|
|
1254
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "offers", children: REPORT_COPY.tabOffers }),
|
|
1255
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "results", children: REPORT_COPY.tabResults })
|
|
1256
|
+
] }) }),
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "results", children: /* @__PURE__ */ jsxRuntime.jsx(OfferReport, {}) }),
|
|
1258
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "offers", className: "divide-y", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError || !data ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-load-error", children: errorSentence(loadError, OFFERS_COPY.loadFailed) }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1259
|
+
!data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
|
|
1260
|
+
data.source === "config" && data.config.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 py-4", "data-testid": "vonpay-offers-from-config", children: [
|
|
1261
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
|
|
1262
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-disc pl-6", children: data.config.map((c) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: productLabel(c) }) }, c.variant_id)) }),
|
|
1263
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: busy, onClick: () => void run(() => importConfig.mutateAsync(void 0)), children: OFFERS_COPY.importButton }) })
|
|
1057
1264
|
] }),
|
|
1058
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "
|
|
1059
|
-
|
|
1060
|
-
{
|
|
1061
|
-
size: "small",
|
|
1062
|
-
value: filter,
|
|
1063
|
-
onValueChange: (v) => {
|
|
1064
|
-
setFilter(v);
|
|
1065
|
-
setPageIndex(0);
|
|
1066
|
-
},
|
|
1067
|
-
children: [
|
|
1068
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1069
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: ["open", "resolved", "all"].map((f) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: f, children: FILTER_LABEL[f] }, f)) })
|
|
1070
|
-
]
|
|
1071
|
-
}
|
|
1072
|
-
) })
|
|
1073
|
-
] }),
|
|
1074
|
-
data && !data.scan_complete && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: "This list may be incomplete: the store has more records than one page load can check." }) }),
|
|
1075
|
-
isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
|
|
1076
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
|
|
1077
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
|
|
1078
|
-
] }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-3 px-6 py-6", "aria-busy": "true", "aria-label": "Loading held payments", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-6 w-full" }, i)) }) : count === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-10", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: filter === "resolved" ? "Nothing resolved yet." : "No held payments. When a buyer is charged but the order can't be completed safely, it appears here." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1079
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1265
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-error", children: error }) }),
|
|
1266
|
+
data.offers.length === 0 ? (data.source === "admin" || data.config.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: data.source === "admin" ? OFFERS_COPY.adminEmpty : OFFERS_COPY.configEmpty }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
1080
1267
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1081
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "
|
|
1082
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "
|
|
1083
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children:
|
|
1084
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children:
|
|
1085
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children:
|
|
1086
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children:
|
|
1268
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
|
|
1269
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
|
|
1272
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
|
|
1273
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
|
|
1274
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
|
|
1087
1275
|
] }) }),
|
|
1088
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.
|
|
1089
|
-
ui.Table.
|
|
1090
|
-
{
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
children:
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1102
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1103
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1104
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
|
|
1105
|
-
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
|
|
1106
|
-
] }) })
|
|
1107
|
-
]
|
|
1108
|
-
},
|
|
1109
|
-
keyOf(row)
|
|
1110
|
-
)) })
|
|
1276
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
|
|
1277
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
|
|
1278
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.product_title === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: OFFERS_COPY.missingVariant }) : productLabel(o) }),
|
|
1279
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.rules}: ${o.name}`, disabled: busy, onClick: () => setEditing(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: o.rules === "unreadable" ? "text-ui-fg-error" : void 0, children: rulesSummary(o.rules) }) }) }),
|
|
1280
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.downsell}: ${o.name}`, disabled: busy, onClick: () => setDownsellFor(o), children: o.downsell_variant_id === null ? OFFERS_COPY.downsellNone : o.downsell_product_title === null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-error", children: OFFERS_COPY.downsellMissing }) : productLabel({ product_title: o.downsell_product_title, variant_title: o.downsell_variant_title, variant_id: o.downsell_variant_id }) }) }),
|
|
1281
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.wording}: ${o.name}`, disabled: busy, onClick: () => setWordingFor(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `max-w-[16rem] truncate ${o.copy === "unreadable" ? "text-ui-fg-error" : ""}`, children: wordingSummary(o.copy) }) }) }),
|
|
1282
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { "aria-label": `${OFFERS_COPY.shown}: ${o.name}`, checked: o.enabled, disabled: busy, onCheckedChange: (on) => void run(() => update.mutateAsync({ id: o.id, enabled: on })) }) }),
|
|
1283
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
|
|
1284
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.up}: ${o.name}`, disabled: busy || i === 0, onClick: () => move(data.offers, i, -1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpMini, {}) }),
|
|
1285
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.down}: ${o.name}`, disabled: busy || i === data.offers.length - 1, onClick: () => move(data.offers, i, 1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowDownMini, {}) }),
|
|
1286
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.remove}: ${o.name}`, disabled: busy, onClick: () => void onRemove(o), children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}) })
|
|
1287
|
+
] }) })
|
|
1288
|
+
] }, o.id)) })
|
|
1111
1289
|
] }) }),
|
|
1112
|
-
|
|
1113
|
-
|
|
1290
|
+
downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1291
|
+
OfferDownsellDrawer,
|
|
1114
1292
|
{
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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,
|
|
1328
|
+
{
|
|
1329
|
+
offer: editing,
|
|
1330
|
+
open: editing !== null,
|
|
1331
|
+
busy,
|
|
1332
|
+
onOpenChange: (o) => !o && setEditing(null),
|
|
1333
|
+
onSave: async (rules) => {
|
|
1334
|
+
try {
|
|
1335
|
+
await update.mutateAsync({ id: editing.id, rules });
|
|
1336
|
+
return null;
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
return errorSentence(err);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
editing.id
|
|
1343
|
+
),
|
|
1344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
|
|
1345
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
|
|
1346
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
|
|
1347
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1348
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
|
|
1349
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
|
|
1350
|
+
] }),
|
|
1351
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1352
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
|
|
1353
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
|
|
1354
|
+
] })
|
|
1355
|
+
] }),
|
|
1356
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
|
|
1357
|
+
] })
|
|
1358
|
+
] }) })
|
|
1359
|
+
] })
|
|
1129
1360
|
] });
|
|
1130
1361
|
};
|
|
1131
|
-
const config$
|
|
1132
|
-
label: "
|
|
1133
|
-
icon: icons.
|
|
1362
|
+
const config$2 = adminSdk.defineRouteConfig({
|
|
1363
|
+
label: "One-click offers",
|
|
1364
|
+
icon: icons.Sparkles,
|
|
1134
1365
|
nested: "/orders"
|
|
1135
1366
|
});
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1367
|
+
function useSetupCheck() {
|
|
1368
|
+
return reactQuery.useMutation({
|
|
1369
|
+
mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
|
|
1370
|
+
retry: false
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
function useMoveToHeld() {
|
|
1374
|
+
const qc = reactQuery.useQueryClient();
|
|
1375
|
+
return reactQuery.useMutation({
|
|
1376
|
+
mutationFn: (m) => sdk.client.fetch("/admin/vonpay/setup-check/move-to-held", { method: "POST", body: { vp_session_id: m.vp_session_id, medusa_payment_session_id: m.medusa_payment_session_id } }),
|
|
1377
|
+
retry: false,
|
|
1378
|
+
onSettled: () => qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] })
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
|
|
1382
|
+
const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
|
|
1383
|
+
function uniqueMoves(moves) {
|
|
1384
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1385
|
+
return (moves ?? []).filter((m) => !seen.has(m.vp_session_id) && (seen.add(m.vp_session_id), true));
|
|
1386
|
+
}
|
|
1387
|
+
const MoveRow = ({ move }) => {
|
|
1388
|
+
const mutation = useMoveToHeld();
|
|
1389
|
+
const [result, setResult] = react.useState(null);
|
|
1390
|
+
const done = result !== null && result.variant !== "error";
|
|
1391
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-center sm:gap-x-3", children: [
|
|
1392
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-mono", children: move.vp_session_id }),
|
|
1393
|
+
!done && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1394
|
+
ui.Button,
|
|
1395
|
+
{
|
|
1396
|
+
size: "small",
|
|
1397
|
+
variant: "secondary",
|
|
1398
|
+
isLoading: mutation.isPending,
|
|
1399
|
+
onClick: () => mutation.mutate(move, {
|
|
1400
|
+
onSuccess: (o) => setResult(moveOutcomeCopy(o)),
|
|
1401
|
+
onError: () => setResult({ variant: "error", text: "That couldn't be done. Nothing was changed — try again." })
|
|
1402
|
+
}),
|
|
1403
|
+
children: "Move to Held payments"
|
|
1404
|
+
}
|
|
1405
|
+
),
|
|
1406
|
+
result && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: result.variant === "error" ? "text-ui-fg-error" : "text-ui-fg-subtle", children: result.text })
|
|
1407
|
+
] });
|
|
1408
|
+
};
|
|
1409
|
+
const SetupCheckPanel = () => {
|
|
1410
|
+
const check = useSetupCheck();
|
|
1411
|
+
const items = check.data?.items ?? [];
|
|
1412
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1413
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
|
|
1414
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1415
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Vonpay setup" }),
|
|
1416
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Checks your Vonpay key, your notification endpoint and the Medusa setup Vonpay needs. With test keys it also sends one test notification (it never creates a payment); that usually takes under 30 seconds." })
|
|
1417
|
+
] }),
|
|
1418
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: check.isPending, onClick: () => check.mutate(), children: "Check Vonpay setup" })
|
|
1419
|
+
] }),
|
|
1420
|
+
check.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The check could not run. Try again, or run npx vonpay-medusa-check on the server." }) }),
|
|
1421
|
+
check.data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
|
|
1422
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: !check.data.ok ? "error" : check.data.complete ? "success" : "warning", children: !check.data.ok ? "Something needs fixing — see the items marked Fix." : check.data.complete ? "No problems found." : `No failures, but ${check.data.unverified} ${check.data.unverified === 1 ? "check" : "checks"} could not run — see the items marked Not checked.` }),
|
|
1423
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-y-3", "aria-label": "Setup check results", children: items.map((i, n) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-start sm:gap-x-3", children: [
|
|
1424
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
|
|
1425
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
|
|
1426
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
|
|
1427
|
+
i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix }),
|
|
1428
|
+
uniqueMoves(i.moves).length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "mt-2 flex flex-col gap-y-2", "aria-label": "Paid checkouts with no order", children: uniqueMoves(i.moves).map((m) => /* @__PURE__ */ jsxRuntime.jsx(MoveRow, { move: m }, m.vp_session_id)) })
|
|
1429
|
+
] })
|
|
1430
|
+
] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
|
|
1431
|
+
] })
|
|
1432
|
+
] });
|
|
1433
|
+
};
|
|
1434
|
+
const relative = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
1435
|
+
const UNITS = [
|
|
1436
|
+
["day", 864e5],
|
|
1437
|
+
["hour", 36e5],
|
|
1438
|
+
["minute", 6e4]
|
|
1439
|
+
];
|
|
1440
|
+
function since(iso) {
|
|
1441
|
+
if (!iso) return "Unknown";
|
|
1442
|
+
const diff = Date.parse(iso) - Date.now();
|
|
1443
|
+
for (const [unit, ms] of UNITS) {
|
|
1444
|
+
if (Math.abs(diff) >= ms) return relative.format((diff - diff % ms) / ms, unit);
|
|
1445
|
+
}
|
|
1446
|
+
return "just now";
|
|
1447
|
+
}
|
|
1448
|
+
const STATUS_COLOR = {
|
|
1449
|
+
open: "orange",
|
|
1450
|
+
nothing_left: "orange",
|
|
1451
|
+
refund_pending: "blue",
|
|
1452
|
+
refund_unknown: "red",
|
|
1453
|
+
refund_sending: "red",
|
|
1454
|
+
resolved: "green"
|
|
1197
1455
|
};
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
const i = m.indexOf(": ");
|
|
1202
|
-
return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
|
|
1456
|
+
const UNKNOWN_STATUS_LABEL = "Status unknown — check in Vonpay";
|
|
1457
|
+
function isKnownStatus(status) {
|
|
1458
|
+
return Object.prototype.hasOwnProperty.call(STATUS_LABEL, status);
|
|
1203
1459
|
}
|
|
1204
|
-
function
|
|
1205
|
-
|
|
1206
|
-
return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
|
|
1460
|
+
function statusLabel(status) {
|
|
1461
|
+
return isKnownStatus(status) ? STATUS_LABEL[status] : UNKNOWN_STATUS_LABEL;
|
|
1207
1462
|
}
|
|
1208
|
-
function
|
|
1209
|
-
|
|
1210
|
-
if (!copy) return OFFERS_COPY.wordingDefault;
|
|
1211
|
-
return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
|
|
1463
|
+
function statusColor(status) {
|
|
1464
|
+
return isKnownStatus(status) && STATUS_COLOR[status] || "grey";
|
|
1212
1465
|
}
|
|
1213
|
-
function
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (rules.customers === "guests") parts.push("Guests");
|
|
1218
|
-
if (rules.customers === "signed_in") parts.push("Signed-in");
|
|
1219
|
-
if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
|
|
1220
|
-
if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
|
|
1221
|
-
if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
|
|
1222
|
-
for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
|
|
1223
|
-
for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
|
|
1224
|
-
return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
|
|
1466
|
+
function routeRefusal(err) {
|
|
1467
|
+
const status = err?.status;
|
|
1468
|
+
const message = err?.message;
|
|
1469
|
+
return typeof status === "number" && status >= 400 && status < 500 && typeof message === "string" && message !== "" ? message : null;
|
|
1225
1470
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const refused = await onSave(value);
|
|
1233
|
-
if (refused === null) onOpenChange(false);
|
|
1234
|
-
else setProblem(refused);
|
|
1471
|
+
function postFailure(err) {
|
|
1472
|
+
const refusal = routeRefusal(err);
|
|
1473
|
+
if (refusal) return { variant: "error", text: refusal };
|
|
1474
|
+
return {
|
|
1475
|
+
variant: "warning",
|
|
1476
|
+
text: "The result is unknown. Reload the list before refunding another way — pressing Refund again is safe: it reuses the same key, so Vonpay can't refund twice."
|
|
1235
1477
|
};
|
|
1236
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1237
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1238
|
-
OFFERS_COPY.downsell,
|
|
1239
|
-
": ",
|
|
1240
|
-
offer.name
|
|
1241
|
-
] }) }),
|
|
1242
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
|
|
1243
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
|
|
1244
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1245
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
|
|
1246
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
|
|
1247
|
-
] }),
|
|
1248
|
-
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
|
|
1249
|
-
] }),
|
|
1250
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1251
|
-
offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
|
|
1252
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1253
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
|
|
1254
|
-
] })
|
|
1255
|
-
] }) });
|
|
1256
1478
|
}
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1479
|
+
const Mono = ({ value, label }) => value ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-1", children: [
|
|
1480
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small font-mono truncate max-w-[180px]", title: value, children: value }),
|
|
1481
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Copy, { content: value, variant: "mini", "aria-label": `Copy ${label}`, onClick: (e) => e.stopPropagation() })
|
|
1482
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: "Unreadable" });
|
|
1483
|
+
const ReasonBadge = ({ reason }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: reason === "unrecognised" ? "grey" : "orange", children: reasonCopy(reason).label });
|
|
1484
|
+
const RowStatus = ({ status }) => /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "whitespace-nowrap", color: statusColor(status), children: statusLabel(status) });
|
|
1485
|
+
const RefundAction = ({ row, onResult }) => {
|
|
1486
|
+
const [open, setOpen] = react.useState(false);
|
|
1487
|
+
const preview = useRefundPreview(row, open);
|
|
1488
|
+
const refund = useRefundHold();
|
|
1489
|
+
const p = preview.data;
|
|
1490
|
+
const description = preview.isLoading ? "Checking the payment with Vonpay…" : preview.isError ? (
|
|
1491
|
+
routeRefusal(preview.error) ?? "Couldn't check the payment with Vonpay. Nothing was refunded."
|
|
1492
|
+
) : p && p.refundable ? refundConfirmText(p) : outcomeCopy({ outcome: "refused", code: p?.code ?? null, recorded: true }).text;
|
|
1493
|
+
const confirm = () => {
|
|
1494
|
+
if (!p || !p.refundable) return;
|
|
1495
|
+
refund.mutate(
|
|
1496
|
+
{ row, amount_minor: p.amount_minor, currency: p.currency },
|
|
1497
|
+
{
|
|
1498
|
+
onSuccess: (o) => onResult(outcomeCopy(o)),
|
|
1499
|
+
onError: (e) => onResult(postFailure(e)),
|
|
1500
|
+
onSettled: () => setOpen(false)
|
|
1501
|
+
}
|
|
1502
|
+
);
|
|
1503
|
+
};
|
|
1504
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1505
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", onClick: () => setOpen(true), children: "Refund in full…" }),
|
|
1506
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !refund.isPending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1507
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1508
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Refund this payment in full?" }),
|
|
1509
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: description }),
|
|
1510
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-refund-subscription-note", children: SUBSCRIPTION_REFUND_NOTE })
|
|
1511
|
+
] }),
|
|
1512
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1513
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: refund.isPending, children: "Cancel" }),
|
|
1514
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1515
|
+
ui.Prompt.Action,
|
|
1516
|
+
{
|
|
1517
|
+
disabled: !p || !p.refundable || refund.isPending,
|
|
1518
|
+
onClick: (e) => {
|
|
1519
|
+
e.preventDefault();
|
|
1520
|
+
confirm();
|
|
1521
|
+
},
|
|
1522
|
+
children: p && p.refundable ? refundActionLabel(p) : "Refund"
|
|
1523
|
+
}
|
|
1524
|
+
)
|
|
1525
|
+
] })
|
|
1526
|
+
] }) })
|
|
1527
|
+
] });
|
|
1261
1528
|
};
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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) })
|
|
1576
|
+
] }),
|
|
1577
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1578
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: mark.isPending, children: "Cancel" }),
|
|
1579
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1580
|
+
ui.Prompt.Action,
|
|
1581
|
+
{
|
|
1582
|
+
disabled: mark.isPending || !matches,
|
|
1583
|
+
onClick: (e) => {
|
|
1584
|
+
e.preventDefault();
|
|
1585
|
+
if (!matches) return;
|
|
1586
|
+
mark.mutate({ row, confirm_vp_session_id: typed }, {
|
|
1587
|
+
onSuccess: (o) => onResult(resolveOutcomeCopy(o)),
|
|
1588
|
+
onError: (e2) => {
|
|
1589
|
+
const f = postFailure(e2);
|
|
1590
|
+
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm that it was saved. Reload the list." });
|
|
1591
|
+
},
|
|
1592
|
+
onSettled: () => setOpen(false)
|
|
1593
|
+
});
|
|
1594
|
+
},
|
|
1595
|
+
children: "Mark resolved"
|
|
1596
|
+
}
|
|
1597
|
+
)
|
|
1598
|
+
] })
|
|
1599
|
+
] }) })
|
|
1600
|
+
] });
|
|
1601
|
+
};
|
|
1602
|
+
const localTime = (iso) => new Date(iso).toLocaleString();
|
|
1603
|
+
const TIMER_MAX_MS = 2147483647;
|
|
1604
|
+
const REPOLL_MS = 15e3;
|
|
1605
|
+
const REPOLL_MAX = 8;
|
|
1606
|
+
const ChargeActions = ({ row, listAt, onResult }) => {
|
|
1607
|
+
const [answered, setAnswer] = react.useState(null);
|
|
1608
|
+
const answer = answered && !(listAt > answered.receivedAt) ? answered : null;
|
|
1609
|
+
const [asking, setAsking] = react.useState(null);
|
|
1610
|
+
const [typed, setTyped] = react.useState("");
|
|
1611
|
+
const [checked, setChecked] = react.useState(false);
|
|
1612
|
+
const act = useChargeAction();
|
|
1613
|
+
const qc = reactQuery.useQueryClient();
|
|
1614
|
+
const rowSays = JSON.stringify([row.held_at, row.actions ?? null, row.hint ?? null]);
|
|
1615
|
+
react.useEffect(() => setAnswer(null), [rowSays]);
|
|
1616
|
+
const actions = answer?.actions ?? row.actions ?? [];
|
|
1617
|
+
const hint = answer ? answer.hint : row.hint ?? null;
|
|
1618
|
+
const closedUntil = actions.filter((a) => !a.available && a.available_at).map((a) => Date.parse(a.available_at)).filter((t) => Number.isFinite(t));
|
|
1619
|
+
const opensAt = closedUntil.filter((t) => t > Date.now());
|
|
1620
|
+
const nextOpen = opensAt.length > 0 ? Math.min(...opensAt) : null;
|
|
1621
|
+
react.useEffect(() => {
|
|
1622
|
+
if (nextOpen === null) return;
|
|
1623
|
+
const timer = setTimeout(() => {
|
|
1624
|
+
setAnswer(null);
|
|
1625
|
+
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
1626
|
+
}, Math.min(TIMER_MAX_MS, Math.max(0, nextOpen - Date.now()) + 1e3));
|
|
1627
|
+
return () => clearTimeout(timer);
|
|
1628
|
+
}, [nextOpen, qc]);
|
|
1629
|
+
const overdue = closedUntil.filter((t) => t <= Date.now());
|
|
1630
|
+
const pastDue = overdue.length > 0 ? Math.min(...overdue) : null;
|
|
1631
|
+
const repolls = react.useRef(0);
|
|
1632
|
+
react.useEffect(() => {
|
|
1633
|
+
if (pastDue === null || repolls.current >= REPOLL_MAX) return;
|
|
1634
|
+
const timer = setTimeout(() => {
|
|
1635
|
+
repolls.current += 1;
|
|
1636
|
+
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
1637
|
+
}, REPOLL_MS);
|
|
1638
|
+
return () => clearTimeout(timer);
|
|
1639
|
+
}, [pastDue, listAt, qc]);
|
|
1640
|
+
const run = (a) => act.mutate(
|
|
1641
|
+
{ row, action: a.id, ...a.confirm === "type_order_id" ? { typed_order_id: typed, checked_in_vonpay: checked } : {} },
|
|
1642
|
+
{
|
|
1643
|
+
onSuccess: (o) => {
|
|
1644
|
+
if (o.recorded === false || o.outcome === "not_saved") {
|
|
1645
|
+
setAnswer({ ...o, actions: [], hint: null, receivedAt: Date.now() });
|
|
1646
|
+
onResult({ variant: "warning", text: o.outcome === "not_saved" && typeof o.text === "string" && o.text ? o.text : NOT_SAVED_TEXT });
|
|
1647
|
+
void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
setAnswer({ ...o, receivedAt: Date.now() });
|
|
1651
|
+
onResult({ variant: o.tone, text: fillTime(o.text, o.at, localTime) });
|
|
1652
|
+
},
|
|
1653
|
+
onError: (e) => {
|
|
1654
|
+
const f = postFailure(e);
|
|
1655
|
+
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
|
|
1656
|
+
},
|
|
1657
|
+
onSettled: () => setAsking(null)
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
const typedOk = !asking || asking.confirm !== "type_order_id" || typed.trim() === row.order_id && checked;
|
|
1661
|
+
const inputId = `vonpay-charge-confirm-${row.payment_collection_id}-${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}-${row.watch_id ?? ""}`;
|
|
1662
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1663
|
+
hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: fillTime(hint.text, hint.at, localTime) }),
|
|
1664
|
+
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1665
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1666
|
+
ui.Button,
|
|
1667
|
+
{
|
|
1668
|
+
size: "small",
|
|
1669
|
+
variant: a.id === "check" ? "primary" : "secondary",
|
|
1670
|
+
disabled: !a.available || act.isPending,
|
|
1671
|
+
isLoading: act.isPending && (asking?.id === a.id || a.id === "check" && !asking),
|
|
1672
|
+
onClick: () => {
|
|
1673
|
+
if (!a.available) return;
|
|
1674
|
+
if (!a.confirm) {
|
|
1675
|
+
run(a);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
setTyped("");
|
|
1679
|
+
setChecked(false);
|
|
1680
|
+
setAsking(a);
|
|
1681
|
+
},
|
|
1682
|
+
children: a.label
|
|
1683
|
+
}
|
|
1684
|
+
),
|
|
1685
|
+
!a.available && a.available_at && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
|
|
1686
|
+
"Opens ",
|
|
1687
|
+
localTime(a.available_at)
|
|
1688
|
+
] })
|
|
1689
|
+
] }, a.id)) }),
|
|
1690
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open: asking !== null, onOpenChange: (o) => !act.isPending && !o && setAsking(null), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1691
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1692
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Title, { children: [
|
|
1693
|
+
asking?.label.replace(/…$/, ""),
|
|
1694
|
+
"?"
|
|
1695
|
+
] }),
|
|
1696
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: asking?.confirm_text })
|
|
1697
|
+
] }),
|
|
1698
|
+
asking?.confirm === "type_order_id" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
|
|
1699
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
|
|
1700
|
+
"Type the order id to confirm: ",
|
|
1701
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.order_id })
|
|
1702
|
+
] }),
|
|
1703
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) }),
|
|
1704
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-x-2", children: [
|
|
1705
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked, onChange: (e) => setChecked(e.target.checked) }),
|
|
1706
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small", children: "I checked this order in the Vonpay dashboard" })
|
|
1707
|
+
] })
|
|
1708
|
+
] }),
|
|
1709
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1710
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: act.isPending, children: "Cancel" }),
|
|
1711
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1712
|
+
ui.Prompt.Action,
|
|
1713
|
+
{
|
|
1714
|
+
disabled: act.isPending || !typedOk,
|
|
1715
|
+
onClick: (e) => {
|
|
1716
|
+
e.preventDefault();
|
|
1717
|
+
if (asking && typedOk) run(asking);
|
|
1718
|
+
},
|
|
1719
|
+
children: asking?.label.replace(/…$/, "")
|
|
1720
|
+
}
|
|
1721
|
+
)
|
|
1722
|
+
] })
|
|
1723
|
+
] }) })
|
|
1284
1724
|
] });
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
}
|
|
1295
|
-
function rulesFromForm(f) {
|
|
1296
|
-
const out = {};
|
|
1297
|
-
if (f.customers !== "all") out.customers = f.customers;
|
|
1298
|
-
if (f.products.length) out.products = f.products;
|
|
1299
|
-
if (f.collections.length) out.collections = f.collections;
|
|
1300
|
-
if (f.groups.length) out.groups = f.groups;
|
|
1301
|
-
for (const r of f.paid) {
|
|
1302
|
-
const c = r.currency.trim().toLowerCase();
|
|
1303
|
-
if (!c && !r.min.trim() && !r.max.trim()) continue;
|
|
1304
|
-
if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
|
|
1305
|
-
const min = amountText(r.min);
|
|
1306
|
-
const max = amountText(r.max);
|
|
1307
|
-
if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
|
|
1308
|
-
if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
|
|
1309
|
-
if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
|
|
1310
|
-
if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
|
|
1311
|
-
if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
|
|
1312
|
-
}
|
|
1313
|
-
if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
|
|
1314
|
-
return Object.keys(out).length ? out : null;
|
|
1315
|
-
}
|
|
1316
|
-
function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1317
|
-
const unreadable = offer.rules === "unreadable";
|
|
1318
|
-
const start = offer.rules === "unreadable" ? null : offer.rules;
|
|
1319
|
-
const [customers, setCustomers] = react.useState(start?.customers ?? "all");
|
|
1320
|
-
const [products, setProducts] = react.useState(start?.products ?? []);
|
|
1321
|
-
const [collections, setCollections] = react.useState(start?.collections ?? []);
|
|
1322
|
-
const [groups, setGroups] = react.useState(start?.groups ?? []);
|
|
1323
|
-
const [paid, setPaid] = react.useState(rowsOf(start));
|
|
1324
|
-
const [problem, setProblem] = react.useState(null);
|
|
1325
|
-
const save = async () => {
|
|
1326
|
-
const r = rulesFromForm({ customers, products, collections, groups, paid });
|
|
1327
|
-
if (typeof r === "string") return setProblem(r);
|
|
1328
|
-
setProblem(null);
|
|
1329
|
-
const refused = await onSave(r);
|
|
1330
|
-
if (refused === null) onOpenChange(false);
|
|
1331
|
-
else setProblem(refused);
|
|
1725
|
+
};
|
|
1726
|
+
const WalletHoldAction = ({ row, kind, onResult }) => {
|
|
1727
|
+
const [open, setOpen] = react.useState(false);
|
|
1728
|
+
const release = useReleaseHold();
|
|
1729
|
+
const complete = useCompleteHold();
|
|
1730
|
+
const pending = release.isPending || complete.isPending;
|
|
1731
|
+
const text = kind === "release" ? { button: "Release…", title: "Release this held payment?", body: "The hold is cancelled at Vonpay and drops off the buyer's card. Nothing was taken, so nothing is refunded. The buyer can't complete an order with this payment afterwards.", action: "Release" } : { button: "Complete the order…", title: "Create the order from this payment?", body: "Medusa completes the buyer's checkout with this held payment and creates the order; the payment is then taken with the order. Do this only if you intend to fulfil it.", action: "Complete the order" };
|
|
1732
|
+
const fail = (e2) => {
|
|
1733
|
+
const f = postFailure(e2);
|
|
1734
|
+
onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
|
|
1332
1735
|
};
|
|
1333
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
1334
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1736
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1737
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: kind === "release" ? "secondary" : "primary", onClick: () => setOpen(true), children: text.button }),
|
|
1738
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open, onOpenChange: (o) => !pending && setOpen(o), variant: "confirmation", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1739
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1740
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: text.title }),
|
|
1741
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: text.body })
|
|
1742
|
+
] }),
|
|
1743
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1744
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: pending, children: "Cancel" }),
|
|
1745
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1746
|
+
ui.Prompt.Action,
|
|
1747
|
+
{
|
|
1748
|
+
disabled: pending,
|
|
1749
|
+
onClick: (e) => {
|
|
1750
|
+
e.preventDefault();
|
|
1751
|
+
if (kind === "release") release.mutate({ row }, { onSuccess: (o) => onResult(releaseOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
|
|
1752
|
+
else complete.mutate({ row }, { onSuccess: (o) => onResult(completeOutcomeCopy(o)), onError: fail, onSettled: () => setOpen(false) });
|
|
1753
|
+
},
|
|
1754
|
+
children: text.action
|
|
1755
|
+
}
|
|
1756
|
+
)
|
|
1757
|
+
] })
|
|
1758
|
+
] }) })
|
|
1759
|
+
] });
|
|
1760
|
+
};
|
|
1761
|
+
const HoldDrawer = ({ row, listAt, onClose }) => {
|
|
1762
|
+
const copy = row ? reasonCopy(row.reason) : void 0;
|
|
1763
|
+
const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
|
|
1764
|
+
const showRefund = !!row && known && canRefund(row);
|
|
1765
|
+
const showMark = !!row && known && canMarkResolved(row);
|
|
1766
|
+
const showRelease = !!row && known && canRelease(row);
|
|
1767
|
+
const showComplete = !!row && known && canComplete(row);
|
|
1768
|
+
const showOpenOrder = !!row && known && canOpenOrder(row);
|
|
1769
|
+
const steps = row ? resolutionSteps(known ? row : { ...row, reason: "unrecognised" }) : [];
|
|
1770
|
+
const [result, setResult] = react.useState(null);
|
|
1771
|
+
const close = () => {
|
|
1772
|
+
setResult(null);
|
|
1773
|
+
onClose();
|
|
1774
|
+
};
|
|
1775
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
1776
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
|
|
1777
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
|
|
1778
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: row && isChargeRow(row) ? void 0 : copy?.what })
|
|
1779
|
+
] }),
|
|
1780
|
+
row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
|
|
1781
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
|
|
1782
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Status" }),
|
|
1783
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
|
|
1784
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Held since" }),
|
|
1785
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown" }),
|
|
1786
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Where" }),
|
|
1787
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: SOURCE_LABEL[row.source] ?? row.source }),
|
|
1788
|
+
row.retry_until && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1789
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Retrying until" }),
|
|
1790
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "txt-compact-small", children: new Date(row.retry_until).toLocaleString() })
|
|
1791
|
+
] }),
|
|
1792
|
+
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1793
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Evidence" }),
|
|
1794
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "flex flex-col gap-y-1", children: [
|
|
1795
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT }),
|
|
1796
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: FROM_BUYER_CHECKOUT_NOTE })
|
|
1797
|
+
] })
|
|
1798
|
+
] }),
|
|
1799
|
+
row.order_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1800
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
|
|
1801
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
|
|
1802
|
+
] }),
|
|
1803
|
+
row.vp_payment_intent_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1804
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay payment" }),
|
|
1805
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_payment_intent_id, label: "Vonpay payment id" }) })
|
|
1806
|
+
] }),
|
|
1807
|
+
row.vp_refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1808
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
|
|
1809
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": "held-row-refund", children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_refund_id, label: "Vonpay refund id" }) })
|
|
1810
|
+
] }),
|
|
1811
|
+
row.subscription_id !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1812
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay subscription" }),
|
|
1813
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": "held-row-subscription", children: row.subscription_id ? /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.subscription_id, label: "Vonpay subscription id" }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: row.subscription_id_note ?? SUBSCRIPTION_NOT_NAMED_TEXT.unknown }) })
|
|
1814
|
+
] }),
|
|
1815
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
|
|
1816
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1817
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
|
|
1818
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1819
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Payment collection" }),
|
|
1820
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.payment_collection_id, label: "payment collection id" }) }),
|
|
1821
|
+
row.resolution && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1822
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: row.status === "resolved" ? "Resolved" : "Last action" }),
|
|
1823
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
|
|
1824
|
+
row.resolution.how ? `${RESOLVED_HOW_LABEL[row.resolution.how] ?? row.resolution.how} · ` : "",
|
|
1825
|
+
new Date(row.resolution.at).toLocaleString(),
|
|
1826
|
+
" · by ",
|
|
1827
|
+
actorLabel(row.resolution)
|
|
1828
|
+
] }),
|
|
1829
|
+
row.resolution.refund_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1830
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay refund" }),
|
|
1831
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.resolution.refund_id, label: "Vonpay refund id" }) })
|
|
1832
|
+
] }),
|
|
1833
|
+
row.resolution.prior && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1834
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Before that" }),
|
|
1835
|
+
/* @__PURE__ */ jsxRuntime.jsxs("dd", { className: "txt-compact-small", children: [
|
|
1836
|
+
row.resolution.prior.how ? `${RESOLVED_HOW_LABEL[row.resolution.prior.how] ?? row.resolution.prior.how}` : statusLabel(row.resolution.prior.state),
|
|
1837
|
+
` · ${new Date(row.resolution.prior.at).toLocaleString()} · by ${actorLabel(row.resolution.prior)}`
|
|
1838
|
+
] })
|
|
1350
1839
|
] })
|
|
1351
1840
|
] })
|
|
1352
1841
|
] }),
|
|
1353
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1354
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1355
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1356
|
-
|
|
1357
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1358
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtMost} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtMost, value: r.max, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, max: e.target.value } : x)) }),
|
|
1363
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `${OFFERS_COPY.removeCurrency} ${r.currency.toUpperCase()}`.trim(), onClick: () => setPaid(paid.filter((_, j) => j !== i)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1364
|
-
] }, i)),
|
|
1365
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", type: "button", onClick: () => setPaid([...paid, { currency: "", min: "", max: "" }]), children: OFFERS_COPY.addCurrency }) })
|
|
1842
|
+
result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
|
|
1843
|
+
closedMoneyNote(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: closedMoneyNote(row) }),
|
|
1844
|
+
(showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
1845
|
+
showOpenOrder && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: orderPath(row.order_id), children: "Open order" }) }),
|
|
1846
|
+
showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
|
|
1847
|
+
showRelease && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "release", onResult: setResult }),
|
|
1848
|
+
showRefund && /* @__PURE__ */ jsxRuntime.jsx(RefundAction, { row, onResult: setResult }),
|
|
1849
|
+
showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
|
|
1850
|
+
showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
|
|
1366
1851
|
] }),
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1852
|
+
row && isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ChargeActions, { row, listAt, onResult: setResult }, `${row.payment_collection_id}:${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}:${row.watch_id ?? ""}`),
|
|
1853
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
|
|
1854
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
|
|
1855
|
+
steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1856
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: step.title }),
|
|
1857
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: step.body }),
|
|
1858
|
+
step.command && /* @__PURE__ */ jsxRuntime.jsxs(ui.CodeBlock, { snippets: [{ label: "Terminal", language: "bash", code: step.command, hideLineNumbers: true }], children: [
|
|
1859
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Header, {}),
|
|
1860
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
|
|
1861
|
+
] })
|
|
1862
|
+
] }, step.title)),
|
|
1863
|
+
!isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
|
|
1864
|
+
] })
|
|
1372
1865
|
] })
|
|
1373
1866
|
] }) });
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
const [
|
|
1378
|
-
const [
|
|
1379
|
-
const [
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1867
|
+
};
|
|
1868
|
+
const FILTER_LABEL = { open: "Needs action", resolved: "Resolved", all: "All" };
|
|
1869
|
+
const HeldPaymentsPage = () => {
|
|
1870
|
+
const [pageIndex, setPageIndex] = react.useState(0);
|
|
1871
|
+
const [filter, setFilter] = react.useState("open");
|
|
1872
|
+
const [opened, setOpened] = react.useState(null);
|
|
1873
|
+
const { data, dataUpdatedAt, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
|
|
1874
|
+
const count = data?.count ?? 0;
|
|
1875
|
+
const remainder = count % HELD_PAGE_SIZE;
|
|
1876
|
+
const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
|
|
1877
|
+
const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}:${row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}${row.subscription_id !== void 0 ? `:subscription:${row.vp_payment_intent_id ?? ""}` : ""}${row.watch_id ? `:watch:${row.watch_id}` : ""}`;
|
|
1878
|
+
const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
|
|
1879
|
+
const onRowKey = (row) => (e) => {
|
|
1880
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1881
|
+
e.preventDefault();
|
|
1882
|
+
setOpened(row);
|
|
1883
|
+
}
|
|
1390
1884
|
};
|
|
1391
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
1392
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1885
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
|
|
1886
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1887
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", children: [
|
|
1888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1889
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Held payments" }),
|
|
1890
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Buyers who were charged through Vonpay but whose order couldn't be completed safely, cancelled orders whose refund Vonpay hasn't confirmed, and Apple Pay / Google Pay payments not yet collected for an order. Nothing here is refunded automatically." })
|
|
1891
|
+
] }),
|
|
1892
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[180px]", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1893
|
+
ui.Select,
|
|
1894
|
+
{
|
|
1895
|
+
size: "small",
|
|
1896
|
+
value: filter,
|
|
1897
|
+
onValueChange: (v) => {
|
|
1898
|
+
setFilter(v);
|
|
1899
|
+
setPageIndex(0);
|
|
1900
|
+
},
|
|
1901
|
+
children: [
|
|
1902
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": "Show", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1903
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: ["open", "resolved", "all"].map((f) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: f, children: FILTER_LABEL[f] }, f)) })
|
|
1904
|
+
]
|
|
1905
|
+
}
|
|
1906
|
+
) })
|
|
1404
1907
|
] }),
|
|
1405
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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: [
|
|
1916
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1917
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reason" }),
|
|
1918
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
|
|
1919
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Held since" }),
|
|
1920
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Vonpay session" }),
|
|
1921
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Medusa payment session" }),
|
|
1922
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Where" })
|
|
1923
|
+
] }) }),
|
|
1924
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.holds.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1925
|
+
ui.Table.Row,
|
|
1926
|
+
{
|
|
1927
|
+
className: "cursor-pointer",
|
|
1928
|
+
tabIndex: 0,
|
|
1929
|
+
role: "button",
|
|
1930
|
+
"aria-label": `${reasonCopy(row.reason).label}, ${statusLabel(row.status)}, open details`,
|
|
1931
|
+
onClick: () => setOpened(row),
|
|
1932
|
+
onKeyDown: onRowKey(row),
|
|
1933
|
+
children: [
|
|
1934
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1935
|
+
/* @__PURE__ */ jsxRuntime.jsx(ReasonBadge, { reason: row.reason }),
|
|
1936
|
+
row.subscription_id && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small font-mono text-ui-fg-subtle", "data-testid": "held-list-subscription", children: row.subscription_id })
|
|
1937
|
+
] }) }),
|
|
1938
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RowStatus, { status: row.status }) }),
|
|
1939
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: row.held_at ? new Date(row.held_at).toLocaleString() : "Unknown", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small whitespace-nowrap", children: since(row.held_at) }) }) }),
|
|
1940
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
|
|
1941
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.medusa_payment_session_id, label: "Medusa payment session id" }) }),
|
|
1942
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1943
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small text-ui-fg-subtle whitespace-nowrap", children: SOURCE_LABEL[row.source] }),
|
|
1944
|
+
fromBuyerCheckout(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { className: "w-fit whitespace-nowrap", color: "grey", children: FROM_BUYER_CHECKOUT })
|
|
1945
|
+
] }) })
|
|
1946
|
+
]
|
|
1947
|
+
},
|
|
1948
|
+
keyOf(row)
|
|
1949
|
+
)) })
|
|
1950
|
+
] }) }),
|
|
1951
|
+
pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1952
|
+
ui.Table.Pagination,
|
|
1953
|
+
{
|
|
1954
|
+
count,
|
|
1955
|
+
pageSize: HELD_PAGE_SIZE,
|
|
1956
|
+
pageIndex,
|
|
1957
|
+
pageCount,
|
|
1958
|
+
canPreviousPage: pageIndex > 0,
|
|
1959
|
+
canNextPage: pageIndex + 1 < pageCount,
|
|
1960
|
+
previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
|
|
1961
|
+
nextPage: () => setPageIndex((p) => p + 1)
|
|
1962
|
+
}
|
|
1963
|
+
)
|
|
1416
1964
|
] }),
|
|
1417
|
-
|
|
1418
|
-
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
|
|
1965
|
+
/* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, listAt: dataUpdatedAt, onClose: () => setOpened(null) })
|
|
1419
1966
|
] }),
|
|
1420
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
const KEY = "vonpay-offers";
|
|
1428
|
-
function useOffers() {
|
|
1429
|
-
return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
|
|
1430
|
-
}
|
|
1431
|
-
function useWrite(send) {
|
|
1432
|
-
const qc = reactQuery.useQueryClient();
|
|
1433
|
-
return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
|
|
1434
|
-
}
|
|
1435
|
-
const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
|
|
1436
|
-
const useUpdateOffer = () => useWrite((v) => {
|
|
1437
|
-
const { id, ...body } = v;
|
|
1438
|
-
return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
|
|
1967
|
+
/* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
|
|
1968
|
+
] });
|
|
1969
|
+
};
|
|
1970
|
+
const config$1 = adminSdk.defineRouteConfig({
|
|
1971
|
+
label: "Held payments",
|
|
1972
|
+
icon: icons.ExclamationCircle,
|
|
1973
|
+
nested: "/orders"
|
|
1439
1974
|
});
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const
|
|
1469
|
-
e.preventDefault();
|
|
1470
|
-
if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
|
|
1471
|
-
setName("");
|
|
1472
|
-
setVariantId("");
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
|
-
const move = (offers, i, by) => {
|
|
1476
|
-
const ids = offers.map((o) => o.id);
|
|
1477
|
-
[ids[i], ids[i + by]] = [ids[i + by], ids[i]];
|
|
1478
|
-
void run(() => reorder.mutateAsync({ ids }));
|
|
1479
|
-
};
|
|
1480
|
-
const onRemove = async (o) => {
|
|
1481
|
-
if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
|
|
1482
|
-
};
|
|
1975
|
+
const EVENT_LABEL = {
|
|
1976
|
+
"subscription.created": "Signed up",
|
|
1977
|
+
"subscription.renewed": "Renewal paid",
|
|
1978
|
+
"subscription.payment_failed": "Renewal declined",
|
|
1979
|
+
"subscription.cancelled": "Cancelled",
|
|
1980
|
+
"subscription.updated": "Changed"
|
|
1981
|
+
};
|
|
1982
|
+
const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
|
|
1983
|
+
const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
|
|
1984
|
+
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.`;
|
|
1985
|
+
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.";
|
|
1986
|
+
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.";
|
|
1987
|
+
const subscriptionOrderPath = (orderId) => `/orders/${encodeURIComponent(orderId)}`;
|
|
1988
|
+
const SUBSCRIPTION_PAGE_SIZE = 20;
|
|
1989
|
+
function useSubscriptionEvents(page) {
|
|
1990
|
+
return reactQuery.useQuery({
|
|
1991
|
+
queryKey: ["vonpay-subscription-events", page.limit, page.offset],
|
|
1992
|
+
queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
|
|
1993
|
+
placeholderData: reactQuery.keepPreviousData
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
const pageCountFor = (count) => {
|
|
1997
|
+
const remainder = count % SUBSCRIPTION_PAGE_SIZE;
|
|
1998
|
+
return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
|
|
1999
|
+
};
|
|
2000
|
+
const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
|
|
2001
|
+
const VonpaySubscriptionsPage = () => {
|
|
2002
|
+
const [pageIndex, setPageIndex] = react.useState(0);
|
|
2003
|
+
const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
|
|
1483
2004
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1484
2005
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
1485
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children:
|
|
1486
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1487
|
-
OFFERS_COPY.intro,
|
|
1488
|
-
" ",
|
|
1489
|
-
OFFERS_COPY.lag
|
|
1490
|
-
] })
|
|
2006
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
|
|
2007
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
|
|
1491
2008
|
] }),
|
|
1492
|
-
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
1496
|
-
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-disc pl-6", children: data.config.map((c) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: productLabel(c) }) }, c.variant_id)) }),
|
|
1497
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: busy, onClick: () => void run(() => importConfig.mutateAsync(void 0)), children: OFFERS_COPY.importButton }) })
|
|
2009
|
+
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: [
|
|
2010
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
2011
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }),
|
|
2012
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", "data-testid": "vonpay-subscriptions-cancel-note", children: CANCEL_IS_SEPARATE_NOTE })
|
|
1498
2013
|
] }),
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
2014
|
+
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: [
|
|
2015
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
2016
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
2017
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
|
|
2018
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Started by order" }),
|
|
2020
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
|
|
2021
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
|
|
2022
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
|
|
2023
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
|
|
2024
|
+
] }) }),
|
|
2025
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
2026
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
|
|
2027
|
+
eventLabel(e.type),
|
|
2028
|
+
e.mode === "test" ? " (test)" : ""
|
|
2029
|
+
] }),
|
|
2030
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
|
|
2031
|
+
/* @__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 }) : "—" }),
|
|
2032
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
|
|
2033
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
|
|
2034
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
|
|
2035
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
|
|
2036
|
+
] }, e.id)) })
|
|
1509
2037
|
] }) }),
|
|
1510
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
] }) })
|
|
1522
|
-
] }, o.id)) })
|
|
1523
|
-
] }) }),
|
|
1524
|
-
downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1525
|
-
OfferDownsellDrawer,
|
|
1526
|
-
{
|
|
1527
|
-
offer: downsellFor,
|
|
1528
|
-
open: downsellFor !== null,
|
|
1529
|
-
busy,
|
|
1530
|
-
onOpenChange: (o) => !o && setDownsellFor(null),
|
|
1531
|
-
onSave: async (downsell) => {
|
|
1532
|
-
try {
|
|
1533
|
-
await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
|
|
1534
|
-
return null;
|
|
1535
|
-
} catch (err) {
|
|
1536
|
-
return errorSentence(err);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
},
|
|
1540
|
-
downsellFor.id
|
|
1541
|
-
),
|
|
1542
|
-
wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1543
|
-
OfferWordingDrawer,
|
|
1544
|
-
{
|
|
1545
|
-
offer: wordingFor,
|
|
1546
|
-
open: wordingFor !== null,
|
|
1547
|
-
busy,
|
|
1548
|
-
onOpenChange: (o) => !o && setWordingFor(null),
|
|
1549
|
-
onSave: async (copy) => {
|
|
1550
|
-
try {
|
|
1551
|
-
await update.mutateAsync({ id: wordingFor.id, copy });
|
|
1552
|
-
return null;
|
|
1553
|
-
} catch (err) {
|
|
1554
|
-
return errorSentence(err);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
},
|
|
1558
|
-
wordingFor.id
|
|
1559
|
-
),
|
|
1560
|
-
editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1561
|
-
OfferRulesDrawer,
|
|
1562
|
-
{
|
|
1563
|
-
offer: editing,
|
|
1564
|
-
open: editing !== null,
|
|
1565
|
-
busy,
|
|
1566
|
-
onOpenChange: (o) => !o && setEditing(null),
|
|
1567
|
-
onSave: async (rules) => {
|
|
1568
|
-
try {
|
|
1569
|
-
await update.mutateAsync({ id: editing.id, rules });
|
|
1570
|
-
return null;
|
|
1571
|
-
} catch (err) {
|
|
1572
|
-
return errorSentence(err);
|
|
1573
|
-
}
|
|
2038
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2039
|
+
ui.Table.Pagination,
|
|
2040
|
+
{
|
|
2041
|
+
count: data.count,
|
|
2042
|
+
pageSize: SUBSCRIPTION_PAGE_SIZE,
|
|
2043
|
+
pageIndex,
|
|
2044
|
+
pageCount: pageCountFor(data.count),
|
|
2045
|
+
canPreviousPage: pageIndex > 0,
|
|
2046
|
+
canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
|
|
2047
|
+
previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
|
|
2048
|
+
nextPage: () => setPageIndex((p) => p + 1)
|
|
1574
2049
|
}
|
|
1575
|
-
|
|
1576
|
-
editing.id
|
|
1577
|
-
),
|
|
1578
|
-
/* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
|
|
1579
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
|
|
1580
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
|
|
1581
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1582
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
|
|
1583
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
|
|
1584
|
-
] }),
|
|
1585
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1586
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
|
|
1587
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
|
|
1588
|
-
] })
|
|
1589
|
-
] }),
|
|
1590
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
|
|
2050
|
+
)
|
|
1591
2051
|
] })
|
|
1592
2052
|
] })
|
|
1593
2053
|
] });
|
|
1594
2054
|
};
|
|
1595
2055
|
const config = adminSdk.defineRouteConfig({
|
|
1596
|
-
label: "
|
|
1597
|
-
icon: icons.
|
|
2056
|
+
label: "Vonpay subscriptions",
|
|
2057
|
+
icon: icons.ArrowPath,
|
|
1598
2058
|
nested: "/orders"
|
|
1599
2059
|
});
|
|
1600
2060
|
const i18nTranslations0 = {};
|
|
@@ -1613,13 +2073,18 @@ const widgetModule = { widgets: [
|
|
|
1613
2073
|
Component: UpdateBanner,
|
|
1614
2074
|
zone: ["order.list.before"],
|
|
1615
2075
|
widgetId: "Widget-5ad3"
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
Component: VonpayVariantSubscription,
|
|
2079
|
+
zone: ["product_variant.details.after"],
|
|
2080
|
+
widgetId: "Widget-671a"
|
|
1616
2081
|
}
|
|
1617
2082
|
] };
|
|
1618
2083
|
const routeModule = {
|
|
1619
2084
|
routes: [
|
|
1620
2085
|
{
|
|
1621
|
-
Component:
|
|
1622
|
-
path: "/vonpay-
|
|
2086
|
+
Component: VonpayOffersPage,
|
|
2087
|
+
path: "/vonpay-offers",
|
|
1623
2088
|
handle: { label: config$2.label, translationNs: config$2.translationNs }
|
|
1624
2089
|
},
|
|
1625
2090
|
{
|
|
@@ -1628,8 +2093,8 @@ const routeModule = {
|
|
|
1628
2093
|
handle: { label: config$1.label, translationNs: config$1.translationNs }
|
|
1629
2094
|
},
|
|
1630
2095
|
{
|
|
1631
|
-
Component:
|
|
1632
|
-
path: "/vonpay-
|
|
2096
|
+
Component: VonpaySubscriptionsPage,
|
|
2097
|
+
path: "/vonpay-subscriptions",
|
|
1633
2098
|
handle: { label: config.label, translationNs: config.translationNs }
|
|
1634
2099
|
}
|
|
1635
2100
|
]
|
|
@@ -1645,16 +2110,16 @@ const menuItemModule = {
|
|
|
1645
2110
|
translationNs: void 0
|
|
1646
2111
|
},
|
|
1647
2112
|
{
|
|
1648
|
-
label: config.label,
|
|
1649
|
-
icon: config.icon,
|
|
2113
|
+
label: config$2.label,
|
|
2114
|
+
icon: config$2.icon,
|
|
1650
2115
|
path: "/vonpay-offers",
|
|
1651
2116
|
nested: "/orders",
|
|
1652
2117
|
rank: void 0,
|
|
1653
2118
|
translationNs: void 0
|
|
1654
2119
|
},
|
|
1655
2120
|
{
|
|
1656
|
-
label: config
|
|
1657
|
-
icon: config
|
|
2121
|
+
label: config.label,
|
|
2122
|
+
icon: config.icon,
|
|
1658
2123
|
path: "/vonpay-subscriptions",
|
|
1659
2124
|
nested: "/orders",
|
|
1660
2125
|
rank: void 0,
|