@vonpay/checkout-cli 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.js +576 -52
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ All commands are namespaced under `vonpay checkout`:
|
|
|
56
56
|
| `vonpay checkout tokens create` | Create a payment-method token for saved-card / MIT flows |
|
|
57
57
|
| `vonpay checkout subscription-products list` | List your subscription products, newest first, each with its price, how often it renews and the terms a sign-up on it states (`--product-ref`, `--status active\|archived`, `--limit`, `--starting-after`). Read-only |
|
|
58
58
|
| `vonpay checkout subscription-products terms <product>` | Read one product's renewal terms (by id or reference): the disclosure to show next to the order button before the sign-up charge, its price phrase and terms version. `--amount` for an intro first payment in minor units. Read-only |
|
|
59
|
+
| `vonpay checkout subscription-products get <product>` | Read one product (by id, or the reference of an active one): price, schedule, routing profile, stores, how many subscriptions are on it (active, past due, paused or held). Read-only. *Unreleased: the API is not served yet* |
|
|
60
|
+
| `vonpay checkout subscription-products create` / `update <product>` / `change-price <product>` / `archive <product>` / `unarchive <product>` | Create and change subscription products (the dashboard's rules). Every write needs `--idempotency-key` (never generated; reuse it only to retry that exact change) and takes `--actor-label` (1-64 lowercase letters, digits and `_ . : -`, starting with a letter or digit). On a live key, a person confirms at the terminal `update`, `archive`, `unarchive` and `change-price --apply-to-existing` (it lowers what existing subscribers pay), after the product is looked up; `create` and a price for new sign-ups only are not prompted. `change-price` exits 0 while a large cut is still being applied (it is saved): a script reading `--json` must check `outcome` (`completed` or `in_progress`) and, on `in_progress`, run the same command with the same `--idempotency-key` again. *Unreleased: the API is not served yet* |
|
|
59
61
|
| `vonpay checkout capabilities` | Show what this merchant's binder supports |
|
|
60
62
|
| `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) |
|
|
61
63
|
| `vonpay checkout listen` | Stream live webhook delivery attempts; optionally re-forward each to a local URL with `--forward-to` |
|
|
@@ -97,7 +99,7 @@ The money commands always send an idempotency key. **On a live key you must pass
|
|
|
97
99
|
|
|
98
100
|
**A live key is only sent to a Von Payments API host** (`https://checkout.vonpay.com` or `https://checkout-staging.vonpay.com`). If `VONPAY_BASE_URL` or `baseUrl` in `~/.vonpay/config.json` points anywhere else, live-key commands refuse (before any confirmation is asked), and `doctor` skips its key-carrying probe and says why. For a proxy you run yourself, set `VONPAY_ALLOW_CUSTOM_BASE_URL=1` **and** pass `--allow-custom-base-url` on each command (it is accepted after any command); either alone is refused. The environment variable, like the base URL it unlocks, can be left behind by someone else and would then apply to every later command; the flag has to be typed on each command, where it shows. Whoever controls your shell can do both, which is why the money commands still need a person at the terminal and show the API host they will use. The variable is read from the real environment only (the CLI loads no `.env` file). Sandbox keys are not restricted, but a base URL with a user name or password in it, or one that is not a valid URL, is refused for every key.
|
|
99
101
|
|
|
100
|
-
**A local audit log of every command (on by default).** Each command appends one line to `~/.vonpay/audit/cli-YYYY-MM-DD.jsonl` (UTC date; the folder is readable only by you, each file `0600`): when, which command, test or live key, a key fingerprint, and what happened — `sent`, `local` (a command that sends nothing to the API, such as `login` or `trigger`), `approved` (a person typed `confirm`), `declined`, `withheld` (no person at a terminal), `lookup_refused`, `refused_local` (including a command line the CLI cannot parse, such as an unknown command or option) or `error` (including `interrupted` for Ctrl-C and `internal_error` for a crash) — with the API's error code and request id when there is one, and for money commands the amount, currency and payment, transaction or session id. A money command also writes a `pending` line just before it sends, so a run that is killed mid-send still leaves a trace. **The key fingerprint is the key's first 12 and last 4 characters.** The first 12 are `vp_sk_live_` or `vp_sk_test_` plus one character of the secret, so the log holds 5 of your key's secret characters — enough to tell your keys apart, not enough to use one. A key with a prefix the SDK does not know is written as `[unknown key]` and a short hash instead, with none of its characters. It never holds a request body, metadata, a reason or note, a description, a URL, an email, card data, an idempotency key, the full key, the command line you typed or any other free text; an id, error code or request id that does not have the API's exact shape is written as a fixed placeholder. After an incident, read it to see what was run on this machine — by you, a script or an AI agent — and what happened. `VONPAY_AUDIT_DIR` moves it, `VONPAY_AUDIT_LOG=off` turns it off, and files older than `VONPAY_AUDIT_RETENTION_DAYS` (1–365, default 30) are deleted when a command starts. The folder must be your own real folder: a symbolic link, or a folder another user owns, is refused (with one warning). A day's file holds at most 50 MiB, and the last 5 MiB are kept for money lines: once the file reaches 45 MiB only `pending`, `approved` and `budget_refused` lines, and `sent` / `error` lines of a command that moves money, are written (one warning on stderr), so a loop of cheap refused calls cannot crowd out a later money record. A sandbox key's lines never use those last 5 MiB, so a sandbox loop cannot use them up either. When even a money line no longer fits, the file ends with ONE final line, `{"ts":"…","surface":"cli","version":"…","outcome":"log_full"}`, and nothing more is written to it that day (one more warning). The key's mode is the Node SDK's `keyMode`: `test`, `live`, `publishable_test`, `publishable_live` or `unknown` (held to the live-key rules). If the log cannot be written, the command warns once on stderr and carries on.
|
|
102
|
+
**A local audit log of every command (on by default).** Each command appends one line to `~/.vonpay/audit/cli-YYYY-MM-DD.jsonl` (UTC date; the folder is readable only by you, each file `0600`): when, which command, test or live key, a key fingerprint, and what happened — `sent`, `local` (a command that sends nothing to the API, such as `login` or `trigger`), `approved` (a person typed `confirm`), `declined`, `withheld` (no person at a terminal), `lookup_refused`, `refused_local` (including a command line the CLI cannot parse, such as an unknown command or option) or `error` (including `interrupted` for Ctrl-C and `internal_error` for a crash) — with the API's error code and request id when there is one, and for money commands the amount, currency and payment, transaction or session id (for a subscription product write: the product id, and the price and currency where the command takes them). A money command (and, on a live key, a subscription product write) also writes a `pending` line just before it sends, so a run that is killed mid-send still leaves a trace. **The key fingerprint is the key's first 12 and last 4 characters.** The first 12 are `vp_sk_live_` or `vp_sk_test_` plus one character of the secret, so the log holds 5 of your key's secret characters — enough to tell your keys apart, not enough to use one. A key with a prefix the SDK does not know is written as `[unknown key]` and a short hash instead, with none of its characters. It never holds a request body, metadata, a reason or note, a description, a URL, an email, card data, an idempotency key, the full key, the command line you typed or any other free text; an id, error code or request id that does not have the API's exact shape is written as a fixed placeholder. After an incident, read it to see what was run on this machine — by you, a script or an AI agent — and what happened. `VONPAY_AUDIT_DIR` moves it, `VONPAY_AUDIT_LOG=off` turns it off, and files older than `VONPAY_AUDIT_RETENTION_DAYS` (1–365, default 30) are deleted when a command starts. The folder must be your own real folder: a symbolic link, or a folder another user owns, is refused (with one warning). A day's file holds at most 50 MiB, and the last 5 MiB are kept for money lines: once the file reaches 45 MiB only `pending`, `approved` and `budget_refused` lines, and `sent` / `error` lines of a command that moves money, are written (one warning on stderr), so a loop of cheap refused calls cannot crowd out a later money record. A sandbox key's lines never use those last 5 MiB, so a sandbox loop cannot use them up either. When even a money line no longer fits, the file ends with ONE final line, `{"ts":"…","surface":"cli","version":"…","outcome":"log_full"}`, and nothing more is written to it that day (one more warning). The key's mode is the Node SDK's `keyMode`: `test`, `live`, `publishable_test`, `publishable_live` or `unknown` (held to the live-key rules). If the log cannot be written, the command warns once on stderr and carries on.
|
|
101
103
|
|
|
102
104
|
`trigger` and `listen --forward-to` never follow a redirect from your endpoint: a 3xx answer is reported as a failure, so the signed event is never re-sent somewhere else.
|
|
103
105
|
|
package/dist/index.js
CHANGED
|
@@ -41,9 +41,9 @@ function jsonErrorsEnabled() {
|
|
|
41
41
|
function printJsonError(obj) {
|
|
42
42
|
console.error(JSON.stringify(obj));
|
|
43
43
|
}
|
|
44
|
-
function emitCliError(json,
|
|
44
|
+
function emitCliError(json, text2) {
|
|
45
45
|
if (jsonErrors) printJsonError(json);
|
|
46
|
-
else console.error(
|
|
46
|
+
else console.error(text2);
|
|
47
47
|
return process.exit(1);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -245,6 +245,12 @@ var COMMAND_KIND = {
|
|
|
245
245
|
"checkout tokens create": "api",
|
|
246
246
|
"checkout subscription-products list": "api",
|
|
247
247
|
"checkout subscription-products terms": "api",
|
|
248
|
+
"checkout subscription-products get": "api",
|
|
249
|
+
"checkout subscription-products create": "api",
|
|
250
|
+
"checkout subscription-products update": "api",
|
|
251
|
+
"checkout subscription-products change-price": "api",
|
|
252
|
+
"checkout subscription-products archive": "api",
|
|
253
|
+
"checkout subscription-products unarchive": "api",
|
|
248
254
|
"checkout capabilities": "api",
|
|
249
255
|
"checkout listen": "api",
|
|
250
256
|
"checkout health": "api",
|
|
@@ -255,7 +261,12 @@ var MONEY = {
|
|
|
255
261
|
"checkout payment-intents capture": (o, a) => ({ amount: wholeNumber(o.amountToCapture), target: a[0] }),
|
|
256
262
|
"checkout payment-intents void": (_o, a) => ({ target: a[0] }),
|
|
257
263
|
"checkout refunds create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency, target: o.paymentIntent ?? o.transaction }),
|
|
258
|
-
"checkout sessions create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency })
|
|
264
|
+
"checkout sessions create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency }),
|
|
265
|
+
"checkout subscription-products create": (o) => ({ amount: wholeNumber(o.renewalPrice), currency: o.currency }),
|
|
266
|
+
"checkout subscription-products update": (_o, a) => ({ target: a[0] }),
|
|
267
|
+
"checkout subscription-products change-price": (o, a) => ({ amount: wholeNumber(o.renewalPrice), target: a[0], apply_to_existing: o.applyToExisting === true }),
|
|
268
|
+
"checkout subscription-products archive": (_o, a) => ({ target: a[0] }),
|
|
269
|
+
"checkout subscription-products unarchive": (_o, a) => ({ target: a[0] })
|
|
259
270
|
};
|
|
260
271
|
function isMoneyCommand(command) {
|
|
261
272
|
return Object.prototype.hasOwnProperty.call(MONEY, command);
|
|
@@ -674,15 +685,15 @@ import chalk6 from "chalk";
|
|
|
674
685
|
|
|
675
686
|
// src/utils/spinner.ts
|
|
676
687
|
import ora from "ora";
|
|
677
|
-
function startSpinner(
|
|
678
|
-
return ora({ text, isSilent: jsonErrorsEnabled() }).start();
|
|
688
|
+
function startSpinner(text2) {
|
|
689
|
+
return ora({ text: text2, isSilent: jsonErrorsEnabled() }).start();
|
|
679
690
|
}
|
|
680
691
|
|
|
681
692
|
// src/commands/sessions.ts
|
|
682
693
|
import { formatMinorUnits } from "@vonpay/checkout-node";
|
|
683
694
|
|
|
684
695
|
// src/client-factory.ts
|
|
685
|
-
import { VonPayCheckout } from "@vonpay/checkout-node";
|
|
696
|
+
import { VonPayCheckout, heldAsLive } from "@vonpay/checkout-node";
|
|
686
697
|
|
|
687
698
|
// src/app-info.ts
|
|
688
699
|
import { readFileSync as readFileSync3 } from "fs";
|
|
@@ -797,19 +808,19 @@ function uniqueKey(cleaned, taken) {
|
|
|
797
808
|
}
|
|
798
809
|
function cleanJsonValue(value, options = {}) {
|
|
799
810
|
const { lines = "keep", maxChars = Number.MAX_SAFE_INTEGER, redact = redactSecretShapes, exactKeys } = options;
|
|
800
|
-
const
|
|
811
|
+
const text2 = (s) => {
|
|
801
812
|
if (PLAIN_TEXT_PATTERN.test(s)) return truncate(s, maxChars);
|
|
802
813
|
return lines === "join" ? safeInline(s, maxChars) : truncate(stripAnsi(s.replace(LINE_SEPARATOR_PATTERN, " ")), maxChars);
|
|
803
814
|
};
|
|
804
815
|
const walk = (v, exact) => {
|
|
805
|
-
if (typeof v === "string") return exact ?
|
|
816
|
+
if (typeof v === "string") return exact ? text2(v) : redact(text2(v));
|
|
806
817
|
if (v === null || typeof v !== "object") return v;
|
|
807
818
|
const toJSON = v.toJSON;
|
|
808
819
|
if (typeof toJSON === "function") return walk(toJSON.call(v), exact);
|
|
809
820
|
if (Array.isArray(v)) return v.map((item) => walk(item, exact));
|
|
810
821
|
const taken = /* @__PURE__ */ new Set();
|
|
811
822
|
return Object.fromEntries(
|
|
812
|
-
Object.entries(v).map(([k, item]) => [uniqueKey(
|
|
823
|
+
Object.entries(v).map(([k, item]) => [uniqueKey(text2(k), taken), walk(item, exact || exactKeys?.has(k) === true)])
|
|
813
824
|
);
|
|
814
825
|
};
|
|
815
826
|
return walk(value, false);
|
|
@@ -890,6 +901,10 @@ var FIELD_PRINTERS = {
|
|
|
890
901
|
amountCurrency: (e) => e.amountCurrency ? `Renewal charge currency: ${safeInline(String(e.amountCurrency))}` : null,
|
|
891
902
|
pendingKind: (e) => e.pendingKind ? `Schedule change waiting for its customer notice: ${safeInline(String(e.pendingKind))}` : null,
|
|
892
903
|
nextChangeAllowedOn: (e) => e.nextChangeAllowedOn ? `Customer's next schedule change allowed from: ${safeInline(String(e.nextChangeAllowedOn))}` : null,
|
|
904
|
+
// subscription_pause_too_long (served by checkout production since 2026-11-14):
|
|
905
|
+
// the business's customer pause limit, checked by the SDK.
|
|
906
|
+
maxCycles: (e) => typeof e.maxCycles === "number" ? `Longest customer pause this business allows: ${e.maxCycles} billing cycles` : null,
|
|
907
|
+
maxResumesOn: (e) => e.maxResumesOn ? `Latest resume date for a customer pause: ${safeInline(String(e.maxResumesOn))}` : null,
|
|
893
908
|
// A charge the payment provider refused before any money moved (served in
|
|
894
909
|
// production since 2026-10-30): the failed payment intent that records
|
|
895
910
|
// the attempt. The SDK has checked it is a vpi_* id; printed through
|
|
@@ -905,6 +920,9 @@ var FIELD_PRINTERS = {
|
|
|
905
920
|
// A payment report too large to answer (report_too_large): which limit refused it. The SDK
|
|
906
921
|
// validates both as positive whole numbers.
|
|
907
922
|
maxGroups: (e) => typeof e.maxGroups === "number" ? `Most groups one summary returns: ${e.maxGroups} \u2014 group by fewer dimensions or a coarser time unit, or add filters` : null,
|
|
923
|
+
// A product price change refused presets_not_lower: the keep-offer prices at or above the
|
|
924
|
+
// new renewal price. The SDK validates them as positive whole numbers.
|
|
925
|
+
conflictingPrices: (e) => Array.isArray(e.conflictingPrices) && e.conflictingPrices.length > 0 ? `Keep-offer prices at or above the new price: ${e.conflictingPrices.join(", ")} (minor units) \u2014 send --keep-offer-prices below the new price` : null,
|
|
908
926
|
timeoutMs: (e) => typeof e.timeoutMs === "number" ? `Time limit exceeded: ${e.timeoutMs} ms \u2014 use a shorter window or add filters (retrying unchanged fails the same way)` : null,
|
|
909
927
|
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,
|
|
910
928
|
// On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
|
|
@@ -939,9 +957,9 @@ function errorJson(err) {
|
|
|
939
957
|
const message = err instanceof Error ? err.message : String(err);
|
|
940
958
|
return { error: scrubSecrets(oneLine(message)), code: null };
|
|
941
959
|
}
|
|
942
|
-
function exitWithCliError(message,
|
|
960
|
+
function exitWithCliError(message, text2) {
|
|
943
961
|
auditNote({ outcome: "refused_local", code: "local_refusal" }, true);
|
|
944
|
-
return emitCliError({ error: cleanJsonValue(message, ERROR_JSON_CLEANING), code: null },
|
|
962
|
+
return emitCliError({ error: cleanJsonValue(message, ERROR_JSON_CLEANING), code: null }, text2);
|
|
945
963
|
}
|
|
946
964
|
function handleError(err) {
|
|
947
965
|
auditError(err);
|
|
@@ -985,8 +1003,12 @@ var MONEY_METHODS = {
|
|
|
985
1003
|
refunds: ["create"],
|
|
986
1004
|
sessions: ["create"]
|
|
987
1005
|
};
|
|
988
|
-
|
|
989
|
-
|
|
1006
|
+
var LIVE_MONEY_METHODS = {
|
|
1007
|
+
subscriptionProducts: ["create", "update", "changePrice", "archive", "unarchive"]
|
|
1008
|
+
};
|
|
1009
|
+
function withAuditIntent(client, live) {
|
|
1010
|
+
const watched = { ...MONEY_METHODS, ...live ? LIVE_MONEY_METHODS : {} };
|
|
1011
|
+
for (const [resource, methods] of Object.entries(watched)) {
|
|
990
1012
|
const target = client[resource];
|
|
991
1013
|
for (const method of methods) {
|
|
992
1014
|
const original = target?.[method];
|
|
@@ -1003,7 +1025,7 @@ function makeCheckoutClient() {
|
|
|
1003
1025
|
const apiKey = requireApiKey();
|
|
1004
1026
|
const baseUrl = getBaseUrl();
|
|
1005
1027
|
refuseUnsafeLiveDestination(apiKey);
|
|
1006
|
-
|
|
1028
|
+
const client = new VonPayCheckout({
|
|
1007
1029
|
apiKey,
|
|
1008
1030
|
...baseUrl ? { baseUrl } : {},
|
|
1009
1031
|
// The SDK applies the same host rule at construction; the opt-in is the
|
|
@@ -1013,14 +1035,15 @@ function makeCheckoutClient() {
|
|
|
1013
1035
|
// The CLI prints every error itself (handleError / --json). Without this the SDK's fallback
|
|
1014
1036
|
// reporter adds its own `[vonpay] …` line to stderr on EVERY API error, live key included.
|
|
1015
1037
|
errorReporter: SILENT_ERROR_REPORTER
|
|
1016
|
-
})
|
|
1038
|
+
});
|
|
1039
|
+
return withAuditIntent(client, heldAsLive(apiKey));
|
|
1017
1040
|
}
|
|
1018
1041
|
|
|
1019
1042
|
// src/utils/live-key.ts
|
|
1020
1043
|
import { createInterface as createInterface2 } from "readline";
|
|
1021
1044
|
import chalk5 from "chalk";
|
|
1022
1045
|
import { Option as Option2 } from "commander";
|
|
1023
|
-
import { heldAsLive, isLiveKey } from "@vonpay/checkout-node";
|
|
1046
|
+
import { heldAsLive as heldAsLive2, isLiveKey } from "@vonpay/checkout-node";
|
|
1024
1047
|
var CONFIRM_WORD = "confirm";
|
|
1025
1048
|
var terminal = {
|
|
1026
1049
|
/**
|
|
@@ -1052,7 +1075,7 @@ function keyFingerprint(key) {
|
|
|
1052
1075
|
return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
|
|
1053
1076
|
}
|
|
1054
1077
|
async function confirmLiveAction(key, summary, opts = {}) {
|
|
1055
|
-
if (!
|
|
1078
|
+
if (!heldAsLive2(key)) return;
|
|
1056
1079
|
if (!terminal.isInteractive()) {
|
|
1057
1080
|
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.`;
|
|
1058
1081
|
auditNote({ outcome: "withheld", code: "no_terminal" });
|
|
@@ -1087,7 +1110,7 @@ function retiredConfirmLiveOption() {
|
|
|
1087
1110
|
).hideHelp();
|
|
1088
1111
|
}
|
|
1089
1112
|
function warnLiveSecretFlag(key, flag) {
|
|
1090
|
-
if (!
|
|
1113
|
+
if (!heldAsLive2(key)) return;
|
|
1091
1114
|
console.warn(
|
|
1092
1115
|
chalk5.yellow(
|
|
1093
1116
|
`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.`
|
|
@@ -1098,9 +1121,9 @@ function warnLiveSecretFlag(key, flag) {
|
|
|
1098
1121
|
// src/utils/amount.ts
|
|
1099
1122
|
var WHOLE_NUMBER = /^\d+$/;
|
|
1100
1123
|
function parseMinorUnits(raw, flag) {
|
|
1101
|
-
const
|
|
1102
|
-
const value = Number(
|
|
1103
|
-
if (!WHOLE_NUMBER.test(
|
|
1124
|
+
const text2 = raw.trim();
|
|
1125
|
+
const value = Number(text2);
|
|
1126
|
+
if (!WHOLE_NUMBER.test(text2) || !Number.isSafeInteger(value)) {
|
|
1104
1127
|
return {
|
|
1105
1128
|
ok: false,
|
|
1106
1129
|
message: `${flag} must be a whole number in minor units (the currency's smallest unit): for example 1499 = 14.99 USD. Got ${JSON.stringify(raw)}.`
|
|
@@ -1291,7 +1314,7 @@ import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
|
|
|
1291
1314
|
// src/utils/idempotency.ts
|
|
1292
1315
|
import { randomUUID } from "crypto";
|
|
1293
1316
|
import chalk7 from "chalk";
|
|
1294
|
-
import { heldAsLive as
|
|
1317
|
+
import { heldAsLive as heldAsLive3 } from "@vonpay/checkout-node";
|
|
1295
1318
|
function resolveIdempotencyKey(flag) {
|
|
1296
1319
|
const given = flag?.trim();
|
|
1297
1320
|
if (given) return { key: given, generated: false };
|
|
@@ -1299,7 +1322,7 @@ function resolveIdempotencyKey(flag) {
|
|
|
1299
1322
|
}
|
|
1300
1323
|
function idempotencyKeyFor(flag, what, apiKey) {
|
|
1301
1324
|
const { key, generated } = resolveIdempotencyKey(flag);
|
|
1302
|
-
if (generated && apiKey !== void 0 &&
|
|
1325
|
+
if (generated && apiKey !== void 0 && heldAsLive3(apiKey)) {
|
|
1303
1326
|
const message = `Refusing to send this ${what} with a live key without --idempotency-key. Choose one key for this ${what} (for example your order id plus the action, such as "order_1042:${what}") and pass the SAME key if you ever run the command again: a new key is a new action and can move money twice. Nothing was sent.`;
|
|
1304
1327
|
auditNote({ outcome: "refused_local", code: "idempotency_key_required" });
|
|
1305
1328
|
exitWithCliError(message, chalk7.red(message));
|
|
@@ -1313,6 +1336,13 @@ function idempotencyKeyFor(flag, what, apiKey) {
|
|
|
1313
1336
|
}
|
|
1314
1337
|
return key;
|
|
1315
1338
|
}
|
|
1339
|
+
function requiredIdempotencyKey(flag, what) {
|
|
1340
|
+
const given = flag?.trim();
|
|
1341
|
+
if (given) return given;
|
|
1342
|
+
const message = `Refusing to send this ${what} without --idempotency-key: Von Payments refuses it without one, and the CLI never makes one up. Choose one key for this ${what} (for example "spring-offer:${what.replace(/ /g, "-")}") and pass the SAME key if you ever run the command again: a new key is a new change. Nothing was sent.`;
|
|
1343
|
+
auditNote({ outcome: "refused_local", code: "idempotency_key_required" });
|
|
1344
|
+
exitWithCliError(message, chalk7.red(message));
|
|
1345
|
+
}
|
|
1316
1346
|
var DISPLAYABLE_KEY = /^[A-Za-z0-9_:.-]{1,64}$/;
|
|
1317
1347
|
function displayIdempotencyKey(key) {
|
|
1318
1348
|
return DISPLAYABLE_KEY.test(key) ? key : "[custom key withheld]";
|
|
@@ -1358,7 +1388,7 @@ function parseMetadata(pairs) {
|
|
|
1358
1388
|
return out;
|
|
1359
1389
|
}
|
|
1360
1390
|
var METADATA_FILTER_KEY = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
1361
|
-
var METADATA_FILTER_VALUE_MAX =
|
|
1391
|
+
var METADATA_FILTER_VALUE_MAX = 128;
|
|
1362
1392
|
function assertMetadataFilterKey(key, flag) {
|
|
1363
1393
|
if (!METADATA_FILTER_KEY.test(key)) {
|
|
1364
1394
|
throw new Error(
|
|
@@ -1378,6 +1408,41 @@ function parseMetadataFilter(raw, flag) {
|
|
|
1378
1408
|
return { [key]: value };
|
|
1379
1409
|
}
|
|
1380
1410
|
|
|
1411
|
+
// src/utils/processor-detail.ts
|
|
1412
|
+
import { SAFE_CODE_PATTERN, SAFE_CONNECTION_ID_PATTERN, safeValueOrWithheld } from "@vonpay/checkout-node";
|
|
1413
|
+
var INCLUDE_PROCESSOR_DETAIL_HELP = "With --json: also print the card processor's own answer (untrusted text), every try with its connection name, the connection's name, and the card's first six digits";
|
|
1414
|
+
var PROCESSOR_DETAIL_KEYS = ["processorResponse", "attempts", "attemptsStatus"];
|
|
1415
|
+
function cardForDisplay(card) {
|
|
1416
|
+
if (card === null || typeof card !== "object" || Array.isArray(card)) return card;
|
|
1417
|
+
const c = card;
|
|
1418
|
+
return { brand: c.brand ?? null, last4: c.last4 ?? null };
|
|
1419
|
+
}
|
|
1420
|
+
function attemptSummary(a) {
|
|
1421
|
+
return {
|
|
1422
|
+
connectionId: safeValueOrWithheld(a.processorConnection?.id, SAFE_CONNECTION_ID_PATTERN),
|
|
1423
|
+
result: safeValueOrWithheld(a.result, SAFE_CODE_PATTERN),
|
|
1424
|
+
declineCode: safeValueOrWithheld(a.declineCode, SAFE_CODE_PATTERN),
|
|
1425
|
+
outcome: safeValueOrWithheld(a.outcome, SAFE_CODE_PATTERN)
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function attemptsStatusForDisplay(value) {
|
|
1429
|
+
return safeValueOrWithheld(value, SAFE_CODE_PATTERN);
|
|
1430
|
+
}
|
|
1431
|
+
function connectionForDisplay(connection) {
|
|
1432
|
+
if (connection === null || typeof connection !== "object" || Array.isArray(connection)) return connection;
|
|
1433
|
+
return { id: connection.id ?? null };
|
|
1434
|
+
}
|
|
1435
|
+
function withoutProcessorDetail(obj, includeProcessorDetail) {
|
|
1436
|
+
const src = obj;
|
|
1437
|
+
if (includeProcessorDetail) return src;
|
|
1438
|
+
const out = { ...src };
|
|
1439
|
+
for (const k of PROCESSOR_DETAIL_KEYS) delete out[k];
|
|
1440
|
+
if ("card" in out) out.card = cardForDisplay(out.card);
|
|
1441
|
+
if ("processorConnection" in out) out.processorConnection = connectionForDisplay(out.processorConnection);
|
|
1442
|
+
return out;
|
|
1443
|
+
}
|
|
1444
|
+
var PROCESSOR_DETAIL_NEEDS_JSON = "--include-processor-detail is used with --json.";
|
|
1445
|
+
|
|
1381
1446
|
// src/commands/payment-reports.ts
|
|
1382
1447
|
import chalk8 from "chalk";
|
|
1383
1448
|
import { formatMinorUnits as formatMinorUnits2 } from "@vonpay/checkout-node";
|
|
@@ -1430,12 +1495,24 @@ var PAYMENT_LIST_JSON_FIELDS = [
|
|
|
1430
1495
|
"declineCode",
|
|
1431
1496
|
"action",
|
|
1432
1497
|
"ruleCode",
|
|
1433
|
-
"networkAdvice"
|
|
1498
|
+
"networkAdvice",
|
|
1499
|
+
// Each try as the safe summary only (connection id, result, decline code, outcome), and
|
|
1500
|
+
// whether that list is the full record. --include-processor-detail prints the whole tries.
|
|
1501
|
+
"attempts",
|
|
1502
|
+
"attemptsStatus"
|
|
1434
1503
|
];
|
|
1435
|
-
function listItemJson(pi, includeMetadata) {
|
|
1436
|
-
if (includeMetadata) return pi;
|
|
1504
|
+
function listItemJson(pi, includeMetadata, includeProcessorDetail) {
|
|
1437
1505
|
const src = pi;
|
|
1438
|
-
|
|
1506
|
+
const out = includeMetadata ? { ...src } : Object.fromEntries(PAYMENT_LIST_JSON_FIELDS.filter((k) => src[k] !== void 0).map((k) => [k, src[k]]));
|
|
1507
|
+
if (includeProcessorDetail) {
|
|
1508
|
+
if (src.processorResponse !== void 0) out.processorResponse = src.processorResponse;
|
|
1509
|
+
return out;
|
|
1510
|
+
}
|
|
1511
|
+
const safe = withoutProcessorDetail(out, false);
|
|
1512
|
+
if (src.attemptsStatus !== void 0) safe.attemptsStatus = attemptsStatusForDisplay(src.attemptsStatus);
|
|
1513
|
+
if (pi.attempts === null) safe.attempts = null;
|
|
1514
|
+
else if (Array.isArray(pi.attempts)) safe.attempts = pi.attempts.map(attemptSummary);
|
|
1515
|
+
return safe;
|
|
1439
1516
|
}
|
|
1440
1517
|
var ALL_DEFAULT_MAX_ITEMS = 1e4;
|
|
1441
1518
|
function stoppedAtDefaultCap() {
|
|
@@ -1450,6 +1527,7 @@ function buildListParams(opts) {
|
|
|
1450
1527
|
if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
|
|
1451
1528
|
if (opts.maxItems !== void 0 && !opts.all) throw new Error("--max-items is used with --all.");
|
|
1452
1529
|
if (opts.includeMetadata && !opts.json) throw new Error("--include-metadata is used with --json.");
|
|
1530
|
+
if (opts.includeProcessorDetail && !opts.json) throw new Error(PROCESSOR_DETAIL_NEEDS_JSON);
|
|
1453
1531
|
const maxItems = opts.maxItems !== void 0 ? positiveWholeNumber(opts.maxItems, "--max-items") : opts.all ? ALL_DEFAULT_MAX_ITEMS : void 0;
|
|
1454
1532
|
return { params, ...maxItems !== void 0 ? { maxItems } : {} };
|
|
1455
1533
|
}
|
|
@@ -1514,7 +1592,7 @@ var FILTER_HELP = {
|
|
|
1514
1592
|
createdLt: "End of the window, exclusive (default: now). At most 31 days after the start (7 with summary --group-by hour)",
|
|
1515
1593
|
outcome: "Only these outcomes, comma-separated (e.g. bank_declined,refused_before_bank)",
|
|
1516
1594
|
store: "Only payments for this connected store hostname (e.g. shop.example.com)",
|
|
1517
|
-
metadata: "Only payments with this metadata tag, key=value (ONE tag; key 1-64 of letters, digits, _ . : -; value 1-
|
|
1595
|
+
metadata: "Only payments with this metadata tag, key=value (ONE tag; key 1-64 of letters, digits, _ . : -; value 1-128 characters)",
|
|
1518
1596
|
cardBrand: "Only these card brands, comma-separated (visa, mastercard, amex, discover, unionpay, jcb, diners, unknown)",
|
|
1519
1597
|
currency: "Only payments in this currency (e.g. USD)",
|
|
1520
1598
|
initiator: "merchant (merchant-initiated charges on a saved card) or customer",
|
|
@@ -1541,7 +1619,7 @@ function addPaymentReportCommands(intents) {
|
|
|
1541
1619
|
).option("--max-items <n>", `With --all: stop after this many payments (default ${ALL_DEFAULT_MAX_ITEMS})`).option(
|
|
1542
1620
|
"--json",
|
|
1543
1621
|
"Output JSON: each payment's status, outcome, amount, card, store and decline fields \u2014 not your metadata (with --all: one JSON array)"
|
|
1544
|
-
).option("--include-metadata", "With --json: print every field of each payment, your metadata included").action(async (opts) => {
|
|
1622
|
+
).option("--include-metadata", "With --json: print every field of each payment, your metadata included (not the processor detail: see --include-processor-detail)").option("--include-processor-detail", INCLUDE_PROCESSOR_DETAIL_HELP).action(async (opts) => {
|
|
1545
1623
|
let built;
|
|
1546
1624
|
try {
|
|
1547
1625
|
built = buildListParams(opts);
|
|
@@ -1558,7 +1636,7 @@ function addPaymentReportCommands(intents) {
|
|
|
1558
1636
|
});
|
|
1559
1637
|
if (opts.json) {
|
|
1560
1638
|
const items = [];
|
|
1561
|
-
for await (const pi of pager) items.push(listItemJson(pi, opts.includeMetadata));
|
|
1639
|
+
for await (const pi of pager) items.push(listItemJson(pi, opts.includeMetadata, opts.includeProcessorDetail));
|
|
1562
1640
|
spinner.stop();
|
|
1563
1641
|
console.log(jsonOutput(items));
|
|
1564
1642
|
if (opts.maxItems === void 0 && items.length >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
|
|
@@ -1579,7 +1657,12 @@ function addPaymentReportCommands(intents) {
|
|
|
1579
1657
|
const page = await client.paymentIntents.list(built.params);
|
|
1580
1658
|
spinner.stop();
|
|
1581
1659
|
if (opts.json) {
|
|
1582
|
-
console.log(
|
|
1660
|
+
console.log(
|
|
1661
|
+
jsonOutput({
|
|
1662
|
+
...page,
|
|
1663
|
+
data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata, opts.includeProcessorDetail))
|
|
1664
|
+
})
|
|
1665
|
+
);
|
|
1583
1666
|
return;
|
|
1584
1667
|
}
|
|
1585
1668
|
for (const line of paymentListLines(page)) console.log(line);
|
|
@@ -1620,6 +1703,9 @@ function addPaymentReportCommands(intents) {
|
|
|
1620
1703
|
}
|
|
1621
1704
|
|
|
1622
1705
|
// src/commands/payment-intents.ts
|
|
1706
|
+
function refuseProcessorDetailWithoutJson(opts) {
|
|
1707
|
+
if (opts.includeProcessorDetail && !opts.json) exitWithCliError(PROCESSOR_DETAIL_NEEDS_JSON, chalk9.red(PROCESSOR_DETAIL_NEEDS_JSON));
|
|
1708
|
+
}
|
|
1623
1709
|
var MIT_INITIATORS = ["merchant", "customer"];
|
|
1624
1710
|
var MIT_REASONS = [
|
|
1625
1711
|
"recurring",
|
|
@@ -1755,7 +1841,8 @@ function paymentIntentsCommand() {
|
|
|
1755
1841
|
).option("--buyer-email <email>", "Buyer email for this charge").option(
|
|
1756
1842
|
"--idempotency-key <key>",
|
|
1757
1843
|
"Idempotency key for this charge. Reuse the SAME key to retry the same charge safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1758
|
-
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
|
|
1844
|
+
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").option("--include-processor-detail", INCLUDE_PROCESSOR_DETAIL_HELP).action(async (raw) => {
|
|
1845
|
+
refuseProcessorDetailWithoutJson(raw);
|
|
1759
1846
|
const apiKey = requireApiKey();
|
|
1760
1847
|
const opts = { ...raw, amount: minorUnitsOrExit(raw.amount, "--amount") };
|
|
1761
1848
|
const captureMethod = opts.captureMethod?.toLowerCase();
|
|
@@ -1797,7 +1884,7 @@ function paymentIntentsCommand() {
|
|
|
1797
1884
|
});
|
|
1798
1885
|
spinner.stop();
|
|
1799
1886
|
if (opts.json) {
|
|
1800
|
-
console.log(jsonOutput(intent));
|
|
1887
|
+
console.log(jsonOutput(withoutProcessorDetail(intent, opts.includeProcessorDetail)));
|
|
1801
1888
|
} else {
|
|
1802
1889
|
console.log(chalk9.green("\u2713 Payment intent created"));
|
|
1803
1890
|
printPaymentIntent(intent);
|
|
@@ -1809,14 +1896,15 @@ function paymentIntentsCommand() {
|
|
|
1809
1896
|
});
|
|
1810
1897
|
intents.command("get").description(
|
|
1811
1898
|
"Read a payment intent back \u2014 the way to check a payment whose outcome was unclear before doing anything else"
|
|
1812
|
-
).argument("<id>", "Payment intent ID (vpi_*)").option("--json", "Output raw JSON").action(async (id, opts) => {
|
|
1899
|
+
).argument("<id>", "Payment intent ID (vpi_*)").option("--json", "Output raw JSON").option("--include-processor-detail", INCLUDE_PROCESSOR_DETAIL_HELP).action(async (id, opts) => {
|
|
1900
|
+
refuseProcessorDetailWithoutJson(opts);
|
|
1813
1901
|
const client = makeCheckoutClient();
|
|
1814
1902
|
const spinner = startSpinner("Fetching payment intent...");
|
|
1815
1903
|
try {
|
|
1816
1904
|
const intent = await client.paymentIntents.retrieve(id);
|
|
1817
1905
|
spinner.stop();
|
|
1818
1906
|
if (opts.json) {
|
|
1819
|
-
console.log(jsonOutput(intent));
|
|
1907
|
+
console.log(jsonOutput(withoutProcessorDetail(intent, opts.includeProcessorDetail)));
|
|
1820
1908
|
} else {
|
|
1821
1909
|
printPaymentIntent(intent);
|
|
1822
1910
|
}
|
|
@@ -1831,8 +1919,9 @@ function paymentIntentsCommand() {
|
|
|
1831
1919
|
).option(
|
|
1832
1920
|
"--idempotency-key <key>",
|
|
1833
1921
|
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1834
|
-
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
|
|
1922
|
+
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").option("--include-processor-detail", INCLUDE_PROCESSOR_DETAIL_HELP).action(
|
|
1835
1923
|
async (id, opts) => {
|
|
1924
|
+
refuseProcessorDetailWithoutJson(opts);
|
|
1836
1925
|
const apiKey = requireApiKey();
|
|
1837
1926
|
const amountToCapture = optionalMinorUnitsOrExit(
|
|
1838
1927
|
opts.amountToCapture,
|
|
@@ -1866,7 +1955,7 @@ function paymentIntentsCommand() {
|
|
|
1866
1955
|
);
|
|
1867
1956
|
spinner.stop();
|
|
1868
1957
|
if (opts.json) {
|
|
1869
|
-
console.log(jsonOutput(intent));
|
|
1958
|
+
console.log(jsonOutput(withoutProcessorDetail(intent, opts.includeProcessorDetail)));
|
|
1870
1959
|
} else {
|
|
1871
1960
|
const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits3(amountToCapture, intent.currency)})` : "";
|
|
1872
1961
|
console.log(chalk9.green(`\u2713 Payment intent captured${captureLabel}`));
|
|
@@ -1881,8 +1970,9 @@ function paymentIntentsCommand() {
|
|
|
1881
1970
|
intents.command("void").description("Void an authorized (uncaptured) payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
|
|
1882
1971
|
"--idempotency-key <key>",
|
|
1883
1972
|
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1884
|
-
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
|
|
1973
|
+
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").option("--include-processor-detail", INCLUDE_PROCESSOR_DETAIL_HELP).action(
|
|
1885
1974
|
async (id, opts) => {
|
|
1975
|
+
refuseProcessorDetailWithoutJson(opts);
|
|
1886
1976
|
const apiKey = requireApiKey();
|
|
1887
1977
|
refuseUnsafeLiveDestination(apiKey);
|
|
1888
1978
|
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "void", apiKey);
|
|
@@ -1905,7 +1995,7 @@ function paymentIntentsCommand() {
|
|
|
1905
1995
|
const intent = await client.paymentIntents.void(id, { idempotencyKey });
|
|
1906
1996
|
spinner.stop();
|
|
1907
1997
|
if (opts.json) {
|
|
1908
|
-
console.log(jsonOutput(intent));
|
|
1998
|
+
console.log(jsonOutput(withoutProcessorDetail(intent, opts.includeProcessorDetail)));
|
|
1909
1999
|
} else {
|
|
1910
2000
|
console.log(chalk9.green("\u2713 Payment intent voided"));
|
|
1911
2001
|
printPaymentIntent(intent);
|
|
@@ -1923,7 +2013,7 @@ function paymentIntentsCommand() {
|
|
|
1923
2013
|
// src/commands/refunds.ts
|
|
1924
2014
|
import { Command as Command6 } from "commander";
|
|
1925
2015
|
import chalk10 from "chalk";
|
|
1926
|
-
import { formatMinorUnits as formatMinorUnits4, heldAsLive as
|
|
2016
|
+
import { formatMinorUnits as formatMinorUnits4, heldAsLive as heldAsLive4, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
|
|
1927
2017
|
function present(v) {
|
|
1928
2018
|
return v !== void 0 && v.trim() !== "";
|
|
1929
2019
|
}
|
|
@@ -2085,11 +2175,11 @@ function refundsCommand() {
|
|
|
2085
2175
|
handleError(err);
|
|
2086
2176
|
}
|
|
2087
2177
|
const target = resolveRefundTarget(opts);
|
|
2088
|
-
const sessionProblem = refundSessionProblem(target, opts.sessionId,
|
|
2178
|
+
const sessionProblem = refundSessionProblem(target, opts.sessionId, heldAsLive4(apiKey));
|
|
2089
2179
|
if (sessionProblem) exitWithCliError(sessionProblem, chalk10.red(sessionProblem));
|
|
2090
2180
|
refuseUnsafeLiveDestination(apiKey);
|
|
2091
2181
|
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund", apiKey);
|
|
2092
|
-
const lookedUp =
|
|
2182
|
+
const lookedUp = heldAsLive4(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
|
|
2093
2183
|
await confirmLiveAction(
|
|
2094
2184
|
apiKey,
|
|
2095
2185
|
{
|
|
@@ -2262,8 +2352,9 @@ function formatTokenCard(card) {
|
|
|
2262
2352
|
// src/commands/subscription-products.ts
|
|
2263
2353
|
import { Command as Command8 } from "commander";
|
|
2264
2354
|
import chalk12 from "chalk";
|
|
2265
|
-
import { formatMinorUnits as formatMinorUnits5 } from "@vonpay/checkout-node";
|
|
2355
|
+
import { formatMinorUnits as formatMinorUnits5, heldAsLive as heldAsLive5, VonPayError as VonPayError3 } from "@vonpay/checkout-node";
|
|
2266
2356
|
var STATUSES = ["active", "archived"];
|
|
2357
|
+
var INTERVALS = ["day", "week", "month"];
|
|
2267
2358
|
function buildListParams2(opts) {
|
|
2268
2359
|
const params = {};
|
|
2269
2360
|
if (opts.productRef !== void 0) params.productRef = opts.productRef;
|
|
@@ -2285,18 +2376,149 @@ function buildTermsParams(opts) {
|
|
|
2285
2376
|
if (parsed.value < 1) throw new Error(`--amount must be at least 1 (minor units); got ${JSON.stringify(opts.amount)}.`);
|
|
2286
2377
|
return { amount: parsed.value };
|
|
2287
2378
|
}
|
|
2379
|
+
function minorUnits(raw, flag) {
|
|
2380
|
+
const parsed = parseMinorUnits(raw, flag);
|
|
2381
|
+
if (!parsed.ok) throw new Error(parsed.message);
|
|
2382
|
+
return parsed.value;
|
|
2383
|
+
}
|
|
2384
|
+
function nonEmpty(raw, flag) {
|
|
2385
|
+
if (raw.trim() === "") throw new Error(`${flag} must not be empty.`);
|
|
2386
|
+
return raw;
|
|
2387
|
+
}
|
|
2388
|
+
function intervalFlag(raw) {
|
|
2389
|
+
const unit = raw.trim().toLowerCase();
|
|
2390
|
+
if (!INTERVALS.includes(unit)) {
|
|
2391
|
+
throw new Error(`--interval must be day, week or month; got ${JSON.stringify(raw)}.`);
|
|
2392
|
+
}
|
|
2393
|
+
return unit;
|
|
2394
|
+
}
|
|
2395
|
+
var STORE_LINK_KEYS = ["platform", "store", "store_product_id", "store_variant_id"];
|
|
2396
|
+
var STORE_LINK_REQUIRED = ["platform", "store", "store_product_id"];
|
|
2397
|
+
function parseStoreLink(raw) {
|
|
2398
|
+
const fields = {};
|
|
2399
|
+
for (const part of raw.split(",")) {
|
|
2400
|
+
const idx = part.indexOf("=");
|
|
2401
|
+
const key = (idx === -1 ? part : part.slice(0, idx)).trim();
|
|
2402
|
+
const value = idx === -1 ? "" : part.slice(idx + 1).trim();
|
|
2403
|
+
if (!STORE_LINK_KEYS.includes(key)) {
|
|
2404
|
+
throw new Error(
|
|
2405
|
+
`--store-link: ${JSON.stringify(key)} is not a store link field. Use ${STORE_LINK_KEYS.join(", ")} as key=value pairs separated by commas; got ${JSON.stringify(raw)}.`
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
const k = key;
|
|
2409
|
+
if (fields[k] !== void 0) throw new Error(`--store-link: ${k} is given twice in ${JSON.stringify(raw)}.`);
|
|
2410
|
+
if (value === "") throw new Error(`--store-link: ${k} has no value in ${JSON.stringify(raw)}.`);
|
|
2411
|
+
fields[k] = value;
|
|
2412
|
+
}
|
|
2413
|
+
const missing = STORE_LINK_REQUIRED.filter((k) => fields[k] === void 0);
|
|
2414
|
+
if (missing.length > 0) {
|
|
2415
|
+
throw new Error(`--store-link needs ${missing.join(", ")}; got ${JSON.stringify(raw)}.`);
|
|
2416
|
+
}
|
|
2417
|
+
return {
|
|
2418
|
+
platform: fields.platform,
|
|
2419
|
+
store: fields.store,
|
|
2420
|
+
storeProductId: fields.store_product_id,
|
|
2421
|
+
...fields.store_variant_id !== void 0 ? { storeVariantId: fields.store_variant_id } : {}
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
function buildCreateParams2(opts) {
|
|
2425
|
+
const required = [
|
|
2426
|
+
["name", "--name"],
|
|
2427
|
+
["productRef", "--product-ref"],
|
|
2428
|
+
["renewalPrice", "--renewal-price"],
|
|
2429
|
+
["currency", "--currency"],
|
|
2430
|
+
["interval", "--interval"],
|
|
2431
|
+
["intervalCount", "--interval-count"]
|
|
2432
|
+
];
|
|
2433
|
+
const missing = required.filter(([k]) => opts[k] === void 0).map(([, flag]) => flag);
|
|
2434
|
+
if (missing.length > 0) throw new Error(`A new subscription product needs ${missing.join(", ")}.`);
|
|
2435
|
+
const params = {
|
|
2436
|
+
name: nonEmpty(opts.name, "--name"),
|
|
2437
|
+
productRef: nonEmpty(opts.productRef, "--product-ref"),
|
|
2438
|
+
renewalPrice: minorUnits(opts.renewalPrice, "--renewal-price"),
|
|
2439
|
+
currency: nonEmpty(opts.currency, "--currency").trim().toUpperCase(),
|
|
2440
|
+
interval: intervalFlag(opts.interval),
|
|
2441
|
+
intervalCount: positiveWholeNumber(opts.intervalCount, "--interval-count")
|
|
2442
|
+
};
|
|
2443
|
+
if (opts.routingProfile !== void 0) params.routingProfile = nonEmpty(opts.routingProfile, "--routing-profile").trim();
|
|
2444
|
+
if (opts.storeLink !== void 0 && opts.storeLink.length > 0) params.storeLinks = opts.storeLink.map(parseStoreLink);
|
|
2445
|
+
return params;
|
|
2446
|
+
}
|
|
2447
|
+
function buildUpdateParams(opts) {
|
|
2448
|
+
if (opts.routingProfile !== void 0 && opts.removeRoutingProfile) {
|
|
2449
|
+
throw new Error("Pass --routing-profile or --remove-routing-profile, not both.");
|
|
2450
|
+
}
|
|
2451
|
+
if (opts.storeLink !== void 0 && opts.storeLink.length > 0 && opts.removeStoreLinks) {
|
|
2452
|
+
throw new Error("Pass --store-link or --remove-store-links, not both.");
|
|
2453
|
+
}
|
|
2454
|
+
if (opts.interval === void 0 !== (opts.intervalCount === void 0)) {
|
|
2455
|
+
throw new Error("Pass --interval and --interval-count together (the schedule is one setting).");
|
|
2456
|
+
}
|
|
2457
|
+
const params = {};
|
|
2458
|
+
if (opts.name !== void 0) params.name = nonEmpty(opts.name, "--name");
|
|
2459
|
+
if (opts.interval !== void 0) {
|
|
2460
|
+
params.interval = intervalFlag(opts.interval);
|
|
2461
|
+
params.intervalCount = positiveWholeNumber(opts.intervalCount, "--interval-count");
|
|
2462
|
+
}
|
|
2463
|
+
if (opts.routingProfile !== void 0) params.routingProfile = nonEmpty(opts.routingProfile, "--routing-profile").trim();
|
|
2464
|
+
if (opts.removeRoutingProfile) params.routingProfile = null;
|
|
2465
|
+
if (opts.storeLink !== void 0 && opts.storeLink.length > 0) params.storeLinks = opts.storeLink.map(parseStoreLink);
|
|
2466
|
+
if (opts.removeStoreLinks) params.storeLinks = [];
|
|
2467
|
+
if (Object.keys(params).length === 0) {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
"Nothing to change: pass at least one of --name, --interval with --interval-count, --routing-profile, --remove-routing-profile, --store-link, --remove-store-links. The price changes with change-price."
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
return params;
|
|
2473
|
+
}
|
|
2474
|
+
function buildChangePriceParams(opts) {
|
|
2475
|
+
if (opts.renewalPrice === void 0) throw new Error("A price change needs --renewal-price (the new price, in minor units).");
|
|
2476
|
+
const params = {
|
|
2477
|
+
renewalPrice: minorUnits(opts.renewalPrice, "--renewal-price"),
|
|
2478
|
+
applyToExisting: opts.applyToExisting === true
|
|
2479
|
+
};
|
|
2480
|
+
if (opts.keepOfferPrices !== void 0) {
|
|
2481
|
+
params.keepOfferPrices = parseListFlag(opts.keepOfferPrices, "--keep-offer-prices").map((p) => minorUnits(p, "--keep-offer-prices"));
|
|
2482
|
+
}
|
|
2483
|
+
return params;
|
|
2484
|
+
}
|
|
2485
|
+
var ACTOR_LABEL_PATTERN = /^[a-z0-9][a-z0-9_.:-]{0,63}$/;
|
|
2486
|
+
function actorLabelFlag(raw) {
|
|
2487
|
+
const label = raw.trim();
|
|
2488
|
+
if (label === "") throw new Error("--actor-label must not be empty.");
|
|
2489
|
+
if (!ACTOR_LABEL_PATTERN.test(label)) {
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
"--actor-label must be 1-64 characters: lowercase letters, digits and _ . : - , starting with a letter or digit (for example campaign-tool). Nothing was sent."
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
return label;
|
|
2495
|
+
}
|
|
2496
|
+
function writeOptions(opts, what) {
|
|
2497
|
+
const idempotencyKey = requiredIdempotencyKey(opts.idempotencyKey, what);
|
|
2498
|
+
if (opts.actorLabel === void 0) return { idempotencyKey };
|
|
2499
|
+
let actorLabel;
|
|
2500
|
+
try {
|
|
2501
|
+
actorLabel = actorLabelFlag(opts.actorLabel);
|
|
2502
|
+
} catch (err) {
|
|
2503
|
+
exitOnBadFlag2(err);
|
|
2504
|
+
}
|
|
2505
|
+
return { idempotencyKey, actorLabel };
|
|
2506
|
+
}
|
|
2288
2507
|
var LEGAL_TEXT_MAX_CHARS = 500;
|
|
2289
2508
|
var CUT_MARK = "\u2026 [CUT: not the full text; use --json to read all of it]";
|
|
2290
2509
|
function legalText(raw) {
|
|
2291
2510
|
const clean = safeInline(raw, Number.MAX_SAFE_INTEGER);
|
|
2292
2511
|
return clean.length > LEGAL_TEXT_MAX_CHARS ? { text: `${clean.slice(0, LEGAL_TEXT_MAX_CHARS)}${CUT_MARK}`, cut: true } : { text: clean, cut: false };
|
|
2293
2512
|
}
|
|
2513
|
+
function text(v, max = 200) {
|
|
2514
|
+
return typeof v === "string" ? safeInline(v, max) : "?";
|
|
2515
|
+
}
|
|
2294
2516
|
function money(amount, currency) {
|
|
2295
2517
|
if (typeof amount !== "number") return "?";
|
|
2296
|
-
return currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
|
|
2518
|
+
return typeof currency === "string" && currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
|
|
2297
2519
|
}
|
|
2298
2520
|
function every(count, interval) {
|
|
2299
|
-
if (!interval) return "";
|
|
2521
|
+
if (typeof interval !== "string" || !interval) return "";
|
|
2300
2522
|
const unit = safeInline(interval);
|
|
2301
2523
|
return typeof count === "number" && count !== 1 ? `every ${count} ${unit}s` : `every ${unit}`;
|
|
2302
2524
|
}
|
|
@@ -2304,13 +2526,63 @@ function statusTone2(status) {
|
|
|
2304
2526
|
return status === "active" ? chalk12.green(status) : chalk12.yellow(status);
|
|
2305
2527
|
}
|
|
2306
2528
|
function productLines(p) {
|
|
2307
|
-
const head = ` ${
|
|
2529
|
+
const head = ` ${text(p.id)} ${statusTone2(text(p.status))} ${text(p.name, 120)} (ref ${text(p.productRef, 128)})`;
|
|
2308
2530
|
const schedule = p.interval ? `first payment ${money(p.firstPrice, p.currency)}, then ${money(p.renewalPrice, p.currency)} ${every(p.intervalCount, p.interval)}` : `${money(p.renewalPrice, p.currency)} \u2014 no renewal schedule yet (it cannot start a subscription)`;
|
|
2309
2531
|
const lines = [head, ` ${schedule}`];
|
|
2310
2532
|
const terms = p.terms;
|
|
2311
2533
|
if (terms && typeof terms.disclosure === "string") lines.push(` Disclosure: ${legalText(terms.disclosure).text}`);
|
|
2312
2534
|
return lines;
|
|
2313
2535
|
}
|
|
2536
|
+
function productDetailLines(p) {
|
|
2537
|
+
const lines = productLines(p);
|
|
2538
|
+
const rp = p.routingProfile;
|
|
2539
|
+
if (rp && typeof rp === "object") {
|
|
2540
|
+
const r = rp;
|
|
2541
|
+
lines.push(` Routing profile: ${text(r.id)} (${text(r.name, 120)}, ${text(r.status)})`);
|
|
2542
|
+
} else {
|
|
2543
|
+
lines.push(" Routing profile: none");
|
|
2544
|
+
}
|
|
2545
|
+
const links = Array.isArray(p.storeLinks) ? p.storeLinks : [];
|
|
2546
|
+
if (links.length === 0) lines.push(" Sold in: no store");
|
|
2547
|
+
for (const raw of links) {
|
|
2548
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2549
|
+
const l = raw;
|
|
2550
|
+
const variant = typeof l.storeVariantId === "string" ? ` variant ${text(l.storeVariantId)}` : "";
|
|
2551
|
+
const storePrice = typeof l.storePrice === "number" ? ` \u2014 the store's price when last read: ${money(l.storePrice, l.storeCurrency)} (subscribers pay the product's price)` : "";
|
|
2552
|
+
lines.push(` Sold in: ${text(l.platform)} ${text(l.store)} product ${text(l.storeProductId)}${variant}${storePrice}`);
|
|
2553
|
+
}
|
|
2554
|
+
lines.push(` Subscriptions on it (active, past due, paused or held): ${typeof p.activeSubscriptionCount === "number" ? p.activeSubscriptionCount : "?"}`);
|
|
2555
|
+
const warnings = Array.isArray(p.warnings) ? p.warnings : [];
|
|
2556
|
+
for (const raw of warnings) {
|
|
2557
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2558
|
+
const w = raw;
|
|
2559
|
+
lines.push(chalk12.yellow(` \u26A0 ${text(w.code)} (${text(w.field)}): ${text(w.message, 500)}`));
|
|
2560
|
+
}
|
|
2561
|
+
return lines;
|
|
2562
|
+
}
|
|
2563
|
+
var REPLAYED_LINE = " This answers an earlier request with the same --idempotency-key: nothing was changed a second time.";
|
|
2564
|
+
var UNCHANGED_LINE = " Nothing changed: the product was already that way (the key was not kept).";
|
|
2565
|
+
function changedLines(result, done) {
|
|
2566
|
+
const lines = [result.changed === false ? chalk12.yellow(UNCHANGED_LINE) : chalk12.green(`\u2713 Subscription product ${done}`)];
|
|
2567
|
+
if (result.replayed === true) lines.push(REPLAYED_LINE);
|
|
2568
|
+
lines.push(...productDetailLines(result));
|
|
2569
|
+
return lines;
|
|
2570
|
+
}
|
|
2571
|
+
function inProgressLine(updated) {
|
|
2572
|
+
return `The price cut is still being applied: ${typeof updated === "number" ? updated : "?"} subscriptions lowered so far. Run the SAME command again, with the SAME --idempotency-key and the same flags, to continue (Von Payments also finishes it on its own). Never use a new key for it: a new key is a new price change.`;
|
|
2573
|
+
}
|
|
2574
|
+
function priceChangeLines(result) {
|
|
2575
|
+
if (result.outcome === "in_progress") {
|
|
2576
|
+
const lines2 = [chalk12.yellow(` ${inProgressLine(result.updatedSubscriptionCount)}`)];
|
|
2577
|
+
if (result.replayed === true) lines2.push(REPLAYED_LINE);
|
|
2578
|
+
return lines2;
|
|
2579
|
+
}
|
|
2580
|
+
const lines = [chalk12.green("\u2713 Renewal price changed")];
|
|
2581
|
+
if (result.replayed === true) lines.push(REPLAYED_LINE);
|
|
2582
|
+
lines.push(` Existing subscriptions lowered: ${typeof result.updatedSubscriptionCount === "number" ? result.updatedSubscriptionCount : "?"}`);
|
|
2583
|
+
if (result.product && typeof result.product === "object") lines.push(...productDetailLines(result.product));
|
|
2584
|
+
return lines;
|
|
2585
|
+
}
|
|
2314
2586
|
function productListLines(page) {
|
|
2315
2587
|
const lines = [];
|
|
2316
2588
|
if (page.data.length === 0) lines.push(" No subscription products match.");
|
|
@@ -2337,9 +2609,112 @@ function exitOnBadFlag2(err) {
|
|
|
2337
2609
|
const message = err.message;
|
|
2338
2610
|
exitWithCliError(message, chalk12.red(`Error: ${message}`));
|
|
2339
2611
|
}
|
|
2612
|
+
function errorCodeSuffix2(err) {
|
|
2613
|
+
const code = err instanceof VonPayError3 ? err.code : void 0;
|
|
2614
|
+
return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? ` (error code: ${code})` : "";
|
|
2615
|
+
}
|
|
2616
|
+
var ROUTING_PROFILE_ID = /^vsrp_(test|live)_[A-Za-z0-9_-]{1,128}$/;
|
|
2617
|
+
function routingProfileShown(id) {
|
|
2618
|
+
return ROUTING_PROFILE_ID.test(id) ? id : "[unrecognised id withheld]";
|
|
2619
|
+
}
|
|
2620
|
+
function currencyOf(p) {
|
|
2621
|
+
return typeof p.currency === "string" && /^[A-Z]{3}$/.test(p.currency) ? p.currency : void 0;
|
|
2622
|
+
}
|
|
2623
|
+
function currentProductLines(current) {
|
|
2624
|
+
return [
|
|
2625
|
+
["Current price", money(current.renewalPrice, currencyOf(current))],
|
|
2626
|
+
["Status", STATUSES.includes(current.status) ? current.status : "[unrecognised status withheld]"],
|
|
2627
|
+
["Subscriptions", typeof current.activeSubscriptionCount === "number" ? `${current.activeSubscriptionCount} (active, past due, paused or held)` : "not reported"]
|
|
2628
|
+
];
|
|
2629
|
+
}
|
|
2630
|
+
async function confirmLiveProductWrite(product, write, idempotencyKey, opts) {
|
|
2631
|
+
const apiKey = requireApiKey();
|
|
2632
|
+
refuseUnsafeLiveDestination(apiKey);
|
|
2633
|
+
let current;
|
|
2634
|
+
if (heldAsLive5(apiKey) && terminal.isInteractive()) {
|
|
2635
|
+
try {
|
|
2636
|
+
current = await makeCheckoutClient().subscriptionProducts.retrieve(product);
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
const message = `Refusing the ${write.what}: the product could not be looked up${errorCodeSuffix2(err)}, so what it changes cannot be shown to you before you confirm. Nothing was sent.`;
|
|
2639
|
+
auditNote({ outcome: "lookup_refused", code: "lookup_failed" });
|
|
2640
|
+
exitWithCliError(message, chalk12.red(message));
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
await confirmLiveAction(
|
|
2644
|
+
apiKey,
|
|
2645
|
+
{
|
|
2646
|
+
action: write.action,
|
|
2647
|
+
details: [
|
|
2648
|
+
...apiHostDetail(),
|
|
2649
|
+
["Product", product],
|
|
2650
|
+
...write.details(current),
|
|
2651
|
+
...current ? currentProductLines(current) : [],
|
|
2652
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
2653
|
+
],
|
|
2654
|
+
consequence: write.consequence
|
|
2655
|
+
},
|
|
2656
|
+
opts
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
function updateWrite(params) {
|
|
2660
|
+
const replacesLinks = params.storeLinks !== void 0;
|
|
2661
|
+
return {
|
|
2662
|
+
action: "change a subscription product",
|
|
2663
|
+
what: "product update",
|
|
2664
|
+
details: (current) => {
|
|
2665
|
+
const lines = [];
|
|
2666
|
+
if (params.name !== void 0) lines.push(["Name", "changes (the new name is not shown here; check it in the dashboard after)"]);
|
|
2667
|
+
if (params.interval !== void 0) lines.push(["New sign-ups renew", every(params.intervalCount, params.interval)]);
|
|
2668
|
+
if (params.routingProfile !== void 0) {
|
|
2669
|
+
lines.push(["Routing profile", params.routingProfile === null ? "removed (new sign-ups)" : `${routingProfileShown(params.routingProfile)} (new sign-ups)`]);
|
|
2670
|
+
}
|
|
2671
|
+
if (params.storeLinks !== void 0) {
|
|
2672
|
+
const n = params.storeLinks.length;
|
|
2673
|
+
lines.push(["Store links", n === 0 ? "ALL REMOVED: it is sold in no store" : `REPLACED by ${n} link${n === 1 ? "" : "s"} (store details not shown here)`]);
|
|
2674
|
+
if (current) {
|
|
2675
|
+
const now = Array.isArray(current.storeLinks) ? current.storeLinks.length : void 0;
|
|
2676
|
+
lines.push(["Store links now", now === void 0 ? "not reported" : `${now} (every one is dropped)`]);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return lines;
|
|
2680
|
+
},
|
|
2681
|
+
consequence: replacesLinks ? "The store links given here REPLACE the current ones, and the current ones are dropped for good: putting one back means typing it in again. A schedule or routing change applies to new sign-ups only." : "A schedule or routing change applies to new sign-ups only; existing subscribers keep what they have."
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function priceCutWrite(renewalPrice) {
|
|
2685
|
+
return {
|
|
2686
|
+
action: "lower a subscription product's price for its EXISTING subscribers",
|
|
2687
|
+
what: "price change",
|
|
2688
|
+
details: (current) => {
|
|
2689
|
+
if (!current) return [["New price", `${renewalPrice} in minor units`]];
|
|
2690
|
+
const lines = [["New price", money(renewalPrice, currencyOf(current))]];
|
|
2691
|
+
if (typeof current.renewalPrice === "number" && renewalPrice >= current.renewalPrice) {
|
|
2692
|
+
lines.push(["\u26A0 Check", "the new price is NOT lower than the current price: only a cut can apply to existing subscribers"]);
|
|
2693
|
+
}
|
|
2694
|
+
return lines;
|
|
2695
|
+
},
|
|
2696
|
+
consequence: "Every subscription still on the old price is charged the new, lower price from its next renewal. Raising the price again later does not raise them back."
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
var ARCHIVE_WRITE = {
|
|
2700
|
+
action: "archive a subscription product",
|
|
2701
|
+
what: "archive",
|
|
2702
|
+
details: () => [],
|
|
2703
|
+
consequence: "No new sign-ups or switches to it; existing subscribers keep renewing. Its reference is freed at once: if another product takes it, unarchive is refused, so this may not be undoable."
|
|
2704
|
+
};
|
|
2705
|
+
var UNARCHIVE_WRITE = {
|
|
2706
|
+
action: "open an archived subscription product to new sign-ups again",
|
|
2707
|
+
what: "unarchive",
|
|
2708
|
+
details: () => [],
|
|
2709
|
+
consequence: "New sign-ups and switches to it start again, at its current price."
|
|
2710
|
+
};
|
|
2711
|
+
var IDEMPOTENCY_HELP = "REQUIRED (every key, sandbox too; never generated). One key per change you mean to make; reuse the SAME key only to retry that exact change";
|
|
2712
|
+
var ACTOR_LABEL_HELP = "Your own name for the system making the change (for example campaign-tool), shown in the settings history: 1-64 lowercase letters, digits and _ . : - , starting with a letter or digit. Never personal data";
|
|
2713
|
+
var LIVE_CONFIRM_NOTE = "On a live key a person confirms it at the terminal";
|
|
2714
|
+
var STORE_LINK_HELP = "A store it is sold in, as key=value pairs separated by commas: platform=nextcommerce,store=<store address>,store_product_id=<id>[,store_variant_id=<id>]. Repeat the flag for each store";
|
|
2340
2715
|
function subscriptionProductsCommand() {
|
|
2341
2716
|
const products = new Command8("subscription-products").description(
|
|
2342
|
-
"Read your subscription products and the renewal terms a sign-up states
|
|
2717
|
+
"Read, create and change your subscription products, and read the renewal terms a sign-up states"
|
|
2343
2718
|
);
|
|
2344
2719
|
products.command("list").description("List your subscription products, newest first, each with the terms a sign-up on it states").option("--product-ref <ref>", "Only the product with this reference (your store's product number), matched exactly").option("--status <status>", "Only products with this status: active or archived").option("--limit <n>", "Page size, 1 to 100 (default 10)").option("--starting-after <id>", "The id of the last product on the previous page (printed by a previous run)").option("--json", "Output raw JSON").action(async (opts) => {
|
|
2345
2720
|
let params;
|
|
@@ -2387,6 +2762,155 @@ function subscriptionProductsCommand() {
|
|
|
2387
2762
|
handleError(err);
|
|
2388
2763
|
}
|
|
2389
2764
|
});
|
|
2765
|
+
products.command("get <product>").description(
|
|
2766
|
+
"Read one subscription product: its price, schedule, routing profile, the stores it is sold in and how many subscriptions use it. <product> is its id (vsprod_*) or the reference of an ACTIVE product (an archived one is found by id only)"
|
|
2767
|
+
).option("--json", "Output raw JSON").action(async (product, opts) => {
|
|
2768
|
+
const client = makeCheckoutClient();
|
|
2769
|
+
const spinner = startSpinner("Reading subscription product...");
|
|
2770
|
+
try {
|
|
2771
|
+
const detail = await client.subscriptionProducts.retrieve(product);
|
|
2772
|
+
spinner.stop();
|
|
2773
|
+
if (opts.json) {
|
|
2774
|
+
console.log(jsonOutput(detail));
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
for (const line of productDetailLines(detail)) console.log(line);
|
|
2778
|
+
} catch (err) {
|
|
2779
|
+
spinner.stop();
|
|
2780
|
+
handleError(err);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
products.command("create").description(
|
|
2784
|
+
"Create a subscription product (the same rules as the dashboard). It is made in your ACCOUNT's mode: make test products on your sandbox account with its test key"
|
|
2785
|
+
).option("--name <name>", "REQUIRED. The product's name, 1-120 characters").option("--product-ref <ref>", "REQUIRED. Your store's product number, unique among your active products").option("--renewal-price <minor>", "REQUIRED. What each renewal charges, in minor units (e.g. 2999 = 29.99 USD)").option("--currency <code>", "REQUIRED. ISO 4217 currency (e.g. USD)").option("--interval <unit>", "REQUIRED. How often it renews: day, week or month (at most a year between charges)").option("--interval-count <n>", "REQUIRED. How many units between charges, 1-365 (e.g. --interval month --interval-count 3)").option("--routing-profile <id>", "A routing profile of yours (vsrp_*) in the key's mode").option("--store-link <link...>", STORE_LINK_HELP).option("--idempotency-key <key>", IDEMPOTENCY_HELP).option("--actor-label <label>", ACTOR_LABEL_HELP).option("--json", "Output raw JSON").action(async (opts) => {
|
|
2786
|
+
let params;
|
|
2787
|
+
try {
|
|
2788
|
+
params = buildCreateParams2(opts);
|
|
2789
|
+
} catch (err) {
|
|
2790
|
+
exitOnBadFlag2(err);
|
|
2791
|
+
}
|
|
2792
|
+
const options = writeOptions(opts, "product create");
|
|
2793
|
+
const client = makeCheckoutClient();
|
|
2794
|
+
const spinner = startSpinner("Creating subscription product...");
|
|
2795
|
+
try {
|
|
2796
|
+
const created = await client.subscriptionProducts.create(params, options);
|
|
2797
|
+
spinner.stop();
|
|
2798
|
+
if (opts.json) {
|
|
2799
|
+
console.log(jsonOutput(created));
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
console.log(chalk12.green("\u2713 Subscription product created"));
|
|
2803
|
+
if (created.replayed === true) console.log(REPLAYED_LINE);
|
|
2804
|
+
for (const line of productDetailLines(created)) console.log(line);
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
spinner.stop();
|
|
2807
|
+
handleError(err);
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
products.command("update <product>").description(
|
|
2811
|
+
`Change a product's name, schedule, routing profile or store links. A schedule or routing change applies to NEW sign-ups only. ${LIVE_CONFIRM_NOTE}. <product> is its id (vsprod_*). The price changes with change-price`
|
|
2812
|
+
).option("--name <name>", "The new name, 1-120 characters").option("--interval <unit>", "The new schedule: day, week or month. With --interval-count").option("--interval-count <n>", "How many units between charges, 1-365. With --interval").option("--routing-profile <id>", "A routing profile of yours (vsrp_*) in the key's mode. A restricted key also needs subscriptions.write_billing").option("--remove-routing-profile", "Take the routing profile off the product. A restricted key also needs subscriptions.write_billing").option("--store-link <link...>", `${STORE_LINK_HELP}. The links given REPLACE every current one, and the current ones are dropped for good`).option("--remove-store-links", "Take every store link off the product (they are dropped for good)").option("--idempotency-key <key>", IDEMPOTENCY_HELP).option("--actor-label <label>", ACTOR_LABEL_HELP).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (product, opts) => {
|
|
2813
|
+
let params;
|
|
2814
|
+
try {
|
|
2815
|
+
params = buildUpdateParams(opts);
|
|
2816
|
+
} catch (err) {
|
|
2817
|
+
exitOnBadFlag2(err);
|
|
2818
|
+
}
|
|
2819
|
+
const options = writeOptions(opts, "product update");
|
|
2820
|
+
await confirmLiveProductWrite(product, updateWrite(params), options.idempotencyKey, opts);
|
|
2821
|
+
const client = makeCheckoutClient();
|
|
2822
|
+
const spinner = startSpinner("Updating subscription product...");
|
|
2823
|
+
try {
|
|
2824
|
+
const changed = await client.subscriptionProducts.update(product, params, options);
|
|
2825
|
+
spinner.stop();
|
|
2826
|
+
if (opts.json) {
|
|
2827
|
+
console.log(jsonOutput(changed));
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
for (const line of changedLines(changed, "updated")) console.log(line);
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
spinner.stop();
|
|
2833
|
+
handleError(err);
|
|
2834
|
+
}
|
|
2835
|
+
});
|
|
2836
|
+
products.command("change-price <product>").description(
|
|
2837
|
+
"Change a product's renewal price (the same rule as the dashboard). A raise applies to new sign-ups only. A cut with --apply-to-existing also lowers subscriptions still on the old price, from their next charge. <product> is its id (vsprod_*)"
|
|
2838
|
+
).option("--renewal-price <minor>", "REQUIRED. The new renewal price, in minor units (e.g. 2499 = 24.99 USD)").option(
|
|
2839
|
+
"--apply-to-existing",
|
|
2840
|
+
`A cut ALSO lowers what existing subscribers on the old price pay, from their next charge (never raises anyone). ${LIVE_CONFIRM_NOTE}. A restricted key also needs subscriptions.write_billing`
|
|
2841
|
+
).option(
|
|
2842
|
+
"--keep-offer-prices <prices>",
|
|
2843
|
+
"New keep-offer prices in minor units, comma-separated, each below --renewal-price. Send them when the cut would go below a current keep-offer price"
|
|
2844
|
+
).option("--idempotency-key <key>", `${IDEMPOTENCY_HELP}. A large cut answers "still being applied": run the SAME command with the SAME key to continue`).option("--actor-label <label>", ACTOR_LABEL_HELP).addOption(retiredConfirmLiveOption()).option(
|
|
2845
|
+
"--json",
|
|
2846
|
+
'Output raw JSON. The exit code is 0 also while a cut is still being applied (it is saved): a script must read "outcome" ("completed" or "in_progress") and, on "in_progress", run the same command with the same key again'
|
|
2847
|
+
).action(async (product, opts) => {
|
|
2848
|
+
let params;
|
|
2849
|
+
try {
|
|
2850
|
+
params = buildChangePriceParams(opts);
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
exitOnBadFlag2(err);
|
|
2853
|
+
}
|
|
2854
|
+
const options = writeOptions(opts, "price change");
|
|
2855
|
+
if (params.applyToExisting === true) {
|
|
2856
|
+
await confirmLiveProductWrite(product, priceCutWrite(params.renewalPrice), options.idempotencyKey, opts);
|
|
2857
|
+
}
|
|
2858
|
+
const client = makeCheckoutClient();
|
|
2859
|
+
const spinner = startSpinner("Changing the renewal price...");
|
|
2860
|
+
try {
|
|
2861
|
+
const result = await client.subscriptionProducts.changePrice(product, params, options);
|
|
2862
|
+
spinner.stop();
|
|
2863
|
+
if (opts.json) {
|
|
2864
|
+
console.log(jsonOutput(result));
|
|
2865
|
+
if (result.outcome === "in_progress") console.error(chalk12.yellow(inProgressLine(result.updatedSubscriptionCount)));
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
for (const line of priceChangeLines(result)) console.log(line);
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
spinner.stop();
|
|
2871
|
+
handleError(err);
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
products.command("archive <product>").description(
|
|
2875
|
+
`Archive a product: no new sign-ups or customer switches to it; existing subscribers keep renewing; its reference is freed at once, so another product can take it (unarchive is then refused). ${LIVE_CONFIRM_NOTE}. <product> is its id (vsprod_*)`
|
|
2876
|
+
).option("--idempotency-key <key>", IDEMPOTENCY_HELP).option("--actor-label <label>", ACTOR_LABEL_HELP).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (product, opts) => {
|
|
2877
|
+
const options = writeOptions(opts, "archive");
|
|
2878
|
+
await confirmLiveProductWrite(product, ARCHIVE_WRITE, options.idempotencyKey, opts);
|
|
2879
|
+
const client = makeCheckoutClient();
|
|
2880
|
+
const spinner = startSpinner("Archiving subscription product...");
|
|
2881
|
+
try {
|
|
2882
|
+
const changed = await client.subscriptionProducts.archive(product, options);
|
|
2883
|
+
spinner.stop();
|
|
2884
|
+
if (opts.json) {
|
|
2885
|
+
console.log(jsonOutput(changed));
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
for (const line of changedLines(changed, "archived")) console.log(line);
|
|
2889
|
+
} catch (err) {
|
|
2890
|
+
spinner.stop();
|
|
2891
|
+
handleError(err);
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
products.command("unarchive <product>").description(
|
|
2895
|
+
`Open an archived product to new sign-ups again. Refused when another active product now has its reference. ${LIVE_CONFIRM_NOTE}. <product> is its id (vsprod_*)`
|
|
2896
|
+
).option("--idempotency-key <key>", IDEMPOTENCY_HELP).option("--actor-label <label>", ACTOR_LABEL_HELP).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (product, opts) => {
|
|
2897
|
+
const options = writeOptions(opts, "unarchive");
|
|
2898
|
+
await confirmLiveProductWrite(product, UNARCHIVE_WRITE, options.idempotencyKey, opts);
|
|
2899
|
+
const client = makeCheckoutClient();
|
|
2900
|
+
const spinner = startSpinner("Unarchiving subscription product...");
|
|
2901
|
+
try {
|
|
2902
|
+
const changed = await client.subscriptionProducts.unarchive(product, options);
|
|
2903
|
+
spinner.stop();
|
|
2904
|
+
if (opts.json) {
|
|
2905
|
+
console.log(jsonOutput(changed));
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
for (const line of changedLines(changed, "unarchived")) console.log(line);
|
|
2909
|
+
} catch (err) {
|
|
2910
|
+
spinner.stop();
|
|
2911
|
+
handleError(err);
|
|
2912
|
+
}
|
|
2913
|
+
});
|
|
2390
2914
|
return products;
|
|
2391
2915
|
}
|
|
2392
2916
|
|
|
@@ -3652,7 +4176,7 @@ function lifecycleSeverity(kind) {
|
|
|
3652
4176
|
// src/commands/health.ts
|
|
3653
4177
|
import { Command as Command12 } from "commander";
|
|
3654
4178
|
import chalk16 from "chalk";
|
|
3655
|
-
import { VonPayError as
|
|
4179
|
+
import { VonPayError as VonPayError4 } from "@vonpay/checkout-node";
|
|
3656
4180
|
function healthCommand() {
|
|
3657
4181
|
return new Command12("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3658
4182
|
const client = makeCheckoutClient();
|
|
@@ -3674,7 +4198,7 @@ function healthCommand() {
|
|
|
3674
4198
|
spinner.stop();
|
|
3675
4199
|
auditError(err);
|
|
3676
4200
|
if (jsonErrorsEnabled()) handleError(err);
|
|
3677
|
-
if (err instanceof
|
|
4201
|
+
if (err instanceof VonPayError4) {
|
|
3678
4202
|
console.error(chalk16.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
|
|
3679
4203
|
console.error(chalk16.dim(` Code: ${safeInline(String(err.code))}`));
|
|
3680
4204
|
} else {
|
|
@@ -3697,7 +4221,7 @@ import { homedir as homedir2 } from "os";
|
|
|
3697
4221
|
import { existsSync as existsSync3 } from "fs";
|
|
3698
4222
|
import {
|
|
3699
4223
|
VonPayCheckout as VonPayCheckout3,
|
|
3700
|
-
VonPayError as
|
|
4224
|
+
VonPayError as VonPayError5,
|
|
3701
4225
|
helpFor
|
|
3702
4226
|
} from "@vonpay/checkout-node";
|
|
3703
4227
|
var RELEVANT_ENV_VARS = [
|
|
@@ -3845,7 +4369,7 @@ async function gather() {
|
|
|
3845
4369
|
);
|
|
3846
4370
|
}
|
|
3847
4371
|
} catch (err) {
|
|
3848
|
-
if (err instanceof
|
|
4372
|
+
if (err instanceof VonPayError5) {
|
|
3849
4373
|
const help = helpFor(err.code);
|
|
3850
4374
|
report.validateProbe = {
|
|
3851
4375
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vonpay/checkout-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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.
|
|
23
|
+
"@vonpay/checkout-node": "^4.11.0",
|
|
24
24
|
"commander": "^13.0.0",
|
|
25
25
|
"chalk": "^5.4.0",
|
|
26
26
|
"ora": "^8.2.0"
|