import {
assertNoUserErrors,
nodesFromConnection,
shopifyGraphql,
trimString,
} from './lib/client.ts'
import { toGid } from './lib/shop.ts'
import type { ShopifyClientOptions } from './types.ts'
export type FulfillmentOrderLineItem = {
id: string
remainingQuantity: number | null
totalQuantity: number | null
lineItem: { id: string | null; title: string | null; sku: string | null }
}
export type FulfillmentOrderSummary = {
id: string
status: string | null
assignedLocation: { name: string | null } | null
lineItems: Array<FulfillmentOrderLineItem>
}
/**
* List fulfillment orders for one order.
*
* @example
* import { listFulfillmentOrders } from 'kody:@kody/shopify/fulfillments'
* const { items } = await listFulfillmentOrders({ shop: 'acme', orderId: '123' })
*/
export async function listFulfillmentOrders(
params: ShopifyClientOptions & { orderId: string },
) {
const id = toGid('Order', params.orderId)
const data = await shopifyGraphql<{
order: { fulfillmentOrders: unknown } | null
}>({
shop: params.shop,
apiVersion: params.apiVersion,
query: `query FulfillmentOrders($id: ID!) {
order(id: $id) {
fulfillmentOrders(first: 20) {
nodes {
id
status
assignedLocation { name }
lineItems(first: 50) {
nodes {
id
remainingQuantity
totalQuantity
lineItem { id title sku }
}
}
}
}
}
}`,
variables: { id },
})
if (!data.order) throw new Error(`Order not found: ${id}`)
const items = nodesFromConnection<Record<string, unknown>>(
data.order.fulfillmentOrders,
).map((node): FulfillmentOrderSummary => {
const location =
node.assignedLocation && typeof node.assignedLocation === 'object'
? (node.assignedLocation as Record<string, unknown>)
: null
return {
id: String(node.id || ''),
status: typeof node.status === 'string' ? node.status : null,
assignedLocation: location
? {
name: typeof location.name === 'string' ? location.name : null,
}
: null,
lineItems: nodesFromConnection<Record<string, unknown>>(
node.lineItems,
).map((item) => {
const lineItem =
item.lineItem && typeof item.lineItem === 'object'
? (item.lineItem as Record<string, unknown>)
: null
return {
id: String(item.id || ''),
remainingQuantity:
typeof item.remainingQuantity === 'number'
? item.remainingQuantity
: null,
totalQuantity:
typeof item.totalQuantity === 'number' ? item.totalQuantity : null,
lineItem: {
id: typeof lineItem?.id === 'string' ? lineItem.id : null,
title: typeof lineItem?.title === 'string' ? lineItem.title : null,
sku: typeof lineItem?.sku === 'string' ? lineItem.sku : null,
},
}
}),
}
})
return { items }
}
export type CreateFulfillmentParams = ShopifyClientOptions & {
fulfillmentOrderId: string
notifyCustomer?: boolean
trackingNumber?: string
trackingUrl?: string
trackingCompany?: string
/** When omitted, remaining quantities on the fulfillment order are used. */
lineItems?: Array<{ id: string; quantity: number }>
dryRun?: boolean
}
/**
* Create a fulfillment for one fulfillment order. Defaults to dry-run.
*/
export async function createFulfillment(params: CreateFulfillmentParams) {
const fulfillmentOrderId = toGid('FulfillmentOrder', params.fulfillmentOrderId)
const lineItems = (params.lineItems || []).map((item) => ({
id: toGid('FulfillmentOrderLineItem', item.id),
quantity: item.quantity,
}))
const fulfillment = {
notifyCustomer: params.notifyCustomer === true,
lineItemsByFulfillmentOrder: [
{
fulfillmentOrderId,
fulfillmentOrderLineItems: lineItems.length ? lineItems : undefined,
},
],
trackingInfo:
trimString(params.trackingNumber) || trimString(params.trackingUrl)
? {
number: trimString(params.trackingNumber) || undefined,
url: trimString(params.trackingUrl) || undefined,
company: trimString(params.trackingCompany) || undefined,
}
: undefined,
}
if (params.dryRun !== false) {
return {
dryRun: true as const,
wouldCall: { mutation: 'fulfillmentCreate', fulfillment },
}
}
const data = await shopifyGraphql<{ fulfillmentCreate: unknown }>({
shop: params.shop,
apiVersion: params.apiVersion,
query: `mutation CreateFulfillment($fulfillment: FulfillmentInput!) {
fulfillmentCreate(fulfillment: $fulfillment) {
fulfillment { id status }
userErrors { field message }
}
}`,
variables: { fulfillment },
})
assertNoUserErrors(data.fulfillmentCreate, 'fulfillmentCreate')
return data.fulfillmentCreate
}
/** Default export: list fulfillment orders. */
export default listFulfillmentOrders