1 | |
2 |
|
3 | |
4 | * Create Order |
5 | * Create an order directly (no checkout). Each line item is either a variant ({ variant_id, quantity }) or a custom item ({ title, price, quantity }; price in currency, default the shop currency). financial_status PAID records a manual payment for the full amount (so the order can be refunded later); other statuses are only recorded. No card is charged. Inventory is not decremented unless inventory_behaviour says so. |
6 | */ |
7 | export async function main( |
8 | auth: RT.Shopify, |
9 | line_items: { |
10 | variant_id?: string |
11 | title?: string |
12 | price?: number |
13 | quantity: number |
14 | }[], |
15 | customer_id: string | undefined, |
16 | email: string | undefined, |
17 | financial_status: |
18 | | "PAID" |
19 | | "PENDING" |
20 | | "AUTHORIZED" |
21 | | "PARTIALLY_PAID" |
22 | | "PARTIALLY_REFUNDED" |
23 | | "REFUNDED" |
24 | | "VOIDED" |
25 | | "EXPIRED" |
26 | | undefined, |
27 | shipping_address: |
28 | | { |
29 | firstName?: string |
30 | lastName?: string |
31 | company?: string |
32 | address1?: string |
33 | address2?: string |
34 | city?: string |
35 | provinceCode?: string |
36 | zip?: string |
37 | countryCode?: string |
38 | phone?: string |
39 | } |
40 | | undefined, |
41 | note: string | undefined, |
42 | tags: string[] | undefined, |
43 | currency: string | undefined, |
44 | inventory_behaviour: |
45 | | "BYPASS" |
46 | | "DECREMENT_OBEYING_POLICY" |
47 | | "DECREMENT_IGNORING_POLICY" |
48 | | undefined, |
49 | send_receipt: boolean | undefined, |
50 | test: boolean | undefined |
51 | ) { |
52 | const endpoint = `https://${auth.store_name}.myshopify.com/admin/api/2026-10/graphql.json` |
53 | const call = async (body: { query: string; variables?: object }) => { |
54 | const response = await fetch(endpoint, { |
55 | method: "POST", |
56 | headers: { |
57 | "X-Shopify-Access-Token": auth.token, |
58 | "Content-Type": "application/json", |
59 | Accept: "application/json", |
60 | }, |
61 | body: JSON.stringify(body), |
62 | }) |
63 | if (!response.ok) { |
64 | throw new Error(`${response.status} ${await response.text()}`) |
65 | } |
66 | const { data, errors } = await response.json() |
67 | if (errors) { |
68 | throw new Error(JSON.stringify(errors)) |
69 | } |
70 | return data |
71 | } |
72 | const gid = (value: string, type: string) => |
73 | value.startsWith("gid://") ? value : `gid://shopify/${type}/${value}` |
74 |
|
75 | |
76 | const currencyCode = |
77 | currency || |
78 | (line_items.some((item) => item.price != null) |
79 | ? (await call({ query: `query { shop { currencyCode } }` })).shop |
80 | .currencyCode |
81 | : undefined) |
82 |
|
83 | const data = await call({ |
84 | query: `mutation ($order: OrderCreateOrderInput!, $options: OrderCreateOptionsInput) { orderCreate(order: $order, options: $options) { order { id } userErrors { field message } } }`, |
85 | variables: { |
86 | order: { |
87 | lineItems: line_items.map((item) => ({ |
88 | variantId: item.variant_id |
89 | ? gid(item.variant_id, "ProductVariant") |
90 | : undefined, |
91 | title: item.title || undefined, |
92 | priceSet: |
93 | item.price != null |
94 | ? { shopMoney: { amount: item.price, currencyCode } } |
95 | : undefined, |
96 | quantity: item.quantity, |
97 | })), |
98 | customer: customer_id |
99 | ? { toAssociate: { id: gid(customer_id, "Customer") } } |
100 | : undefined, |
101 | email: email || undefined, |
102 | |
103 | financialStatus: |
104 | financial_status && financial_status !== "PAID" |
105 | ? financial_status |
106 | : undefined, |
107 | shippingAddress: shipping_address || undefined, |
108 | note: note || undefined, |
109 | tags: tags?.length ? tags : undefined, |
110 | currency: currency || undefined, |
111 | test: test || undefined, |
112 | }, |
113 | options: { |
114 | inventoryBehaviour: inventory_behaviour || undefined, |
115 | sendReceipt: send_receipt || undefined, |
116 | }, |
117 | }, |
118 | }) |
119 | if (data.orderCreate.userErrors.length) { |
120 | throw new Error(JSON.stringify(data.orderCreate.userErrors)) |
121 | } |
122 | const id = data.orderCreate.order.id |
123 |
|
124 | if (financial_status === "PAID") { |
125 | |
126 | for (let attempt = 1; ; attempt++) { |
127 | const paid = await call({ |
128 | query: `mutation ($input: OrderMarkAsPaidInput!) { orderMarkAsPaid(input: $input) { userErrors { field message } } }`, |
129 | variables: { input: { id } }, |
130 | }) |
131 | const userErrors = paid.orderMarkAsPaid.userErrors |
132 | if (!userErrors.length) break |
133 | if ( |
134 | attempt === 10 || |
135 | !/temporarily unavailable/i.test(userErrors[0].message) |
136 | ) { |
137 | throw new Error(JSON.stringify(userErrors)) |
138 | } |
139 | await new Promise((resolve) => setTimeout(resolve, 1000)) |
140 | } |
141 | } |
142 |
|
143 | return ( |
144 | await call({ |
145 | query: `query ($id: ID!) { order(id: $id) { id legacyResourceId name email createdAt displayFinancialStatus displayFulfillmentStatus tags note totalPriceSet { shopMoney { amount currencyCode } } totalOutstandingSet { shopMoney { amount currencyCode } } customer { id displayName } lineItems(first: 50) { nodes { id name quantity variant { id } originalUnitPriceSet { shopMoney { amount currencyCode } } } } } }`, |
146 | variables: { id }, |
147 | }) |
148 | ).order |
149 | } |
150 |
|