import { assertNoUserErrors, clampInt, nodesFromConnection, pageInfoFromConnection, shopifyGraphql, trimString, } from './lib/client.ts' import { CUSTOMER_FIELDS, PAGE_INFO_FIELDS } from './lib/fragments.ts' import { toGid } from './lib/shop.ts' import type { Money, ShopifyClientOptions, ShopifyPageInfo } from './types.ts' export type CustomerSummary = { id: string displayName: string | null firstName: string | null lastName: string | null email: string | null phone: string | null createdAt: string | null updatedAt: string | null numberOfOrders: string | number | null amountSpent: Money | null defaultAddress: { id: string | null formatted: Array city: string | null province: string | null country: string | null zip: string | null } | null } export type ListCustomersParams = ShopifyClientOptions & { query?: string first?: number after?: string } function mapMoney(value: unknown): Money | null { if (!value || typeof value !== 'object') return null const amount = trimString((value as { amount?: unknown }).amount) const currencyCode = trimString( (value as { currencyCode?: unknown }).currencyCode, ) if (!amount || !currencyCode) return null return { amount, currencyCode } } function mapCustomer(node: Record): CustomerSummary { const address = node.defaultAddress && typeof node.defaultAddress === 'object' ? (node.defaultAddress as Record) : null return { id: String(node.id || ''), displayName: typeof node.displayName === 'string' ? node.displayName : null, firstName: typeof node.firstName === 'string' ? node.firstName : null, lastName: typeof node.lastName === 'string' ? node.lastName : null, email: typeof node.email === 'string' ? node.email : null, phone: typeof node.phone === 'string' ? node.phone : null, createdAt: typeof node.createdAt === 'string' ? node.createdAt : null, updatedAt: typeof node.updatedAt === 'string' ? node.updatedAt : null, numberOfOrders: typeof node.numberOfOrders === 'number' || typeof node.numberOfOrders === 'string' ? node.numberOfOrders : null, amountSpent: mapMoney(node.amountSpent), defaultAddress: address ? { id: typeof address.id === 'string' ? address.id : null, formatted: Array.isArray(address.formatted) ? address.formatted.map(String) : [], city: typeof address.city === 'string' ? address.city : null, province: typeof address.province === 'string' ? address.province : null, country: typeof address.country === 'string' ? address.country : null, zip: typeof address.zip === 'string' ? address.zip : null, } : null, } } /** * List customers with optional Shopify search syntax. * * @example * import { listCustomers } from 'kody:@kody/shopify/customers' * const { items } = await listCustomers({ shop: 'acme', query: 'email:ada@example.com' }) */ export async function listCustomers(params: ListCustomersParams = {}) { const first = clampInt(params.first, 1, 50, 20) const data = await shopifyGraphql<{ customers: unknown }>({ shop: params.shop, apiVersion: params.apiVersion, query: `query ListCustomers($first: Int!, $after: String, $query: String) { customers(first: $first, after: $after, query: $query) { ${PAGE_INFO_FIELDS} nodes { ${CUSTOMER_FIELDS} } } }`, variables: { first, after: trimString(params.after) || null, query: trimString(params.query) || null, }, }) return { items: nodesFromConnection>(data.customers).map( mapCustomer, ), pageInfo: pageInfoFromConnection(data.customers) as ShopifyPageInfo, } } /** * Get one customer by GID or numeric id. */ export async function getCustomer( params: ShopifyClientOptions & { id: string }, ): Promise { const id = toGid('Customer', params.id) const data = await shopifyGraphql<{ customer: Record | null }>({ shop: params.shop, apiVersion: params.apiVersion, query: `query GetCustomer($id: ID!) { customer(id: $id) { ${CUSTOMER_FIELDS} } }`, variables: { id }, }) if (!data.customer) throw new Error(`Customer not found: ${id}`) return mapCustomer(data.customer) } export type CreateCustomerParams = ShopifyClientOptions & { email?: string firstName?: string lastName?: string phone?: string note?: string dryRun?: boolean } /** * Create a customer. Defaults to dry-run; pass `dryRun: false` to write. */ export async function createCustomer(params: CreateCustomerParams) { const input = { email: trimString(params.email) || undefined, firstName: trimString(params.firstName) || undefined, lastName: trimString(params.lastName) || undefined, phone: trimString(params.phone) || undefined, note: trimString(params.note) || undefined, } if (!input.email && !input.phone) { throw new Error('createCustomer requires email or phone.') } if (params.dryRun !== false) { return { dryRun: true as const, wouldCall: { mutation: 'customerCreate', input }, } } const data = await shopifyGraphql<{ customerCreate: unknown }>({ shop: params.shop, apiVersion: params.apiVersion, query: `mutation CreateCustomer($input: CustomerInput!) { customerCreate(input: $input) { customer { ${CUSTOMER_FIELDS} } userErrors { field message } } }`, variables: { input }, }) assertNoUserErrors(data.customerCreate, 'customerCreate') const created = (data.customerCreate as { customer?: Record }) .customer if (!created) throw new Error('customerCreate returned no customer.') return mapCustomer(created) } /** Default export: list customers. */ export default listCustomers