@commercelayer/agent-cli 0.8.1 → 1.0.0-dev.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.
Files changed (67) hide show
  1. package/README.md +95 -215
  2. package/dist/argv.js +214 -0
  3. package/dist/bin.js +15 -178
  4. package/dist/builtins/auth.js +169 -0
  5. package/dist/builtins/extensions.js +123 -0
  6. package/dist/builtins/index.js +8 -0
  7. package/dist/builtins/loops.js +215 -0
  8. package/dist/builtins/sandbox.js +186 -0
  9. package/dist/config.js +211 -0
  10. package/dist/confirm.js +56 -0
  11. package/dist/engine.js +173 -0
  12. package/dist/errors.js +146 -0
  13. package/dist/help.js +78 -0
  14. package/dist/http.js +117 -0
  15. package/dist/login.js +133 -0
  16. package/dist/resources.js +41 -0
  17. package/dist/schema.js +152 -0
  18. package/dist/schemas.js +72 -0
  19. package/dist/secrets.js +189 -0
  20. package/dist/version.js +4 -8
  21. package/package.json +4 -9
  22. package/dist/api/client.js +0 -190
  23. package/dist/api/server.js +0 -26
  24. package/dist/api/stub/db.js +0 -37
  25. package/dist/api/stub/http.js +0 -18
  26. package/dist/api/stub/index.js +0 -52
  27. package/dist/api/stub/locale.js +0 -52
  28. package/dist/api/stub/management.js +0 -409
  29. package/dist/api/stub/messages.js +0 -126
  30. package/dist/api/stub/orders.js +0 -625
  31. package/dist/api/stub/search.js +0 -78
  32. package/dist/api/stub/shopping.js +0 -673
  33. package/dist/api/stub/theme.js +0 -197
  34. package/dist/api/stub/ucp.js +0 -377
  35. package/dist/api/transport.js +0 -57
  36. package/dist/api/types.js +0 -4
  37. package/dist/assets.js +0 -100
  38. package/dist/catalog.js +0 -372
  39. package/dist/check.js +0 -187
  40. package/dist/commands.js +0 -1241
  41. package/dist/cursor.js +0 -31
  42. package/dist/files.js +0 -98
  43. package/dist/inbox/cursor.js +0 -11
  44. package/dist/inbox/fake.js +0 -95
  45. package/dist/inbox/imap.js +0 -139
  46. package/dist/inbox/mailbox.js +0 -25
  47. package/dist/inbox/message.js +0 -88
  48. package/dist/inspect.js +0 -237
  49. package/dist/languages.js +0 -49
  50. package/dist/links.js +0 -118
  51. package/dist/locales.js +0 -43
  52. package/dist/ops.js +0 -190
  53. package/dist/orderRequest.js +0 -141
  54. package/dist/output.js +0 -39
  55. package/dist/products.js +0 -151
  56. package/dist/promotions.js +0 -291
  57. package/dist/store.js +0 -379
  58. package/dist/storefront.js +0 -285
  59. package/dist/ucp/buy.js +0 -90
  60. package/dist/ucp/check.js +0 -362
  61. package/dist/ucp/client.js +0 -129
  62. package/dist/ucp/images.js +0 -62
  63. package/dist/ucp/platform.js +0 -90
  64. package/dist/ucp/shop.js +0 -357
  65. package/dist/ucp/signing.js +0 -67
  66. package/dist/web.js +0 -316
  67. package/dist/website.js +0 -390
@@ -1,285 +0,0 @@
1
- // The storefront scaffold: a static site on drop-in.js, no build step. Three files:
2
- // index.html (the config block, the drop-in elements, the products grouped by
3
- // top-level category), styles.css (theme tokens first, then the drop-in.css
4
- // overrides, then the page) and app.js (market switch, category links, and a
5
- // product page at #/p/<sku>). Product images are the store's, at /img/<file> (from
6
- // .commercelayer/img/, uploaded by apply); a product without one shows the tile the
7
- // store host draws at /tiles/<sku>.svg. The agent then makes it the human's.
8
- import { isLocalImage } from './store.js';
9
- /** img/<file> is the store's image at /img/<file> on the store host, whatever page links it. */
10
- const imageSrc = (url) => (isLocalImage(url) ? `/${url}` : url);
11
- /** A product without an image gets the tile the store host draws for it, at /tiles/<sku>.svg. */
12
- const productImage = (p) => (p.image_url ? imageSrc(p.image_url) : `/tiles/${encodeURIComponent(p.sku)}.svg`);
13
- const esc = (t) => String(t ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
14
- /** The scaffold's files, by path relative to the storefront folder. */
15
- export function storefrontFiles(store, clientId) {
16
- return { 'index.html': indexHtml(store, clientId), 'styles.css': STYLES, 'app.js': APP };
17
- }
18
- function indexHtml(store, clientId) {
19
- const name = esc(store.name);
20
- const card = (p, h) => {
21
- const href = `#/p/${encodeURIComponent(p.sku)}`;
22
- return ` <article class="product" id="p-${esc(p.sku)}">
23
- <a class="media" href="${esc(href)}" tabindex="-1"><img src="${esc(productImage(p))}" alt="${esc(p.name)}"${p.image_url ? '' : ' loading="lazy"'}></a>
24
- <${h}><a href="${esc(href)}">${esc(p.name)}</a></${h}>
25
- ${p.description ? ` <p>${esc(p.description)}</p>\n` : ''} <cl-price code="${esc(p.sku)}"><cl-price-amount type="compare-at"></cl-price-amount> <cl-price-amount type="price"></cl-price-amount></cl-price>
26
- <cl-availability code="${esc(p.sku)}"><cl-availability-status type="available">In stock</cl-availability-status><cl-availability-status type="unavailable">Sold out</cl-availability-status></cl-availability>
27
- <cl-add-to-cart code="${esc(p.sku)}">Add to cart</cl-add-to-cart>
28
- </article>`;
29
- };
30
- // Products are grouped by the top level of their category, in order of first appearance;
31
- // products with no category come last, under "Other". No categories: one grid.
32
- const groups = new Map();
33
- for (const p of store.products) {
34
- const top = p.category?.split(' > ')[0] ?? '';
35
- groups.set(top, [...(groups.get(top) ?? []), p]);
36
- }
37
- let listing;
38
- let categoryNav = '';
39
- if (![...groups.keys()].some((k) => k))
40
- listing = ` <div class="grid">\n${store.products.map((p) => card(p, 'h2')).join('\n')}\n </div>`;
41
- else {
42
- const ordered = [...groups.keys()].filter((k) => k).concat(groups.has('') ? [''] : []);
43
- const used = new Set();
44
- const ids = ordered.map((k) => {
45
- const base = `category-${(k || 'other').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'other'}`;
46
- let id = base;
47
- for (let n = 2; used.has(id); n++)
48
- id = `${base}-${n}`;
49
- used.add(id);
50
- return id;
51
- });
52
- categoryNav = ` <nav class="categories">\n${ordered.map((k, i) => ` <a href="#${ids[i]}">${esc(k || 'Other')}</a>`).join('\n')}\n </nav>\n`;
53
- listing = ordered
54
- .map((k, i) => ` <section id="${ids[i]}">\n <h2>${esc(k || 'Other')}</h2>\n <div class="grid">\n${groups.get(k).map((p) => card(p, 'h3').replace(/^/gm, ' ')).join('\n')}\n </div>\n </section>`)
55
- .join('\n');
56
- }
57
- const options = store.markets.map((m) => ` <option value="${esc(m.code)}">${esc(m.name)} (${esc(m.currency)})</option>`).join('\n');
58
- // What the product page shows, by sku. Prices and availability come live from drop-in.js.
59
- const data = Object.fromEntries(store.products.map((p) => [p.sku, { name: p.name, description: p.description, image_url: productImage(p), category: p.category }]));
60
- const json = JSON.stringify(data, null, 2).replace(/</g, '\\u003c');
61
- return `<!doctype html>
62
- <html lang="en">
63
- <head>
64
- <meta charset="utf-8">
65
- <meta name="viewport" content="width=device-width, initial-scale=1">
66
- <title>${name}</title>
67
- <!-- Commerce Layer drop-in.js: prices, availability, add to cart, the minicart (a slide-in panel, styled by minicart.css) and links to the hosted cart and checkout. -->
68
- <link href="https://cdn.jsdelivr.net/npm/@commercelayer/drop-in.js@2/dist/drop-in/drop-in.css" rel="stylesheet">
69
- <link href="https://cdn.jsdelivr.net/npm/@commercelayer/drop-in.js@2/dist/drop-in/minicart.css" rel="stylesheet">
70
- <!-- After the drop-in.js stylesheets: styles.css overrides them with the same selectors. Restyle through its tokens.
71
- Product images are the store's, at /img/ (.commercelayer/img/, uploaded by apply). Put this site's own files in assets/: /img/ is not served from here. -->
72
- <link href="styles.css" rel="stylesheet">
73
- <script>
74
- // The market comes from ?market=<code>; the first market in store.yml is the default. Keep clientId as it is.
75
- var market = new URLSearchParams(location.search).get('market') || '${esc(store.markets[0].code)}';
76
- window.commercelayerConfig = {
77
- clientId: '${esc(clientId)}',
78
- scope: 'market:code:' + market,
79
- defaultAttributes: { orders: { language_code: 'en', return_url: location.origin + location.pathname } }
80
- };
81
- </script>
82
- <script type="module" src="https://cdn.jsdelivr.net/npm/@commercelayer/drop-in.js@2/dist/drop-in/drop-in.esm.js"></script>
83
- <script src="app.js" defer></script>
84
- </head>
85
- <body>
86
- <header>
87
- <h1><a href="#">${name}</a></h1>
88
- <nav>
89
- <label><span class="market-label">Market</span>
90
- <select id="market">
91
- ${options}
92
- </select>
93
- </label>
94
- <cl-cart-link aria-label="Cart">Cart (<cl-cart-count></cl-cart-count>)<cl-cart type="mini" open-on-add></cl-cart></cl-cart-link>
95
- <cl-checkout-link>Checkout</cl-checkout-link>
96
- </nav>
97
- </header>
98
- ${categoryNav} <main>
99
- <div id="listing">
100
- ${listing}
101
- </div>
102
- <!-- The product page: app.js fills it from #product-template and #products when the address is #/p/<sku>. -->
103
- <div id="product-page" hidden></div>
104
- </main>
105
- <footer>Sandbox store: test payments only.</footer>
106
-
107
- <!-- The product page's markup. Elements with data-field get the product's data; elements with data-code get code="<sku>" for drop-in.js. -->
108
- <template id="product-template">
109
- <article class="product-page">
110
- <a class="back" href="#">&larr; All products</a>
111
- <div class="media"><img data-field="image_url" alt=""><div class="placeholder" aria-hidden="true"></div></div>
112
- <div class="info">
113
- <p class="category" data-field="category"></p>
114
- <h2 data-field="name"></h2>
115
- <p data-field="description"></p>
116
- <cl-price data-code><cl-price-amount type="compare-at"></cl-price-amount> <cl-price-amount type="price"></cl-price-amount></cl-price>
117
- <cl-availability data-code><cl-availability-status type="available">In stock</cl-availability-status><cl-availability-status type="unavailable">Sold out</cl-availability-status></cl-availability>
118
- <cl-add-to-cart data-code>Add to cart</cl-add-to-cart>
119
- </div>
120
- </article>
121
- </template>
122
- <template id="not-found-template">
123
- <article class="product-page">
124
- <a class="back" href="#">&larr; All products</a>
125
- <p>This product is not in the store.</p>
126
- </article>
127
- </template>
128
- <!-- The products, by sku: name, description, image_url, category. Keep it in step with the cards above. -->
129
- <script type="application/json" id="products">
130
- ${json}
131
- </script>
132
- </body>
133
- </html>
134
- `;
135
- }
136
- const STYLES = `/* The storefront's styles, in three parts: theme tokens, drop-in.js overrides, the page.
137
- Restyle the page by changing the tokens; a dark theme is color-scheme: dark with paper and ink swapped.
138
- index.html links this file after the drop-in.js stylesheets: the overrides use the same selectors as
139
- drop-in.css, so they win by coming later. Keep that order. */
140
-
141
- /* 1. Theme tokens */
142
- :root {
143
- color-scheme: light;
144
- --paper: #ffffff; /* page background */
145
- --ink: #16181d; /* text and prices */
146
- --muted: #5d6572; /* secondary text, compare-at prices, availability */
147
- --line: #e3e6ea; /* borders, disabled buttons */
148
- --accent: #1f5eff; /* buttons, link hover */
149
- --on-accent: #ffffff; /* text on the accent */
150
- --radius: 8px;
151
- --font: system-ui, sans-serif;
152
- /* drop-in.css's own defaults (add to cart, cart count) read these. */
153
- --cl-color-primary: var(--accent);
154
- --cl-color-white: var(--on-accent);
155
- }
156
-
157
- /* 2. drop-in.js overrides */
158
- /* drop-in.css colours cl-price-amount itself (#101111), so inheriting is not enough: set it here. */
159
- cl-price-amount { color: var(--ink); font-weight: 600; }
160
- cl-price-amount[type="compare-at"] { color: var(--muted); font-weight: 400; }
161
- cl-availability { font-size: 13px; color: var(--muted); }
162
- /* cl-add-to-cart is the button itself (it has no ::part); drop-in.css colours it with these same selectors. */
163
- cl-add-to-cart { align-self: flex-start; padding: 10px 16px; border-radius: calc(var(--radius) - 2px); font: inherit; font-weight: 600; background-color: var(--line); color: var(--muted); }
164
- cl-add-to-cart:not([aria-disabled=true]) { background-color: var(--accent); color: var(--on-accent); }
165
- /* cl-checkout-link renders a plain <a> (no shadow DOM); without an order it is aria-disabled. */
166
- cl-checkout-link a { display: inline-block; padding: 8px 14px; border-radius: calc(var(--radius) - 2px); background: var(--accent); color: var(--on-accent); font-weight: 600; text-decoration: none; }
167
- cl-checkout-link[aria-disabled="true"] a { background: var(--line); color: var(--muted); }
168
- /* drop-in.css styles the count as a badge beside a cart icon (absolute, pushed right). The link is text here, so render it inline: "Cart (3)". */
169
- cl-cart-link cl-cart-count { position: static; transform: none; display: inline; min-width: 0; height: auto; padding: 0; border-radius: 0; background: none; color: inherit; font-size: inherit; }
170
-
171
- /* 3. The page */
172
- body { margin: 0; font-family: var(--font); color: var(--ink); background: var(--paper); }
173
- a { color: inherit; }
174
- header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; padding: 16px 24px; border-bottom: 1px solid var(--line); }
175
- header h1 { margin: 0; font-size: 20px; }
176
- header h1 a { text-decoration: none; }
177
- /* The nav never wraps inside: on a narrow screen it moves under the store name as a whole. */
178
- header nav { display: flex; align-items: center; gap: 16px; margin-left: auto; white-space: nowrap; }
179
- header select { font: inherit; color: inherit; background: var(--paper); border: 1px solid var(--line); border-radius: calc(var(--radius) / 2); padding: 4px 6px; }
180
- nav.categories { display: flex; flex-wrap: wrap; gap: 8px 20px; padding: 14px 24px; border-bottom: 1px solid var(--line); }
181
- nav.categories a { text-decoration: none; font-size: 15px; }
182
- nav.categories a:hover { color: var(--accent); }
183
- main { max-width: 1100px; margin: 0 auto; padding: 32px 24px; }
184
- main section { scroll-margin-top: 16px; }
185
- main section + section { margin-top: 40px; }
186
- main section > h2 { margin: 0 0 16px; font-size: 22px; }
187
- .grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
188
- .product { display: flex; flex-direction: column; gap: 8px; padding: 20px; border: 1px solid var(--line); border-radius: var(--radius); scroll-margin-top: 16px; }
189
- .product h2, .product h3 { margin: 0; font-size: 17px; }
190
- .product h2 a, .product h3 a { text-decoration: none; }
191
- .product h2 a:hover, .product h3 a:hover { color: var(--accent); }
192
- .product p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.5; }
193
- .product cl-add-to-cart { margin-top: auto; }
194
- .media img, .media .placeholder { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: calc(var(--radius) - 2px); }
195
- /* No image: a plain tile a shade off the page, in any theme. */
196
- .placeholder { background: color-mix(in srgb, var(--ink) 6%, var(--paper)); }
197
- .product-page { display: grid; gap: 32px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
198
- .product-page .back { grid-column: 1 / -1; text-decoration: none; color: var(--muted); }
199
- .product-page .back:hover { color: var(--accent); }
200
- .product-page .info { display: flex; flex-direction: column; gap: 12px; }
201
- .product-page .info h2 { margin: 0; font-size: 28px; }
202
- .product-page .info p { margin: 0; line-height: 1.6; }
203
- .product-page .category { color: var(--muted); font-size: 14px; }
204
- footer { padding: 24px; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; }
205
- @media (max-width: 720px) {
206
- .product-page { grid-template-columns: 1fr; }
207
- }
208
- @media (max-width: 480px) {
209
- header, nav.categories { padding-left: 16px; padding-right: 16px; }
210
- header nav { gap: 12px; }
211
- .market-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
212
- main { padding: 24px 16px; }
213
- }
214
- `;
215
- const APP = `// The storefront's behaviour, no build step: the market switch, the category links,
216
- // and the product page at #/p/<sku>, filled from #product-template and the #products data.
217
- // drop-in.js does the commerce: prices, availability, cart, checkout.
218
-
219
- (function () {
220
- var products = JSON.parse(document.getElementById('products').textContent);
221
- var listing = document.getElementById('listing');
222
- var page = document.getElementById('product-page');
223
- var storeTitle = document.title;
224
-
225
- // Market: ?market=<code> picks the drop-in.js scope (see the config block in index.html).
226
- var select = document.getElementById('market');
227
- select.value = market;
228
- select.addEventListener('change', function () {
229
- location.search = '?market=' + encodeURIComponent(select.value);
230
- });
231
-
232
- function fill(root, product, sku) {
233
- root.querySelectorAll('[data-field]').forEach(function (el) {
234
- var value = product[el.dataset.field];
235
- if (el.tagName === 'IMG') {
236
- if (!value) return el.remove();
237
- el.src = value;
238
- el.alt = product.name;
239
- if (el.nextElementSibling) el.nextElementSibling.remove();
240
- } else if (value) el.textContent = value;
241
- else el.remove();
242
- });
243
- // code is set before the elements join the page, so drop-in.js reads it when they connect.
244
- root.querySelectorAll('[data-code]').forEach(function (el) {
245
- el.setAttribute('code', sku);
246
- el.removeAttribute('data-code');
247
- });
248
- }
249
-
250
- function showProduct(sku) {
251
- var product = products[sku];
252
- var view = document.getElementById(product ? 'product-template' : 'not-found-template').content.cloneNode(true);
253
- if (product) fill(view, product, sku);
254
- page.replaceChildren(view);
255
- listing.hidden = true;
256
- page.hidden = false;
257
- document.title = (product ? product.name + ' · ' : '') + storeTitle;
258
- window.scrollTo(0, 0);
259
- }
260
-
261
- function showListing(anchor) {
262
- page.hidden = true;
263
- page.replaceChildren();
264
- listing.hidden = false;
265
- document.title = storeTitle;
266
- var target = anchor && document.getElementById(anchor);
267
- if (target) target.scrollIntoView();
268
- }
269
-
270
- var lastSku = null;
271
- function route() {
272
- var hash = decodeURIComponent(location.hash.slice(1));
273
- if (hash.indexOf('/p/') === 0) {
274
- lastSku = hash.slice(3);
275
- return showProduct(lastSku);
276
- }
277
- // A category link (#category-...), or the listing (#): back from a product, return to its card.
278
- showListing(hash || (lastSku && 'p-' + lastSku));
279
- lastSku = null;
280
- }
281
-
282
- window.addEventListener('hashchange', route);
283
- route();
284
- })();
285
- `;
package/dist/ucp/buy.js DELETED
@@ -1,90 +0,0 @@
1
- // Buying from a store the way any shopping agent would: discover, search, cart,
2
- // checkout, address, pay with the sandbox handler, read the order back.
3
- import { fail } from '../output.js';
4
- import { discover, replay, shopping, UcpError } from './client.js';
5
- import { SANDBOX_CREDENTIAL_TYPE, SANDBOX_HANDLER_ID, SANDBOX_TOKEN_DECLINED, SANDBOX_TOKEN_SUCCESS } from './platform.js';
6
- export const TEST_BUYER = { first_name: 'Test', last_name: 'Buyer', email: 'buyer@example.com', phone_number: '+10000000000' };
7
- export function testAddress(country) {
8
- return { id: 'dest_1', first_name: 'Test', last_name: 'Buyer', street_address: '1 Test Street', address_locality: 'Testville', postal_code: '00000', address_country: country };
9
- }
10
- export function sandboxInstrument(decline = false) {
11
- return {
12
- id: 'pi_1',
13
- handler_id: SANDBOX_HANDLER_ID,
14
- type: 'card',
15
- selected: true,
16
- display: { brand: 'visa', last_digits: '4242', expiry_month: 12, expiry_year: 2030 },
17
- credential: { type: SANDBOX_CREDENTIAL_TYPE, token: decline ? SANDBOX_TOKEN_DECLINED : SANDBOX_TOKEN_SUCCESS, expiry: new Date(Date.now() + 1800_000).toISOString() },
18
- };
19
- }
20
- export const TEST_SIGNALS = { 'dev.ucp.buyer_ip': '203.0.113.1', 'dev.ucp.user_agent': 'c11r' };
21
- function note(steps, step, call, result) {
22
- steps.push({ step, request: `${call.method} ${new URL(call.url).pathname}`, status: call.status, result, ...(call.signature ? { signature: call.signature } : {}) });
23
- return call.body;
24
- }
25
- /** Country the product ships to: from the catalog's fulfillment data, else from the caller. */
26
- function shipTo(variant, wanted) {
27
- if (wanted)
28
- return wanted.toUpperCase();
29
- const plain = variant.fulfillment?.methods?.[0]?.description?.plain ?? '';
30
- const m = plain.match(/Ships to ([A-Z]{2})/);
31
- return m ? m[1] : 'US';
32
- }
33
- export async function buy(origin, opts = {}) {
34
- const steps = [];
35
- const store = await discover(origin);
36
- steps.push({ step: 'discover', request: 'GET /.well-known/ucp', status: 200, result: `UCP ${store.profile.ucp.version}, ${store.capabilities.size} shared capabilities` });
37
- try {
38
- const search = note(steps, 'search', await shopping.search(store, { query: opts.sku ?? '', context: opts.country ? { address_country: opts.country.toUpperCase() } : {} }), '');
39
- steps[steps.length - 1].result = `${search.products.length} products`;
40
- const product = opts.sku ? search.products.find((p) => p.variants.some((v) => v.id === opts.sku || v.sku === opts.sku)) : search.products[0];
41
- if (!product)
42
- fail('no_product', opts.sku ? `The catalog has no product ${opts.sku}.` : 'The catalog is empty.', 'Add products to .commercelayer/store.yml or .commercelayer/products/ and run `c11r apply`.');
43
- const variant = opts.sku ? product.variants.find((v) => v.id === opts.sku || v.sku === opts.sku) : product.variants[0];
44
- const country = shipTo(variant, opts.country);
45
- const quantity = opts.quantity ?? 1;
46
- const context = { address_country: country, currency: variant.price.currency };
47
- const lineItems = [{ item: { id: variant.id }, quantity }];
48
- const discounts = opts.code ? { discounts: { codes: [opts.code] } } : {};
49
- let create;
50
- if (opts.viaCart && store.capabilities.has('dev.ucp.shopping.cart')) {
51
- const cart = note(steps, 'cart', await shopping.createCart(store, { line_items: lineItems, context, ...discounts }), '');
52
- steps[steps.length - 1].result = `cart ${cart.id}, ${cart.totals.at(-1).amount} ${cart.currency}`;
53
- create = { cart_id: cart.id, buyer: TEST_BUYER, context };
54
- }
55
- else
56
- create = { line_items: lineItems, buyer: TEST_BUYER, context, ...discounts };
57
- let checkout = note(steps, 'checkout', await shopping.createCheckout(store, create), '');
58
- steps[steps.length - 1].result = `checkout ${checkout.id} ${checkout.status}`;
59
- const update = replay(checkout);
60
- update.buyer = { ...TEST_BUYER, ...(checkout.buyer?.consent ? { consent: checkout.buyer.consent } : {}) };
61
- if (update.fulfillment) {
62
- update.fulfillment.methods[0].destinations = [testAddress(country)];
63
- update.fulfillment.methods[0].selected_destination_id = 'dest_1';
64
- }
65
- checkout = note(steps, 'address', await shopping.updateCheckout(store, checkout.id, update), '');
66
- steps[steps.length - 1].result = `${checkout.status}, total ${checkout.totals.at(-1).amount} ${checkout.currency}`;
67
- if (checkout.status !== 'ready_for_complete') {
68
- const why = (checkout.messages ?? []).filter((m) => m.type === 'error').map((m) => `${m.code} at ${m.path}: ${m.content}`).join('; ');
69
- fail('checkout_not_ready', `The checkout is ${checkout.status}: ${why}`, 'Fix the store (markets, countries, prices) and run the command again.');
70
- }
71
- // The checkout as it was before paying: its warnings (a code that applied nothing) and discounts.
72
- const ready = checkout;
73
- const completeCall = await shopping.completeCheckout(store, checkout.id, { payment: { instruments: [sandboxInstrument(opts.decline)] }, signals: TEST_SIGNALS });
74
- checkout = note(steps, 'pay', completeCall, '');
75
- steps[steps.length - 1].result = checkout.status === 'completed' ? `completed, order ${checkout.order.id}` : `${checkout.status}: ${(checkout.messages ?? []).map((m) => m.code).join(', ')}`;
76
- let order = null;
77
- if (checkout.order) {
78
- order = note(steps, 'order', await shopping.getOrder(store, checkout.order.id), '');
79
- steps[steps.length - 1].result = `${order.label}, ${order.line_items.length} line item(s), ${order.totals.at(-1).amount} ${order.currency}`;
80
- }
81
- return { store, steps, checkout, ready, order };
82
- }
83
- catch (e) {
84
- if (e instanceof UcpError) {
85
- steps.push({ step: 'failed', request: `${e.call.method} ${new URL(e.call.url).pathname}`, status: e.call.status, result: e.message });
86
- fail('ucp_error', e.message, `The store answered in a way the buyer could not continue from. Steps so far: ${steps.map((s) => `${s.step} (${s.result})`).join(' → ')}. Run \`c11r ucp check\` for the full conformance report.`);
87
- }
88
- throw e;
89
- }
90
- }