@vonpay/checkout-cli 0.15.1 → 0.17.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +218 -29
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -53,7 +53,7 @@ All commands are namespaced under `vonpay checkout`:
53
53
  | `vonpay checkout refunds list` | List one payment's refunds and its refund totals (`--payment-intent` or `--transaction`) |
54
54
  | `vonpay checkout tokens create` | Create a payment-method token for saved-card / MIT flows |
55
55
  | `vonpay checkout capabilities` | Show what this merchant's binder supports |
56
- | `vonpay checkout trigger <event>` | Trigger a synthetic webhook event against your local endpoint (`session.succeeded`, `session.failed`, `payment_intent.*`, `charge.refunded`) |
56
+ | `vonpay checkout trigger <event>` | Trigger a synthetic webhook event against your local endpoint (`charge.*`, `refund.failed`, `payment_intent.*`, `mirror.order.created`, `subscription.*`, `merchant.card_testing_suspected`, `session.*`; `vonpay checkout trigger --help` lists them all) |
57
57
  | `vonpay checkout listen` | Stream live webhook delivery attempts; optionally re-forward each to a local URL with `--forward-to` |
58
58
  | `vonpay checkout health` | Check API health and latency |
59
59
  | `vonpay checkout doctor` | Diagnose runtime + env + live API probe |
package/dist/index.js CHANGED
@@ -36,6 +36,12 @@ function emitCliError(json, text) {
36
36
  // src/utils/key-mode.ts
37
37
  import { isLiveKey } from "@vonpay/checkout-node";
38
38
  var TEST_SECRET_KEY_PREFIX = "vp_sk_test_";
39
+ function requiresLiveConfirmation(key) {
40
+ if (typeof key !== "string") return true;
41
+ if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return false;
42
+ if (key.startsWith("vp_pk_")) return false;
43
+ return true;
44
+ }
39
45
  function secretKeyMode(key) {
40
46
  if (typeof key !== "string") return null;
41
47
  if (isLiveKey(key)) return "live";
@@ -373,8 +379,19 @@ import {
373
379
  RETRY_WITHHELD_HELP,
374
380
  VonPayError
375
381
  } from "@vonpay/checkout-node";
382
+
383
+ // src/utils/secret-patterns.ts
376
384
  var SECRET_PATTERNS = [
385
+ // Checkout-session client secret (2026-10-21): keep the session id, which is
386
+ // useful in a support thread, and drop the secret after `_secret_`.
387
+ [/(vp_cs_(?:test|live)_[A-Za-z0-9_-]{1,64}?)_secret_[A-Za-z0-9]+/g, "$1_secret_***"],
377
388
  [/vp_sk_(?:test|live)_[A-Za-z0-9_-]+/g, "vp_sk_***"],
389
+ // A legacy API key (`vp_key_*`), which checkout still accepts and treats as
390
+ // secret, and a single-use subscription portal code (`vsc_` + 43 base64url
391
+ // characters; a longer run is taken whole). Both case-sensitive, as
392
+ // checkout's own rules (src/lib/scrub.ts). Added 2026-10-22.
393
+ [/vp_key_(?:test|live)_[A-Za-z0-9_-]+/g, "vp_key_***"],
394
+ [/vsc_[A-Za-z0-9_-]{43,}/g, "vsc_***"],
378
395
  [/ss_(?:test|live)_[A-Za-z0-9_-]+/g, "ss_***"],
379
396
  [/whsec_[A-Za-z0-9_-]+/g, "whsec_***"],
380
397
  [/sk_(?:test|live)_[A-Za-z0-9_-]+/g, "sk_***"],
@@ -385,6 +402,25 @@ function scrubSecrets(s) {
385
402
  for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
386
403
  return out;
387
404
  }
405
+
406
+ // src/utils/sanitize.ts
407
+ var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
408
+ var C0_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
409
+ function stripAnsi(s) {
410
+ return s.replace(ANSI_PATTERN, "").replace(C0_CONTROL_PATTERN, "");
411
+ }
412
+ function truncate(s, max) {
413
+ if (s.length <= max) return s;
414
+ return s.slice(0, max - 1) + "\u2026";
415
+ }
416
+ function safeDisplay(s, max = 200) {
417
+ return truncate(stripAnsi(s), max);
418
+ }
419
+ function safeInline(s, max = 200) {
420
+ return safeDisplay(s.replace(/[\r\n]+/g, " "), max);
421
+ }
422
+
423
+ // src/utils/errors.ts
388
424
  var CLI_READ_BACK = "From this CLI: `vonpay checkout payment-intents get <id>` (charge, capture, void), `vonpay checkout refunds list --payment-intent <id>` or `--transaction <id>` (refund), `sessions get <id>` (checkout session).";
389
425
  var READ_BACK_FIRST = "Check the payment first: read it back (`vonpay checkout payment-intents get <id>` for a charge, capture or void; `vonpay checkout refunds list --payment-intent <id>` (or `--transaction <id>`) for a refund; `sessions get <id>` for a checkout session) and act only on what it shows.";
390
426
  var UNKNOWN_OUTCOME_GUIDANCE = `${READ_BACK_FIRST} If you then retry, reuse the SAME --idempotency-key as the first attempt, never a new one: a new key is a new action and can move money twice.`;
@@ -401,7 +437,10 @@ var FIELD_PRINTERS = {
401
437
  // What to do is the SDK's RETRY_WITHHELD_HELP, printed on the Hint line
402
438
  // (the SDK puts it in llmHint; quoted here only when the error carries
403
439
  // something else). This line adds the CLI commands for the read-back.
404
- "Retry withheld: the outcome is unknown and the CLI did not retry it. " + (e.llmHint === RETRY_WITHHELD_HELP.llmHint ? "" : `${RETRY_WITHHELD_HELP.llmHint} `) + CLI_READ_BACK
440
+ "Retry withheld: the outcome is unknown and the CLI did not retry it. " + // startsWith, not equality: the SDK extends the base hint for one code
441
+ // (SETTLE_ONLY_UNVERIFIABLE_WITHHELD_HELP), and the Hint line already
442
+ // carries it then, so it must not be printed twice.
443
+ (typeof e.llmHint === "string" && e.llmHint.startsWith(RETRY_WITHHELD_HELP.llmHint) ? "" : `${RETRY_WITHHELD_HELP.llmHint} `) + CLI_READ_BACK
405
444
  ) : null,
406
445
  captureOutcome: (e) => e.captureOutcome ? `Capture outcome: ${str(e.captureOutcome)} \u2014 the provider accepted the capture but it could not be recorded, so money may have moved. Do NOT capture again; read the payment intent back first.` : null,
407
446
  voidOutcome: (e) => e.voidOutcome ? `Void outcome: ${str(e.voidOutcome)} \u2014 the provider accepted the void but it could not be recorded. Do NOT void again or tell the buyer the hold was released until a read-back confirms it.` : null,
@@ -416,6 +455,44 @@ var FIELD_PRINTERS = {
416
455
  rawReason: (e) => e.rawReason && e.rawReason !== e.reason ? `Reason as sent: ${str(e.rawReason)}` : null,
417
456
  retryAfterSeconds: (e) => typeof e.retryAfterSeconds === "number" ? `Retry after: ${e.retryAfterSeconds} seconds \u2014 then ask again` : null,
418
457
  session: (e) => e.session ? `Session: ${JSON.stringify(e.session)}` : null,
458
+ // Restricted API keys (built ahead of the API; served in production since 2026-10-27).
459
+ // The SDK validates these; they are server-supplied all the same, so every
460
+ // value goes through safeInline (no escape sequence, control character or
461
+ // line break reaches the terminal) before it is printed.
462
+ limit: (e) => e.limit ? `Key limit: ${safeInline(String(e.limit))}` : null,
463
+ limitMinor: (e) => typeof e.limitMinor === "number" ? `Key limit amount: ${safeInline(String(e.limitMinor))} (minor units)` : null,
464
+ usedMinor: (e) => typeof e.usedMinor === "number" ? `Used in the last 24 hours: ${safeInline(String(e.usedMinor))} (minor units)` : null,
465
+ limitCurrency: (e) => e.limitCurrency ? `Key limit currency: ${safeInline(String(e.limitCurrency))}` : null,
466
+ refusedFields: (e) => Array.isArray(e.refusedFields) && e.refusedFields.length > 0 ? `Fields this key may not send: ${e.refusedFields.map((f) => safeInline(String(f), 128)).join(", ")}` : null,
467
+ // A refused payment-only settle (mirror_settle_only_*; built ahead of the
468
+ // API's production release): the charge asked for and the store order's
469
+ // facts. Validated by the SDK, printed through safeInline all the same.
470
+ chargeAmount: (e) => typeof e.chargeAmount === "number" ? `Charge asked for: ${safeInline(String(e.chargeAmount))} (minor units)` : null,
471
+ chargeCurrency: (e) => e.chargeCurrency ? `Charge currency: ${safeInline(String(e.chargeCurrency))}` : null,
472
+ orderBalance: (e) => typeof e.orderBalance === "number" ? `Store order balance: ${safeInline(String(e.orderBalance))} (minor units) \u2014 on an amount mismatch, charge this instead` : e.orderBalance === null ? "Store order balance: unknown \u2014 the store could not be read" : null,
473
+ orderBalanceRaw: (e) => e.orderBalanceRaw ? `Store's own balance figure: ${safeInline(String(e.orderBalanceRaw))}` : null,
474
+ orderCurrency: (e) => e.orderCurrency ? `Store order currency: ${safeInline(String(e.orderCurrency))}` : null,
475
+ orderStatus: (e) => e.orderStatus ? `Store order status: ${safeInline(String(e.orderStatus))}` : null,
476
+ orderPaymentStatus: (e) => e.orderPaymentStatus ? `Store order payment status: ${safeInline(String(e.orderPaymentStatus))}` : null,
477
+ // A refused subscription change (built ahead of the API's production release). Validated by
478
+ // the SDK, printed through safeInline all the same.
479
+ subscriptionStatus: (e) => e.subscriptionStatus ? `Subscription status: ${safeInline(String(e.subscriptionStatus))}` : null,
480
+ refusalReason: (e) => e.refusalReason ? `Refused because: ${safeInline(String(e.refusalReason))}` : null,
481
+ minDate: (e) => e.minDate ? `Earliest date allowed: ${safeInline(String(e.minDate))}` : null,
482
+ maxDate: (e) => e.maxDate ? `Latest date allowed: ${safeInline(String(e.maxDate))}` : null,
483
+ currentAmount: (e) => typeof e.currentAmount === "number" ? `Current highest renewal charge: ${safeInline(String(e.currentAmount))} (minor units)` : null,
484
+ maxAmount: (e) => typeof e.maxAmount === "number" ? `Most this key may make a renewal charge: ${safeInline(String(e.maxAmount))} (minor units)` : e.maxAmount === null ? "Most this key may make a renewal charge: none \u2014 this key may only lower the price" : null,
485
+ amountCurrency: (e) => e.amountCurrency ? `Renewal charge currency: ${safeInline(String(e.amountCurrency))}` : null,
486
+ pendingKind: (e) => e.pendingKind ? `Schedule change waiting for its customer notice: ${safeInline(String(e.pendingKind))}` : null,
487
+ // A charge the payment provider refused before any money moved (built ahead
488
+ // of the API's production release): the failed payment intent that records
489
+ // the attempt. The SDK has checked it is a vpi_* id; printed through
490
+ // safeInline all the same. The next step depends on the code: a card the
491
+ // processor is still storing (payment_method_unvaulted) is retried as is
492
+ // after a short wait; every other refusal needs the request or card fixed.
493
+ // Both under a NEW key: the refused record does not hold the key, and the
494
+ // processor may refuse a resend under the same one.
495
+ paymentIntentId: (e) => e.paymentIntentId ? `Refused attempt recorded as: ${safeInline(String(e.paymentIntentId))} (status failed, nothing was charged; ` + (e.code === "payment_method_unvaulted" ? "wait a few seconds, then send the same charge again with a NEW --idempotency-key)" : "fix the request or the card, then charge with a NEW --idempotency-key)") : null,
419
496
  // On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
420
497
  // one wording for every surface — so it is printed as is.
421
498
  llmHint: (e) => e.llmHint ? `Hint: ${str(e.llmHint)}` : null,
@@ -535,6 +612,22 @@ function optionalMinorUnitsOrExit(raw, flag) {
535
612
  return raw === void 0 ? void 0 : minorUnitsOrExit(raw, flag);
536
613
  }
537
614
 
615
+ // src/utils/mask-secrets.ts
616
+ function maskSecretsInJson(json) {
617
+ return scrubSecrets(json);
618
+ }
619
+ var SHOW_SECRETS_HELP = "With --json, print the session's client secret in full. \u26A0 The output then contains a secret: do not paste it or let it reach a log";
620
+ function withClientSecret(session) {
621
+ if (session === null || typeof session !== "object" || Array.isArray(session)) return session;
622
+ if (!("clientSecret" in session)) return session;
623
+ return { ...session, clientSecret: session.clientSecret };
624
+ }
625
+ function sessionJson(session, showSecrets) {
626
+ const json = JSON.stringify(withClientSecret(session), null, 2);
627
+ if (json === void 0) return String(json);
628
+ return showSecrets === true ? json : maskSecretsInJson(json);
629
+ }
630
+
538
631
  // src/commands/sessions.ts
539
632
  var makeClient = makeCheckoutClient;
540
633
  function sessionLines(session) {
@@ -582,7 +675,7 @@ function sessionsCommand() {
582
675
  const sessions = new Command4("sessions").description(
583
676
  "Create and manage checkout sessions"
584
677
  );
585
- sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units, a whole number (e.g. 1499 = $14.99)").requiredOption("--currency <code>", "Currency code (e.g. USD)").option("--country <code>", "Country code (e.g. US)").option("--description <text>", "Session description").option("--success-url <url>", "Redirect URL on success").option("--cancel-url <url>", "Redirect URL on cancel").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--dry-run", "Validate without creating a session").option("--json", "Output raw JSON").action(async (opts) => {
678
+ sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units, a whole number (e.g. 1499 = $14.99)").requiredOption("--currency <code>", "Currency code (e.g. USD)").option("--country <code>", "Country code (e.g. US)").option("--description <text>", "Session description").option("--success-url <url>", "Redirect URL on success").option("--cancel-url <url>", "Redirect URL on cancel").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--dry-run", "Validate without creating a session").option("--json", "Output raw JSON (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (opts) => {
586
679
  const amount = minorUnitsOrExit(String(opts.amount), "--amount");
587
680
  const client = makeClient();
588
681
  const spinner = startSpinner("Creating session...");
@@ -619,7 +712,7 @@ function sessionsCommand() {
619
712
  );
620
713
  spinner.stop();
621
714
  if (opts.json) {
622
- console.log(JSON.stringify(session, null, 2));
715
+ console.log(sessionJson(session, opts.showSecrets));
623
716
  } else {
624
717
  console.log(chalk5.green("\u2713 Session created"));
625
718
  console.log(` ID: ${session.id}`);
@@ -631,14 +724,14 @@ function sessionsCommand() {
631
724
  handleError(err);
632
725
  }
633
726
  });
634
- sessions.command("get").description("Get session status").argument("<id>", "Session ID").option("--json", "Output raw JSON").action(async (id, opts) => {
727
+ sessions.command("get").description("Get session status").argument("<id>", "Session ID").option("--json", "Output raw JSON (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (id, opts) => {
635
728
  const client = makeClient();
636
729
  const spinner = startSpinner("Fetching session...");
637
730
  try {
638
731
  const session = await client.sessions.get(id);
639
732
  spinner.stop();
640
733
  if (opts.json) {
641
- console.log(JSON.stringify(session, null, 2));
734
+ console.log(sessionJson(session, opts.showSecrets));
642
735
  } else {
643
736
  for (const line of sessionLines(session)) console.log(line);
644
737
  }
@@ -715,20 +808,6 @@ function parseMetadata(pairs) {
715
808
  return out;
716
809
  }
717
810
 
718
- // src/utils/sanitize.ts
719
- var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
720
- var C0_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
721
- function stripAnsi(s) {
722
- return s.replace(ANSI_PATTERN, "").replace(C0_CONTROL_PATTERN, "");
723
- }
724
- function truncate(s, max) {
725
- if (s.length <= max) return s;
726
- return s.slice(0, max - 1) + "\u2026";
727
- }
728
- function safeDisplay(s, max = 200) {
729
- return truncate(stripAnsi(s), max);
730
- }
731
-
732
811
  // src/utils/live-key.ts
733
812
  import { createInterface as createInterface2 } from "readline";
734
813
  import chalk7 from "chalk";
@@ -762,10 +841,10 @@ var terminal = {
762
841
  };
763
842
  function keyFingerprint(key) {
764
843
  const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
765
- return m ? `${m[1]}${m[2]}\u2026` : "vp_sk_live_\u2026";
844
+ return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
766
845
  }
767
846
  async function confirmLiveAction(key, summary, opts = {}) {
768
- if (!isLiveKey2(key)) return;
847
+ if (!requiresLiveConfirmation(key)) return;
769
848
  if (!terminal.isInteractive()) {
770
849
  const message = `Refusing to ${summary.action} with a live key: no person is at an interactive terminal to confirm it. A live-key action must be confirmed by a person typing at a terminal (stdin and stdout both a terminal); scripts, CI jobs and AI agents cannot confirm it, and --confirm-live is no longer enough. Use a sandbox key (vp_sk_test_\u2026) for automation, or run the command yourself in a terminal. Nothing was sent.`;
771
850
  exitWithCliError(message, chalk7.red(message));
@@ -796,7 +875,7 @@ function retiredConfirmLiveOption() {
796
875
  ).hideHelp();
797
876
  }
798
877
  function warnLiveSecretFlag(key, flag) {
799
- if (!isLiveKey2(key)) return;
878
+ if (!requiresLiveConfirmation(key)) return;
800
879
  console.warn(
801
880
  chalk7.yellow(
802
881
  `Warning: passing ${flag} as a command argument with a live key exposes it to shell history and process-list (\`ps\`). Consider piping or env-injecting the value instead.`
@@ -872,8 +951,15 @@ function buildMITBlock(opts) {
872
951
  originalTransactionId
873
952
  };
874
953
  }
954
+ function statusTone(status) {
955
+ if (status === "succeeded") return "green";
956
+ if (status === "authorized") return "cyan";
957
+ if (status === "requires_action") return "yellow";
958
+ if (status === "failed" || status === "cancelled") return "red";
959
+ return "gray";
960
+ }
875
961
  function printPaymentIntent(intent) {
876
- const statusColor = intent.status === "succeeded" ? chalk8.green : intent.status === "authorized" ? chalk8.cyan : intent.status === "requires_action" ? chalk8.yellow : intent.status === "failed" || intent.status === "canceled" ? chalk8.red : chalk8.gray;
962
+ const statusColor = chalk8[statusTone(intent.status)];
877
963
  console.log(` ID: ${intent.id}`);
878
964
  console.log(` Status: ${statusColor(intent.status)}`);
879
965
  console.log(
@@ -893,6 +979,12 @@ function printPaymentIntent(intent) {
893
979
  if (intent.declineCode) {
894
980
  console.log(` Decline code: ${chalk8.red(safeDisplay(intent.declineCode))}`);
895
981
  }
982
+ if (intent.networkAdvice) {
983
+ console.log(` Network advice: ${chalk8.red(safeInline(intent.networkAdvice))}`);
984
+ if (typeof intent.networkAdviceRetryAfterMs === "number") {
985
+ console.log(` Retry after: ${intent.networkAdviceRetryAfterMs} ms (earliest)`);
986
+ }
987
+ }
896
988
  console.log(` Created: ${intent.createdAt}`);
897
989
  }
898
990
  var AMOUNT_HELP = "Amount in minor units, a whole number (e.g. 1499 = $14.99)";
@@ -1607,6 +1699,15 @@ var SUPPORTED_EVENTS = [
1607
1699
  "payment_intent.failed",
1608
1700
  "payment_intent.cancelled",
1609
1701
  "mirror.order.created",
1702
+ // Opt-in events (an endpoint gets them only when it lists each by name).
1703
+ // Payloads follow checkout's own "Send test event" samples, which are built
1704
+ // by the same code as the real events.
1705
+ "subscription.created",
1706
+ "subscription.renewed",
1707
+ "subscription.payment_failed",
1708
+ "subscription.cancelled",
1709
+ "subscription.updated",
1710
+ "merchant.card_testing_suspected",
1610
1711
  "session.succeeded",
1611
1712
  "session.failed"
1612
1713
  ];
@@ -1627,6 +1728,58 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
1627
1728
  );
1628
1729
  return true;
1629
1730
  }
1731
+ var PROCESSOR_CONNECTION = null;
1732
+ function subscriptionEventData(event, paymentIntentId, amount, currency) {
1733
+ const now = Math.floor(Date.now() / 1e3);
1734
+ const inAMonth = now + 30 * 24 * 60 * 60;
1735
+ const base = {
1736
+ subscription_id: `vsub_test_${randomUUID2().replace(/-/g, "").slice(0, 16)}`,
1737
+ status: "active",
1738
+ billing_cycle: null,
1739
+ next_bill_at: inAMonth,
1740
+ amount: null,
1741
+ renewal_amount: amount,
1742
+ currency,
1743
+ payment_intent_id: null,
1744
+ action: null,
1745
+ decline_message: null,
1746
+ decline_code: null,
1747
+ cancel_reason: null,
1748
+ changed_fields: null,
1749
+ // Who made the change (served since checkout's production release of 2026-10-30): an API key
1750
+ // with no id or label on these samples. Set on every event.
1751
+ actor: { type: "api_key", api_key_id: null, label: null },
1752
+ // A cancel scheduled for the end of the paid period, and the discount on future
1753
+ // renewals (served since 2026-10-30): none on these samples.
1754
+ cancel_at_period_end: false,
1755
+ discount: null,
1756
+ // The merchant's own sign-up metadata; `{}` when none was sent.
1757
+ metadata: {}
1758
+ };
1759
+ switch (event) {
1760
+ case "subscription.created":
1761
+ return { ...base, billing_cycle: 1, amount, payment_intent_id: paymentIntentId };
1762
+ case "subscription.renewed":
1763
+ return { ...base, billing_cycle: 2, amount, payment_intent_id: paymentIntentId };
1764
+ case "subscription.payment_failed":
1765
+ return {
1766
+ ...base,
1767
+ status: "past_due",
1768
+ billing_cycle: 2,
1769
+ next_bill_at: now + 3 * 24 * 60 * 60,
1770
+ amount,
1771
+ payment_intent_id: paymentIntentId,
1772
+ // The same three values `charge.failed` carries for this code.
1773
+ action: "soft",
1774
+ decline_message: "The card was declined for insufficient funds.",
1775
+ decline_code: "insufficient_funds"
1776
+ };
1777
+ case "subscription.cancelled":
1778
+ return { ...base, status: "cancelled", next_bill_at: null, cancel_reason: "too_expensive" };
1779
+ case "subscription.updated":
1780
+ return { ...base, renewal_amount: Math.max(1, Math.floor(amount * 0.9)), changed_fields: ["renewal_amount"] };
1781
+ }
1782
+ }
1630
1783
  function buildEventData(event, ids, amount, currency) {
1631
1784
  const base = {
1632
1785
  session_id: ids.sessionId,
@@ -1635,19 +1788,27 @@ function buildEventData(event, ids, amount, currency) {
1635
1788
  amount,
1636
1789
  currency
1637
1790
  };
1791
+ const notARenewal = { subscription_id: null, billing_cycle: null };
1638
1792
  switch (event) {
1639
1793
  case "session.succeeded":
1640
- case "payment_intent.succeeded":
1641
1794
  return base;
1795
+ case "payment_intent.succeeded":
1796
+ return { ...base, processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
1642
1797
  case "charge.succeeded":
1643
- return { ...base, vp_tx_id: ids.transactionId, card: null };
1798
+ return { ...base, vp_tx_id: ids.transactionId, card: null, processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
1644
1799
  case "session.failed":
1645
1800
  case "payment_intent.failed":
1646
1801
  return {
1647
1802
  ...base,
1648
1803
  failure_reason: "Card declined",
1649
1804
  failure_code: "card_declined",
1650
- network_decline_code: "05"
1805
+ network_decline_code: "05",
1806
+ // The card network's advice: null when the network gave none that
1807
+ // could be read (as on this ordinary decline) — never permission to retry.
1808
+ network_advice: null,
1809
+ network_advice_retry_after_ms: null,
1810
+ // payment_intent.failed only: session.failed sends neither.
1811
+ ...event === "payment_intent.failed" ? { processor_connection: PROCESSOR_CONNECTION, ...notARenewal } : {}
1651
1812
  };
1652
1813
  case "charge.failed":
1653
1814
  return {
@@ -1659,7 +1820,12 @@ function buildEventData(event, ids, amount, currency) {
1659
1820
  rule_code: null,
1660
1821
  // Always null on a decline: no money moved, no settlement row.
1661
1822
  vp_tx_id: null,
1662
- network_decline_code: "05"
1823
+ network_decline_code: "05",
1824
+ // Same two fields and values as payment_intent.failed / session.failed.
1825
+ network_advice: null,
1826
+ network_advice_retry_after_ms: null,
1827
+ processor_connection: PROCESSOR_CONNECTION,
1828
+ ...notARenewal
1663
1829
  };
1664
1830
  case "mirror.order.created":
1665
1831
  return {
@@ -1668,7 +1834,28 @@ function buildEventData(event, ids, amount, currency) {
1668
1834
  order_status_url: null
1669
1835
  };
1670
1836
  case "payment_intent.cancelled":
1671
- return { ...base, cancellation_reason: "buyer_abandoned" };
1837
+ return { ...base, cancellation_reason: "buyer_abandoned", processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
1838
+ case "subscription.created":
1839
+ case "subscription.renewed":
1840
+ case "subscription.payment_failed":
1841
+ case "subscription.cancelled":
1842
+ case "subscription.updated":
1843
+ return subscriptionEventData(event, ids.paymentIntentId, amount, currency);
1844
+ case "merchant.card_testing_suspected": {
1845
+ const now = Math.floor(Date.now() / 1e3);
1846
+ return {
1847
+ patterns: ["expiry_enumeration", "refusal_burst"],
1848
+ window_start: now - 60 * 60,
1849
+ window_end: now,
1850
+ attempts: 30,
1851
+ refused: 24,
1852
+ accepted: 3,
1853
+ distinct_cards: 12,
1854
+ card: { brand: "visa", last4: "4242" },
1855
+ expiry_variants: 9,
1856
+ blocked: false
1857
+ };
1858
+ }
1672
1859
  case "charge.refunded":
1673
1860
  return {
1674
1861
  ...base,
@@ -1682,7 +1869,9 @@ function buildEventData(event, ids, amount, currency) {
1682
1869
  reason: "customer_request",
1683
1870
  is_partial: false,
1684
1871
  original_charge_amount: amount,
1685
- card: null
1872
+ card: null,
1873
+ processor_connection: PROCESSOR_CONNECTION,
1874
+ ...notARenewal
1686
1875
  };
1687
1876
  case "refund.failed":
1688
1877
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonpay/checkout-cli",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Von Payments CLI — developer tools for checkout integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  "node": ">=20"
21
21
  },
22
22
  "dependencies": {
23
- "@vonpay/checkout-node": "^4.0.0",
23
+ "@vonpay/checkout-node": "^4.4.0",
24
24
  "commander": "^13.0.0",
25
25
  "chalk": "^5.4.0",
26
26
  "ora": "^8.2.0"