@curless/sinocare-demo 0.65.0 → 0.66.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 +217 -191
- 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,165 @@ 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
|
+
paymentIntentId: session.sessionId,
|
|
385
|
+
merchantId: session.merchantId,
|
|
386
|
+
card: { id: card.id, last4: card.panLastFour },
|
|
387
|
+
product: session.name,
|
|
388
|
+
total: session.amountDisplay,
|
|
389
|
+
settlement: {
|
|
390
|
+
settled: false,
|
|
391
|
+
by: "merchant",
|
|
392
|
+
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"
|
|
393
|
+
},
|
|
394
|
+
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."
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
var uaagCheckoutStatus = async (deps, paymentIntentId) => {
|
|
398
|
+
if (!deps.agentToken) return { error: "this storefront has no agent token configured" };
|
|
399
|
+
if (!paymentIntentId) return { error: "paymentIntentId is required" };
|
|
400
|
+
const res = await fetch(
|
|
401
|
+
`${deps.apiBase}/v1/payment-intents/${encodeURIComponent(paymentIntentId)}`,
|
|
402
|
+
{ headers: { authorization: `Bearer ${deps.agentToken}` } }
|
|
403
|
+
);
|
|
404
|
+
const pi = await res.json().catch(() => ({}));
|
|
405
|
+
const uaag = pi.protocolContext?.uaag;
|
|
406
|
+
const issued = uaag?.credential !== void 0;
|
|
407
|
+
return {
|
|
408
|
+
issued,
|
|
409
|
+
// ⚠️ `issued` is not `paid`, and the two are one word apart in every
|
|
410
|
+
// language this will be read in.
|
|
411
|
+
ordered: false,
|
|
412
|
+
paid: false,
|
|
413
|
+
paymentIntentId,
|
|
414
|
+
credential: uaag?.credential ?? null,
|
|
415
|
+
issuedAt: uaag?.payloadAt ?? null,
|
|
416
|
+
paymentIntentStatus: pi.status ?? "unknown",
|
|
417
|
+
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",
|
|
418
|
+
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."
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
|
|
236
422
|
// src/index.ts
|
|
237
423
|
import {
|
|
238
424
|
completeCheckout,
|
|
@@ -250,11 +436,7 @@ import {
|
|
|
250
436
|
signSolanaX402Payment
|
|
251
437
|
} from "@curless/agentbank-protocols/solana";
|
|
252
438
|
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";
|
|
439
|
+
import "@curless/agentbank-protocols/uaag";
|
|
258
440
|
import {
|
|
259
441
|
cdpAccountSigner,
|
|
260
442
|
encodeXPaymentHeader,
|
|
@@ -364,7 +546,7 @@ var mintSharedPaymentToken = async (amountMinor, currency) => {
|
|
|
364
546
|
"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
547
|
);
|
|
366
548
|
}
|
|
367
|
-
const
|
|
549
|
+
const post2 = async (path, body, preview = false) => {
|
|
368
550
|
const res = await fetch(`https://api.stripe.com/v1/${path}`, {
|
|
369
551
|
method: "POST",
|
|
370
552
|
headers: {
|
|
@@ -378,8 +560,8 @@ var mintSharedPaymentToken = async (amountMinor, currency) => {
|
|
|
378
560
|
if (!res.ok) throw new Error(`stripe ${path} -> ${res.status}: ${json.error?.message ?? ""}`);
|
|
379
561
|
return json;
|
|
380
562
|
};
|
|
381
|
-
const pm = AGENT_PM ?? String((await
|
|
382
|
-
const spt = await
|
|
563
|
+
const pm = AGENT_PM ?? String((await post2("payment_methods", { type: "card", "card[token]": "tok_visa" })).id);
|
|
564
|
+
const spt = await post2(
|
|
383
565
|
"shared_payment/issued_tokens",
|
|
384
566
|
{
|
|
385
567
|
payment_method: pm,
|
|
@@ -775,36 +957,6 @@ var RAIL_TOOLS = [
|
|
|
775
957
|
}
|
|
776
958
|
}
|
|
777
959
|
},
|
|
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
960
|
{
|
|
809
961
|
name: "buy_sinocare_product_acp",
|
|
810
962
|
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 +1384,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1232
1384
|
});
|
|
1233
1385
|
}
|
|
1234
1386
|
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
1387
|
if (!wallet.current()) {
|
|
1239
1388
|
const login = await pendingLogin();
|
|
1240
1389
|
return text({
|
|
1390
|
+
ordered: false,
|
|
1391
|
+
paid: false,
|
|
1241
1392
|
needsLogin: true,
|
|
1242
1393
|
loginUrl: login.verificationUriComplete,
|
|
1243
1394
|
userCode: login.userCode,
|
|
@@ -1245,158 +1396,33 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1245
1396
|
next: "show the person this link and stop. Do not pay another way."
|
|
1246
1397
|
});
|
|
1247
1398
|
}
|
|
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
|
-
});
|
|
1399
|
+
return text(
|
|
1400
|
+
await startUaagCheckout(
|
|
1401
|
+
{
|
|
1402
|
+
apiBase: API_BASE,
|
|
1403
|
+
agentToken: TOKEN,
|
|
1404
|
+
openCheckout: (sku2, quantity2) => backend("/checkout-acp", {
|
|
1405
|
+
method: "POST",
|
|
1406
|
+
body: JSON.stringify({ sku: sku2, quantity: quantity2, ...BUYER ? { buyer: BUYER } : {} })
|
|
1407
|
+
}),
|
|
1408
|
+
wallet
|
|
1409
|
+
},
|
|
1410
|
+
args
|
|
1411
|
+
)
|
|
1412
|
+
);
|
|
1365
1413
|
}
|
|
1366
1414
|
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
|
-
});
|
|
1415
|
+
return text(
|
|
1416
|
+
await uaagCheckoutStatus(
|
|
1417
|
+
{
|
|
1418
|
+
apiBase: API_BASE,
|
|
1419
|
+
agentToken: TOKEN,
|
|
1420
|
+
openCheckout: null,
|
|
1421
|
+
wallet: null
|
|
1422
|
+
},
|
|
1423
|
+
String(args.paymentIntentId ?? "")
|
|
1424
|
+
)
|
|
1425
|
+
);
|
|
1400
1426
|
}
|
|
1401
1427
|
if (req.params.name === "start_sinocare_wallet_handoff") {
|
|
1402
1428
|
if (!TOKEN) {
|