@agenzo/token-cli 0.3.4 → 0.4.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/dist/index.js +442 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -560,6 +560,29 @@ var STATUS_ICONS = {
|
|
|
560
560
|
warning: "\u26A0",
|
|
561
561
|
loading: "\u280B"
|
|
562
562
|
};
|
|
563
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
564
|
+
function createSpinner(message, intervalMs = 60) {
|
|
565
|
+
let frameIdx = 0;
|
|
566
|
+
let currentMessage = message;
|
|
567
|
+
const render2 = () => {
|
|
568
|
+
process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[frameIdx]} ${currentMessage}`);
|
|
569
|
+
frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
|
|
570
|
+
};
|
|
571
|
+
render2();
|
|
572
|
+
const timer = setInterval(render2, intervalMs);
|
|
573
|
+
return {
|
|
574
|
+
update(msg) {
|
|
575
|
+
currentMessage = msg;
|
|
576
|
+
},
|
|
577
|
+
stop(type, finalMessage) {
|
|
578
|
+
clearInterval(timer);
|
|
579
|
+
process.stdout.write("\r\x1B[K");
|
|
580
|
+
if (type && finalMessage) {
|
|
581
|
+
console.log(Formatter.status(type, finalMessage));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
563
586
|
var Formatter = class _Formatter {
|
|
564
587
|
/** Get display width of a string (CJK characters count as 2) */
|
|
565
588
|
static displayWidth(str) {
|
|
@@ -743,6 +766,167 @@ async function collectPaymentMethodParams(type, flags) {
|
|
|
743
766
|
return params;
|
|
744
767
|
}
|
|
745
768
|
|
|
769
|
+
// src/verb-schema.ts
|
|
770
|
+
var CLI_NAME = "agenzo-token-cli";
|
|
771
|
+
function wantsJsonSchema(argv = process.argv) {
|
|
772
|
+
for (let i = 0; i < argv.length; i++) {
|
|
773
|
+
const a = argv[i];
|
|
774
|
+
if (a === "--format=json") return true;
|
|
775
|
+
if (a === "--format" && argv[i + 1] === "json") return true;
|
|
776
|
+
}
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
function emitSchema(schema) {
|
|
780
|
+
console.log(JSON.stringify(schema, null, 2));
|
|
781
|
+
}
|
|
782
|
+
function attachSchemaHelp(cmd, schema) {
|
|
783
|
+
const baseHelp = cmd.helpInformation.bind(cmd);
|
|
784
|
+
cmd.helpInformation = (context) => {
|
|
785
|
+
if (!wantsJsonSchema()) return baseHelp(context);
|
|
786
|
+
emitSchema(schema);
|
|
787
|
+
return "";
|
|
788
|
+
};
|
|
789
|
+
return cmd;
|
|
790
|
+
}
|
|
791
|
+
var pmAddSchema = {
|
|
792
|
+
cli: CLI_NAME,
|
|
793
|
+
noun: "payment-methods",
|
|
794
|
+
verb: "add",
|
|
795
|
+
description: "Add a payment method. --payment-brand evo (default): card binding + 3DS via Drop-in session. --payment-brand unionpay: UPI Agent Pay enrollment (requires --member); returns enroll_url, result arrives asynchronously via webhook.",
|
|
796
|
+
flags: {
|
|
797
|
+
"payment-brand": {
|
|
798
|
+
type: "string",
|
|
799
|
+
required: false,
|
|
800
|
+
default: "evo",
|
|
801
|
+
description: 'Payment brand: "evo" (default; 3DS/Drop-in binding) or "unionpay" (UPI Agent Pay enrollment)',
|
|
802
|
+
constraints: "evo | unionpay"
|
|
803
|
+
},
|
|
804
|
+
member: {
|
|
805
|
+
type: "string",
|
|
806
|
+
required: "conditional",
|
|
807
|
+
description: "End-user member id (required when --payment-brand unionpay; identifies which user this card belongs to). Ignored for evo payment brand.",
|
|
808
|
+
source: "from_previous_step",
|
|
809
|
+
from: "user.member_id or auth context"
|
|
810
|
+
},
|
|
811
|
+
mode: {
|
|
812
|
+
type: "string",
|
|
813
|
+
required: false,
|
|
814
|
+
default: "dropin",
|
|
815
|
+
description: 'Add mode (evo payment brand only): "manual" (CLI collects card details) or "dropin" (mint a Drop-in session)',
|
|
816
|
+
constraints: "manual | dropin"
|
|
817
|
+
},
|
|
818
|
+
email: {
|
|
819
|
+
type: "string",
|
|
820
|
+
required: false,
|
|
821
|
+
description: "Email for 3DS verification (evo/manual) or Drop-in session reference (evo/dropin)"
|
|
822
|
+
},
|
|
823
|
+
"no-poll": {
|
|
824
|
+
type: "bool",
|
|
825
|
+
required: false,
|
|
826
|
+
default: false,
|
|
827
|
+
description: "Dropin mode: mint session and exit immediately without polling verification status"
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
response: {
|
|
831
|
+
id: { type: "string", description: "Payment method id" },
|
|
832
|
+
status: {
|
|
833
|
+
type: "string",
|
|
834
|
+
description: "PENDING (awaiting enrollment/3DS) / ACTIVE / FAILED / DISABLED"
|
|
835
|
+
},
|
|
836
|
+
"payment-brand": { type: "string", description: "evo or unionpay" },
|
|
837
|
+
session_id: {
|
|
838
|
+
type: "string|absent",
|
|
839
|
+
description: "Drop-in session id (evo/dropin mode only)"
|
|
840
|
+
},
|
|
841
|
+
enroll_url: {
|
|
842
|
+
type: "string|absent",
|
|
843
|
+
description: "UnionPay enrollment URL (unionpay payment brand only) \u2014 user opens this to bind card"
|
|
844
|
+
},
|
|
845
|
+
correlation_id: {
|
|
846
|
+
type: "string|absent",
|
|
847
|
+
description: "src_correlation_id for tracking enrollment result (unionpay payment brand only)"
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
example: {
|
|
851
|
+
command: "agenzo-token-cli payment-methods add --payment-brand unionpay --member usr_abc123 --format json",
|
|
852
|
+
output_summary: "Returns enroll_url. User opens the URL in a browser to complete card binding. Result arrives async via webhook; poll with `payment-methods list` or `payment-methods get`."
|
|
853
|
+
},
|
|
854
|
+
error_recovery: {
|
|
855
|
+
PARAM_INVALID: "Fix the offending flag (--payment-brand must be evo|unionpay; --member required for unionpay).",
|
|
856
|
+
UPSTREAM_ERROR: "UPI enrollment initiation failed. Retry after a short delay (may be transient network).",
|
|
857
|
+
MEMBER_REQUIRED: "UnionPay payment brand requires --member <id>. Supply the end-user member identifier."
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var pmListSchema = {
|
|
861
|
+
cli: CLI_NAME,
|
|
862
|
+
noun: "payment-methods",
|
|
863
|
+
verb: "list",
|
|
864
|
+
description: "List all payment methods for the authenticated developer",
|
|
865
|
+
flags: {},
|
|
866
|
+
response: {
|
|
867
|
+
payment_methods: {
|
|
868
|
+
type: "array",
|
|
869
|
+
description: "List of payment methods",
|
|
870
|
+
items: {
|
|
871
|
+
id: { type: "string", description: "Payment method id" },
|
|
872
|
+
status: { type: "string", description: "PENDING / ACTIVE / FAILED / DISABLED" },
|
|
873
|
+
payment_brand: { type: "string", description: "evo or unionpay" },
|
|
874
|
+
brand: { type: "string|null", description: "Card brand (Visa, Mastercard, UnionPay, etc.)" },
|
|
875
|
+
last4: { type: "string|null", description: "Last 4 digits of card" },
|
|
876
|
+
exp_month: { type: "int|null", description: "Expiry month" },
|
|
877
|
+
exp_year: { type: "int|null", description: "Expiry year" }
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
example: {
|
|
882
|
+
command: "agenzo-token-cli payment-methods list --format json",
|
|
883
|
+
output_summary: "Returns array of payment methods with status and card info."
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
var pmGetSchema = {
|
|
887
|
+
cli: CLI_NAME,
|
|
888
|
+
noun: "payment-methods",
|
|
889
|
+
verb: "get",
|
|
890
|
+
description: "Get a payment method by ID",
|
|
891
|
+
flags: {
|
|
892
|
+
id: { type: "string", required: true, description: "Payment method id", source: "from_previous_step", from: "payment_methods[].id" }
|
|
893
|
+
},
|
|
894
|
+
response: {
|
|
895
|
+
id: { type: "string", description: "Payment method id" },
|
|
896
|
+
status: { type: "string", description: "PENDING / ACTIVE / FAILED / DISABLED" },
|
|
897
|
+
payment_brand: { type: "string", description: "evo or unionpay" },
|
|
898
|
+
brand: { type: "string|null", description: "Card brand" },
|
|
899
|
+
last4: { type: "string|null", description: "Last 4 digits" },
|
|
900
|
+
exp_month: { type: "int|null", description: "Expiry month" },
|
|
901
|
+
exp_year: { type: "int|null", description: "Expiry year" }
|
|
902
|
+
},
|
|
903
|
+
example: {
|
|
904
|
+
command: "agenzo-token-cli payment-methods get --id pm_abc123 --format json",
|
|
905
|
+
output_summary: "Returns full payment method details including card info and status."
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var pmDisableSchema = {
|
|
909
|
+
cli: CLI_NAME,
|
|
910
|
+
noun: "payment-methods",
|
|
911
|
+
verb: "disable",
|
|
912
|
+
description: "Disable a payment method (cascades revoke on active tokens)",
|
|
913
|
+
flags: {
|
|
914
|
+
id: { type: "string", required: true, description: "Payment method id to disable", source: "from_previous_step", from: "payment_methods[].id" }
|
|
915
|
+
},
|
|
916
|
+
response: {
|
|
917
|
+
id: { type: "string", description: "Payment method id" },
|
|
918
|
+
status: { type: "string", description: "DISABLED" }
|
|
919
|
+
},
|
|
920
|
+
example: {
|
|
921
|
+
command: "agenzo-token-cli payment-methods disable --id pm_abc123 --format json",
|
|
922
|
+
output_summary: "Disables the payment method and revokes any active tokens."
|
|
923
|
+
},
|
|
924
|
+
error_recovery: {
|
|
925
|
+
NOT_FOUND: "Payment method id does not exist. Check the id with `payment-methods list`.",
|
|
926
|
+
ALREADY_DISABLED: "Card is already disabled. No action needed."
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
746
930
|
// src/payment-methods/add.ts
|
|
747
931
|
var MANUAL_POLL_INTERVAL_MS = 3e3;
|
|
748
932
|
var MANUAL_POLL_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
@@ -751,6 +935,13 @@ var DROPIN_POLL_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
|
751
935
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ACTIVE", "FAILED", "EXPIRED"]);
|
|
752
936
|
function registerAddCommand(parent, deps) {
|
|
753
937
|
const cmd = parent.command("add").description("Add a payment method (manual 3DS or Drop-in session)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Payment method type (default: card)", "card").option(
|
|
938
|
+
"--payment-brand <brand>",
|
|
939
|
+
'Payment brand: "evo" (default; existing 3DS/Drop-in binding) or "unionpay" (UPI Agent Pay enrollment)',
|
|
940
|
+
"evo"
|
|
941
|
+
).option(
|
|
942
|
+
"--member <id>",
|
|
943
|
+
"End-user member id (required when --payment-brand unionpay; identifies which end-user this card belongs to)"
|
|
944
|
+
).option(
|
|
754
945
|
"--mode <mode>",
|
|
755
946
|
'Add mode: "manual" (default; CLI collects card details and polls 3DS) or "dropin" (mint a Drop-in session and poll until the user finishes adding the payment method in the browser)',
|
|
756
947
|
"manual"
|
|
@@ -764,10 +955,22 @@ function registerAddCommand(parent, deps) {
|
|
|
764
955
|
"--no-poll",
|
|
765
956
|
"Dropin mode: mint the session, print it, and exit immediately without polling verification status (for server/SDK-driven flows where the front-end completes the binding)"
|
|
766
957
|
);
|
|
958
|
+
attachSchemaHelp(cmd, pmAddSchema);
|
|
767
959
|
cmd.action(async () => {
|
|
768
960
|
const opts = cmd.optsWithGlobals();
|
|
769
961
|
const format = resolveFormat(opts.format);
|
|
770
962
|
const isYes = Boolean(opts.yes);
|
|
963
|
+
const paymentBrand = String(opts.paymentBrand ?? "evo").toLowerCase();
|
|
964
|
+
if (paymentBrand !== "evo" && paymentBrand !== "unionpay") {
|
|
965
|
+
throw new CliError(
|
|
966
|
+
"PARAM_INVALID",
|
|
967
|
+
`Unknown --payment-brand "${opts.paymentBrand}". Expected "evo" or "unionpay".`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (paymentBrand === "unionpay") {
|
|
971
|
+
await handleUnionpayPaymentBrand(deps, opts, format);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
771
974
|
const mode = String(opts.mode ?? "manual").toLowerCase();
|
|
772
975
|
if (mode !== "manual" && mode !== "dropin") {
|
|
773
976
|
throw new CliError(
|
|
@@ -891,6 +1094,96 @@ async function handleManualMode(deps, opts, format, isYes) {
|
|
|
891
1094
|
}
|
|
892
1095
|
}
|
|
893
1096
|
}
|
|
1097
|
+
async function handleUnionpayPaymentBrand(deps, opts, format) {
|
|
1098
|
+
const isYes = Boolean(opts.yes);
|
|
1099
|
+
let member;
|
|
1100
|
+
if (opts.member) {
|
|
1101
|
+
member = String(opts.member);
|
|
1102
|
+
} else if (isYes) {
|
|
1103
|
+
throw new CliError(
|
|
1104
|
+
"PARAM_INVALID",
|
|
1105
|
+
"Missing required --member <id> for --payment-brand unionpay (required in --yes mode)"
|
|
1106
|
+
);
|
|
1107
|
+
} else {
|
|
1108
|
+
member = await PromptEngine.resolveInput(void 0, {
|
|
1109
|
+
message: "Member ID (end-user identity this card belongs to):",
|
|
1110
|
+
validate: (v) => v.trim().length > 0 || "Member ID is required for --payment-brand unionpay"
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
1114
|
+
message: "API Key:",
|
|
1115
|
+
type: "password"
|
|
1116
|
+
});
|
|
1117
|
+
const email = await PromptEngine.resolveInput(opts.email, {
|
|
1118
|
+
message: "Email:"
|
|
1119
|
+
});
|
|
1120
|
+
const result = await deps.apiClient.post(
|
|
1121
|
+
"/payment-methods/create",
|
|
1122
|
+
{ type: "api-key", key: apiKey },
|
|
1123
|
+
{ type: "card", payment_brand: "unionpay", member_id: member, email }
|
|
1124
|
+
);
|
|
1125
|
+
if (!result.success) {
|
|
1126
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1127
|
+
}
|
|
1128
|
+
const pm = result.data;
|
|
1129
|
+
notify(format, "success", "Card binding initiated");
|
|
1130
|
+
const createdResult = {
|
|
1131
|
+
data: pm,
|
|
1132
|
+
text: () => Formatter.keyValue([
|
|
1133
|
+
["ID", pm.id],
|
|
1134
|
+
["Status", pm.status],
|
|
1135
|
+
["Enroll URL", pm.enroll_url ?? "-"],
|
|
1136
|
+
["Correlation ID", pm.correlation_id ?? "-"]
|
|
1137
|
+
])
|
|
1138
|
+
};
|
|
1139
|
+
const configManager = new ConfigManager();
|
|
1140
|
+
await renderWithContext(createdResult, { format }, configManager);
|
|
1141
|
+
notify(
|
|
1142
|
+
format,
|
|
1143
|
+
"info",
|
|
1144
|
+
"Open the Enroll URL in a browser to complete card binding. Waiting for result..."
|
|
1145
|
+
);
|
|
1146
|
+
const UNIONPAY_POLL_INTERVAL_MS = 5e3;
|
|
1147
|
+
const UNIONPAY_POLL_TIMEOUT_MS = 6e4;
|
|
1148
|
+
const startTime = Date.now();
|
|
1149
|
+
const spinner = format !== "json" ? createSpinner("Waiting for card binding result...") : null;
|
|
1150
|
+
while (Date.now() - startTime < UNIONPAY_POLL_TIMEOUT_MS) {
|
|
1151
|
+
await sleep(UNIONPAY_POLL_INTERVAL_MS);
|
|
1152
|
+
const pollResult = await deps.apiClient.get(
|
|
1153
|
+
`/payment-methods/${pm.id}`,
|
|
1154
|
+
{ type: "api-key", key: apiKey }
|
|
1155
|
+
);
|
|
1156
|
+
if (pollResult.success) {
|
|
1157
|
+
const status = pollResult.data.status;
|
|
1158
|
+
if (status === "ACTIVE") {
|
|
1159
|
+
spinner?.stop();
|
|
1160
|
+
notify(format, "success", "Payment method activated");
|
|
1161
|
+
const activatedPm = pollResult.data;
|
|
1162
|
+
const activatedResult = {
|
|
1163
|
+
data: activatedPm,
|
|
1164
|
+
text: () => {
|
|
1165
|
+
const lines = [
|
|
1166
|
+
["ID", activatedPm.id],
|
|
1167
|
+
["Type", activatedPm.type ?? "card"],
|
|
1168
|
+
["Status", activatedPm.status]
|
|
1169
|
+
];
|
|
1170
|
+
if (activatedPm.brand) lines.push(["Brand", activatedPm.brand]);
|
|
1171
|
+
if (activatedPm.first6) lines.push(["First 6", activatedPm.first6]);
|
|
1172
|
+
if (activatedPm.last4) lines.push(["Last 4", activatedPm.last4]);
|
|
1173
|
+
return Formatter.keyValue(lines);
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
await renderWithContext(activatedResult, { format }, configManager);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (status === "FAILED") {
|
|
1180
|
+
spinner?.stop("error", "Card binding failed.");
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
spinner?.stop("info", "Timed out waiting for card binding result. Check status later with: payment-methods get " + pm.id);
|
|
1186
|
+
}
|
|
894
1187
|
async function handleDropinMode(deps, opts, format) {
|
|
895
1188
|
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
896
1189
|
message: "API Key:",
|
|
@@ -1014,6 +1307,7 @@ function sleep(ms) {
|
|
|
1014
1307
|
// src/payment-methods/list.ts
|
|
1015
1308
|
function registerListCommand(parent, deps) {
|
|
1016
1309
|
const cmd = parent.command("list").description("List payment methods").option("--api-key <key>", "API Key for authentication").option("--member <member_id>", "Filter by member ID");
|
|
1310
|
+
attachSchemaHelp(cmd, pmListSchema);
|
|
1017
1311
|
cmd.action(async () => {
|
|
1018
1312
|
const opts = cmd.optsWithGlobals();
|
|
1019
1313
|
const format = resolveFormat(opts.format);
|
|
@@ -1060,6 +1354,7 @@ function registerListCommand(parent, deps) {
|
|
|
1060
1354
|
// src/payment-methods/get.ts
|
|
1061
1355
|
function registerGetCommand(parent, deps) {
|
|
1062
1356
|
const cmd = parent.command("get <pm_id>").description("Get a payment method by ID").option("--api-key <key>", "API key for authentication");
|
|
1357
|
+
attachSchemaHelp(cmd, pmGetSchema);
|
|
1063
1358
|
cmd.action(async (pmId) => {
|
|
1064
1359
|
const opts = cmd.optsWithGlobals();
|
|
1065
1360
|
const format = resolveFormat(opts.format);
|
|
@@ -1105,6 +1400,7 @@ function registerDisableCommand(parent, deps) {
|
|
|
1105
1400
|
"--idempotency-key <key>",
|
|
1106
1401
|
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
1107
1402
|
);
|
|
1403
|
+
attachSchemaHelp(cmd, pmDisableSchema);
|
|
1108
1404
|
cmd.action(async (pmId) => {
|
|
1109
1405
|
const opts = cmd.optsWithGlobals();
|
|
1110
1406
|
const format = resolveFormat(opts.format);
|
|
@@ -1234,6 +1530,7 @@ function registerDropinStatusCommand(parent, deps) {
|
|
|
1234
1530
|
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
1235
1531
|
var DEFAULT_NT_FEE_CENTS = 500;
|
|
1236
1532
|
var USDC_UNIT = 1e6;
|
|
1533
|
+
var DECIMAL_AMOUNT_RE = /^\d+(\.\d+)?$/;
|
|
1237
1534
|
function mapTokenType(cliType) {
|
|
1238
1535
|
if (cliType === "network-token") return "network_token";
|
|
1239
1536
|
return cliType;
|
|
@@ -1311,14 +1608,23 @@ function formatPaymentToken(data) {
|
|
|
1311
1608
|
lines.push(["Currency", String(vcn.currency || "USD")]);
|
|
1312
1609
|
lines.push(["Status", String(vcn.status || data.status || "")]);
|
|
1313
1610
|
} else if (type === "network_token") {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1611
|
+
if (data.status === "PENDING" && data.checkout_url) {
|
|
1612
|
+
lines.push(["Payment Token ID", String(data.id || "")]);
|
|
1613
|
+
lines.push(["Type", "Network Token"]);
|
|
1614
|
+
lines.push(["Status", "PENDING"]);
|
|
1615
|
+
lines.push(["Checkout URL", String(data.checkout_url || "")]);
|
|
1616
|
+
lines.push(["Correlation ID", String(data.correlation_id || "")]);
|
|
1617
|
+
} else {
|
|
1618
|
+
const nt = data.network_token ?? data;
|
|
1619
|
+
lines.push(["Payment Token ID", String(data.id || nt.id || "")]);
|
|
1620
|
+
lines.push(["Type", "Network Token"]);
|
|
1621
|
+
lines.push(["Brand", String(nt.payment_brand || nt.brand || "")]);
|
|
1622
|
+
const eci = nt.eci || "";
|
|
1623
|
+
if (eci) lines.push(["ECI", String(eci)]);
|
|
1624
|
+
lines.push(["Cryptogram", String(nt.token_cryptogram || nt.cryptogram || "")]);
|
|
1625
|
+
lines.push(["Expiry", String(nt.expiry_date || nt.expiry || "")]);
|
|
1626
|
+
lines.push(["Token Number", String(nt.value || "")]);
|
|
1627
|
+
}
|
|
1322
1628
|
} else if (type === "x402") {
|
|
1323
1629
|
const x402 = data.x402 ?? data;
|
|
1324
1630
|
lines.push(["Payment Token ID", String(data.id || x402.id || "")]);
|
|
@@ -1338,8 +1644,13 @@ function formatCentsToUsd(cents) {
|
|
|
1338
1644
|
const remainder = cents % 100;
|
|
1339
1645
|
return `${dollars}.${String(remainder).padStart(2, "0")}`;
|
|
1340
1646
|
}
|
|
1647
|
+
function isValidUnionpayAmount(amountStr) {
|
|
1648
|
+
const trimmed = amountStr.trim();
|
|
1649
|
+
if (!DECIMAL_AMOUNT_RE.test(trimmed)) return false;
|
|
1650
|
+
return parseFloat(trimmed) > 0;
|
|
1651
|
+
}
|
|
1341
1652
|
function registerCreateCommand(parent, deps) {
|
|
1342
|
-
const cmd = parent.command("create").description("Create a payment token (VCN / Network Token / X402)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Token type: vcn | network-token | x402").option("--payment-method-id <id>", "Payment method ID to use").option("--card <last4>", "Match payment method by last 4 digits").option("--member <member_id>", "Member ID").option("--amount <amount>", "Amount in USD (VCN / X402)").option("--currency <currency>", "Currency (default: USD)").option("--pay-to <address>", "Pay-to address (X402)").option("--nonce <nonce>", "Nonce (X402)").option("--network <network>", "Network (X402)").option("--deadline <deadline>", "Deadline (X402)").option("--external-tx-id <id>", "External transaction ID").option(
|
|
1653
|
+
const cmd = parent.command("create").description("Create a payment token (VCN / Network Token / X402)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Token type: vcn | network-token | x402").option("--payment-method-id <id>", "Payment method ID to use").option("--card <last4>", "Match payment method by last 4 digits").option("--member <member_id>", "Member ID").option("--amount <amount>", "Amount in USD (VCN / X402)").option("--currency <currency>", "Currency (default: USD)").option("--pay-to <address>", "Pay-to address (X402)").option("--nonce <nonce>", "Nonce (X402)").option("--network <network>", "Network (X402)").option("--deadline <deadline>", "Deadline (X402)").option("--external-tx-id <id>", "External transaction ID").option("--recipient-first-name <name>", "UnionPay network token: recipient first name (order delivery details)").option("--recipient-last-name <name>", "UnionPay network token: recipient last name (order delivery details)").option("--recipient-email <email>", "UnionPay network token: recipient email (recipient-email or recipient-phone required)").option("--recipient-phone <phone>", "UnionPay network token: recipient phone (recipient-email or recipient-phone required)").option("--unionpay-amount <amount>", 'UnionPay network token: intent amount as a decimal string, e.g. "174.58"').option(
|
|
1343
1654
|
"--idempotency-key <key>",
|
|
1344
1655
|
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
1345
1656
|
);
|
|
@@ -1366,6 +1677,24 @@ function registerCreateCommand(parent, deps) {
|
|
|
1366
1677
|
card: opts.card,
|
|
1367
1678
|
yes: isYes
|
|
1368
1679
|
});
|
|
1680
|
+
let selectedPmPaymentBrand;
|
|
1681
|
+
if (serverType === "network_token") {
|
|
1682
|
+
const pmResult = await deps.apiClient.get(
|
|
1683
|
+
`/payment-methods/${paymentMethodId}`,
|
|
1684
|
+
{ type: "api-key", key: apiKey }
|
|
1685
|
+
);
|
|
1686
|
+
if (!pmResult.success) {
|
|
1687
|
+
throw CliError.fromApi(pmResult, { auth: "api-key" });
|
|
1688
|
+
}
|
|
1689
|
+
selectedPmPaymentBrand = pmResult.data.payment_brand;
|
|
1690
|
+
if (selectedPmPaymentBrand === "unionpay" && opts.card && !opts.paymentMethodId) {
|
|
1691
|
+
throw new CliError(
|
|
1692
|
+
"PARAM_INVALID",
|
|
1693
|
+
"UnionPay cards must be selected via --payment-method-id; --card (last4 matching) is not supported for unionpay payment brand."
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
const isUnionpayNetworkToken = serverType === "network_token" && selectedPmPaymentBrand === "unionpay";
|
|
1369
1698
|
let member = opts.member;
|
|
1370
1699
|
if (!member && !isYes) {
|
|
1371
1700
|
const memberInput = await PromptEngine.resolveInput(void 0, {
|
|
@@ -1414,10 +1743,66 @@ function registerCreateCommand(parent, deps) {
|
|
|
1414
1743
|
typeBody.currency = opts.currency;
|
|
1415
1744
|
}
|
|
1416
1745
|
} else if (serverType === "network_token") {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1746
|
+
if (isUnionpayNetworkToken) {
|
|
1747
|
+
const unionpayAmountStr = await PromptEngine.resolveInput(
|
|
1748
|
+
opts.unionpayAmount,
|
|
1749
|
+
{
|
|
1750
|
+
message: "UnionPay intent amount (USD, e.g. 174.58):",
|
|
1751
|
+
validate: (v) => isValidUnionpayAmount(v) || 'Amount must be a positive decimal, e.g. "174.58"'
|
|
1752
|
+
}
|
|
1753
|
+
);
|
|
1754
|
+
if (!isValidUnionpayAmount(unionpayAmountStr)) {
|
|
1755
|
+
throw new CliError(
|
|
1756
|
+
"PARAM_INVALID",
|
|
1757
|
+
`Invalid --unionpay-amount "${unionpayAmountStr}". Expected a positive decimal string, e.g. "174.58".`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
typeBody.unionpay_amount = unionpayAmountStr.trim();
|
|
1761
|
+
typeBody.recipient_first_name = await PromptEngine.resolveInput(
|
|
1762
|
+
opts.recipientFirstName,
|
|
1763
|
+
{
|
|
1764
|
+
message: "Recipient first name:",
|
|
1765
|
+
validate: (v) => v.trim().length > 0 || "Recipient first name is required"
|
|
1766
|
+
}
|
|
1767
|
+
);
|
|
1768
|
+
typeBody.recipient_last_name = await PromptEngine.resolveInput(
|
|
1769
|
+
opts.recipientLastName,
|
|
1770
|
+
{
|
|
1771
|
+
message: "Recipient last name:",
|
|
1772
|
+
validate: (v) => v.trim().length > 0 || "Recipient last name is required"
|
|
1773
|
+
}
|
|
1774
|
+
);
|
|
1775
|
+
let recipientEmail = opts.recipientEmail;
|
|
1776
|
+
let recipientPhone = opts.recipientPhone;
|
|
1777
|
+
if (!recipientEmail && !recipientPhone) {
|
|
1778
|
+
if (isYes) {
|
|
1779
|
+
throw new CliError(
|
|
1780
|
+
"PARAM_INVALID",
|
|
1781
|
+
"--recipient-email or --recipient-phone is required for unionpay network tokens."
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
const emailInput = await PromptEngine.resolveInput(void 0, {
|
|
1785
|
+
message: "Recipient email (optional, press Enter to skip and enter phone instead):",
|
|
1786
|
+
validate: () => true
|
|
1787
|
+
});
|
|
1788
|
+
if (emailInput.trim()) {
|
|
1789
|
+
recipientEmail = emailInput.trim();
|
|
1790
|
+
} else {
|
|
1791
|
+
const phoneInput = await PromptEngine.resolveInput(void 0, {
|
|
1792
|
+
message: "Recipient phone (required, since no email was given):",
|
|
1793
|
+
validate: (v) => v.trim().length > 0 || "Recipient email or phone is required"
|
|
1794
|
+
});
|
|
1795
|
+
recipientPhone = phoneInput.trim();
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (recipientEmail) typeBody.recipient_email = recipientEmail;
|
|
1799
|
+
if (recipientPhone) typeBody.recipient_phone = recipientPhone;
|
|
1800
|
+
} else {
|
|
1801
|
+
feeCents = await fetchNetworkTokenFee(deps.apiClient, apiKey);
|
|
1802
|
+
freezeAmountCents = feeCents;
|
|
1803
|
+
feeDisplay = `$${formatCentsToUsd(feeCents)}`;
|
|
1804
|
+
freezeDisplay = feeDisplay;
|
|
1805
|
+
}
|
|
1421
1806
|
} else if (serverType === "x402") {
|
|
1422
1807
|
const amountStr = await PromptEngine.resolveInput(opts.amount, {
|
|
1423
1808
|
message: "Amount (USDC):",
|
|
@@ -1459,10 +1844,15 @@ function registerCreateCommand(parent, deps) {
|
|
|
1459
1844
|
typeBody.deadline = deadlineNum;
|
|
1460
1845
|
}
|
|
1461
1846
|
if (!isYes) {
|
|
1462
|
-
|
|
1463
|
-
|
|
1847
|
+
if (freezeDisplay !== void 0 && feeDisplay !== void 0) {
|
|
1848
|
+
const warningLines = [`Freeze: ${freezeDisplay}`, `Fee: ${feeDisplay}`];
|
|
1849
|
+
notify(format, "warning", warningLines.join(" | "));
|
|
1850
|
+
} else if (isUnionpayNetworkToken) {
|
|
1851
|
+
notify(format, "info", "UnionPay: no fee (clearing network not yet enabled)");
|
|
1852
|
+
}
|
|
1853
|
+
const confirmMessage = isUnionpayNetworkToken ? "Proceed with UnionPay network token request?" : "Proceed with token creation?";
|
|
1464
1854
|
const confirmed = await confirm({
|
|
1465
|
-
message:
|
|
1855
|
+
message: confirmMessage,
|
|
1466
1856
|
default: true
|
|
1467
1857
|
});
|
|
1468
1858
|
if (!confirmed) {
|
|
@@ -1519,6 +1909,42 @@ function registerCreateCommand(parent, deps) {
|
|
|
1519
1909
|
}
|
|
1520
1910
|
};
|
|
1521
1911
|
await renderWithContext(commandResult, { format }, configManager);
|
|
1912
|
+
if (isUnionpayNetworkToken) {
|
|
1913
|
+
notify(
|
|
1914
|
+
format,
|
|
1915
|
+
"info",
|
|
1916
|
+
"Open the Checkout URL to complete the UnionPay payment verification. Waiting for result..."
|
|
1917
|
+
);
|
|
1918
|
+
const UNIONPAY_TOKEN_POLL_INTERVAL_MS = 5e3;
|
|
1919
|
+
const UNIONPAY_TOKEN_POLL_TIMEOUT_MS = 6e4;
|
|
1920
|
+
const pollStart = Date.now();
|
|
1921
|
+
const tokenId = tokenData.id;
|
|
1922
|
+
while (Date.now() - pollStart < UNIONPAY_TOKEN_POLL_TIMEOUT_MS) {
|
|
1923
|
+
await new Promise((resolve) => setTimeout(resolve, UNIONPAY_TOKEN_POLL_INTERVAL_MS));
|
|
1924
|
+
const pollResult = await deps.apiClient.get(
|
|
1925
|
+
`/payment-tokens/${tokenId}`,
|
|
1926
|
+
{ type: "api-key", key: apiKey }
|
|
1927
|
+
);
|
|
1928
|
+
if (pollResult.success) {
|
|
1929
|
+
const status = pollResult.data.status;
|
|
1930
|
+
if (status === "ACTIVE") {
|
|
1931
|
+
notify(format, "success", "UnionPay network token activated!");
|
|
1932
|
+
if (!pollResult.data.type) pollResult.data.type = serverType;
|
|
1933
|
+
const activatedResult = {
|
|
1934
|
+
data: pollResult.data,
|
|
1935
|
+
text: () => formatPaymentToken(pollResult.data)
|
|
1936
|
+
};
|
|
1937
|
+
await renderWithContext(activatedResult, { format }, configManager);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (status === "FAILED") {
|
|
1941
|
+
notify(format, "error", "UnionPay network token failed.");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
notify(format, "info", `Timed out waiting for token activation. Check status later with: payment-tokens get ${tokenId}`);
|
|
1947
|
+
}
|
|
1522
1948
|
});
|
|
1523
1949
|
}
|
|
1524
1950
|
async function checkVcnFeatureEnabled(apiClient, apiKey) {
|