vonpay-medusa 1.0.2 → 1.0.3

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.
@@ -134,6 +134,10 @@ const REASON_COPY = {
134
134
  label: "Late offer charge — refund failed",
135
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
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
+ },
137
141
  upsell_refund_pending: {
138
142
  label: "Late offer charge — refund in progress",
139
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."
@@ -1164,17 +1168,17 @@ const routeModule = {
1164
1168
  const menuItemModule = {
1165
1169
  menuItems: [
1166
1170
  {
1167
- label: config.label,
1168
- icon: config.icon,
1169
- path: "/vonpay-subscriptions",
1171
+ label: config$1.label,
1172
+ icon: config$1.icon,
1173
+ path: "/vonpay-holds",
1170
1174
  nested: "/orders",
1171
1175
  rank: void 0,
1172
1176
  translationNs: void 0
1173
1177
  },
1174
1178
  {
1175
- label: config$1.label,
1176
- icon: config$1.icon,
1177
- path: "/vonpay-holds",
1179
+ label: config.label,
1180
+ icon: config.icon,
1181
+ path: "/vonpay-subscriptions",
1178
1182
  nested: "/orders",
1179
1183
  rank: void 0,
1180
1184
  translationNs: void 0
@@ -131,6 +131,10 @@ const REASON_COPY = {
131
131
  label: "Late offer charge — refund failed",
132
132
  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."
133
133
  },
134
+ upsell_refund_unclear: {
135
+ label: "Late offer charge — refund unclear",
136
+ 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."
137
+ },
134
138
  upsell_refund_pending: {
135
139
  label: "Late offer charge — refund in progress",
136
140
  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."
@@ -1161,17 +1165,17 @@ const routeModule = {
1161
1165
  const menuItemModule = {
1162
1166
  menuItems: [
1163
1167
  {
1164
- label: config.label,
1165
- icon: config.icon,
1166
- path: "/vonpay-subscriptions",
1168
+ label: config$1.label,
1169
+ icon: config$1.icon,
1170
+ path: "/vonpay-holds",
1167
1171
  nested: "/orders",
1168
1172
  rank: void 0,
1169
1173
  translationNs: void 0
1170
1174
  },
1171
1175
  {
1172
- label: config$1.label,
1173
- icon: config$1.icon,
1174
- path: "/vonpay-holds",
1176
+ label: config.label,
1177
+ icon: config.icon,
1178
+ path: "/vonpay-subscriptions",
1175
1179
  nested: "/orders",
1176
1180
  rank: void 0,
1177
1181
  translationNs: void 0
@@ -180,7 +180,7 @@ function upsellsFromCollection(c, now = Date.now()) {
180
180
  vp_session_id: null,
181
181
  medusa_payment_session_id: null,
182
182
  payment_collection_id: c.id,
183
- reason: p.stuck === "amount_mismatch" ? "amount_mismatch" : "upsell_refund_failed",
183
+ reason: p.stuck === "amount_mismatch" ? "amount_mismatch" : p.stuck === "refund_unclear" ? "upsell_refund_unclear" : "upsell_refund_failed",
184
184
  source: "upsell",
185
185
  held_at: isoOrNull(p.at),
186
186
  order_id: medusaId(r.order_id),
@@ -952,6 +952,34 @@ class VonpayProviderService extends utils_1.AbstractPaymentProvider {
952
952
  if (!Number.isInteger(step) || step < 0)
953
953
  return "retry";
954
954
  const ids = { vp_payment_intent_id: pi, step };
955
+ const ours = `vpm_upsell_late_rf_${pi}`;
956
+ if (step > 0) {
957
+ try {
958
+ const list = await this.vonpay((c) => c.refunds.list({ paymentIntent: pi }, { limit: 100 }));
959
+ const rows = Array.isArray(list.data) ? list.data : [];
960
+ const done = typeof list.amountRefunded === "number" ? list.amountRefunded : NaN;
961
+ if (!Number.isFinite(done))
962
+ return "unread";
963
+ const mine = (r) => typeof r.idempotencyKey === "string" && r.idempotencyKey.startsWith(ours);
964
+ if (rows.some((r) => mine(r) && REFUND_CONFIRMED.has(String(r.status)))) {
965
+ this.log("info", "refund_transport", { ...ids, outcome: "already_refunded" });
966
+ return "refunded";
967
+ }
968
+ if (done > 0) {
969
+ this.log("warn", "refund_transport", { ...ids, outcome: "refunded_elsewhere" });
970
+ return "failed";
971
+ }
972
+ if (list.refundable === false) {
973
+ this.log("info", "refund_transport", { ...ids, outcome: "not_refundable_yet" });
974
+ return "unsettled";
975
+ }
976
+ }
977
+ catch (err) {
978
+ const reading = readRefundError(err);
979
+ this.log("warn", "refund_transport", { ...ids, outcome: "list_unreadable", ...errorFields(err) });
980
+ return reading.kind === "unknown" ? "unread" : "failed";
981
+ }
982
+ }
955
983
  const key = `vpm_upsell_late_rf_${pi}${step === 0 ? "" : `_s${step}`}`;
956
984
  try {
957
985
  const refund = await this.vonpay((c) => c.refunds.create({ paymentIntent: pi, amount: amountMinor, currency }, { idempotencyKey: key }));
@@ -962,7 +990,7 @@ class VonpayProviderService extends utils_1.AbstractPaymentProvider {
962
990
  if (REFUND_DEFINITELY_NOT_MADE.has(status)) {
963
991
  if (replay) {
964
992
  this.log("info", "refund_transport", { ...ids, outcome: "key_spent" });
965
- return "unsettled";
993
+ return "spent";
966
994
  }
967
995
  return "failed";
968
996
  }
@@ -1934,7 +1962,7 @@ class VonpayProviderService extends utils_1.AbstractPaymentProvider {
1934
1962
  return { sent: true, delivered: out.delivered === true, response_status: typeof out.responseStatus === "number" ? out.responseStatus : null };
1935
1963
  }
1936
1964
  catch (err) {
1937
- if (err instanceof checkout_node_1.VonPayError && err.status === 502)
1965
+ if (err instanceof checkout_node_1.VonPayError && (err.code === "webhook_test_delivery_failed" || err.status === 502))
1938
1966
  return { sent: true, delivered: false, response_status: null, unreachable: true };
1939
1967
  return { sent: false, why: "failed" };
1940
1968
  }
@@ -3403,7 +3431,8 @@ class VonpayProviderService extends utils_1.AbstractPaymentProvider {
3403
3431
  };
3404
3432
  let updated;
3405
3433
  try {
3406
- updated = await this.vonpay((c) => c.sessions.update(vpSessionId, { amount: amountMinor, currency }));
3434
+ const meta = await descriptor();
3435
+ updated = await this.vonpay((c) => c.sessions.update(vpSessionId, { amount: amountMinor, currency, ...(Object.keys(meta).length > 0 ? { metadata: meta } : {}) }));
3407
3436
  }
3408
3437
  catch (err) {
3409
3438
  if (!(err instanceof checkout_node_1.SessionNotModifiableError))
@@ -95,6 +95,7 @@ const HOLD_REASONS = [
95
95
  "upsell_hold_unsettled",
96
96
  "upsell_bank_check_open",
97
97
  "upsell_refund_failed",
98
+ "upsell_refund_unclear",
98
99
  "upsell_refund_pending",
99
100
  "upsell_refunded_late",
100
101
  ];
@@ -547,6 +548,7 @@ exports.UPSELL_HOLD_REASONS = new Set([
547
548
  "upsell_hold_not_released",
548
549
  "upsell_take_unconfirmed",
549
550
  "upsell_refund_failed",
551
+ "upsell_refund_unclear",
550
552
  ]);
551
553
  exports.SEALED_HOLD_SESSION = /^[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{24,400}$/;
552
554
  exports.UPSELL_WALLET_ONLY_STATES = new Set(["tap", "authorized", "taking", "released"]);
@@ -611,6 +613,8 @@ function isUpsellRecord(v) {
611
613
  }
612
614
  if (r.rechecked === true && (wallet || !["declined", "skipped", "refunding", "refunded"].includes(r.state)))
613
615
  return false;
616
+ if (r.refund_unseen_step !== undefined && (r.state !== "refunding" || !Number.isInteger(r.refund_unseen_step) || r.refund_unseen_step < 0))
617
+ return false;
614
618
  if (r.prior_intents !== undefined) {
615
619
  const list = r.prior_intents;
616
620
  if (wallet || !Array.isArray(list) || list.length < 1 || list.length > exports.UPSELL_MAX_ATTEMPTS - 1)
@@ -621,7 +625,9 @@ function isUpsellRecord(v) {
621
625
  return false;
622
626
  if (typeof x.currency !== "string" || !CURRENCY.test(x.currency) || typeof x.at !== "string" || !Number.isFinite(Date.parse(x.at)))
623
627
  return false;
624
- if (!(x.stuck === undefined || x.stuck === "refund_failed" || x.stuck === "amount_mismatch"))
628
+ if (!(x.stuck === undefined || x.stuck === "refund_failed" || x.stuck === "amount_mismatch" || x.stuck === "refund_unclear"))
629
+ return false;
630
+ if (!(x.unseen_step === undefined || (Number.isInteger(x.unseen_step) && x.unseen_step >= 0)))
625
631
  return false;
626
632
  }
627
633
  }
@@ -503,7 +503,7 @@ async function latestOutcome(host, input, orderId, latest) {
503
503
  case "released":
504
504
  return { outcome: "released", why: latest.release_reason ?? "abandoned", retryable: (0, session_data_1.upsellMayRetry)(latest) };
505
505
  case "held":
506
- return latest.hold_reason === "upsell_refund_failed" ? { outcome: "refunded", amount_minor: latest.amount_minor, currency: latest.currency, pending: true } : { outcome: "unknown" };
506
+ return latest.hold_reason === "upsell_refund_failed" || latest.hold_reason === "upsell_refund_unclear" ? { outcome: "refunded", amount_minor: latest.amount_minor, currency: latest.currency, pending: true } : { outcome: "unknown" };
507
507
  case "stopped":
508
508
  return { outcome: "declined", retryable: false, decline_kind: "bank_check_unfinished" };
509
509
  case "refunding":
@@ -908,6 +908,8 @@ async function advance(ctx, from, next) {
908
908
  for (const k of Object.keys(value))
909
909
  if (value[k] === undefined)
910
910
  delete value[k];
911
+ if (value.state !== "refunding")
912
+ delete value.refund_unseen_step;
911
913
  if (!(0, session_data_1.isUpsellRecord)(value)) {
912
914
  ctx.host.log("error", "upsell_record_failed", { ...ctx.ids, state: next.state, why: "invalid_record", vp_payment_intent_id: from.vp_payment_intent_id });
913
915
  return undefined;
@@ -1019,24 +1021,53 @@ async function refundLate(ctx, r0, extra = {}) {
1019
1021
  await advance(ctx, r, { state: "held", hold_reason: "upsell_paid_unrecorded", decline_kind: null, rechecked: undefined });
1020
1022
  return { outcome: "unknown" };
1021
1023
  }
1022
- const res = await host.refundLate(r.vp_payment_intent_id, r.amount_minor, r.currency, lateRefundStep(host.now(), r0.state === "refunding" ? r0.at : null));
1024
+ if (!refundFitsLock(ctx)) {
1025
+ host.log("warn", "upsell_late_refund", { ...ids, result: "lock_time" });
1026
+ return answer(false);
1027
+ }
1028
+ const step = r.refund_unseen_step ?? lateRefundStep(host.now(), r0.state === "refunding" ? r0.at : null);
1029
+ const res = await host.refundLate(r.vp_payment_intent_id, r.amount_minor, r.currency, step);
1023
1030
  if (res === "refunded") {
1024
1031
  host.log("info", "upsell_late_refund", { ...ids, result: "refunded" });
1025
1032
  return answer(!!(await advance(ctx, r, { state: "refunded" })));
1026
1033
  }
1027
- const waitedOut = res === "unsettled" && r0.state === "refunding" && host.now() - Date.parse(r0.at) >= exports.UPSELL_UNSETTLED_REFUND_MS;
1034
+ if (res === "spent" && r.refund_unseen_step !== undefined) {
1035
+ host.log("error", "upsell_late_refund", { ...ids, result: "unclear", step });
1036
+ await advance(ctx, r, { state: "held", hold_reason: "upsell_refund_unclear", decline_kind: null, rechecked: undefined, refund_unseen_step: undefined });
1037
+ return answer(false);
1038
+ }
1039
+ const waiting = res === "unsettled" || res === "spent";
1040
+ const waitedOut = waiting && r0.state === "refunding" && host.now() - Date.parse(r0.at) >= exports.UPSELL_UNSETTLED_REFUND_MS;
1028
1041
  if (res === "failed" || waitedOut) {
1029
1042
  host.log("error", "upsell_late_refund", { ...ids, result: res === "failed" ? "failed" : "not_settled" });
1030
- await advance(ctx, r, { state: "held", hold_reason: "upsell_refund_failed", decline_kind: null, rechecked: undefined });
1043
+ await advance(ctx, r, { state: "held", hold_reason: r.refund_unseen_step !== undefined ? "upsell_refund_unclear" : "upsell_refund_failed", decline_kind: null, rechecked: undefined, refund_unseen_step: undefined });
1044
+ }
1045
+ else if (waiting) {
1046
+ host.log("info", "upsell_late_refund", { ...ids, result: "not_settled_yet" });
1047
+ }
1048
+ else if (res === "unread") {
1049
+ host.log(stuck ? "error" : "warn", "upsell_late_refund", { ...ids, result: "list_unreadable", step });
1031
1050
  }
1032
1051
  else {
1033
- if (res === "unsettled")
1034
- host.log("info", "upsell_late_refund", { ...ids, result: "not_settled_yet" });
1035
- else
1036
- host.log(stuck ? "error" : "warn", "upsell_late_refund", { ...ids, result: stuck ? "still_pending" : "retry" });
1052
+ if (r.refund_unseen_step === undefined)
1053
+ await writeSameAt(ctx, r, { refund_unseen_step: step });
1054
+ host.log(stuck ? "error" : "warn", "upsell_late_refund", { ...ids, result: stuck ? "still_pending" : "retry", step });
1037
1055
  }
1038
1056
  return answer(false);
1039
1057
  }
1058
+ function refundFitsLock(ctx) {
1059
+ const t = ctx.host.timing();
1060
+ return ctx.lockRemainingMs() >= 2 * t.vonpayMs + t.sendMarginMs;
1061
+ }
1062
+ async function writeSameAt(ctx, r, patch) {
1063
+ const value = { ...r, ...patch };
1064
+ for (const k of Object.keys(value))
1065
+ if (value[k] === undefined)
1066
+ delete value[k];
1067
+ if (!(0, session_data_1.isUpsellRecord)(value))
1068
+ return r;
1069
+ return (await ctx.host.writeUpsell(ctx.collectionId, value, { expect: "at", at: r.at })) ? value : r;
1070
+ }
1040
1071
  async function stopOpenCheck(ctx, r) {
1041
1072
  if (!(await cancelOwnEdit(ctx, r)))
1042
1073
  return "waiting";
@@ -1052,7 +1083,7 @@ async function readStoppedIntent(ctx, r) {
1052
1083
  return { kind: "mismatch", declineKind: null, pi };
1053
1084
  return { kind: "charged", declineKind: null, pi };
1054
1085
  }
1055
- if (pi.status === "failed" || pi.status === "voided") {
1086
+ if (pi.status === "failed" || pi.status === "voided" || pi.status === "cancelled") {
1056
1087
  const code = pi.declineCode;
1057
1088
  return { kind: "ended", declineKind: code === "authentication_not_completed" ? "bank_check_unfinished" : null, pi };
1058
1089
  }
@@ -1083,7 +1114,7 @@ async function recheckPrior(ctx, r) {
1083
1114
  next.push(p);
1084
1115
  continue;
1085
1116
  }
1086
- if (pi.status === "failed" || pi.status === "voided") {
1117
+ if (pi.status === "failed" || pi.status === "voided" || pi.status === "cancelled") {
1087
1118
  host.log("info", "upsell_rechecked", { ...ids, result: "ended" });
1088
1119
  changed = true;
1089
1120
  continue;
@@ -1097,21 +1128,42 @@ async function recheckPrior(ctx, r) {
1097
1128
  stuck("amount_mismatch");
1098
1129
  continue;
1099
1130
  }
1131
+ if (!refundFitsLock(ctx)) {
1132
+ host.log("warn", "upsell_late_refund", { ...ids, result: "lock_time" });
1133
+ next.push(p);
1134
+ continue;
1135
+ }
1100
1136
  host.log("warn", "upsell_charged_after_stop", ids);
1101
- const res = await host.refundLate(p.id, p.amount_minor, p.currency, lateRefundStep(host.now(), p.at));
1102
- const waitedOut = res === "unsettled" && age >= exports.UPSELL_UNSETTLED_REFUND_MS;
1137
+ const step = p.unseen_step ?? lateRefundStep(host.now(), p.at);
1138
+ const res = await host.refundLate(p.id, p.amount_minor, p.currency, step);
1139
+ const waiting = res === "unsettled" || (res === "spent" && p.unseen_step === undefined);
1140
+ const waitedOut = waiting && age >= exports.UPSELL_UNSETTLED_REFUND_MS;
1103
1141
  if (res === "refunded") {
1104
1142
  host.log("info", "upsell_late_refund", { ...ids, result: "refunded" });
1105
1143
  changed = true;
1106
1144
  }
1145
+ else if (res === "spent" && p.unseen_step !== undefined) {
1146
+ host.log("error", "upsell_late_refund", { ...ids, result: "unclear", step });
1147
+ stuck("refund_unclear");
1148
+ }
1107
1149
  else if (res === "failed" || waitedOut) {
1108
1150
  host.log("error", "upsell_late_refund", { ...ids, result: res === "failed" ? "failed" : "not_settled" });
1109
- stuck("refund_failed");
1151
+ stuck(p.unseen_step !== undefined ? "refund_unclear" : "refund_failed");
1110
1152
  }
1111
- else {
1112
- host.log(res === "unsettled" ? "info" : "warn", "upsell_late_refund", { ...ids, result: res === "unsettled" ? "not_settled_yet" : "retry" });
1153
+ else if (waiting) {
1154
+ host.log("info", "upsell_late_refund", { ...ids, result: "not_settled_yet" });
1155
+ next.push(p);
1156
+ }
1157
+ else if (res === "unread") {
1158
+ host.log("warn", "upsell_late_refund", { ...ids, result: "list_unreadable", step });
1113
1159
  next.push(p);
1114
1160
  }
1161
+ else {
1162
+ host.log("warn", "upsell_late_refund", { ...ids, result: "retry", step });
1163
+ if (p.unseen_step === undefined)
1164
+ changed = true;
1165
+ next.push({ ...p, unseen_step: step });
1166
+ }
1115
1167
  }
1116
1168
  if (!changed)
1117
1169
  return r;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vonpay-medusa",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Vonpay payment provider for Medusa v2",
5
5
  "author": "Von Payments",
6
6
  "license": "MIT",
@@ -60,6 +60,7 @@
60
60
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tests/contract/tsconfig.json && tsc --noEmit -p src/admin/tsconfig.json && tsc --noEmit -p tests/integration/tsconfig.json && tsc --noEmit -p tests/admin/tsconfig.json",
61
61
  "check:money": "node scripts/ci/check-money-arithmetic.mjs src && node scripts/ci/check-money-arithmetic.mjs --self-test",
62
62
  "check:release-install": "node scripts/ci/check-release-install.mjs && node scripts/ci/check-release-install.mjs --self-test",
63
+ "check:ci-scope": "node scripts/ci/check-ci-scope.mjs && node scripts/ci/check-ci-scope.mjs --self-test",
63
64
  "check:harness-isolation": "node scripts/ci/check-harness-isolation.mjs src apps && node scripts/ci/check-harness-isolation.mjs --self-test",
64
65
  "check:pack": "node scripts/ci/check-pack.mjs && node scripts/ci/check-pack.mjs --self-test",
65
66
  "check:pan-canary": "node scripts/ci/check-pan-canary.mjs && node scripts/ci/check-pan-canary.mjs --self-test",
@@ -183,7 +184,7 @@
183
184
  }
184
185
  },
185
186
  "dependencies": {
186
- "@vonpay/checkout-node": "^3.11.0",
187
+ "@vonpay/checkout-node": "^4.2.0",
187
188
  "bignumber.js": "^9.3.1"
188
189
  },
189
190
  "bin": {