Skip to content

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

Package listing

@kody/shopify

src/orders.ts

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

export type OrderLineItem = {
	id: string
	title: string | null
	sku: string | null
	quantity: number | null
	originalUnitPrice: Money | null
}

export type OrderSummary = {
	id: string
	name: string | null
	email: string | null
	createdAt: string | null
	updatedAt: string | null
	displayFinancialStatus: string | null
	displayFulfillmentStatus: string | null
	note: string | null
	tags: Array<string>
	currentTotalPrice: Money | null
	customer: {
		id: string
		displayName: string | null
		email: string | null
	} | null
	lineItems: Array<OrderLineItem>
}

export type ListOrdersParams = ShopifyClientOptions & {
	/** Shopify search query, e.g. `financial_status:paid fulfillment_status:unfulfilled`. */
	query?: string
	first?: number
	after?: string
}

function mapLineItem(node: Record<string, unknown>): OrderLineItem {
	return {
		id: String(node.id || ''),
		title: typeof node.title === 'string' ? node.title : null,
		sku: typeof node.sku === 'string' ? node.sku : null,
		quantity: typeof node.quantity === 'number' ? node.quantity : null,
		originalUnitPrice: moneyFromSet(node.originalUnitPriceSet),
	}
}

function mapOrder(node: Record<string, unknown>): OrderSummary {
	const customer =
		node.customer && typeof node.customer === 'object'
			? (node.customer as Record<string, unknown>)
			: null
	return {
		id: String(node.id || ''),
		name: typeof node.name === 'string' ? node.name : 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,
		displayFinancialStatus:
			typeof node.displayFinancialStatus === 'string'
				? node.displayFinancialStatus
				: null,
		displayFulfillmentStatus:
			typeof node.displayFulfillmentStatus === 'string'
				? node.displayFulfillmentStatus
				: null,
		note: typeof node.note === 'string' ? node.note : null,
		tags: Array.isArray(node.tags) ? node.tags.map(String) : [],
		currentTotalPrice: moneyFromSet(node.currentTotalPriceSet),
		customer: customer
			? {
					id: String(customer.id || ''),
					displayName:
						typeof customer.displayName === 'string'
							? customer.displayName
							: null,
					email: typeof customer.email === 'string' ? customer.email : null,
				}
			: null,
		lineItems: nodesFromConnection<Record<string, unknown>>(node.lineItems).map(
			mapLineItem,
		),
	}
}

/**
 * List orders with optional Shopify search syntax.
 *
 * @example
 * import { listOrders } from 'kody:@kody/shopify/orders'
 * const { items } = await listOrders({
 *   shop: 'acme',
 *   query: 'fulfillment_status:unfulfilled',
 *   first: 10,
 * })
 */
export async function listOrders(params: ListOrdersParams = {}) {
	const first = clampInt(params.first, 1, 50, 20)
	const data = await shopifyGraphql<{ orders: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query ListOrders($first: Int!, $after: String, $query: String) {
			orders(first: $first, after: $after, query: $query, sortKey: CREATED_AT, reverse: true) {
				${PAGE_INFO_FIELDS}
				nodes { ${ORDER_FIELDS} }
			}
		}`,
		variables: {
			first,
			after: trimString(params.after) || null,
			query: trimString(params.query) || null,
		},
	})
	return {
		items: nodesFromConnection<Record<string, unknown>>(data.orders).map(
			mapOrder,
		),
		pageInfo: pageInfoFromConnection(data.orders) as ShopifyPageInfo,
	}
}

/**
 * Get one order by GID or numeric id.
 */
export async function getOrder(
	params: ShopifyClientOptions & { id: string },
): Promise<OrderSummary> {
	const id = toGid('Order', params.id)
	const data = await shopifyGraphql<{ order: Record<string, unknown> | null }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query GetOrder($id: ID!) {
			order(id: $id) { ${ORDER_FIELDS} }
		}`,
		variables: { id },
	})
	if (!data.order) throw new Error(`Order not found: ${id}`)
	return mapOrder(data.order)
}

/** Default export: list orders. */
export default listOrders