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 } /** * 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>( data.order.fulfillmentOrders, ).map((node): FulfillmentOrderSummary => { const location = node.assignedLocation && typeof node.assignedLocation === 'object' ? (node.assignedLocation as Record) : 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>( node.lineItems, ).map((item) => { const lineItem = item.lineItem && typeof item.lineItem === 'object' ? (item.lineItem as Record) : 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