@curless/sinocare-demo 0.65.0 → 0.67.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 +228 -194
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -171,6 +171,33 @@ var storefrontTools = (opts) => [
|
|
|
171
171
|
required: ["sku"]
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
|
+
{
|
|
175
|
+
name: "start_sinocare_uaag_handoff",
|
|
176
|
+
description: "PAY WITH A UNIONPAY CARD the person bound to their Curless wallet. Sinocare prices the order; the card comes from their wallet, not from you. Returns a URL THEY open \u2014 UnionPay authenticates the cardholder on their own page, with 3DS and a passkey, so no card number ever reaches this server. \u26A0\uFE0F This is a HANDOFF, not a completed purchase, and the distinction is subtle enough that the first person through it got it wrong: UnionPay\u2019s page is titled \u300C\u786E\u8BA4\u652F\u4ED8\u300D and shows the amount, merchant and items, so the cardholder really is approving THIS purchase. What their approval produces is a one-time token + DTVC, which the spec calls the INPUT to an authorization \u2014 a message nobody has sent yet, and the MERCHANT\u2019s acquirer is who sends it. Sinocare has no UnionPay acceptance, so nothing is charged. Returns ordered:false and paid:false always. Then call get_sinocare_uaag_status.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
sku,
|
|
181
|
+
query,
|
|
182
|
+
quantity,
|
|
183
|
+
cardId: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description: "OPTIONAL, and normally omit it. The storefront reads the bound UnionPay cards from the person\u2019s own wallet and uses the one they have; pass this only to pick between several. A uaagcard_\u2026 you did not get from that list is a guess, and UnionPay answers a wrong one with a 404 that does not say what was wrong."
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "get_sinocare_uaag_status",
|
|
192
|
+
description: "Has UnionPay issued the payment credential yet? Pass the paymentIntentId from start_sinocare_uaag_handoff. The cardholder authenticates in their own browser and this server is never told when they finish, so this is the only way to know. \u26A0\uFE0F `issued: true` means the cardholder confirmed and a one-time token now exists \u2014 it does NOT mean the order is paid. The MERCHANT charges that credential through their own UnionPay acceptance, which Sinocare does not have, so this demo stops there on purpose. Say both halves; never report it as paid.",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
required: ["paymentIntentId"],
|
|
196
|
+
properties: {
|
|
197
|
+
paymentIntentId: { type: "string", description: "from start_sinocare_uaag_handoff" }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
174
201
|
{
|
|
175
202
|
name: "get_sinocare_order_status",
|
|
176
203
|
description: "Has this checkout been paid? Use after pay_sinocare_hosted \u2014 the approval happens in a browser and this server is never told when it does. `completed` means the money moved.",
|
|
@@ -233,6 +260,173 @@ var ordersView = (orders) => ({
|
|
|
233
260
|
)
|
|
234
261
|
});
|
|
235
262
|
|
|
263
|
+
// ../../packages/internal/sinocare-storefront/src/uaag.ts
|
|
264
|
+
var post = async (deps, path, body) => {
|
|
265
|
+
const res = await fetch(`${deps.apiBase}${path}`, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${deps.agentToken}` },
|
|
268
|
+
body: JSON.stringify(body)
|
|
269
|
+
});
|
|
270
|
+
const text2 = await res.text();
|
|
271
|
+
let json = {};
|
|
272
|
+
try {
|
|
273
|
+
json = JSON.parse(text2);
|
|
274
|
+
} catch {
|
|
275
|
+
json = { raw: text2.slice(0, 300) };
|
|
276
|
+
}
|
|
277
|
+
return { ok: res.ok, status: res.status, json };
|
|
278
|
+
};
|
|
279
|
+
var errorOf = (json, status) => {
|
|
280
|
+
const e = json.error;
|
|
281
|
+
return e?.message ?? `HTTP ${status}`;
|
|
282
|
+
};
|
|
283
|
+
var startUaagCheckout = async (deps, args) => {
|
|
284
|
+
if (!deps.agentToken) return { error: "this storefront has no agent token configured" };
|
|
285
|
+
if (!deps.wallet) {
|
|
286
|
+
return {
|
|
287
|
+
needsLogin: true,
|
|
288
|
+
message: "\u8981\u7528\u94F6\u8054\u5361\u4ED8\u6B3E\uFF0C\u5F97\u5148\u767B\u5F55\u4F60\u81EA\u5DF1\u7684 Curless \u94B1\u5305 \u2014\u2014 \u5361\u7ED1\u5728\u90A3\u91CC\uFF0C\u53EA\u6709\u5B83\u77E5\u9053\u4F60\u7ED1\u4E86\u54EA\u51E0\u5F20\u3002",
|
|
289
|
+
next: "show the person the sign-in link this storefront returns for the wallet, and stop."
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const { cards } = await deps.wallet.unionPayCards();
|
|
293
|
+
if (cards.length === 0) {
|
|
294
|
+
return {
|
|
295
|
+
ordered: false,
|
|
296
|
+
paid: false,
|
|
297
|
+
blocked: "no_unionpay_card",
|
|
298
|
+
message: "\u4F60\u7684 Curless \u94B1\u5305\u91CC\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u7684\u94F6\u8054\u5361\u3002\u7ED1\u5361\u5728\u94B1\u5305\u91CC\u505A\uFF0C\u4E0D\u5728\u8FD9\u4E2A\u5E97\u91CC \u2014\u2014 \u94F6\u8054\u8981\u5728\u4ED6\u4EEC\u81EA\u5DF1\u7684\u9875\u9762\u4E0A\u6536\u5361\u3002",
|
|
299
|
+
next: "tell them to bind a UnionPay card in their wallet first. Do NOT retry, and do NOT offer to take the card number here."
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const named = typeof args.cardId === "string" ? args.cardId : "";
|
|
303
|
+
const card = named ? cards.find((c) => c.id === named) : cards[0];
|
|
304
|
+
if (!card) {
|
|
305
|
+
return {
|
|
306
|
+
ordered: false,
|
|
307
|
+
paid: false,
|
|
308
|
+
blocked: "unknown_card",
|
|
309
|
+
message: `\u4F60\u7684\u94B1\u5305\u91CC\u6CA1\u6709\u8FD9\u5F20\u5361\uFF1A${named}`,
|
|
310
|
+
cards: cards.map((c) => ({ id: c.id, last4: c.panLastFour }))
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const quantity2 = Math.max(Number(args.quantity) || 1, 1);
|
|
314
|
+
const session = await deps.openCheckout(String(args.sku ?? args.query ?? ""), quantity2);
|
|
315
|
+
const me = await deps.wallet.me();
|
|
316
|
+
const [firstName, ...rest] = (me.name ?? "Curless Buyer").trim().split(/\s+/);
|
|
317
|
+
const unit = (session.amount / quantity2 / 100).toFixed(2);
|
|
318
|
+
const intent = await post(deps, "/v1/uaag/intents", {
|
|
319
|
+
paymentIntentId: session.sessionId,
|
|
320
|
+
cardId: card.id,
|
|
321
|
+
merchantName: "Sinocare",
|
|
322
|
+
merchantUrl: "https://www.sinocare.com",
|
|
323
|
+
items: [
|
|
324
|
+
{
|
|
325
|
+
itemName: session.name,
|
|
326
|
+
itemDescription: session.name,
|
|
327
|
+
itemUrl: `https://www.sinocare.com/products/${session.sku}`,
|
|
328
|
+
itemUnitPrice: { amount: unit, currencyCode: session.currency },
|
|
329
|
+
itemQuantity: String(quantity2)
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
delivery: {
|
|
333
|
+
firstName: firstName || "Curless",
|
|
334
|
+
lastName: rest.join(" ") || "Buyer",
|
|
335
|
+
emailAddress: me.email,
|
|
336
|
+
// ⚠️ Required whenever anything ships, which is every item unless the
|
|
337
|
+
// whole order is DIGITAL — the field table calls it conditional and the
|
|
338
|
+
// condition is almost always true. A demo address on purpose: this shop
|
|
339
|
+
// never collected one, and a plausible-looking invented address would be
|
|
340
|
+
// worse than an obvious placeholder.
|
|
341
|
+
deliveryAddress: {
|
|
342
|
+
name: me.name ?? "Curless Buyer",
|
|
343
|
+
line1: "1 Demo Street",
|
|
344
|
+
city: "HongKong",
|
|
345
|
+
state: "HK",
|
|
346
|
+
countryCode: "HK",
|
|
347
|
+
zip: "999077"
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
if (!intent.ok) {
|
|
352
|
+
return {
|
|
353
|
+
ordered: false,
|
|
354
|
+
paid: false,
|
|
355
|
+
blocked: "uaag_intent_refused",
|
|
356
|
+
message: `\u94F6\u8054\u6CA1\u6709\u63A5\u53D7\u8FD9\u7B14\u610F\u56FE\uFF1A${errorOf(intent.json, intent.status)}`,
|
|
357
|
+
sessionId: session.sessionId,
|
|
358
|
+
next: "repeat what UnionPay said and stop. Do NOT retry, and do NOT pay another way without asking."
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const checkout = await post(deps, "/v1/uaag/checkout", {
|
|
362
|
+
paymentIntentId: session.sessionId,
|
|
363
|
+
cardId: card.id
|
|
364
|
+
});
|
|
365
|
+
if (!checkout.ok) {
|
|
366
|
+
return {
|
|
367
|
+
ordered: false,
|
|
368
|
+
paid: false,
|
|
369
|
+
blocked: "uaag_checkout_refused",
|
|
370
|
+
message: `\u94F6\u8054\u6536\u94F6\u53F0\u6CA1\u80FD\u5F00\uFF1A${errorOf(checkout.json, checkout.status)}`,
|
|
371
|
+
sessionId: session.sessionId,
|
|
372
|
+
next: "repeat what UnionPay said and stop."
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const upi = checkout.json.upi ?? {};
|
|
376
|
+
return {
|
|
377
|
+
// Never true on this path. UnionPay issues a credential; the merchant
|
|
378
|
+
// charges it, and Sinocare cannot.
|
|
379
|
+
ordered: false,
|
|
380
|
+
paid: false,
|
|
381
|
+
awaitingPayment: true,
|
|
382
|
+
message: "\u94F6\u8054\u6536\u94F6\u53F0\u5DF2\u5F00\u597D\u3002\u9875\u9762\u4E0A\u5199\u7684\u662F\u300C\u786E\u8BA4\u652F\u4ED8\u300D\u5E76\u663E\u793A\u91D1\u989D \u2014\u2014 \u4F60\u786E\u8BA4\u7684\u662F\u8FD9\u7B14\u8D2D\u4E70\u672C\u8EAB\uFF0C\u786E\u8BA4\u540E\u94F6\u8054\u628A\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\u56DE\u8C03\u7ED9\u6211\u4EEC\u3002\u26A0\uFE0F \u5361\u4E0D\u4F1A\u5728\u90A3\u4E00\u523B\u88AB\u6263\u6B3E\uFF1A\u6263\u6B3E\u8981\u5546\u6237\u62FF\u8FD9\u5F20\u51ED\u8BC1\u8D70\u81EA\u5DF1\u7684\u94F6\u8054\u6536\u5355\uFF0C\u4E09\u8BFA\u6CA1\u6709\uFF0C\u6240\u4EE5\u6F14\u793A\u5230\u51ED\u8BC1\u4E3A\u6B62\u3002",
|
|
383
|
+
checkoutUrl: upi.checkoutUrl ?? null,
|
|
384
|
+
// ⚠️ The card's own field names. `payUrl` is what it renders a "go pay"
|
|
385
|
+
// button from, and `uaagPaymentIntentId` is what it polls — deliberately
|
|
386
|
+
// NOT `sessionId`/`merchantId`, which would send it to the hosted poller
|
|
387
|
+
// that asks whether the ACP session was PAID. A UnionPay checkout never
|
|
388
|
+
// makes it paid, so that poller would spin for ten minutes and then time
|
|
389
|
+
// out on a flow that had, in its own terms, succeeded.
|
|
390
|
+
payUrl: upi.checkoutUrl ?? null,
|
|
391
|
+
uaagPaymentIntentId: session.sessionId,
|
|
392
|
+
paymentIntentId: session.sessionId,
|
|
393
|
+
merchantId: session.merchantId,
|
|
394
|
+
card: { id: card.id, last4: card.panLastFour },
|
|
395
|
+
product: session.name,
|
|
396
|
+
total: session.amountDisplay,
|
|
397
|
+
settlement: {
|
|
398
|
+
settled: false,
|
|
399
|
+
by: "merchant",
|
|
400
|
+
reason: "UnionPay \u628A\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\u4EA4\u7ED9 Agent\uFF0C\u7531\u5546\u6237\u7684\u6536\u5355\u673A\u6784\u6263\u6B3E\u3002\u4E09\u8BFA\u6CA1\u6709\u94F6\u8054\u6536\u5355\uFF0C\u6240\u4EE5\u8FD9\u7B14\u4E0D\u4F1A\u88AB\u6263\u6B3E\uFF0C\u4E5F\u4E0D\u4F1A\u751F\u6210\u8BA2\u5355\u3002"
|
|
401
|
+
},
|
|
402
|
+
next: "show the person the checkoutUrl. When they say they are done, call the UnionPay status tool with this paymentIntentId \u2014 this server is never told when they finish. Do NOT claim the order was placed."
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
var uaagCheckoutStatus = async (deps, paymentIntentId) => {
|
|
406
|
+
if (!deps.agentToken) return { error: "this storefront has no agent token configured" };
|
|
407
|
+
if (!paymentIntentId) return { error: "paymentIntentId is required" };
|
|
408
|
+
const res = await fetch(
|
|
409
|
+
`${deps.apiBase}/v1/payment-intents/${encodeURIComponent(paymentIntentId)}`,
|
|
410
|
+
{ headers: { authorization: `Bearer ${deps.agentToken}` } }
|
|
411
|
+
);
|
|
412
|
+
const pi = await res.json().catch(() => ({}));
|
|
413
|
+
const uaag = pi.protocolContext?.uaag;
|
|
414
|
+
const issued = uaag?.credential !== void 0;
|
|
415
|
+
return {
|
|
416
|
+
issued,
|
|
417
|
+
// ⚠️ `issued` is not `paid`, and the two are one word apart in every
|
|
418
|
+
// language this will be read in.
|
|
419
|
+
ordered: false,
|
|
420
|
+
paid: false,
|
|
421
|
+
paymentIntentId,
|
|
422
|
+
credential: uaag?.credential ?? null,
|
|
423
|
+
issuedAt: uaag?.payloadAt ?? null,
|
|
424
|
+
paymentIntentStatus: pi.status ?? "unknown",
|
|
425
|
+
message: issued ? "\u6301\u5361\u4EBA\u5DF2\u5728\u94F6\u8054\u9875\u9762\u786E\u8BA4\u652F\u4ED8\uFF0C\u94F6\u8054\u5DF2\u7B7E\u53D1\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\uFF08\u4EE4\u724C + DTVC\uFF09\u3002\u26A0\uFE0F \u4F46\u94B1\u8FD8\u6CA1\u52A8\uFF1A\u51ED\u8BC1\u662F\u4E00\u6761\u5C1A\u672A\u53D1\u51FA\u7684\u6388\u6743\u62A5\u6587\u7684\u5165\u53C2\uFF0C\u8981\u7531\u5546\u6237\u7684\u94F6\u8054\u6536\u5355\u673A\u6784\u53BB\u53D1\u3002\u4E09\u8BFA\u6CA1\u6709\u94F6\u8054\u6536\u5355\uFF0C\u6240\u4EE5\u6F14\u793A\u5230\u6B64\u4E3A\u6B62\u3002" : "\u8FD8\u6CA1\u6709\u3002\u6301\u5361\u4EBA\u8FD8\u6CA1\u5728\u94F6\u8054\u9875\u9762\u4E0A\u70B9\u300C\u786E\u8BA4\u652F\u4ED8\u300D\uFF0C\u6216\u8005\u521A\u70B9\u5B8C\u3001\u56DE\u8C03\u8FD8\u6CA1\u5230\u3002",
|
|
426
|
+
next: issued ? "Say BOTH halves, in this order: the cardholder confirmed the payment and UnionPay issued the credential \u2014 AND nothing has been charged yet, because that credential is the input to an authorization the MERCHANT\u2019s acquirer still has to send. Do not say it failed, and do not say it is paid. Both are wrong." : "tell them it is not done yet and let them finish on UnionPay\u2019s page. Do not retry in a tight loop."
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
236
430
|
// src/index.ts
|
|
237
431
|
import {
|
|
238
432
|
completeCheckout,
|
|
@@ -250,11 +444,7 @@ import {
|
|
|
250
444
|
signSolanaX402Payment
|
|
251
445
|
} from "@curless/agentbank-protocols/solana";
|
|
252
446
|
import { payMppTempoChallenge } from "@curless/agentbank-protocols/tempo";
|
|
253
|
-
import
|
|
254
|
-
checkout as uaagCheckout,
|
|
255
|
-
checkoutState as uaagCheckoutState,
|
|
256
|
-
createIntent as uaagCreateIntent
|
|
257
|
-
} from "@curless/agentbank-protocols/uaag";
|
|
447
|
+
import "@curless/agentbank-protocols/uaag";
|
|
258
448
|
import {
|
|
259
449
|
cdpAccountSigner,
|
|
260
450
|
encodeXPaymentHeader,
|
|
@@ -364,7 +554,7 @@ var mintSharedPaymentToken = async (amountMinor, currency) => {
|
|
|
364
554
|
"refusing to mint with a LIVE Stripe key \u2014 a order would charge a real card for the full product price. Set SINOCARE_ALLOW_LIVE_WALLET=1 if that is genuinely intended."
|
|
365
555
|
);
|
|
366
556
|
}
|
|
367
|
-
const
|
|
557
|
+
const post2 = async (path, body, preview = false) => {
|
|
368
558
|
const res = await fetch(`https://api.stripe.com/v1/${path}`, {
|
|
369
559
|
method: "POST",
|
|
370
560
|
headers: {
|
|
@@ -378,8 +568,8 @@ var mintSharedPaymentToken = async (amountMinor, currency) => {
|
|
|
378
568
|
if (!res.ok) throw new Error(`stripe ${path} -> ${res.status}: ${json.error?.message ?? ""}`);
|
|
379
569
|
return json;
|
|
380
570
|
};
|
|
381
|
-
const pm = AGENT_PM ?? String((await
|
|
382
|
-
const spt = await
|
|
571
|
+
const pm = AGENT_PM ?? String((await post2("payment_methods", { type: "card", "card[token]": "tok_visa" })).id);
|
|
572
|
+
const spt = await post2(
|
|
383
573
|
"shared_payment/issued_tokens",
|
|
384
574
|
{
|
|
385
575
|
payment_method: pm,
|
|
@@ -649,8 +839,8 @@ var SINOCARE_GROUPS = [
|
|
|
649
839
|
rows: [
|
|
650
840
|
{
|
|
651
841
|
title: "\u94F6\u8054\u5361(UAAG)",
|
|
652
|
-
sub: "\u7528\u4F60\u94B1\u5305\u91CC\u7ED1\u7684\u94F6\u8054\u5361 \xB7 \u94F6\u8054\
|
|
653
|
-
actions: [{ label: "\
|
|
842
|
+
sub: "\u7528\u4F60\u94B1\u5305\u91CC\u7ED1\u7684\u94F6\u8054\u5361 \xB7 \u5728\u94F6\u8054\u81EA\u5DF1\u7684\u9875\u9762\u4E0A\u786E\u8BA4",
|
|
843
|
+
actions: [{ label: "\u7528\u94F6\u8054\u4ED8\u6B3E", tool: "start_sinocare_uaag_handoff" }]
|
|
654
844
|
}
|
|
655
845
|
]
|
|
656
846
|
},
|
|
@@ -756,7 +946,7 @@ var pickView = (pick, r) => checkoutView({
|
|
|
756
946
|
}
|
|
757
947
|
});
|
|
758
948
|
var CHECKOUT_URI_PREFIX = "ui://sinocare/";
|
|
759
|
-
var CHECKOUT_URI = `${CHECKOUT_URI_PREFIX}card-
|
|
949
|
+
var CHECKOUT_URI = `${CHECKOUT_URI_PREFIX}card-v34.html`;
|
|
760
950
|
var CHECKOUT_UI_META = uiMeta(CHECKOUT_URI);
|
|
761
951
|
var WIDGET_META = widgetMeta();
|
|
762
952
|
var SHARED_TOOLS = storefrontTools({
|
|
@@ -775,36 +965,6 @@ var RAIL_TOOLS = [
|
|
|
775
965
|
}
|
|
776
966
|
}
|
|
777
967
|
},
|
|
778
|
-
{
|
|
779
|
-
name: "start_sinocare_uaag_handoff",
|
|
780
|
-
description: "Buy a Sinocare product over UnionPay Agent Pay (UAAG). Sinocare prices it and opens a checkout session; UnionPay then authenticates the CARDHOLDER on its own page (3DS / Passkey) and issues a one-time payment token. \u26A0\uFE0F This is a HANDOFF, not a completed purchase \u2014 and the distinction is subtle enough that the first person through it got it wrong. UnionPay\u2019s page is titled \u300C\u786E\u8BA4\u652F\u4ED8\u300D and shows the amount, merchant and items, so the cardholder really is approving THIS purchase. What their approval produces is a one-time token + DTVC, which the spec describes as the INPUT to an authorization (\u201Cused in authorization\u201D, \u201Csubmitted \u2026 for payment authorization\u201D, \u201CF23 of authorization\u201D) \u2014 a message nobody has sent yet. So after they confirm: they authorised it, AND no money moved. Saying only one of those is wrong either way. UnionPay\u2019s model has the MERCHANT charge that credential \u2014 it is shaped like an ordinary UnionPay card (token as the card number, DTVC as the CVV2), so the merchant needs UnionPay acceptance. Sinocare\u2019s demo checkout has none, so nothing is charged and no order is placed. Returns ordered:false always.",
|
|
781
|
-
inputSchema: {
|
|
782
|
-
type: "object",
|
|
783
|
-
required: ["sku"],
|
|
784
|
-
properties: {
|
|
785
|
-
sku: { type: "string" },
|
|
786
|
-
quantity: { type: "integer", description: "how many, default 1" },
|
|
787
|
-
cardId: {
|
|
788
|
-
type: "string",
|
|
789
|
-
description: "OPTIONAL, and normally omit it. The storefront reads the buyer's bound UnionPay cards from their own wallet session and uses the one they have; pass this only to pick between several. A uaagcard_\u2026 you did not get from that list is a guess, and UnionPay answers a wrong one with a 404 that does not say what was wrong."
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
name: "get_sinocare_uaag_status",
|
|
796
|
-
description: "Has UnionPay issued the payment credential yet? Pass the paymentIntentId from start_sinocare_uaag_handoff. The cardholder authenticates in their own browser and this server is never told when they finish, so this is the only way to know. \u26A0\uFE0F `issued: true` means a one-time token + DTVC now exist \u2014 it does NOT mean the order is paid. The MERCHANT charges that credential through their own UnionPay acceptance, which Sinocare does not have, so this demo stops there on purpose. Never report it as paid.",
|
|
797
|
-
inputSchema: {
|
|
798
|
-
type: "object",
|
|
799
|
-
required: ["paymentIntentId"],
|
|
800
|
-
properties: {
|
|
801
|
-
paymentIntentId: {
|
|
802
|
-
type: "string",
|
|
803
|
-
description: "from start_sinocare_uaag_handoff"
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
},
|
|
808
968
|
{
|
|
809
969
|
name: "buy_sinocare_product_acp",
|
|
810
970
|
description: "Buy a Sinocare product over ACP (Agentic Commerce Protocol) \u2014 Sinocare prices the product and opens a merchant-quoted checkout session; this agent completes it with a delegated card credential, settling on the card rail (test mode). One of THREE ways to pay: ACP (card) / UCP (card) / x402 (USDC). The card rail is an implementation detail, not a separate payment choice. Returns the ordered order.",
|
|
@@ -1232,12 +1392,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1232
1392
|
});
|
|
1233
1393
|
}
|
|
1234
1394
|
if (req.params.name === "start_sinocare_uaag_handoff") {
|
|
1235
|
-
if (!TOKEN) {
|
|
1236
|
-
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|
|
1237
|
-
}
|
|
1238
1395
|
if (!wallet.current()) {
|
|
1239
1396
|
const login = await pendingLogin();
|
|
1240
1397
|
return text({
|
|
1398
|
+
ordered: false,
|
|
1399
|
+
paid: false,
|
|
1241
1400
|
needsLogin: true,
|
|
1242
1401
|
loginUrl: login.verificationUriComplete,
|
|
1243
1402
|
userCode: login.userCode,
|
|
@@ -1245,158 +1404,33 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1245
1404
|
next: "show the person this link and stop. Do not pay another way."
|
|
1246
1405
|
});
|
|
1247
1406
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
blocked: "unknown_card",
|
|
1263
|
-
message: `\u4F60\u7684\u94B1\u5305\u91CC\u6CA1\u6709\u8FD9\u5F20\u5361:${named}`,
|
|
1264
|
-
cards: cards.map((c) => ({ id: c.id, last4: c.panLastFour }))
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
const sku2 = String(args.sku ?? "");
|
|
1268
|
-
const quantity2 = Math.max(Number(args.quantity) || 1, 1);
|
|
1269
|
-
const session = await backend("/checkout-acp", {
|
|
1270
|
-
method: "POST",
|
|
1271
|
-
body: JSON.stringify({ sku: sku2, quantity: quantity2, ...BUYER ? { buyer: BUYER } : {} })
|
|
1272
|
-
});
|
|
1273
|
-
const me = await wallet.me();
|
|
1274
|
-
const [firstName, ...rest] = (me.name ?? "Curless Buyer").trim().split(/\s+/);
|
|
1275
|
-
const uaagOpts = { baseUrl: API_BASE, token: TOKEN };
|
|
1276
|
-
const unit = (session.amount / quantity2 / 100).toFixed(2);
|
|
1277
|
-
try {
|
|
1278
|
-
await uaagCreateIntent(uaagOpts, {
|
|
1279
|
-
paymentIntentId: session.sessionId,
|
|
1280
|
-
cardId: card.id,
|
|
1281
|
-
merchantName: "Sinocare",
|
|
1282
|
-
merchantUrl: "https://www.sinocare.com",
|
|
1283
|
-
items: [
|
|
1284
|
-
{
|
|
1285
|
-
itemName: session.name,
|
|
1286
|
-
itemDescription: session.name,
|
|
1287
|
-
itemUrl: `https://www.sinocare.com/products/${session.sku}`,
|
|
1288
|
-
itemUnitPrice: { amount: unit, currencyCode: session.currency },
|
|
1289
|
-
itemQuantity: String(quantity2)
|
|
1290
|
-
}
|
|
1291
|
-
],
|
|
1292
|
-
delivery: {
|
|
1293
|
-
firstName: firstName || "Curless",
|
|
1294
|
-
lastName: rest.join(" ") || "Buyer",
|
|
1295
|
-
emailAddress: me.email,
|
|
1296
|
-
// ⚠️ Required whenever anything ships, which is every item unless
|
|
1297
|
-
// the whole order is DIGITAL — the field table calls it conditional
|
|
1298
|
-
// and the condition is almost always true. Demo address: this shop
|
|
1299
|
-
// never collected one, and inventing a plausible-looking real
|
|
1300
|
-
// address would be worse than an obvious placeholder.
|
|
1301
|
-
deliveryAddress: {
|
|
1302
|
-
name: me.name ?? "Curless Buyer",
|
|
1303
|
-
line1: "1 Demo Street",
|
|
1304
|
-
city: "HongKong",
|
|
1305
|
-
state: "HK",
|
|
1306
|
-
countryCode: "HK",
|
|
1307
|
-
zip: "999077"
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
|
-
} catch (err) {
|
|
1312
|
-
const e = err;
|
|
1313
|
-
return text({
|
|
1314
|
-
ordered: false,
|
|
1315
|
-
blocked: "uaag_intent_refused",
|
|
1316
|
-
message: `\u94F6\u8054\u6CA1\u6709\u63A5\u53D7\u8FD9\u7B14\u610F\u56FE:${e.message}`,
|
|
1317
|
-
declined: e.declined ?? false,
|
|
1318
|
-
sessionId: session.sessionId,
|
|
1319
|
-
next: "repeat what UnionPay said and stop. Do NOT retry, and do NOT pay another way without asking."
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
let checkout;
|
|
1323
|
-
try {
|
|
1324
|
-
checkout = await uaagCheckout(uaagOpts, {
|
|
1325
|
-
paymentIntentId: session.sessionId,
|
|
1326
|
-
cardId: card.id
|
|
1327
|
-
});
|
|
1328
|
-
} catch (err) {
|
|
1329
|
-
const e = err;
|
|
1330
|
-
return text({
|
|
1331
|
-
ordered: false,
|
|
1332
|
-
blocked: "uaag_checkout_refused",
|
|
1333
|
-
message: `\u94F6\u8054\u6536\u94F6\u53F0\u6CA1\u80FD\u5F00:${e.message}`,
|
|
1334
|
-
declined: e.declined ?? false,
|
|
1335
|
-
sessionId: session.sessionId,
|
|
1336
|
-
next: "repeat what UnionPay said and stop."
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
return text({
|
|
1340
|
-
// Never true on this path. UnionPay issues a credential; the merchant
|
|
1341
|
-
// charges it, and Sinocare cannot.
|
|
1342
|
-
ordered: false,
|
|
1343
|
-
awaitingPayment: true,
|
|
1344
|
-
// ⚠️ Tell them what the page says BEFORE they see it. It is titled
|
|
1345
|
-
// 确认支付 and shows the amount — so "authenticate" undersells it and
|
|
1346
|
-
// the person is left thinking they paid. They authorise; nobody charges.
|
|
1347
|
-
message: "\u94F6\u8054\u6536\u94F6\u53F0\u5DF2\u5F00\u597D\u3002\u9875\u9762\u4E0A\u5199\u7684\u662F\u300C\u786E\u8BA4\u652F\u4ED8\u300D\u5E76\u663E\u793A\u91D1\u989D \u2014\u2014 \u4F60\u786E\u8BA4\u7684\u662F\u8FD9\u7B14\u8D2D\u4E70\u672C\u8EAB\uFF0C\u786E\u8BA4\u540E\u94F6\u8054\u628A\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\u56DE\u8C03\u7ED9\u6211\u4EEC\u3002\u26A0\uFE0F \u5361\u4E0D\u4F1A\u5728\u90A3\u4E00\u523B\u88AB\u6263\u6B3E\uFF1A\u6263\u6B3E\u8981\u5546\u6237\u62FF\u8FD9\u5F20\u51ED\u8BC1\u8D70\u81EA\u5DF1\u7684\u94F6\u8054\u6536\u5355\uFF0C\u4E09\u8BFA\u6CA1\u6709\uFF0C\u6240\u4EE5\u6F14\u793A\u5230\u51ED\u8BC1\u4E3A\u6B62\u3002",
|
|
1348
|
-
checkoutUrl: checkout.upi.checkoutUrl ?? null,
|
|
1349
|
-
srcCorrelationId: checkout.srcCorrelationId,
|
|
1350
|
-
paymentIntentId: session.sessionId,
|
|
1351
|
-
sessionId: session.sessionId,
|
|
1352
|
-
merchantId: session.merchantId,
|
|
1353
|
-
card: { id: card.id, last4: card.panLastFour },
|
|
1354
|
-
product: session.name,
|
|
1355
|
-
total: session.amountDisplay,
|
|
1356
|
-
// ⚠️ The honest ending, as data rather than prose — so the card and the
|
|
1357
|
-
// model both get it instead of one of them inferring a sale.
|
|
1358
|
-
settlement: {
|
|
1359
|
-
settled: false,
|
|
1360
|
-
by: "merchant",
|
|
1361
|
-
reason: "UnionPay \u628A\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\u4EA4\u7ED9 Agent,\u7531\u5546\u6237\u7684\u6536\u5355\u673A\u6784\u6263\u6B3E\u3002\u4E09\u8BFA\u6CA1\u6709\u94F6\u8054\u6536\u5355,\u6240\u4EE5\u8FD9\u7B14\u4E0D\u4F1A\u88AB\u6263\u6B3E,\u4E5F\u4E0D\u4F1A\u751F\u6210\u8BA2\u5355\u3002"
|
|
1362
|
-
},
|
|
1363
|
-
next: "show the person the checkoutUrl. When they say they are done, call get_sinocare_uaag_status with this paymentIntentId \u2014 this server is never told when they finish. Do NOT claim the order was placed, and do NOT fall back to another rail without asking."
|
|
1364
|
-
});
|
|
1407
|
+
return text(
|
|
1408
|
+
await startUaagCheckout(
|
|
1409
|
+
{
|
|
1410
|
+
apiBase: API_BASE,
|
|
1411
|
+
agentToken: TOKEN,
|
|
1412
|
+
openCheckout: (sku2, quantity2) => backend("/checkout-acp", {
|
|
1413
|
+
method: "POST",
|
|
1414
|
+
body: JSON.stringify({ sku: sku2, quantity: quantity2, ...BUYER ? { buyer: BUYER } : {} })
|
|
1415
|
+
}),
|
|
1416
|
+
wallet
|
|
1417
|
+
},
|
|
1418
|
+
args
|
|
1419
|
+
)
|
|
1420
|
+
);
|
|
1365
1421
|
}
|
|
1366
1422
|
if (req.params.name === "get_sinocare_uaag_status") {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
issued: state.issued,
|
|
1379
|
-
ordered: false,
|
|
1380
|
-
paid: false,
|
|
1381
|
-
paymentIntentId,
|
|
1382
|
-
credential: state.credential ?? null,
|
|
1383
|
-
issuedAt: state.issuedAt ?? null,
|
|
1384
|
-
paymentIntentStatus: state.paymentIntentStatus,
|
|
1385
|
-
// ⚠️ THE WORDING MATTERS, AND THE FIRST PERSON THROUGH THIS GOT IT
|
|
1386
|
-
// WRONG — reasonably. UnionPay's page says 确认支付 and shows the amount,
|
|
1387
|
-
// the merchant and the items, so the cardholder DID confirm a payment.
|
|
1388
|
-
// Answering that with a bare `paid: false` reads as "it failed".
|
|
1389
|
-
//
|
|
1390
|
-
// What is true is narrower and stranger: they authorised it, and the
|
|
1391
|
-
// ISO 8583 authorization that would actually move money has not been
|
|
1392
|
-
// sent. The spec says so in the fields themselves — paymentToken "can
|
|
1393
|
-
// be used in authorization", the cryptogram is "submitted along with
|
|
1394
|
-
// the payment token for payment authorization", panSequenceNumber goes
|
|
1395
|
-
// "in F23 of authorization". Those are inputs to a message nobody has
|
|
1396
|
-
// sent yet, and the party who sends it is the merchant's acquirer.
|
|
1397
|
-
message: state.issued ? "\u6301\u5361\u4EBA\u5DF2\u5728\u94F6\u8054\u9875\u9762\u786E\u8BA4\u652F\u4ED8\uFF0C\u94F6\u8054\u5DF2\u7B7E\u53D1\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\uFF08\u4EE4\u724C + DTVC\uFF09\u3002\u26A0\uFE0F \u4F46\u94B1\u8FD8\u6CA1\u52A8\uFF1A\u51ED\u8BC1\u662F\u4E00\u6761\u5C1A\u672A\u53D1\u51FA\u7684\u6388\u6743\u62A5\u6587\u7684\u5165\u53C2\uFF0C\u8981\u7531\u5546\u6237\u7684\u94F6\u8054\u6536\u5355\u673A\u6784\u53BB\u53D1\u3002\u4E09\u8BFA\u6CA1\u6709\u94F6\u8054\u6536\u5355\uFF0C\u6240\u4EE5\u6F14\u793A\u5230\u6B64\u4E3A\u6B62\u3002" : "\u8FD8\u6CA1\u6709\u3002\u6301\u5361\u4EBA\u8FD8\u6CA1\u5728\u94F6\u8054\u9875\u9762\u4E0A\u70B9\u300C\u786E\u8BA4\u652F\u4ED8\u300D\uFF0C\u6216\u8005\u521A\u70B9\u5B8C\u3001\u56DE\u8C03\u8FD8\u6CA1\u5230\u3002",
|
|
1398
|
-
next: state.issued ? "Say BOTH halves, in this order: the cardholder confirmed the payment and UnionPay issued the credential \u2014 AND nothing has been charged yet, because the credential is the input to an authorization the MERCHANT\u2019s acquirer still has to send, and Sinocare has no UnionPay acceptance. Do not say it failed, and do not say it is paid. Both are wrong." : "tell them it is not done yet and let them finish on UnionPay\u2019s page. Do not retry in a tight loop."
|
|
1399
|
-
});
|
|
1423
|
+
return text(
|
|
1424
|
+
await uaagCheckoutStatus(
|
|
1425
|
+
{
|
|
1426
|
+
apiBase: API_BASE,
|
|
1427
|
+
agentToken: TOKEN,
|
|
1428
|
+
openCheckout: null,
|
|
1429
|
+
wallet: null
|
|
1430
|
+
},
|
|
1431
|
+
String(args.paymentIntentId ?? "")
|
|
1432
|
+
)
|
|
1433
|
+
);
|
|
1400
1434
|
}
|
|
1401
1435
|
if (req.params.name === "start_sinocare_wallet_handoff") {
|
|
1402
1436
|
if (!TOKEN) {
|