@itpay/cli 0.2.17 → 2.0.0-rc.1
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 +148 -447
- package/bin/itp +1 -150
- package/dist/src/client/backend.js +154 -0
- package/dist/src/client/http.js +76 -0
- package/dist/src/client/types.js +4 -0
- package/dist/src/commands/buy.js +351 -0
- package/dist/src/commands/cart.js +264 -0
- package/dist/src/commands/catalog.js +26 -0
- package/dist/src/commands/checkout.js +106 -0
- package/dist/src/commands/docs.js +61 -0
- package/dist/src/commands/guidance.js +422 -0
- package/dist/src/commands/install.js +95 -0
- package/dist/src/commands/order.js +78 -0
- package/dist/src/commands/orders.js +22 -0
- package/dist/src/commands/pay.js +26 -0
- package/dist/src/commands/readyz.js +8 -0
- package/dist/src/commands/refund.js +20 -0
- package/dist/src/commands/services.js +317 -0
- package/dist/src/main.js +606 -0
- package/dist/src/render/feishu.js +201 -0
- package/dist/src/render/ide.js +321 -0
- package/dist/src/render/index.js +57 -0
- package/dist/src/render/interaction.js +49 -0
- package/dist/src/render/markdown.js +83 -0
- package/dist/src/render/output.js +42 -0
- package/dist/src/render/plain_chat.js +60 -0
- package/dist/src/render/plan.js +31 -0
- package/dist/src/render/qr.js +32 -0
- package/dist/src/render/sink.js +6 -0
- package/dist/src/render/status.js +37 -0
- package/dist/src/render/telegram.js +172 -0
- package/dist/src/render/terminal.js +148 -0
- package/dist/src/render/terminal_image.js +19 -0
- package/dist/src/state/cart_session.js +151 -0
- package/dist/src/state/client_context.js +73 -0
- package/dist/src/state/config.js +82 -0
- package/dist/src/state/device_authority.js +217 -0
- package/dist/src/state/operation_journal.js +80 -0
- package/docs/agent/buyer/cart-checkout.json +56 -94
- package/docs/agent/buyer/catalog-list.json +47 -0
- package/docs/agent/buyer/install-and-setup.json +82 -0
- package/docs/agent/buyer/orders-refunds.json +76 -0
- package/docs/agent/buyer/payment-flow.json +77 -0
- package/docs/agent/buyer/quickstart.json +143 -75
- package/docs/agent/buyer/render-hosts.json +79 -0
- package/package.json +32 -13
- package/skills/itpay-buyer/SKILL.md +107 -238
- package/docs/agent/buyer/account-portal.json +0 -81
- package/docs/agent/buyer/catalog-search.json +0 -106
- package/docs/agent/buyer/human-claim-ui.json +0 -77
- package/docs/agent/buyer/payment-qr.json +0 -97
- package/docs/agent/buyer/payment-wait.json +0 -84
- package/docs/agent/buyer/product-recommendation.json +0 -80
- package/docs/agent/buyer/qr-refresh.json +0 -67
- package/docs/agent/buyer/recovery.json +0 -85
- package/docs/agent/buyer/safety-policy.json +0 -70
- package/docs/agent/buyer/secure-delivery.json +0 -90
- package/docs/agent/buyer/vault-agent-read.json +0 -95
- package/install.ps1 +0 -65
- package/install.sh +0 -66
- package/lib/account-status.js +0 -157
- package/lib/buyer.js +0 -2332
- package/lib/client-context.js +0 -126
- package/lib/docs.js +0 -200
- package/lib/env.js +0 -723
- package/lib/http.js +0 -151
- package/lib/ops.js +0 -135
- package/lib/render-human.js +0 -718
- package/lib/runtime.js +0 -1456
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// V3 `buy` flow:
|
|
2
|
+
// 1. validate client context (host/target)
|
|
3
|
+
// 2. create cart via POST /v1/carts
|
|
4
|
+
// 3. create checkout via POST /v1/checkouts (consumes the cart)
|
|
5
|
+
// 4. build a checkout_qr render plan + dispatch to host renderer
|
|
6
|
+
// 5. [--pay] create payment intent → render payment QR → optionally SSE wait
|
|
7
|
+
// 6. [--json] output JSON instead of terminal text
|
|
8
|
+
//
|
|
9
|
+
// The display token is passed to the renderer and persisted in the local cart
|
|
10
|
+
// session so a restarted agent can resume the same checkout.
|
|
11
|
+
import { operationID } from "../state/config.js";
|
|
12
|
+
import { validateContext } from "../state/client_context.js";
|
|
13
|
+
import { dispatchRender } from "../render/index.js";
|
|
14
|
+
import { dispatchInteractionRequest } from "../render/interaction.js";
|
|
15
|
+
import { ensureIdeImageAttach, readFileAsDataURL } from "../render/ide.js";
|
|
16
|
+
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
17
|
+
export async function runBuy(backend, config, options) {
|
|
18
|
+
const err = validateContext(options.host, options.target);
|
|
19
|
+
if (err) {
|
|
20
|
+
throw new Error(`${err.code}: ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
const snap = options.cartSession.show();
|
|
23
|
+
if (!options.cartID && snap.items.length === 0) {
|
|
24
|
+
throw new Error("cart is empty; add an item with `itpay cart add` first");
|
|
25
|
+
}
|
|
26
|
+
const missingContactFields = findMissingContactFields(options.contact, options.requiredContactFields ?? []);
|
|
27
|
+
if (missingContactFields.length > 0) {
|
|
28
|
+
const interactionRequest = buildMissingContactInteractionRequest(missingContactFields);
|
|
29
|
+
await dispatchInteractionRequest(options.host, interactionRequest, {
|
|
30
|
+
...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
|
|
31
|
+
...(options.target ? { target: options.target } : {}),
|
|
32
|
+
...(options.output ? { output: options.output } : {}),
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
kind: "interaction_requested",
|
|
36
|
+
interactionRequest,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
let cart;
|
|
40
|
+
if (options.cartID) {
|
|
41
|
+
cart = await backend.getCart(options.cartID);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const request = options.cartSession.toCreateCartRequest();
|
|
45
|
+
request.client_context = {
|
|
46
|
+
host: options.host,
|
|
47
|
+
target: options.target,
|
|
48
|
+
agent_device_id: options.cartSession.ensureAgentDeviceID(config.agentDeviceID),
|
|
49
|
+
};
|
|
50
|
+
const agentDeviceID = options.cartSession.ensureAgentDeviceID(config.agentDeviceID);
|
|
51
|
+
request.agent_device_id = agentDeviceID;
|
|
52
|
+
cart = await backend.createCart(request);
|
|
53
|
+
}
|
|
54
|
+
const checkoutRequest = {
|
|
55
|
+
cart_id: cart.cart_id,
|
|
56
|
+
client_reference_id: options.clientReferenceID ?? await operationID(config, `checkout.create:${cart.cart_id}`),
|
|
57
|
+
...(options.contact ? { delivery_contact: options.contact } : {}),
|
|
58
|
+
};
|
|
59
|
+
const checkout = await backend.createCheckout(checkoutRequest);
|
|
60
|
+
options.cartSession.rememberCheckout({
|
|
61
|
+
cartID: cart.cart_id,
|
|
62
|
+
checkoutID: checkout.checkout.checkout_id,
|
|
63
|
+
displayToken: checkout.display_token,
|
|
64
|
+
checkoutURL: tokenizedCheckoutURL(checkout.checkout_url, checkout.display_token, checkout.qr_payload),
|
|
65
|
+
});
|
|
66
|
+
const checkoutID = checkout.checkout.checkout_id;
|
|
67
|
+
const displayToken = checkout.display_token;
|
|
68
|
+
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
69
|
+
const orderItems = cart.items.map((item) => ({
|
|
70
|
+
title: item.title,
|
|
71
|
+
quantity: item.quantity,
|
|
72
|
+
amountMinor: item.amount_minor,
|
|
73
|
+
currency: item.currency,
|
|
74
|
+
}));
|
|
75
|
+
// --- Payment flow (optional) ---
|
|
76
|
+
let paymentIntent;
|
|
77
|
+
let waitStatus = "skipped";
|
|
78
|
+
if (options.pay) {
|
|
79
|
+
const method = options.payMethod ?? "alipay";
|
|
80
|
+
paymentIntent = await backend.createPaymentIntent(checkoutID, { payment_method_type: method, display_token: displayToken }, await operationID(config, `payment.intent:${checkoutID}:${method}`));
|
|
81
|
+
if (!options.jsonOutput) {
|
|
82
|
+
process.stdout.write("\n--- payment intent ---\n");
|
|
83
|
+
process.stdout.write(` id: ${paymentIntent.payment_intent_id}\n`);
|
|
84
|
+
process.stdout.write(` method: ${paymentIntent.payment_method_type}\n`);
|
|
85
|
+
process.stdout.write(` status: ${paymentIntent.status}\n`);
|
|
86
|
+
if (paymentIntent.action?.qr_image_url) {
|
|
87
|
+
process.stdout.write(` qr: ${paymentIntent.action.qr_image_url}\n`);
|
|
88
|
+
}
|
|
89
|
+
if (paymentIntent.action?.mobile_wallet_url) {
|
|
90
|
+
process.stdout.write(` wallet: ${paymentIntent.action.mobile_wallet_url}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!options.noWait) {
|
|
94
|
+
waitStatus = await waitForPaymentSSE(backend, checkoutID, displayToken, options.payTimeoutSec ?? 120);
|
|
95
|
+
if (!options.jsonOutput) {
|
|
96
|
+
process.stdout.write(` wait: ${waitStatus}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// --- Build render plan (after payment if --pay) ---
|
|
101
|
+
const planInput = {
|
|
102
|
+
host: options.host,
|
|
103
|
+
checkoutID,
|
|
104
|
+
checkoutURL,
|
|
105
|
+
displayToken,
|
|
106
|
+
qrPayload: checkout.qr_payload,
|
|
107
|
+
nextAction: checkout.checkout.next_action,
|
|
108
|
+
orderItems,
|
|
109
|
+
orderCurrency: checkout.checkout.currency,
|
|
110
|
+
};
|
|
111
|
+
if (checkout.qr_png_url)
|
|
112
|
+
planInput.qrPNGURL = checkout.qr_png_url;
|
|
113
|
+
if (paymentIntent) {
|
|
114
|
+
planInput.paymentIntentID = paymentIntent.payment_intent_id;
|
|
115
|
+
planInput.paymentMethod = paymentIntent.payment_method_type;
|
|
116
|
+
planInput.paymentStatus = paymentIntent.status;
|
|
117
|
+
}
|
|
118
|
+
const plan = buildCheckoutQRPlan(planInput);
|
|
119
|
+
// --- Download brand QR for IDE image attach (every output mode) ---
|
|
120
|
+
await ensureIdeImageAttach(plan, {
|
|
121
|
+
enabled: config.ideImageAttach,
|
|
122
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
123
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
124
|
+
});
|
|
125
|
+
// --- Output ---
|
|
126
|
+
if (options.jsonOutput) {
|
|
127
|
+
const jsonInput = {
|
|
128
|
+
checkout: { ...checkout, checkout_url: checkoutURL },
|
|
129
|
+
cart,
|
|
130
|
+
waitStatus,
|
|
131
|
+
plan,
|
|
132
|
+
};
|
|
133
|
+
if (paymentIntent) {
|
|
134
|
+
jsonInput.paymentIntent = paymentIntent;
|
|
135
|
+
}
|
|
136
|
+
if (plan.ideImageAttach) {
|
|
137
|
+
jsonInput.ideImageAttach = {
|
|
138
|
+
localPath: plan.ideImageAttach.localPath,
|
|
139
|
+
mirrors: plan.ideImageAttach.mirrors,
|
|
140
|
+
mimeType: plan.ideImageAttach.mimeType,
|
|
141
|
+
...(plan.ideImageAttach.caption ? { caption: plan.ideImageAttach.caption } : {}),
|
|
142
|
+
mustRenderReason: plan.ideImageAttach.mustRenderReason,
|
|
143
|
+
status: plan.ideImageAttach.status,
|
|
144
|
+
...(plan.ideImageAttach.error ? { error: plan.ideImageAttach.error } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const json = buildJSONOutput(jsonInput);
|
|
148
|
+
(options.output ?? ((line) => process.stdout.write(line + "\n")))(JSON.stringify(json, null, 2) + "\n");
|
|
149
|
+
return {
|
|
150
|
+
kind: "checkout_rendered",
|
|
151
|
+
plan,
|
|
152
|
+
checkoutID,
|
|
153
|
+
displayToken,
|
|
154
|
+
json,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Text output — always render QR for non-JSON mode
|
|
158
|
+
const renderOptions = {
|
|
159
|
+
host: options.host,
|
|
160
|
+
isTTY: options.isTTY ?? Boolean(process.stdout.isTTY),
|
|
161
|
+
...(options.target ? { target: options.target } : {}),
|
|
162
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
163
|
+
...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
|
|
164
|
+
...(options.output ? { output: options.output } : {}),
|
|
165
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
166
|
+
baseURL: config.baseURL,
|
|
167
|
+
};
|
|
168
|
+
await dispatchRender(plan, renderOptions);
|
|
169
|
+
return {
|
|
170
|
+
kind: "checkout_rendered",
|
|
171
|
+
plan,
|
|
172
|
+
checkoutID,
|
|
173
|
+
displayToken,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// --- SSE wait for payment verification ---
|
|
177
|
+
async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const timeout = setTimeout(() => {
|
|
181
|
+
controller.abort();
|
|
182
|
+
resolve("timeout");
|
|
183
|
+
}, timeoutSec * 1000);
|
|
184
|
+
backend.streamCheckoutEvents(checkoutID, displayToken, (event) => {
|
|
185
|
+
if (event.type === "payment_intent.verified") {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
controller.abort();
|
|
188
|
+
resolve("verified");
|
|
189
|
+
}
|
|
190
|
+
}, controller.signal).catch(() => {
|
|
191
|
+
// stream ended or aborted
|
|
192
|
+
}).finally(() => {
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export function buildJSONOutput(input) {
|
|
198
|
+
const output = {
|
|
199
|
+
kind: "checkout_created",
|
|
200
|
+
checkout_id: input.checkout.checkout.checkout_id,
|
|
201
|
+
checkout_url: input.checkout.checkout_url,
|
|
202
|
+
display_token: input.checkout.display_token,
|
|
203
|
+
qr_payload: input.checkout.qr_payload,
|
|
204
|
+
checkout_status: input.checkout.checkout.status,
|
|
205
|
+
wait_status: input.waitStatus,
|
|
206
|
+
};
|
|
207
|
+
if (input.checkout.qr_png_url) {
|
|
208
|
+
output.qr_png_url = input.checkout.qr_png_url;
|
|
209
|
+
}
|
|
210
|
+
const serviceExecutions = input.cart?.items
|
|
211
|
+
.filter((item) => item.service_execution_id)
|
|
212
|
+
.map((item) => ({
|
|
213
|
+
service_execution_id: item.service_execution_id,
|
|
214
|
+
...(item.service_capability_id ? { service_capability_id: item.service_capability_id } : {}),
|
|
215
|
+
...(item.title ? { title: item.title } : {}),
|
|
216
|
+
}));
|
|
217
|
+
if (serviceExecutions && serviceExecutions.length > 0) {
|
|
218
|
+
output.service_executions = serviceExecutions;
|
|
219
|
+
}
|
|
220
|
+
if (input.paymentIntent) {
|
|
221
|
+
output.payment_intent_id = input.paymentIntent.payment_intent_id;
|
|
222
|
+
output.payment_status = input.paymentIntent.status;
|
|
223
|
+
if (input.paymentIntent.action) {
|
|
224
|
+
const pa = {};
|
|
225
|
+
if (input.paymentIntent.action.qr_image_url)
|
|
226
|
+
pa.qr_image_url = input.paymentIntent.action.qr_image_url;
|
|
227
|
+
if (input.paymentIntent.action.mobile_wallet_url)
|
|
228
|
+
pa.mobile_wallet_url = input.paymentIntent.action.mobile_wallet_url;
|
|
229
|
+
output.payment_action = pa;
|
|
230
|
+
}
|
|
231
|
+
output.kind = "payment_handoff_required";
|
|
232
|
+
}
|
|
233
|
+
if (input.waitStatus === "verified") {
|
|
234
|
+
output.kind = "payment_verified";
|
|
235
|
+
}
|
|
236
|
+
if (input.ideImageAttach) {
|
|
237
|
+
output.brand_qr_status = input.ideImageAttach.status;
|
|
238
|
+
if (input.ideImageAttach.localPath) {
|
|
239
|
+
output.brand_qr_local_path = input.ideImageAttach.localPath;
|
|
240
|
+
const dataURL = readFileAsDataURL(input.ideImageAttach.localPath, input.ideImageAttach.mimeType);
|
|
241
|
+
if (dataURL)
|
|
242
|
+
output.brand_qr_data_url = dataURL;
|
|
243
|
+
const stableName = input.ideImageAttach.localPath.split("/").pop();
|
|
244
|
+
if (stableName)
|
|
245
|
+
output.brand_qr_stable_name = stableName;
|
|
246
|
+
}
|
|
247
|
+
if (input.ideImageAttach.mirrors.length > 0) {
|
|
248
|
+
output.brand_qr_mirrors = [...input.ideImageAttach.mirrors];
|
|
249
|
+
}
|
|
250
|
+
output.brand_qr_mime_type = input.ideImageAttach.mimeType;
|
|
251
|
+
if (input.ideImageAttach.caption)
|
|
252
|
+
output.brand_qr_caption = input.ideImageAttach.caption;
|
|
253
|
+
if (input.ideImageAttach.error)
|
|
254
|
+
output.brand_qr_error = input.ideImageAttach.error;
|
|
255
|
+
output.brand_qr_must_render_reason = input.ideImageAttach.mustRenderReason;
|
|
256
|
+
output.brand_qr_render_action = "agent_must_read_local_path_into_ide_chat";
|
|
257
|
+
}
|
|
258
|
+
output.next = input.waitStatus === "verified"
|
|
259
|
+
? "itpay orders"
|
|
260
|
+
: `itpay checkout --id ${input.checkout.checkout.checkout_id} --token ${input.checkout.display_token}`;
|
|
261
|
+
if (input.plan)
|
|
262
|
+
output.agent_action = buildAgentChatHandoff(input.plan);
|
|
263
|
+
return output;
|
|
264
|
+
}
|
|
265
|
+
// --- checkout QR plan ---
|
|
266
|
+
export function buildCheckoutQRPlan(input) {
|
|
267
|
+
const summary = `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
|
|
268
|
+
const isPayment = input.paymentIntentID != null;
|
|
269
|
+
const afterCommand = isPayment
|
|
270
|
+
? `itpay checkout --id ${input.checkoutID} --token ${input.displayToken}`
|
|
271
|
+
: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken}`;
|
|
272
|
+
const platform = {
|
|
273
|
+
text: summary,
|
|
274
|
+
links: [
|
|
275
|
+
{ label: "打开付款页面", url: input.checkoutURL },
|
|
276
|
+
],
|
|
277
|
+
buttons: [
|
|
278
|
+
{ label: "打开收银台", kind: "url", url: input.checkoutURL },
|
|
279
|
+
...(input.checkoutID
|
|
280
|
+
? [{ label: "查询 Checkout 状态", kind: "callback", intent: "check_checkout_status", ref: input.checkoutID }]
|
|
281
|
+
: []),
|
|
282
|
+
],
|
|
283
|
+
blocks: [],
|
|
284
|
+
...(input.qrPNGURL ? { media: [{ url: input.qrPNGURL, label: "Branded QR", mimeType: "image/png" }] } : {}),
|
|
285
|
+
};
|
|
286
|
+
const plan = {
|
|
287
|
+
kind: isPayment ? "payment_qr" : "checkout_qr",
|
|
288
|
+
host: input.host,
|
|
289
|
+
summary,
|
|
290
|
+
url: input.qrPayload,
|
|
291
|
+
preferredQRSources: [input.qrPNGURL ?? input.qrPayload],
|
|
292
|
+
checkoutID: input.checkoutID,
|
|
293
|
+
platform,
|
|
294
|
+
afterActionCommand: afterCommand,
|
|
295
|
+
afterActionLabel: "扫码或点击链接完成支付后,执行以下命令查询状态:",
|
|
296
|
+
};
|
|
297
|
+
if (input.orderItems)
|
|
298
|
+
plan.orderItems = input.orderItems;
|
|
299
|
+
if (input.orderCurrency)
|
|
300
|
+
plan.orderCurrency = input.orderCurrency;
|
|
301
|
+
if (input.paymentMethod)
|
|
302
|
+
plan.paymentMethod = input.paymentMethod;
|
|
303
|
+
if (input.paymentStatus)
|
|
304
|
+
plan.paymentStatus = input.paymentStatus;
|
|
305
|
+
if (input.paymentIntentID)
|
|
306
|
+
plan.paymentIntentID = input.paymentIntentID;
|
|
307
|
+
return plan;
|
|
308
|
+
}
|
|
309
|
+
// --- contact field interaction ---
|
|
310
|
+
export function buildMissingContactInteractionRequest(fields) {
|
|
311
|
+
return {
|
|
312
|
+
kind: "input",
|
|
313
|
+
id: "collect_delivery_contact",
|
|
314
|
+
title: "Collect buyer contact",
|
|
315
|
+
prompt: "Before creating the checkout, ask the buyer to provide the missing contact details.",
|
|
316
|
+
fields: fields.map((field) => ({
|
|
317
|
+
id: field,
|
|
318
|
+
label: field === "email" ? "Email" : "Phone number",
|
|
319
|
+
inputType: field === "email" ? "email" : "phone",
|
|
320
|
+
required: true,
|
|
321
|
+
placeholder: field === "email" ? "buyer@example.com" : "+86 138...",
|
|
322
|
+
description: field === "email" ? "Used for receipts or delivery follow-up." : "Used when the order requires buyer verification.",
|
|
323
|
+
})),
|
|
324
|
+
submitLabel: "Submit contact",
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function findMissingContactFields(contact, fields) {
|
|
328
|
+
return fields.filter((field) => {
|
|
329
|
+
const value = contact?.[field];
|
|
330
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
|
|
334
|
+
if (qrPayload.trim().length > 0) {
|
|
335
|
+
return qrPayload;
|
|
336
|
+
}
|
|
337
|
+
if (checkoutURL.trim().length === 0 || displayToken.trim().length === 0) {
|
|
338
|
+
return checkoutURL;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const parsed = new URL(checkoutURL);
|
|
342
|
+
if (!parsed.searchParams.has("display_token")) {
|
|
343
|
+
parsed.searchParams.set("display_token", displayToken);
|
|
344
|
+
}
|
|
345
|
+
return parsed.toString();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
const separator = checkoutURL.includes("?") ? "&" : "?";
|
|
349
|
+
return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// CLI cart commands. Server cart is canonical for agent purchase/free-trial
|
|
2
|
+
// flows; local cart helpers are kept for explicit local draft compatibility.
|
|
3
|
+
import { HttpError } from "../client/http.js";
|
|
4
|
+
import { resolveOutput } from "../render/sink.js";
|
|
5
|
+
import { attachAgentGuidance, buildCartGuidance, printAgentGuidance } from "./guidance.js";
|
|
6
|
+
export function runCartAdd(session, options) {
|
|
7
|
+
const out = resolveOutput(options.output);
|
|
8
|
+
const item = {
|
|
9
|
+
catalogItemID: options.catalogItemID,
|
|
10
|
+
catalogVariantID: options.catalogVariantID,
|
|
11
|
+
offerID: options.offerID,
|
|
12
|
+
quantity: options.quantity,
|
|
13
|
+
...(options.input ? { input: options.input } : {}),
|
|
14
|
+
};
|
|
15
|
+
session.add(item);
|
|
16
|
+
out(`added ${item.quantity} × variant=${item.catalogVariantID} offer=${item.offerID} to cart\n`);
|
|
17
|
+
}
|
|
18
|
+
export async function runCartAddServer(options) {
|
|
19
|
+
const out = resolveOutput(options.output);
|
|
20
|
+
const agentDeviceID = options.session.ensureAgentDeviceID(options.config.agentDeviceID);
|
|
21
|
+
const item = {
|
|
22
|
+
catalog_item_id: options.catalogItemID,
|
|
23
|
+
catalog_variant_id: options.catalogVariantID,
|
|
24
|
+
offer_id: options.offerID,
|
|
25
|
+
quantity: options.quantity,
|
|
26
|
+
...(options.input ? { input: options.input } : {}),
|
|
27
|
+
};
|
|
28
|
+
const clientContext = {
|
|
29
|
+
host: options.host,
|
|
30
|
+
...(options.target ? { target: options.target } : {}),
|
|
31
|
+
agent_device_id: agentDeviceID,
|
|
32
|
+
};
|
|
33
|
+
const cart = options.session.lastCartID
|
|
34
|
+
? await options.backend.addCartItem(options.session.lastCartID, {
|
|
35
|
+
...item,
|
|
36
|
+
agent_device_id: agentDeviceID,
|
|
37
|
+
client_context: clientContext,
|
|
38
|
+
})
|
|
39
|
+
: await options.backend.createCart({
|
|
40
|
+
currency: options.config.checkoutCurrency,
|
|
41
|
+
agent_device_id: agentDeviceID,
|
|
42
|
+
client_context: clientContext,
|
|
43
|
+
items: [item],
|
|
44
|
+
});
|
|
45
|
+
const line = cart.items[cart.items.length - 1];
|
|
46
|
+
options.session.rememberServerCart({
|
|
47
|
+
cartID: cart.cart_id,
|
|
48
|
+
...(line?.cart_item_id ? { cartItemID: line.cart_item_id } : {}),
|
|
49
|
+
...(line?.service_execution_id ? { serviceExecutionID: line.service_execution_id } : {}),
|
|
50
|
+
agentDeviceID,
|
|
51
|
+
});
|
|
52
|
+
const serviceModel = line?.service_execution_id
|
|
53
|
+
? await getServiceReadModel(options.backend, line.service_execution_id)
|
|
54
|
+
: undefined;
|
|
55
|
+
const guidance = buildCartGuidance(cart, serviceModel);
|
|
56
|
+
if (options.jsonOutput) {
|
|
57
|
+
out(JSON.stringify(attachAgentGuidance(cart, guidance), null, 2) + "\n");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
out(`cart: ${cart.cart_id}\n`);
|
|
61
|
+
if (line?.cart_item_id)
|
|
62
|
+
out(`line: ${line.cart_item_id}\n`);
|
|
63
|
+
if (line?.service_execution_id)
|
|
64
|
+
out(`service execution: ${line.service_execution_id}\n`);
|
|
65
|
+
printAgentGuidance(guidance, options.output);
|
|
66
|
+
}
|
|
67
|
+
return cart;
|
|
68
|
+
}
|
|
69
|
+
export function runCartRemove(session, options) {
|
|
70
|
+
const out = resolveOutput(options.output);
|
|
71
|
+
session.remove(options.catalogVariantID, options.offerID);
|
|
72
|
+
out(`removed variant=${options.catalogVariantID} offer=${options.offerID}\n`);
|
|
73
|
+
}
|
|
74
|
+
export async function runCartRemoveServer(backend, session, cartItemID, options = {}) {
|
|
75
|
+
const out = resolveOutput(options.output);
|
|
76
|
+
const cartID = session.lastCartID;
|
|
77
|
+
if (!cartID) {
|
|
78
|
+
throw new Error("no canonical server cart is remembered; use --local for local draft removal");
|
|
79
|
+
}
|
|
80
|
+
const lineID = cartItemID || session.lastCartItemID;
|
|
81
|
+
if (!lineID) {
|
|
82
|
+
throw new Error("missing cart item id; pass --line <cart_item_id>");
|
|
83
|
+
}
|
|
84
|
+
const cart = await backend.removeCartItem(cartID, lineID);
|
|
85
|
+
out(`removed line=${lineID} from cart=${cart.cart_id}\n`);
|
|
86
|
+
out(`cart ${cart.cart_id} (${cart.status}, ${cart.amount_minor} ${cart.currency})\n`);
|
|
87
|
+
return cart;
|
|
88
|
+
}
|
|
89
|
+
export function runCartShow(session, options = {}) {
|
|
90
|
+
const out = resolveOutput(options.output);
|
|
91
|
+
const snap = session.show();
|
|
92
|
+
out(`cart (currency=${snap.currency})\n`);
|
|
93
|
+
if (snap.items.length === 0) {
|
|
94
|
+
out(" (empty)\n");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const item of snap.items) {
|
|
98
|
+
out(` - ${item.quantity} × variant=${item.catalogVariantID} offer=${item.offerID}` +
|
|
99
|
+
(item.input ? ` input=${JSON.stringify(item.input)}` : "") +
|
|
100
|
+
"\n");
|
|
101
|
+
}
|
|
102
|
+
if (snap.lastCheckoutID) {
|
|
103
|
+
out(`last checkout: ${snap.lastCheckoutID}\n`);
|
|
104
|
+
if (snap.lastCheckoutURL) {
|
|
105
|
+
out(`checkout url: ${snap.lastCheckoutURL}\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function runCartShowServer(backend, session, options = {}) {
|
|
110
|
+
const out = resolveOutput(options.output);
|
|
111
|
+
if (!session.lastCartID) {
|
|
112
|
+
runCartShow(session, options);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const cart = await backend.getCart(session.lastCartID);
|
|
116
|
+
out(`cart ${cart.cart_id} (${cart.status}, ${cart.amount_minor} ${cart.currency})\n`);
|
|
117
|
+
if (cart.items.length === 0) {
|
|
118
|
+
out(" (empty)\n");
|
|
119
|
+
return cart;
|
|
120
|
+
}
|
|
121
|
+
for (const item of cart.items) {
|
|
122
|
+
out(` - ${item.quantity} × ${item.title} ${item.amount_minor} ${item.currency}\n`);
|
|
123
|
+
out(` line=${item.cart_item_id ?? ""} variant=${item.catalog_variant_id ?? ""} offer=${item.offer_id ?? ""}\n`);
|
|
124
|
+
if (item.service_execution_id) {
|
|
125
|
+
out(` service_execution=${item.service_execution_id} next_action=${item.next_action ?? ""} checkout_required=${String(item.checkout_required ?? false)}\n`);
|
|
126
|
+
}
|
|
127
|
+
if (item.service_quote_lock_id) {
|
|
128
|
+
out(` service_quote_lock=${item.service_quote_lock_id}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const serviceModel = await getServiceReadModelForCart(backend, cart);
|
|
132
|
+
printAgentGuidance(buildCartGuidance(cart, serviceModel), options.output);
|
|
133
|
+
return cart;
|
|
134
|
+
}
|
|
135
|
+
export function runCartClear(session, options = {}) {
|
|
136
|
+
const out = resolveOutput(options.output);
|
|
137
|
+
session.clear();
|
|
138
|
+
out("cart cleared\n");
|
|
139
|
+
}
|
|
140
|
+
export async function runCartAbandonServer(backend, session, options = {}) {
|
|
141
|
+
const out = resolveOutput(options.output);
|
|
142
|
+
if (!session.lastCartID) {
|
|
143
|
+
runCartClear(session, options);
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
let cart;
|
|
147
|
+
try {
|
|
148
|
+
cart = await backend.abandonCart(session.lastCartID);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error instanceof HttpError && (error.code === "cart_item_locked" || error.status === 409)) {
|
|
152
|
+
const cartID = session.lastCartID;
|
|
153
|
+
session.clear();
|
|
154
|
+
out(`cart local handle cleared; server cart ${cartID} is locked by quote/checkout\n`);
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
session.clear();
|
|
160
|
+
out(`cart abandoned: ${cart.cart_id}\n`);
|
|
161
|
+
return cart;
|
|
162
|
+
}
|
|
163
|
+
export async function runCartNext(backend, session, options = {}) {
|
|
164
|
+
const out = resolveOutput(options.output);
|
|
165
|
+
if (session.lastCartID) {
|
|
166
|
+
const cart = await backend.getCart(session.lastCartID);
|
|
167
|
+
const guidance = buildCartGuidance(cart, await getServiceReadModelForCart(backend, cart));
|
|
168
|
+
if (options.jsonOutput) {
|
|
169
|
+
out(JSON.stringify(guidance, null, 2) + "\n");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
printAgentGuidance(guidance, options.output);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const snap = session.show();
|
|
177
|
+
if (snap.lastServiceExecutionID) {
|
|
178
|
+
const model = await getServiceReadModel(backend, snap.lastServiceExecutionID);
|
|
179
|
+
const guidance = model
|
|
180
|
+
? buildCartGuidance({
|
|
181
|
+
cart_id: snap.lastCartID ?? "<unknown_cart>",
|
|
182
|
+
status: "unknown",
|
|
183
|
+
amount_minor: 0,
|
|
184
|
+
currency: snap.currency,
|
|
185
|
+
items: [{
|
|
186
|
+
title: "Service Execution",
|
|
187
|
+
quantity: 1,
|
|
188
|
+
amount_minor: 0,
|
|
189
|
+
currency: snap.currency,
|
|
190
|
+
service_execution_id: snap.lastServiceExecutionID,
|
|
191
|
+
}],
|
|
192
|
+
}, model)
|
|
193
|
+
: buildCartGuidance({
|
|
194
|
+
cart_id: snap.lastCartID ?? "<unknown_cart>",
|
|
195
|
+
status: "unknown",
|
|
196
|
+
amount_minor: 0,
|
|
197
|
+
currency: snap.currency,
|
|
198
|
+
items: [{
|
|
199
|
+
title: "Service Execution",
|
|
200
|
+
quantity: 1,
|
|
201
|
+
amount_minor: 0,
|
|
202
|
+
currency: snap.currency,
|
|
203
|
+
service_execution_id: snap.lastServiceExecutionID,
|
|
204
|
+
}],
|
|
205
|
+
});
|
|
206
|
+
if (options.jsonOutput) {
|
|
207
|
+
out(JSON.stringify(guidance, null, 2) + "\n");
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
printAgentGuidance(guidance, options.output);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (snap.lastCheckoutID && snap.lastDisplayToken) {
|
|
215
|
+
const guidance = {
|
|
216
|
+
kind: "checkout_handle",
|
|
217
|
+
summary: `checkout ${snap.lastCheckoutID}: continue human checkout`,
|
|
218
|
+
state: { checkout_id: snap.lastCheckoutID },
|
|
219
|
+
next_actions: [{
|
|
220
|
+
id: "open_checkout",
|
|
221
|
+
label: "Open saved checkout presentation",
|
|
222
|
+
command: `itpay checkout --id ${snap.lastCheckoutID} --token ${snap.lastDisplayToken}`,
|
|
223
|
+
requires_human: true,
|
|
224
|
+
}],
|
|
225
|
+
recovery: [],
|
|
226
|
+
};
|
|
227
|
+
if (options.jsonOutput) {
|
|
228
|
+
out(JSON.stringify(guidance, null, 2) + "\n");
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
printAgentGuidance(guidance, options.output);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const guidance = {
|
|
236
|
+
kind: "no_active_handle",
|
|
237
|
+
summary: "no active server cart, service execution, or checkout handle remembered",
|
|
238
|
+
state: {},
|
|
239
|
+
next_actions: [{
|
|
240
|
+
id: "browse_catalog",
|
|
241
|
+
label: "Browse services",
|
|
242
|
+
command: "itpay catalog list",
|
|
243
|
+
}],
|
|
244
|
+
recovery: [],
|
|
245
|
+
};
|
|
246
|
+
if (options.jsonOutput) {
|
|
247
|
+
out(JSON.stringify(guidance, null, 2) + "\n");
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
printAgentGuidance(guidance, options.output);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function getServiceReadModelForCart(backend, cart) {
|
|
254
|
+
const serviceExecutionID = [...cart.items].reverse().find((item) => item.service_execution_id)?.service_execution_id;
|
|
255
|
+
return serviceExecutionID ? getServiceReadModel(backend, serviceExecutionID) : undefined;
|
|
256
|
+
}
|
|
257
|
+
async function getServiceReadModel(backend, serviceExecutionID) {
|
|
258
|
+
try {
|
|
259
|
+
return await backend.getServiceExecution(serviceExecutionID);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { formatMoney } from "../render/output.js";
|
|
2
|
+
import { resolveOutput } from "../render/sink.js";
|
|
3
|
+
export async function runCatalogList(backend, options = {}) {
|
|
4
|
+
const manifest = await backend.getCatalogManifest();
|
|
5
|
+
const items = manifest.manifest.items;
|
|
6
|
+
const out = resolveOutput(options.output);
|
|
7
|
+
if (options.jsonOutput) {
|
|
8
|
+
out(JSON.stringify(manifest, null, 2) + "\n");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
out(`Catalog: ${manifest.version} (${manifest.status}, ${items.length} items)\n\n`);
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
out(` ${item.catalog_item_id}\n`);
|
|
14
|
+
out(` title: ${item.title}\n`);
|
|
15
|
+
out(` provider: ${item.provider} | type: ${item.service_type} | category: ${item.category}\n`);
|
|
16
|
+
if (item.service_id)
|
|
17
|
+
out(` service: ${item.service_id} (use: itpay services start ${item.service_id})\n`);
|
|
18
|
+
if (item.variants.length > 0) {
|
|
19
|
+
out(` variants:\n`);
|
|
20
|
+
for (const variant of item.variants) {
|
|
21
|
+
out(` - ${variant.catalog_variant_id} ${variant.title} ${variant.offer_id} ${formatMoney(variant.amount_minor, variant.currency)}\n`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
out("\n");
|
|
25
|
+
}
|
|
26
|
+
}
|