Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/shopify

src/draft-orders.ts

181 lines · 4.7 KB · TypeScript
import {
	assertNoUserErrors,
	clampInt,
	moneyFromSet,
	nodesFromConnection,
	pageInfoFromConnection,
	shopifyGraphql,
	trimString,
} from './lib/client.ts'
import { MONEY_SET_FIELDS, PAGE_INFO_FIELDS } from './lib/fragments.ts'
import { toGid } from './lib/shop.ts'
import type { Money, ShopifyClientOptions, ShopifyPageInfo } from './types.ts'

export type DraftOrderSummary = {
	id: string
	name: string | null
	status: string | null
	email: string | null
	createdAt: string | null
	updatedAt: string | null
	totalPrice: Money | null
	invoiceUrl: string | null
}

function mapDraft(node: Record<string, unknown>): DraftOrderSummary {
	return {
		id: String(node.id || ''),
		name: typeof node.name === 'string' ? node.name : null,
		status: typeof node.status === 'string' ? node.status : null,
		email: typeof node.email === 'string' ? node.email : null,
		createdAt: typeof node.createdAt === 'string' ? node.createdAt : null,
		updatedAt: typeof node.updatedAt === 'string' ? node.updatedAt : null,
		totalPrice: moneyFromSet(node.totalPriceSet),
		invoiceUrl: typeof node.invoiceUrl === 'string' ? node.invoiceUrl : null,
	}
}

/**
 * List draft orders.
 *
 * @example
 * import { listDraftOrders } from 'kody:@kody/shopify/draft-orders'
 * const { items } = await listDraftOrders({ shop: 'acme', query: 'status:open' })
 */
export async function listDraftOrders(
	params: ShopifyClientOptions & {
		query?: string
		first?: number
		after?: string
	} = {},
) {
	const first = clampInt(params.first, 1, 50, 20)
	const data = await shopifyGraphql<{ draftOrders: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query ListDraftOrders($first: Int!, $after: String, $query: String) {
			draftOrders(first: $first, after: $after, query: $query) {
				${PAGE_INFO_FIELDS}
				nodes {
					id
					name
					status
					email
					createdAt
					updatedAt
					invoiceUrl
					totalPriceSet { ${MONEY_SET_FIELDS} }
				}
			}
		}`,
		variables: {
			first,
			after: trimString(params.after) || null,
			query: trimString(params.query) || null,
		},
	})
	return {
		items: nodesFromConnection<Record<string, unknown>>(data.draftOrders).map(
			mapDraft,
		),
		pageInfo: pageInfoFromConnection(data.draftOrders) as ShopifyPageInfo,
	}
}

/**
 * Get one draft order by GID or numeric id.
 */
export async function getDraftOrder(
	params: ShopifyClientOptions & { id: string },
): Promise<DraftOrderSummary> {
	const id = toGid('DraftOrder', params.id)
	const data = await shopifyGraphql<{
		draftOrder: Record<string, unknown> | null
	}>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query GetDraftOrder($id: ID!) {
			draftOrder(id: $id) {
				id
				name
				status
				email
				createdAt
				updatedAt
				invoiceUrl
				totalPriceSet { ${MONEY_SET_FIELDS} }
			}
		}`,
		variables: { id },
	})
	if (!data.draftOrder) throw new Error(`Draft order not found: ${id}`)
	return mapDraft(data.draftOrder)
}

export type CreateDraftOrderParams = ShopifyClientOptions & {
	email?: string
	note?: string
	lineItems: Array<{
		title?: string
		originalUnitPrice?: string
		quantity: number
		variantId?: string
	}>
	dryRun?: boolean
}

/**
 * Create a draft order. Defaults to dry-run; pass `dryRun: false` to write.
 */
export async function createDraftOrder(params: CreateDraftOrderParams) {
	if (!params.lineItems?.length) {
		throw new Error('createDraftOrder requires at least one line item.')
	}
	const input = {
		email: trimString(params.email) || undefined,
		note: trimString(params.note) || undefined,
		lineItems: params.lineItems.map((item) => ({
			title: trimString(item.title) || undefined,
			originalUnitPrice: trimString(item.originalUnitPrice) || undefined,
			quantity: item.quantity,
			variantId: trimString(item.variantId)
				? toGid('ProductVariant', item.variantId)
				: undefined,
		})),
	}
	if (params.dryRun !== false) {
		return {
			dryRun: true as const,
			wouldCall: { mutation: 'draftOrderCreate', input },
		}
	}
	const data = await shopifyGraphql<{ draftOrderCreate: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `mutation CreateDraftOrder($input: DraftOrderInput!) {
			draftOrderCreate(input: $input) {
				draftOrder {
					id
					name
					status
					email
					createdAt
					updatedAt
					invoiceUrl
					totalPriceSet { ${MONEY_SET_FIELDS} }
				}
				userErrors { field message }
			}
		}`,
		variables: { input },
	})
	assertNoUserErrors(data.draftOrderCreate, 'draftOrderCreate')
	const created = (
		data.draftOrderCreate as { draftOrder?: Record<string, unknown> }
	).draftOrder
	if (!created) throw new Error('draftOrderCreate returned no draft order.')
	return mapDraft(created)
}

/** Default export: list draft orders. */
export default listDraftOrders