Skip to content

Commit 1d77784

Browse files
committed
Add --country to checkout create for presentment currency localization
`checkout create` now sets `checkout.context.address_country` from the explicit global `--country` flag, so the merchant resolves presentment currency to the buyer's country instead of falling back to the agent host's geo-IP. Verified live: `--country GB` returns GBP (was EUR from a non-UK host) while the signed-in saved address is preserved (stdin-supplied context wins). Assisted-By: devx/f92431b1-082a-488b-819b-ad2a77159758
1 parent 739548a commit 1d77784

6 files changed

Lines changed: 73 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/shop-cli': patch
3+
---
4+
5+
Add `--country` to `checkout create`, setting `checkout.context.address_country` so the merchant resolves presentment currency to the buyer's country (stdin context wins; saved address is not overridden).

skill/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ shop catalog get-product gid://shopify/p/abc --select Color=Black --select Size=
6767

6868
### Checkout
6969
```bash
70-
# create from a variant
71-
printf '{"email":"buyer@example.com"}' | shop checkout create --shop-domain example.myshopify.com --variant-id 123 --quantity 1 --checkout-stdin
70+
# create from a variant (--country localizes presentment currency)
71+
printf '{"email":"buyer@example.com"}' | shop checkout create --shop-domain example.myshopify.com --variant-id 123 --quantity 1 --country GB --checkout-stdin
7272
# create from an existing cart
7373
printf '{"cart_id":"cart_123","line_items":[]}' | shop checkout create --shop-domain example.myshopify.com --checkout-stdin
7474
printf '{"fulfillment":{"methods":[]}}' | shop checkout update --shop-domain example.myshopify.com --checkout-id CHECKOUT_ID --checkout-stdin
@@ -164,6 +164,7 @@ When the item is visual (clothing, shoes, accessories, furniture, decor, art) **
164164
**Reading the `checkout create` / `update` response:**
165165
- Inspect `status`, `email`, addresses, `continue_url`, and `payment.instruments`.
166166
- If the buyer's saved shipping details are missing, collect them and pass via `checkout create`/`update`.
167+
- Pass `--country <ISO2>` on `checkout create` to localize presentment currency; without it the merchant may present a foreign currency. It does not override the saved address.
167168
- **Warnings:** display every `messages[]` entry with type `warning` (e.g. `final_sale`, `prop65`, `age_restricted`) before completing. Show `presentation: "disclosure"` warnings verbatim — never omit or summarize them. Never complete a purchase without surfacing these.
168169

169170
Then take one of two paths:

skill/references/direct-api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Create with line items, or pass a checkout body that already contains a `cart_id
110110
},
111111
"checkout": {
112112
"cart_id": "<optional_cart_id>",
113+
"context": { "address_country": "US" },
113114
"line_items": [
114115
{
115116
"quantity": 1,
@@ -142,6 +143,8 @@ Create with line items, or pass a checkout body that already contains a `cart_id
142143
}
143144
```
144145

146+
`context.address_country` (ISO2) localizes presentment currency to the buyer's country; without it the merchant infers it from the request geo-IP. It does not override the saved address.
147+
145148
If response status is `ready_for_complete` and includes a Shop Pay payment token, complete after clear purchase intent. If no payment token is present, present the UCP `continue_url` as a Finish in Shop link. **If the buyer has a delegated budget (see Payment Budget) but the checkout still returns no payment instruments, the merchant does not accept Shop Pay** — hand off `continue_url` or suggest another store; do not re-prompt the user to set up a budget (they already have one).
146149

147150
The checkout response may include a `messages[]` array. You MUST display every `warning` message's `content` to the user (e.g. `final_sale`, `prop65`, `age_restricted`) before completing. Show `presentation: "disclosure"` warnings verbatim and do not omit or summarize them away. Never complete a purchase without surfacing these messages.

src/cli.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export function createProgram(deps: CliDependencies = {}): Command {
287287
quantity: options.quantity,
288288
checkout,
289289
buyerIp: options.buyerIp,
290+
country: explicitCountry(program),
290291
})
291292
return annotateShopPayAvailability(client, result)
292293
})
@@ -400,18 +401,23 @@ export async function main(argv = process.argv, deps: CliDependencies = {}): Pro
400401
}
401402
}
402403

404+
function explicitCountry(program: Command): string | undefined {
405+
return program.getOptionValueSource('country') === 'cli'
406+
? program.optsWithGlobals<GlobalOptions>().country
407+
: undefined
408+
}
409+
403410
function resolveClient(deps: CliDependencies, program: Command): ShopCatalogClient {
404411
const globals = program.optsWithGlobals<GlobalOptions>()
405412
const store = resolveStore(deps, globals)
406413
// Only treat --country as an explicit override when it was actually passed on the CLI;
407414
// otherwise leave it undefined so the client falls back to the stored preference, then
408415
// DEFAULT_COUNTRY. The default value of the option must never override a stored country.
409-
const explicitCountry = program.getOptionValueSource('country') === 'cli' ? globals.country : undefined
410416
return new ShopCatalogClient({
411417
fetch: deps.fetch,
412418
store,
413419
profileUrl: globals.profileUrl,
414-
country: explicitCountry,
420+
country: explicitCountry(program),
415421
})
416422
}
417423

src/shop-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export interface CheckoutCreateInput {
9999
quantity?: number
100100
checkout?: JsonObject
101101
buyerIp?: string
102+
country?: string
102103
}
103104

104105
export interface CheckoutUpdateInput {
@@ -186,6 +187,10 @@ export class ShopCatalogClient {
186187
},
187188
]
188189
}
190+
if (input.country) {
191+
const piped = isPlainObject(checkout.context) ? checkout.context : {}
192+
checkout.context = { address_country: input.country, ...piped }
193+
}
189194

190195
return unwrapMcpResult(await this.callShopMcp(shopDomain, 'create_checkout', { checkout }, token, buyerIp))
191196
}

tests/checkout-orders.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,34 @@ describe('checkout and orders', () => {
6666
})
6767
})
6868

69+
it('sets checkout.context.address_country from country, letting stdin context win', async () => {
70+
const store = createStore({ [ACCESS_TOKEN_ACCOUNT]: 'access' })
71+
const bodies: unknown[] = []
72+
const fetchMock = createFetchMock(async (url, init) => {
73+
if (url.endsWith('/userinfo')) return jsonResponse({ sub: 'user-1' })
74+
if (url === 'https://shop.app/oauth/token') return jsonResponse({ access_token: 'ucp-jwt' })
75+
if (url === 'https://api.ipify.org?format=json') return jsonResponse({ ip: '203.0.113.10' })
76+
bodies.push(await readJsonBody(init))
77+
return jsonResponse({ jsonrpc: '2.0', id: 1, result: { structuredContent: { status: 'ready_for_complete' } } })
78+
})
79+
const client = new ShopCatalogClient({ fetch: fetchMock, store })
80+
81+
await client.createCheckout({ shopDomain: 'example.myshopify.com', variantId: '123', country: 'GB' })
82+
await client.createCheckout({
83+
shopDomain: 'example.myshopify.com',
84+
variantId: '123',
85+
country: 'GB',
86+
checkout: { context: { address_country: 'US' } },
87+
})
88+
89+
expect(bodies[0]).toMatchObject({
90+
params: { name: 'create_checkout', arguments: { checkout: { context: { address_country: 'GB' } } } },
91+
})
92+
expect(bodies[1]).toMatchObject({
93+
params: { name: 'create_checkout', arguments: { checkout: { context: { address_country: 'US' } } } },
94+
})
95+
})
96+
6997
it('unwraps the MCP envelope and returns the checkout payload, not the raw frame', async () => {
7098
const store = createStore({ [ACCESS_TOKEN_ACCOUNT]: 'access' })
7199
const checkout = { id: 'gid://shopify/Checkout/abc', status: 'ready_for_complete', currency: 'GBP' }
@@ -464,6 +492,27 @@ describe('checkout and orders', () => {
464492
expect(names).toEqual(['create_checkout', 'create_checkout', 'update_checkout', 'complete_checkout'])
465493
})
466494

495+
it('wires --country into checkout.context.address_country via the CLI', async () => {
496+
const { createProgram } = await import('../src/cli.js')
497+
const store = createStore({ [ACCESS_TOKEN_ACCOUNT]: 'access' })
498+
let body: { params: { name: string; arguments: { checkout: { context?: { address_country?: string } } } } } | undefined
499+
const fetchMock = createFetchMock(async (url, init) => {
500+
if (url.endsWith('/userinfo')) return jsonResponse({ sub: 'user-1' })
501+
if (url === 'https://shop.app/oauth/token') return jsonResponse({ access_token: 'ucp-jwt' })
502+
if (url === 'https://api.ipify.org?format=json') return jsonResponse({ ip: '203.0.113.10' })
503+
const parsed = (await readJsonBody(init)) as typeof body
504+
if (parsed?.params?.name === 'create_checkout') body = parsed
505+
return jsonResponse({ jsonrpc: '2.0', id: 1, result: { structuredContent: { status: 'ready_for_complete' } } })
506+
})
507+
const base = { fetch: fetchMock, store, stdout: { write: fn() }, stderr: { write: fn() }, exit: (() => undefined) as never }
508+
509+
await createProgram(base).parseAsync([
510+
'node', 'shop', 'checkout', 'create', '--shop-domain', 'example.myshopify.com', '--variant-id', '123', '--country', 'GB',
511+
])
512+
513+
expect(body?.params.arguments.checkout.context).toEqual({ address_country: 'GB' })
514+
})
515+
467516
it('surfaces UCP checkout messages (final_sale, prop65) above the raw JSON on create', async () => {
468517
const { createProgram } = await import('../src/cli.js')
469518
const stdout = { write: fn() }

0 commit comments

Comments
 (0)