@vonpay/checkout-cli 0.21.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 +499 -35
- 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]";
|
|
@@ -1983,7 +2013,7 @@ function paymentIntentsCommand() {
|
|
|
1983
2013
|
// src/commands/refunds.ts
|
|
1984
2014
|
import { Command as Command6 } from "commander";
|
|
1985
2015
|
import chalk10 from "chalk";
|
|
1986
|
-
import { formatMinorUnits as formatMinorUnits4, heldAsLive as
|
|
2016
|
+
import { formatMinorUnits as formatMinorUnits4, heldAsLive as heldAsLive4, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
|
|
1987
2017
|
function present(v) {
|
|
1988
2018
|
return v !== void 0 && v.trim() !== "";
|
|
1989
2019
|
}
|
|
@@ -2145,11 +2175,11 @@ function refundsCommand() {
|
|
|
2145
2175
|
handleError(err);
|
|
2146
2176
|
}
|
|
2147
2177
|
const target = resolveRefundTarget(opts);
|
|
2148
|
-
const sessionProblem = refundSessionProblem(target, opts.sessionId,
|
|
2178
|
+
const sessionProblem = refundSessionProblem(target, opts.sessionId, heldAsLive4(apiKey));
|
|
2149
2179
|
if (sessionProblem) exitWithCliError(sessionProblem, chalk10.red(sessionProblem));
|
|
2150
2180
|
refuseUnsafeLiveDestination(apiKey);
|
|
2151
2181
|
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund", apiKey);
|
|
2152
|
-
const lookedUp =
|
|
2182
|
+
const lookedUp = heldAsLive4(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
|
|
2153
2183
|
await confirmLiveAction(
|
|
2154
2184
|
apiKey,
|
|
2155
2185
|
{
|
|
@@ -2322,8 +2352,9 @@ function formatTokenCard(card) {
|
|
|
2322
2352
|
// src/commands/subscription-products.ts
|
|
2323
2353
|
import { Command as Command8 } from "commander";
|
|
2324
2354
|
import chalk12 from "chalk";
|
|
2325
|
-
import { formatMinorUnits as formatMinorUnits5 } from "@vonpay/checkout-node";
|
|
2355
|
+
import { formatMinorUnits as formatMinorUnits5, heldAsLive as heldAsLive5, VonPayError as VonPayError3 } from "@vonpay/checkout-node";
|
|
2326
2356
|
var STATUSES = ["active", "archived"];
|
|
2357
|
+
var INTERVALS = ["day", "week", "month"];
|
|
2327
2358
|
function buildListParams2(opts) {
|
|
2328
2359
|
const params = {};
|
|
2329
2360
|
if (opts.productRef !== void 0) params.productRef = opts.productRef;
|
|
@@ -2345,18 +2376,149 @@ function buildTermsParams(opts) {
|
|
|
2345
2376
|
if (parsed.value < 1) throw new Error(`--amount must be at least 1 (minor units); got ${JSON.stringify(opts.amount)}.`);
|
|
2346
2377
|
return { amount: parsed.value };
|
|
2347
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
|
+
}
|
|
2348
2507
|
var LEGAL_TEXT_MAX_CHARS = 500;
|
|
2349
2508
|
var CUT_MARK = "\u2026 [CUT: not the full text; use --json to read all of it]";
|
|
2350
2509
|
function legalText(raw) {
|
|
2351
2510
|
const clean = safeInline(raw, Number.MAX_SAFE_INTEGER);
|
|
2352
2511
|
return clean.length > LEGAL_TEXT_MAX_CHARS ? { text: `${clean.slice(0, LEGAL_TEXT_MAX_CHARS)}${CUT_MARK}`, cut: true } : { text: clean, cut: false };
|
|
2353
2512
|
}
|
|
2513
|
+
function text(v, max = 200) {
|
|
2514
|
+
return typeof v === "string" ? safeInline(v, max) : "?";
|
|
2515
|
+
}
|
|
2354
2516
|
function money(amount, currency) {
|
|
2355
2517
|
if (typeof amount !== "number") return "?";
|
|
2356
|
-
return currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
|
|
2518
|
+
return typeof currency === "string" && currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
|
|
2357
2519
|
}
|
|
2358
2520
|
function every(count, interval) {
|
|
2359
|
-
if (!interval) return "";
|
|
2521
|
+
if (typeof interval !== "string" || !interval) return "";
|
|
2360
2522
|
const unit = safeInline(interval);
|
|
2361
2523
|
return typeof count === "number" && count !== 1 ? `every ${count} ${unit}s` : `every ${unit}`;
|
|
2362
2524
|
}
|
|
@@ -2364,13 +2526,63 @@ function statusTone2(status) {
|
|
|
2364
2526
|
return status === "active" ? chalk12.green(status) : chalk12.yellow(status);
|
|
2365
2527
|
}
|
|
2366
2528
|
function productLines(p) {
|
|
2367
|
-
const head = ` ${
|
|
2529
|
+
const head = ` ${text(p.id)} ${statusTone2(text(p.status))} ${text(p.name, 120)} (ref ${text(p.productRef, 128)})`;
|
|
2368
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)`;
|
|
2369
2531
|
const lines = [head, ` ${schedule}`];
|
|
2370
2532
|
const terms = p.terms;
|
|
2371
2533
|
if (terms && typeof terms.disclosure === "string") lines.push(` Disclosure: ${legalText(terms.disclosure).text}`);
|
|
2372
2534
|
return lines;
|
|
2373
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
|
+
}
|
|
2374
2586
|
function productListLines(page) {
|
|
2375
2587
|
const lines = [];
|
|
2376
2588
|
if (page.data.length === 0) lines.push(" No subscription products match.");
|
|
@@ -2397,9 +2609,112 @@ function exitOnBadFlag2(err) {
|
|
|
2397
2609
|
const message = err.message;
|
|
2398
2610
|
exitWithCliError(message, chalk12.red(`Error: ${message}`));
|
|
2399
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";
|
|
2400
2715
|
function subscriptionProductsCommand() {
|
|
2401
2716
|
const products = new Command8("subscription-products").description(
|
|
2402
|
-
"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"
|
|
2403
2718
|
);
|
|
2404
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) => {
|
|
2405
2720
|
let params;
|
|
@@ -2447,6 +2762,155 @@ function subscriptionProductsCommand() {
|
|
|
2447
2762
|
handleError(err);
|
|
2448
2763
|
}
|
|
2449
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
|
+
});
|
|
2450
2914
|
return products;
|
|
2451
2915
|
}
|
|
2452
2916
|
|
|
@@ -3712,7 +4176,7 @@ function lifecycleSeverity(kind) {
|
|
|
3712
4176
|
// src/commands/health.ts
|
|
3713
4177
|
import { Command as Command12 } from "commander";
|
|
3714
4178
|
import chalk16 from "chalk";
|
|
3715
|
-
import { VonPayError as
|
|
4179
|
+
import { VonPayError as VonPayError4 } from "@vonpay/checkout-node";
|
|
3716
4180
|
function healthCommand() {
|
|
3717
4181
|
return new Command12("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3718
4182
|
const client = makeCheckoutClient();
|
|
@@ -3734,7 +4198,7 @@ function healthCommand() {
|
|
|
3734
4198
|
spinner.stop();
|
|
3735
4199
|
auditError(err);
|
|
3736
4200
|
if (jsonErrorsEnabled()) handleError(err);
|
|
3737
|
-
if (err instanceof
|
|
4201
|
+
if (err instanceof VonPayError4) {
|
|
3738
4202
|
console.error(chalk16.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
|
|
3739
4203
|
console.error(chalk16.dim(` Code: ${safeInline(String(err.code))}`));
|
|
3740
4204
|
} else {
|
|
@@ -3757,7 +4221,7 @@ import { homedir as homedir2 } from "os";
|
|
|
3757
4221
|
import { existsSync as existsSync3 } from "fs";
|
|
3758
4222
|
import {
|
|
3759
4223
|
VonPayCheckout as VonPayCheckout3,
|
|
3760
|
-
VonPayError as
|
|
4224
|
+
VonPayError as VonPayError5,
|
|
3761
4225
|
helpFor
|
|
3762
4226
|
} from "@vonpay/checkout-node";
|
|
3763
4227
|
var RELEVANT_ENV_VARS = [
|
|
@@ -3905,7 +4369,7 @@ async function gather() {
|
|
|
3905
4369
|
);
|
|
3906
4370
|
}
|
|
3907
4371
|
} catch (err) {
|
|
3908
|
-
if (err instanceof
|
|
4372
|
+
if (err instanceof VonPayError5) {
|
|
3909
4373
|
const help = helpFor(err.code);
|
|
3910
4374
|
report.validateProbe = {
|
|
3911
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"
|