@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 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 post = async (path, body, preview = false) => {
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 post("payment_methods", { type: "card", "card[token]": "tok_visa" })).id);
382
- const spt = await post(
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\u9875\u9762\u8BA4\u8BC1 \xB7 \u6F14\u793A\u6B62\u4E8E\u51ED\u8BC1\u7B7E\u53D1,\u4E0D\u6263\u6B3E",
653
- actions: [{ label: "\u5728\u804A\u5929\u91CC\u4ED8\u6B3E", tool: "start_sinocare_uaag_handoff", chat: true }]
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-v33.html`;
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
- const { cards } = await wallet.unionPayCards();
1249
- if (cards.length === 0) {
1250
- return text({
1251
- ordered: false,
1252
- blocked: "no_unionpay_card",
1253
- message: "\u4F60\u7684 Curless \u94B1\u5305\u91CC\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u7684\u94F6\u8054\u5361\u3002\u7ED1\u5361\u5728\u94B1\u5305\u8FDE\u63A5\u5668\u91CC\u505A(wallet_bind_unionpay_card),\u4E0D\u5728\u8FD9\u4E2A\u5E97\u91CC \u2014\u2014 \u94F6\u8054\u8981\u5728\u4ED6\u4EEC\u81EA\u5DF1\u7684\u9875\u9762\u4E0A\u6536\u5361\u3002",
1254
- next: "tell them to bind a UnionPay card in their wallet connector first. Do NOT retry this tool, and do NOT offer to take the card number here."
1255
- });
1256
- }
1257
- const named = typeof args.cardId === "string" ? args.cardId : "";
1258
- const card = named ? cards.find((c) => c.id === named) : cards[0];
1259
- if (!card) {
1260
- return text({
1261
- ordered: false,
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
- if (!TOKEN) {
1368
- return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
1369
- }
1370
- const paymentIntentId = String(args.paymentIntentId ?? "");
1371
- if (!paymentIntentId) {
1372
- return text({ error: "paymentIntentId is required (from start_sinocare_uaag_handoff)" });
1373
- }
1374
- const state = await uaagCheckoutState({ baseUrl: API_BASE, token: TOKEN }, paymentIntentId);
1375
- return text({
1376
- // ⚠️ `issued` is not `paid`, and the two are one word apart in every
1377
- // language this will be read in. A credential exists; nobody charged it.
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) {