vonpay-medusa 1.2.0 → 1.2.1

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.
Files changed (49) hide show
  1. package/.medusa/server/src/admin/index.js +378 -173
  2. package/.medusa/server/src/admin/index.mjs +381 -176
  3. package/.medusa/server/src/api/admin/vonpay/holds/resolve/route.js +6 -0
  4. package/.medusa/server/src/api/admin/vonpay/holds/route.js +2 -1
  5. package/.medusa/server/src/api/admin/vonpay/setup-check/move-to-held/route.js +14 -0
  6. package/.medusa/server/src/api/middlewares.js +1 -0
  7. package/.medusa/server/src/lib/errors.js +9 -1
  8. package/.medusa/server/src/lib/held-charge-copy.js +176 -0
  9. package/.medusa/server/src/modules/vonpay-jobs/index.js +11 -0
  10. package/.medusa/server/src/modules/vonpay-jobs/key.js +4 -0
  11. package/.medusa/server/src/modules/vonpay-jobs/migrations/Migration20261105120000.js +15 -0
  12. package/.medusa/server/src/modules/vonpay-jobs/models/job-beat.js +20 -0
  13. package/.medusa/server/src/modules/vonpay-jobs/service.js +7 -0
  14. package/.medusa/server/src/providers/vonpay/admin-holds.js +45 -39
  15. package/.medusa/server/src/providers/vonpay/charge-hold.js +124 -0
  16. package/.medusa/server/src/providers/vonpay/deadline.js +26 -0
  17. package/.medusa/server/src/providers/vonpay/descriptor.js +4 -4
  18. package/.medusa/server/src/providers/vonpay/held-list.js +168 -26
  19. package/.medusa/server/src/providers/vonpay/job-beat.js +57 -0
  20. package/.medusa/server/src/providers/vonpay/move-to-held.js +95 -0
  21. package/.medusa/server/src/providers/vonpay/options.js +11 -1
  22. package/.medusa/server/src/providers/vonpay/reading.js +25 -2
  23. package/.medusa/server/src/providers/vonpay/recovery.js +44 -11
  24. package/.medusa/server/src/providers/vonpay/refund-after-order.js +35 -0
  25. package/.medusa/server/src/providers/vonpay/renewal-check.js +435 -0
  26. package/.medusa/server/src/providers/vonpay/renewal-recovery.js +425 -25
  27. package/.medusa/server/src/providers/vonpay/renewal.js +542 -39
  28. package/.medusa/server/src/providers/vonpay/service.js +534 -86
  29. package/.medusa/server/src/providers/vonpay/session-data.js +243 -9
  30. package/.medusa/server/src/providers/vonpay/setup-check.js +505 -5
  31. package/.medusa/server/src/providers/vonpay/upsell-accept.js +99 -35
  32. package/.medusa/server/src/scripts/vonpay-check.js +23 -1
  33. package/.medusa/server/src/subscribers/vonpay-order-placed.js +2 -0
  34. package/README.md +219 -9
  35. package/bin/vonpay-medusa-check.cjs +23 -3
  36. package/bin/vonpay-medusa.cjs +229 -4
  37. package/package.json +4 -3
  38. package/storefront/src/cache-tag.ts +7 -0
  39. package/storefront/src/cart-fields.ts +1 -0
  40. package/storefront/src/checkout-steps.ts +27 -0
  41. package/storefront/src/context.tsx +1 -1
  42. package/storefront/src/csp.ts +1 -1
  43. package/storefront/src/index.ts +2 -0
  44. package/storefront/src/offer-prepare.ts +3 -2
  45. package/storefront/src/payment-step.ts +99 -0
  46. package/storefront/src/server.ts +49 -12
  47. package/storefront/src/shared.ts +1 -0
  48. package/storefront/src/vonpay-card-payment.tsx +13 -3
  49. package/storefront/src/vora-csp.json +17 -3
@@ -9,6 +9,47 @@ 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 NOT_SAVED_TEXT = "That couldn't be saved. The list is read again: check the row as it stands now before you try again.";
17
+ const CHARGE_REASON_LABEL = {
18
+ renewal_outcome_unknown: "Renewal charge — result unknown",
19
+ renewal_not_settled: "Renewal charge not completed",
20
+ renewal_charged_unrecorded: "Renewal charged, order unpaid",
21
+ renewal_paid_twice: "Renewal charged — order also paid another way",
22
+ renewal_buyer_mismatch: "Renewal charged to a different customer",
23
+ upsell_outcome_unknown: "Offer charge — result unknown",
24
+ upsell_not_settled: "Offer charge not completed",
25
+ upsell_paid_not_added: "Offer paid, item not added",
26
+ upsell_paid_unsettled: "Offer paid — check the order",
27
+ upsell_paid_unrecorded: "Offer paid, payment not recorded",
28
+ upsell_hold_not_released: "Offer payment held — release not confirmed",
29
+ upsell_bank_check_open: "Offer bank check still open",
30
+ upsell_refund_failed: "Late offer charge — refund failed",
31
+ upsell_refund_unclear: "Late offer charge — refund unclear",
32
+ upsell_refund_pending: "Late offer charge — refund in progress",
33
+ upsell_refunded_late: "Late offer charge refunded",
34
+ upsell_hold_unsettled: "Offer payment held — not finished yet",
35
+ upsell_take_unconfirmed: "Offer payment — collection not confirmed"
36
+ };
37
+ const CHARGE_RESOLVED_HOW_LABEL = {
38
+ closed_blocking: "Closed by a person — no more renewal charges for this order",
39
+ freed: "Freed — later renewals of this order may charge the customer",
40
+ closed_no_change: "Closed by a person — no change to money"
41
+ };
42
+ function chargeHandStep(paymentIntentId) {
43
+ 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.` };
44
+ }
45
+ const OFFER_CLOSED_MONEY_TEXT = {
46
+ 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.",
47
+ 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.",
48
+ 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.",
49
+ 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.",
50
+ 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."
51
+ };
52
+ const OFFER_CLOSED_MONEY_DEFAULT = "Closing this row moved no money. What Vonpay holds for this payment is as it was.";
12
53
  const REASON_COPY = {
13
54
  deleted_while_charged: {
14
55
  label: "Charged, then cart changed",
@@ -82,78 +123,7 @@ const REASON_COPY = {
82
123
  label: "Paid, order not created",
83
124
  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
125
  },
85
- renewal_outcome_unknown: {
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
- },
126
+ ...Object.fromEntries(Object.entries(CHARGE_REASON_LABEL).map(([reason, label]) => [reason, { label }])),
157
127
  cancel_refund_missing: {
158
128
  label: "Cancelled, refund failed",
159
129
  what: "The order was cancelled, but Medusa's refund to the buyer did not go through. The buyer may still be charged."
@@ -207,6 +177,7 @@ const RESOLVED_HOW_LABEL = {
207
177
  refunded_earlier: "Already refunded with this payment's key (for example from the command line)",
208
178
  nothing_left: "Vonpay showed nothing left to refund — confirm in the Vonpay dashboard that the buyer got it back",
209
179
  marked: "Marked resolved",
180
+ ...CHARGE_RESOLVED_HOW_LABEL,
210
181
  auto_closed: "Closed automatically — Vonpay confirmed nothing was charged and the payment can no longer be used",
211
182
  paid_after_auto_close: "Re-opened automatically — money arrived after the automatic close",
212
183
  paid_after_resolution: "Re-opened automatically — Vonpay reported this payment as paid after it was marked resolved",
@@ -250,6 +221,9 @@ function isConfirmedRefund(row) {
250
221
  function canRefund(row) {
251
222
  return row.vp_session_id !== null && !isConfirmedRefund(row) && !isAutoClosed(row) && !isUnrecognisedRecord(row) && REFUNDABLE_REASONS.includes(row.reason);
252
223
  }
224
+ function isChargeRow(row) {
225
+ return row.source === "renewal" || row.source === "upsell";
226
+ }
253
227
  function canRelease(row) {
254
228
  return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && WALLET_REASONS.includes(row.reason);
255
229
  }
@@ -257,9 +231,20 @@ function canComplete(row) {
257
231
  return row.source === "collection" && row.vp_session_id !== null && row.status === "open" && row.reason === "wallet_held_no_order";
258
232
  }
259
233
  function canMarkResolved(row) {
234
+ if (isChargeRow(row)) return false;
260
235
  return row.vp_session_id !== null && row.status !== "resolved" && !isUnrecognisedRecord(row) && !WALLET_REASONS.includes(row.reason) && !TAKE_REASONS.includes(row.reason);
261
236
  }
262
237
  function locatorFor(row) {
238
+ if (isChargeRow(row)) {
239
+ return {
240
+ source: row.source,
241
+ payment_collection_id: row.payment_collection_id,
242
+ ...row.order_id ? { order_id: row.order_id } : {},
243
+ ...row.medusa_payment_session_id ? { medusa_payment_session_id: row.medusa_payment_session_id } : {},
244
+ ...row.acceptance_id ? { acceptance_id: row.acceptance_id } : {},
245
+ ...row.prior_intent_id ? { prior_intent_id: row.prior_intent_id } : {}
246
+ };
247
+ }
263
248
  return {
264
249
  source: row.source,
265
250
  vp_session_id: row.vp_session_id ?? "",
@@ -269,6 +254,9 @@ function locatorFor(row) {
269
254
  };
270
255
  }
271
256
  function resolutionSteps(row) {
257
+ if (isChargeRow(row)) {
258
+ return [chargeHandStep(row.vp_payment_intent_id)];
259
+ }
272
260
  if (!row.vp_session_id || row.reason === "unrecognised") {
273
261
  return [{ title: "Check this payment in Vonpay", body: "Find the payment in the Vonpay dashboard before refunding or fulfilling anything." }];
274
262
  }
@@ -348,6 +336,14 @@ function reconcileCopy(o) {
348
336
  function markConfirmMatches(typed, row) {
349
337
  return row.vp_session_id !== null && typed.trim() === row.vp_session_id;
350
338
  }
339
+ function closedMoneyNote(row) {
340
+ if (row.status !== "resolved" || row.resolution?.how !== "closed_no_change") return null;
341
+ return OFFER_CLOSED_MONEY_TEXT[row.reason] ?? OFFER_CLOSED_MONEY_DEFAULT;
342
+ }
343
+ function chargedUncheckedNote(count) {
344
+ if (!(count > 0)) return null;
345
+ 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.`;
346
+ }
351
347
  const REFUSAL_TEXT = {
352
348
  hold_release_not_eligible: "Only a held wallet payment can be released here. Nothing was changed.",
353
349
  hold_release_payment_active: "This held payment belongs to an order. Take it or cancel it from the order. Nothing was changed.",
@@ -369,7 +365,7 @@ const REFUSAL_TEXT = {
369
365
  hold_refund_auto_closed: "This payment was closed automatically because Vonpay confirmed nothing was charged. There's nothing to refund.",
370
366
  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
367
  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.",
368
+ 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
369
  refund_exceeds_remaining: "Vonpay shows nothing left to refund on this payment. Nothing new was refunded.",
374
370
  hold_provider_unavailable: "The Vonpay payment provider couldn't be reached from the admin. Nothing was changed — try again.",
375
371
  hold_provider_ambiguous: "More than one Vonpay provider could own this payment. Nothing was changed — check the store's payment setup.",
@@ -377,7 +373,7 @@ const REFUSAL_TEXT = {
377
373
  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
374
  refund_declined: "The payment provider declined the refund. No money moved.",
379
375
  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.",
376
+ 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
377
  hold_checkout_busy: "The buyer's checkout for this payment is in progress right now. Nothing was changed — try again in a moment.",
382
378
  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
379
  hold_recheck_failed: "The order couldn't be re-checked just before acting. Nothing was changed — try again.",
@@ -387,6 +383,14 @@ const REFUSAL_TEXT = {
387
383
  hold_refund_payment_active: "This payment now belongs to an order that isn't cancelled. Nothing was refunded — refund it from the order.",
388
384
  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
385
  hold_resolve_payment_active: "This payment now belongs to an order that isn't cancelled, so it isn't held. Nothing was changed.",
386
+ move_request_invalid: "That request was missing the payment's ids. Run the check again. Nothing was changed.",
387
+ hold_resolve_confirm_mismatch: "To mark a payment resolved, type its Vonpay session id exactly as the row shows it. Nothing was changed.",
388
+ 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.",
389
+ move_not_paid: "Vonpay doesn't show money taken or held on this payment, so there is nothing to move. Nothing was changed.",
390
+ 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.",
391
+ move_not_this_checkout: "Vonpay's record of this payment names a different checkout. Check it in the Vonpay dashboard. Nothing was changed.",
392
+ 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.",
393
+ move_vonpay_unavailable: "Vonpay couldn't be reached to check this payment again. Nothing was changed — try again shortly.",
390
394
  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
395
  };
392
396
  function outcomeCopy(o) {
@@ -456,6 +460,23 @@ function completeOutcomeCopy(o) {
456
460
  return UNKNOWN_OUTCOME;
457
461
  }
458
462
  }
463
+ function moveOutcomeCopy(o) {
464
+ switch (o.outcome) {
465
+ case "moved":
466
+ return {
467
+ variant: "success",
468
+ 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."
469
+ };
470
+ case "already_held":
471
+ return { variant: "success", text: "It is already on Held payments. Nothing was changed." };
472
+ case "ordered":
473
+ return { variant: "warning", text: "An order holds this payment now, so it was not moved. Nothing was changed." };
474
+ case "refused":
475
+ return { variant: "error", text: o.code && REFUSAL_TEXT[o.code] || "That couldn't be done. Nothing was changed." };
476
+ default:
477
+ return UNKNOWN_OUTCOME;
478
+ }
479
+ }
459
480
  function resolveOutcomeCopy(o) {
460
481
  if (o.outcome === "refused") return { variant: "error", text: o.code && REFUSAL_TEXT[o.code] || "That couldn't be done. Nothing was changed." };
461
482
  if (!o.recorded) return { variant: "warning", text: "Couldn't save that. Try again." };
@@ -466,6 +487,7 @@ const authType = typeof __AUTH_TYPE__ !== "undefined" && __AUTH_TYPE__ ? __AUTH_
466
487
  const sdk = new Medusa__default.default({ baseUrl: backendUrl, auth: { type: authType } });
467
488
  const HELD_PAGE_SIZE = 20;
468
489
  const LIST_KEY = "vonpay-held-payments";
490
+ const HELD_LIST_KEY = LIST_KEY;
469
491
  function useHeldPayments(page) {
470
492
  const status = page.status ?? "open";
471
493
  return reactQuery.useQuery({
@@ -528,6 +550,22 @@ function useMarkResolved() {
528
550
  onSettled: () => qc.invalidateQueries({ queryKey: [LIST_KEY] })
529
551
  });
530
552
  }
553
+ function useChargeAction() {
554
+ const qc = reactQuery.useQueryClient();
555
+ return reactQuery.useMutation({
556
+ mutationFn: (v) => sdk.client.fetch("/admin/vonpay/holds/resolve", {
557
+ method: "POST",
558
+ body: {
559
+ ...locatorFor(v.row),
560
+ action: v.action,
561
+ ...v.typed_order_id !== void 0 ? { confirm_order_id: v.typed_order_id.trim(), confirm_checked_in_vonpay: v.checked_in_vonpay === true } : {},
562
+ ...v.action === "close_unreadable" && v.row.record_sha256 ? { confirm_record_sha256: v.row.record_sha256 } : {}
563
+ }
564
+ }),
565
+ retry: false,
566
+ onSettled: () => qc.invalidateQueries({ queryKey: [LIST_KEY] })
567
+ });
568
+ }
531
569
  function useReconcileHold() {
532
570
  const qc = reactQuery.useQueryClient();
533
571
  return reactQuery.useMutation({
@@ -626,92 +664,48 @@ const UpdateBanner = () => {
626
664
  ] }) });
627
665
  };
628
666
  adminSdk.defineWidgetConfig({ zone: "order.list.before" });
629
- const EVENT_LABEL = {
630
- "subscription.created": "Signed up",
631
- "subscription.renewed": "Renewal paid",
632
- "subscription.payment_failed": "Renewal declined",
633
- "subscription.cancelled": "Cancelled",
634
- "subscription.updated": "Changed"
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) {
642
- return reactQuery.useQuery({
643
- queryKey: ["vonpay-subscription-events", page.limit, page.offset],
644
- queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
645
- placeholderData: reactQuery.keepPreviousData
646
- });
647
- }
648
- const pageCountFor = (count) => {
649
- const remainder = count % SUBSCRIPTION_PAGE_SIZE;
650
- return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
651
- };
652
- const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
653
- const VonpaySubscriptionsPage = () => {
654
- const [pageIndex, setPageIndex] = react.useState(0);
655
- const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
656
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
657
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
658
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
659
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
660
- ] }),
661
- isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
662
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
663
- data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
664
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
665
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
666
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
667
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
668
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
669
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
670
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
671
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
672
- ] }) }),
673
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
674
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
675
- eventLabel(e.type),
676
- e.mode === "test" ? " (test)" : ""
677
- ] }),
678
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
679
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
680
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
681
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
682
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
683
- ] }, e.id)) })
684
- ] }) }),
685
- /* @__PURE__ */ jsxRuntime.jsx(
686
- ui.Table.Pagination,
687
- {
688
- count: data.count,
689
- pageSize: SUBSCRIPTION_PAGE_SIZE,
690
- pageIndex,
691
- pageCount: pageCountFor(data.count),
692
- canPreviousPage: pageIndex > 0,
693
- canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
694
- previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
695
- nextPage: () => setPageIndex((p) => p + 1)
696
- }
697
- )
698
- ] })
699
- ] })
700
- ] });
701
- };
702
- const config$2 = adminSdk.defineRouteConfig({
703
- label: "Vonpay subscriptions",
704
- icon: icons.ArrowPath,
705
- nested: "/orders"
706
- });
707
667
  function useSetupCheck() {
708
668
  return reactQuery.useMutation({
709
669
  mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
710
670
  retry: false
711
671
  });
712
672
  }
673
+ function useMoveToHeld() {
674
+ const qc = reactQuery.useQueryClient();
675
+ return reactQuery.useMutation({
676
+ 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 } }),
677
+ retry: false,
678
+ onSettled: () => qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] })
679
+ });
680
+ }
713
681
  const LABEL = { pass: "OK", warn: "Check", fail: "Fix", not_checked: "Not checked" };
714
682
  const COLOR = { pass: "green", warn: "orange", fail: "red", not_checked: "grey" };
683
+ function uniqueMoves(moves) {
684
+ const seen = /* @__PURE__ */ new Set();
685
+ return (moves ?? []).filter((m) => !seen.has(m.vp_session_id) && (seen.add(m.vp_session_id), true));
686
+ }
687
+ const MoveRow = ({ move }) => {
688
+ const mutation = useMoveToHeld();
689
+ const [result, setResult] = react.useState(null);
690
+ const done = result !== null && result.variant !== "error";
691
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-1 sm:flex-row sm:items-center sm:gap-x-3", children: [
692
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-mono", children: move.vp_session_id }),
693
+ !done && /* @__PURE__ */ jsxRuntime.jsx(
694
+ ui.Button,
695
+ {
696
+ size: "small",
697
+ variant: "secondary",
698
+ isLoading: mutation.isPending,
699
+ onClick: () => mutation.mutate(move, {
700
+ onSuccess: (o) => setResult(moveOutcomeCopy(o)),
701
+ onError: () => setResult({ variant: "error", text: "That couldn't be done. Nothing was changed — try again." })
702
+ }),
703
+ children: "Move to Held payments"
704
+ }
705
+ ),
706
+ result && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: result.variant === "error" ? "text-ui-fg-error" : "text-ui-fg-subtle", children: result.text })
707
+ ] });
708
+ };
715
709
  const SetupCheckPanel = () => {
716
710
  const check = useSetupCheck();
717
711
  const items = check.data?.items ?? [];
@@ -730,7 +724,8 @@ const SetupCheckPanel = () => {
730
724
  /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: COLOR[i.status], className: "w-fit shrink-0", children: LABEL[i.status] }),
731
725
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
732
726
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: i.detail }),
733
- i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix })
727
+ i.fix && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: i.fix }),
728
+ 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)) })
734
729
  ] })
735
730
  ] }, `${i.id}:${i.provider ?? ""}:${n}`)) })
736
731
  ] })
@@ -903,6 +898,130 @@ const MarkResolvedAction = ({ row, onResult }) => {
903
898
  ] }) })
904
899
  ] });
905
900
  };
901
+ const localTime = (iso) => new Date(iso).toLocaleString();
902
+ const TIMER_MAX_MS = 2147483647;
903
+ const REPOLL_MS = 15e3;
904
+ const REPOLL_MAX = 8;
905
+ const ChargeActions = ({ row, listAt, onResult }) => {
906
+ const [answered, setAnswer] = react.useState(null);
907
+ const answer = answered && !(listAt > answered.receivedAt) ? answered : null;
908
+ const [asking, setAsking] = react.useState(null);
909
+ const [typed, setTyped] = react.useState("");
910
+ const [checked, setChecked] = react.useState(false);
911
+ const act = useChargeAction();
912
+ const qc = reactQuery.useQueryClient();
913
+ const rowSays = JSON.stringify([row.held_at, row.actions ?? null, row.hint ?? null]);
914
+ react.useEffect(() => setAnswer(null), [rowSays]);
915
+ const actions = answer?.actions ?? row.actions ?? [];
916
+ const hint = answer ? answer.hint : row.hint ?? null;
917
+ const closedUntil = actions.filter((a) => !a.available && a.available_at).map((a) => Date.parse(a.available_at)).filter((t) => Number.isFinite(t));
918
+ const opensAt = closedUntil.filter((t) => t > Date.now());
919
+ const nextOpen = opensAt.length > 0 ? Math.min(...opensAt) : null;
920
+ react.useEffect(() => {
921
+ if (nextOpen === null) return;
922
+ const timer = setTimeout(() => {
923
+ setAnswer(null);
924
+ void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
925
+ }, Math.min(TIMER_MAX_MS, Math.max(0, nextOpen - Date.now()) + 1e3));
926
+ return () => clearTimeout(timer);
927
+ }, [nextOpen, qc]);
928
+ const overdue = closedUntil.filter((t) => t <= Date.now());
929
+ const pastDue = overdue.length > 0 ? Math.min(...overdue) : null;
930
+ const repolls = react.useRef(0);
931
+ react.useEffect(() => {
932
+ if (pastDue === null || repolls.current >= REPOLL_MAX) return;
933
+ const timer = setTimeout(() => {
934
+ repolls.current += 1;
935
+ void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
936
+ }, REPOLL_MS);
937
+ return () => clearTimeout(timer);
938
+ }, [pastDue, listAt, qc]);
939
+ const run = (a) => act.mutate(
940
+ { row, action: a.id, ...a.confirm === "type_order_id" ? { typed_order_id: typed, checked_in_vonpay: checked } : {} },
941
+ {
942
+ onSuccess: (o) => {
943
+ if (o.recorded === false || o.outcome === "not_saved") {
944
+ setAnswer({ ...o, actions: [], hint: null, receivedAt: Date.now() });
945
+ onResult({ variant: "warning", text: NOT_SAVED_TEXT });
946
+ void qc.invalidateQueries({ queryKey: [HELD_LIST_KEY] });
947
+ return;
948
+ }
949
+ setAnswer({ ...o, receivedAt: Date.now() });
950
+ onResult({ variant: o.tone, text: fillTime(o.text, o.at, localTime) });
951
+ },
952
+ onError: (e) => {
953
+ const f = postFailure(e);
954
+ onResult(f.variant === "error" ? f : { variant: "warning", text: "Couldn't confirm the outcome. Reload the list." });
955
+ },
956
+ onSettled: () => setAsking(null)
957
+ }
958
+ );
959
+ const typedOk = !asking || asking.confirm !== "type_order_id" || typed.trim() === row.order_id && checked;
960
+ const inputId = `vonpay-charge-confirm-${row.payment_collection_id}-${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}`;
961
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
962
+ hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: fillTime(hint.text, hint.at, localTime) }),
963
+ 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: [
964
+ /* @__PURE__ */ jsxRuntime.jsx(
965
+ ui.Button,
966
+ {
967
+ size: "small",
968
+ variant: a.id === "check" ? "primary" : "secondary",
969
+ disabled: !a.available || act.isPending,
970
+ isLoading: act.isPending && (asking?.id === a.id || a.id === "check" && !asking),
971
+ onClick: () => {
972
+ if (!a.available) return;
973
+ if (!a.confirm) {
974
+ run(a);
975
+ return;
976
+ }
977
+ setTyped("");
978
+ setChecked(false);
979
+ setAsking(a);
980
+ },
981
+ children: a.label
982
+ }
983
+ ),
984
+ !a.available && a.available_at && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
985
+ "Opens ",
986
+ localTime(a.available_at)
987
+ ] })
988
+ ] }, a.id)) }),
989
+ /* @__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: [
990
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
991
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Title, { children: [
992
+ asking?.label.replace(/…$/, ""),
993
+ "?"
994
+ ] }),
995
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: asking?.confirm_text })
996
+ ] }),
997
+ asking?.confirm === "type_order_id" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 pb-4", children: [
998
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: inputId, size: "small", children: [
999
+ "Type the order id to confirm: ",
1000
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono", children: row.order_id })
1001
+ ] }),
1002
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: inputId, size: "small", autoComplete: "off", value: typed, onChange: (e) => setTyped(e.target.value) }),
1003
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-x-2", children: [
1004
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked, onChange: (e) => setChecked(e.target.checked) }),
1005
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small", children: "I checked this order in the Vonpay dashboard" })
1006
+ ] })
1007
+ ] }),
1008
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
1009
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: act.isPending, children: "Cancel" }),
1010
+ /* @__PURE__ */ jsxRuntime.jsx(
1011
+ ui.Prompt.Action,
1012
+ {
1013
+ disabled: act.isPending || !typedOk,
1014
+ onClick: (e) => {
1015
+ e.preventDefault();
1016
+ if (asking && typedOk) run(asking);
1017
+ },
1018
+ children: asking?.label.replace(/…$/, "")
1019
+ }
1020
+ )
1021
+ ] })
1022
+ ] }) })
1023
+ ] });
1024
+ };
906
1025
  const WalletHoldAction = ({ row, kind, onResult }) => {
907
1026
  const [open, setOpen] = react.useState(false);
908
1027
  const release = useReleaseHold();
@@ -938,7 +1057,7 @@ const WalletHoldAction = ({ row, kind, onResult }) => {
938
1057
  ] }) })
939
1058
  ] });
940
1059
  };
941
- const HoldDrawer = ({ row, onClose }) => {
1060
+ const HoldDrawer = ({ row, listAt, onClose }) => {
942
1061
  const copy = row ? reasonCopy(row.reason) : void 0;
943
1062
  const known = row ? isKnownStatus(row.status) && !isUnrecognisedRecord(row) : false;
944
1063
  const showRefund = !!row && known && canRefund(row);
@@ -955,7 +1074,7 @@ const HoldDrawer = ({ row, onClose }) => {
955
1074
  return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: row !== null, onOpenChange: (open) => !open && close(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
956
1075
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
957
1076
  /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: copy?.label ?? "Held payment" }),
958
- /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: copy?.what })
1077
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: row && isChargeRow(row) ? void 0 : copy?.what })
959
1078
  ] }),
960
1079
  row && /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
961
1080
  /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2", children: [
@@ -980,6 +1099,10 @@ const HoldDrawer = ({ row, onClose }) => {
980
1099
  /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Order" }),
981
1100
  /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.order_id, label: "order id" }) })
982
1101
  ] }),
1102
+ row.vp_payment_intent_id && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1103
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay payment" }),
1104
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_payment_intent_id, label: "Vonpay payment id" }) })
1105
+ ] }),
983
1106
  /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Vonpay session" }),
984
1107
  /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(Mono, { value: row.vp_session_id, label: "Vonpay session id" }) }),
985
1108
  /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "txt-compact-small text-ui-fg-subtle", children: "Medusa payment session" }),
@@ -1008,6 +1131,7 @@ const HoldDrawer = ({ row, onClose }) => {
1008
1131
  ] })
1009
1132
  ] }),
1010
1133
  result && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: result.variant, children: result.text }),
1134
+ closedMoneyNote(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: closedMoneyNote(row) }),
1011
1135
  (showRefund || showMark || showRelease || showComplete || showOpenOrder) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
1012
1136
  showOpenOrder && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "primary", asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: orderPath(row.order_id), children: "Open order" }) }),
1013
1137
  showComplete && /* @__PURE__ */ jsxRuntime.jsx(WalletHoldAction, { row, kind: "complete", onResult: setResult }),
@@ -1016,6 +1140,7 @@ const HoldDrawer = ({ row, onClose }) => {
1016
1140
  showRefund && /* @__PURE__ */ jsxRuntime.jsx(CheckWithVonpayAction, { row, onResult: setResult }),
1017
1141
  showMark && /* @__PURE__ */ jsxRuntime.jsx(MarkResolvedAction, { row, onResult: setResult })
1018
1142
  ] }),
1143
+ row && isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ChargeActions, { row, listAt, onResult: setResult }, `${row.payment_collection_id}:${row.medusa_payment_session_id ?? row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}`),
1019
1144
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
1020
1145
  /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Or do it by hand" }),
1021
1146
  steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
@@ -1026,7 +1151,7 @@ const HoldDrawer = ({ row, onClose }) => {
1026
1151
  /* @__PURE__ */ jsxRuntime.jsx(ui.CodeBlock.Body, {})
1027
1152
  ] })
1028
1153
  ] }, step.title)),
1029
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
1154
+ !isChargeRow(row) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: STILL_LISTED_NOTE })
1030
1155
  ] })
1031
1156
  ] })
1032
1157
  ] }) });
@@ -1036,11 +1161,11 @@ const HeldPaymentsPage = () => {
1036
1161
  const [pageIndex, setPageIndex] = react.useState(0);
1037
1162
  const [filter, setFilter] = react.useState("open");
1038
1163
  const [opened, setOpened] = react.useState(null);
1039
- const { data, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
1164
+ const { data, dataUpdatedAt, isLoading, isError, refetch } = useHeldPayments({ limit: HELD_PAGE_SIZE, offset: pageIndex * HELD_PAGE_SIZE, status: filter });
1040
1165
  const count = data?.count ?? 0;
1041
1166
  const remainder = count % HELD_PAGE_SIZE;
1042
1167
  const pageCount = Math.max(1, (count - remainder) / HELD_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1043
- const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}`;
1168
+ const keyOf = (row) => `${row.payment_collection_id}:${row.vp_session_id ?? "unreadable"}:${row.source}:${row.payment_id ?? ""}:${row.medusa_payment_session_id ?? ""}:${row.acceptance_id ?? ""}:${row.prior_intent_id ?? ""}`;
1044
1169
  const open = opened ? data?.holds.find((r) => keyOf(r) === keyOf(opened)) ?? opened : null;
1045
1170
  const onRowKey = (row) => (e) => {
1046
1171
  if (e.key === "Enter" || e.key === " ") {
@@ -1072,6 +1197,8 @@ const HeldPaymentsPage = () => {
1072
1197
  ) })
1073
1198
  ] }),
1074
1199
  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." }) }),
1200
+ 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)),
1201
+ 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) }) }),
1075
1202
  isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-3 px-6 py-6", children: [
1076
1203
  /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "Couldn't load held payments." }),
1077
1204
  /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => refetch(), children: "Try again" })
@@ -1123,12 +1250,12 @@ const HeldPaymentsPage = () => {
1123
1250
  }
1124
1251
  )
1125
1252
  ] }),
1126
- /* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, onClose: () => setOpened(null) })
1253
+ /* @__PURE__ */ jsxRuntime.jsx(HoldDrawer, { row: open, listAt: dataUpdatedAt, onClose: () => setOpened(null) })
1127
1254
  ] }),
1128
1255
  /* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
1129
1256
  ] });
1130
1257
  };
1131
- const config$1 = adminSdk.defineRouteConfig({
1258
+ const config$2 = adminSdk.defineRouteConfig({
1132
1259
  label: "Held payments",
1133
1260
  icon: icons.ExclamationCircle,
1134
1261
  nested: "/orders"
@@ -1592,11 +1719,89 @@ const VonpayOffersPage = () => {
1592
1719
  ] })
1593
1720
  ] });
1594
1721
  };
1595
- const config = adminSdk.defineRouteConfig({
1722
+ const config$1 = adminSdk.defineRouteConfig({
1596
1723
  label: "One-click offers",
1597
1724
  icon: icons.Sparkles,
1598
1725
  nested: "/orders"
1599
1726
  });
1727
+ const EVENT_LABEL = {
1728
+ "subscription.created": "Signed up",
1729
+ "subscription.renewed": "Renewal paid",
1730
+ "subscription.payment_failed": "Renewal declined",
1731
+ "subscription.cancelled": "Cancelled",
1732
+ "subscription.updated": "Changed"
1733
+ };
1734
+ const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
1735
+ const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
1736
+ const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
1737
+ 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.";
1738
+ const SUBSCRIPTION_PAGE_SIZE = 20;
1739
+ function useSubscriptionEvents(page) {
1740
+ return reactQuery.useQuery({
1741
+ queryKey: ["vonpay-subscription-events", page.limit, page.offset],
1742
+ queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
1743
+ placeholderData: reactQuery.keepPreviousData
1744
+ });
1745
+ }
1746
+ const pageCountFor = (count) => {
1747
+ const remainder = count % SUBSCRIPTION_PAGE_SIZE;
1748
+ return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1749
+ };
1750
+ const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
1751
+ const VonpaySubscriptionsPage = () => {
1752
+ const [pageIndex, setPageIndex] = react.useState(0);
1753
+ const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
1754
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1755
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1756
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
1757
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
1758
+ ] }),
1759
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1760
+ /* @__PURE__ */ jsxRuntime.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 }) }),
1761
+ data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1762
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1763
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1764
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
1765
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
1766
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
1767
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
1768
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
1769
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
1770
+ ] }) }),
1771
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1772
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
1773
+ eventLabel(e.type),
1774
+ e.mode === "test" ? " (test)" : ""
1775
+ ] }),
1776
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
1777
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
1778
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
1779
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
1780
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
1781
+ ] }, e.id)) })
1782
+ ] }) }),
1783
+ /* @__PURE__ */ jsxRuntime.jsx(
1784
+ ui.Table.Pagination,
1785
+ {
1786
+ count: data.count,
1787
+ pageSize: SUBSCRIPTION_PAGE_SIZE,
1788
+ pageIndex,
1789
+ pageCount: pageCountFor(data.count),
1790
+ canPreviousPage: pageIndex > 0,
1791
+ canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
1792
+ previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1793
+ nextPage: () => setPageIndex((p) => p + 1)
1794
+ }
1795
+ )
1796
+ ] })
1797
+ ] })
1798
+ ] });
1799
+ };
1800
+ const config = adminSdk.defineRouteConfig({
1801
+ label: "Vonpay subscriptions",
1802
+ icon: icons.ArrowPath,
1803
+ nested: "/orders"
1804
+ });
1600
1805
  const i18nTranslations0 = {};
1601
1806
  const widgetModule = { widgets: [
1602
1807
  {
@@ -1617,19 +1822,19 @@ const widgetModule = { widgets: [
1617
1822
  ] };
1618
1823
  const routeModule = {
1619
1824
  routes: [
1620
- {
1621
- Component: VonpaySubscriptionsPage,
1622
- path: "/vonpay-subscriptions",
1623
- handle: { label: config$2.label, translationNs: config$2.translationNs }
1624
- },
1625
1825
  {
1626
1826
  Component: HeldPaymentsPage,
1627
1827
  path: "/vonpay-holds",
1628
- handle: { label: config$1.label, translationNs: config$1.translationNs }
1828
+ handle: { label: config$2.label, translationNs: config$2.translationNs }
1629
1829
  },
1630
1830
  {
1631
1831
  Component: VonpayOffersPage,
1632
1832
  path: "/vonpay-offers",
1833
+ handle: { label: config$1.label, translationNs: config$1.translationNs }
1834
+ },
1835
+ {
1836
+ Component: VonpaySubscriptionsPage,
1837
+ path: "/vonpay-subscriptions",
1633
1838
  handle: { label: config.label, translationNs: config.translationNs }
1634
1839
  }
1635
1840
  ]
@@ -1637,24 +1842,24 @@ const routeModule = {
1637
1842
  const menuItemModule = {
1638
1843
  menuItems: [
1639
1844
  {
1640
- label: config$1.label,
1641
- icon: config$1.icon,
1845
+ label: config$2.label,
1846
+ icon: config$2.icon,
1642
1847
  path: "/vonpay-holds",
1643
1848
  nested: "/orders",
1644
1849
  rank: void 0,
1645
1850
  translationNs: void 0
1646
1851
  },
1647
1852
  {
1648
- label: config.label,
1649
- icon: config.icon,
1853
+ label: config$1.label,
1854
+ icon: config$1.icon,
1650
1855
  path: "/vonpay-offers",
1651
1856
  nested: "/orders",
1652
1857
  rank: void 0,
1653
1858
  translationNs: void 0
1654
1859
  },
1655
1860
  {
1656
- label: config$2.label,
1657
- icon: config$2.icon,
1861
+ label: config.label,
1862
+ icon: config.icon,
1658
1863
  path: "/vonpay-subscriptions",
1659
1864
  nested: "/orders",
1660
1865
  rank: void 0,