import type { AdminApiContext } from "@shopify/shopify-app-react-router/server"; /** * Raw shape of the data we need from the Shopify Admin GraphQL API to * compose an invoice. Kept narrow so the composer is testable with fixtures. */ export interface RawOrderForInvoice { id: string; name: string; orderNumber: number; createdAt: string; processedAt: string | null; currencyCode: string; displayFinancialStatus: string | null; paymentGatewayNames: string[]; customer: { firstName: string | null; lastName: string | null; email: string | null; locale: string | null; } | null; billingAddress: RawAddress | null; shippingAddress: RawAddress | null; lineItems: RawLineItem[]; taxLines: RawTaxLine[]; shippingLine: RawShippingLine | null; fulfillments: RawFulfillment[]; /** Discount codes applied to the cart (e.g. `["SUMMER10"]`). Empty when * no codes were used. Manual / automatic discounts without a code are * not exposed here. */ discountCodes: string[]; taxesIncluded: boolean; subtotalSet: { shopMoney: RawMoney } | null; totalTaxSet: { shopMoney: RawMoney } | null; totalPriceSet: { shopMoney: RawMoney } | null; purchasingEntity: { company?: { name: string; vatId: string | null; address: RawAddress | null; } | null; } | null; } export interface RawAddress { name: string | null; company: string | null; address1: string | null; address2: string | null; zip: string | null; city: string | null; province: string | null; countryCode: string | null; } export interface RawMoney { amount: string; currencyCode: string; } export interface RawLineItem { title: string; sku: string | null; quantity: number; originalUnitPriceSet: { shopMoney: RawMoney }; /** Per-unit price after Shopify has allocated cart-level discounts to this * line. May be null when no discount applied (in which case use the * original price). */ discountedUnitPriceSet: { shopMoney: RawMoney } | null; taxLines: RawTaxLine[]; imageUrl: string | null; } export interface RawTaxLine { title: string | null; rate: number | null; ratePercentage: number | null; priceSet: { shopMoney: RawMoney }; } export interface RawShippingLine { title: string | null; code: string | null; source: string | null; carrierIdentifier: string | null; originalPriceSet: { shopMoney: RawMoney } | null; discountedPriceSet: { shopMoney: RawMoney } | null; taxLines: RawTaxLine[]; } export interface RawTrackingInfo { number: string | null; url: string | null; company: string | null; } export interface RawFulfillment { /** ISO timestamp of when the fulfillment was created (i.e. when the goods * were dispatched / handed over). Used for the legally-required delivery * date on the invoice when present. */ createdAt: string | null; trackingInfo: RawTrackingInfo[]; } const QUERY = `#graphql query OrderForInvoice($id: ID!) { order(id: $id) { id name number createdAt processedAt currencyCode displayFinancialStatus paymentGatewayNames taxesIncluded customer { firstName lastName email locale } billingAddress { name company address1 address2 zip city province countryCode: countryCodeV2 } shippingAddress { name company address1 address2 zip city province countryCode: countryCodeV2 } subtotalPriceSet { shopMoney { amount currencyCode } } totalTaxSet { shopMoney { amount currencyCode } } totalPriceSet { shopMoney { amount currencyCode } } taxLines { title rate ratePercentage priceSet { shopMoney { amount currencyCode } } } discountCode discountCodes shippingLine { title code source carrierIdentifier originalPriceSet { shopMoney { amount currencyCode } } discountedPriceSet { shopMoney { amount currencyCode } } taxLines { title rate ratePercentage priceSet { shopMoney { amount currencyCode } } } } fulfillments(first: 10) { createdAt trackingInfo { number url company } } lineItems(first: 250) { edges { node { title sku quantity originalUnitPriceSet { shopMoney { amount currencyCode } } discountedUnitPriceSet { shopMoney { amount currencyCode } } image { url altText } taxLines { title rate ratePercentage priceSet { shopMoney { amount currencyCode } } } } } } purchasingEntity { ... on PurchasingCompany { company { name } location { taxRegistrationId billingAddress { address1 address2 zip city countryCode } } } } } } `; interface RawAdminResponse { data?: { order?: { id: string; name: string; number: number; createdAt: string; processedAt: string | null; currencyCode: string; displayFinancialStatus: string | null; paymentGatewayNames: string[] | null; taxesIncluded: boolean; customer: { firstName: string | null; lastName: string | null; email: string | null; locale: string | null; } | null; billingAddress: RawAddress | null; shippingAddress: RawAddress | null; subtotalPriceSet: { shopMoney: RawMoney } | null; totalTaxSet: { shopMoney: RawMoney } | null; totalPriceSet: { shopMoney: RawMoney } | null; taxLines: RawTaxLine[]; discountCode: string | null; discountCodes: string[] | null; shippingLine: RawShippingLine | null; fulfillments: RawFulfillment[] | null; lineItems: { edges: { node: RawLineItem }[] }; purchasingEntity: { company?: { name: string } | null; location?: { taxRegistrationId: string | null; billingAddress: RawAddress | null; } | null; } | null; } | null; }; } export async function loadOrderForInvoice( admin: AdminApiContext, orderGid: string, ): Promise { const response = await admin.graphql(QUERY, { variables: { id: orderGid } }); const json = (await response.json()) as RawAdminResponse; const order = json.data?.order; if (!order) { throw new Error(`Order ${orderGid} not found.`); } const purchasingCompany = order.purchasingEntity?.company ? { name: order.purchasingEntity.company.name, vatId: order.purchasingEntity.location?.taxRegistrationId ?? null, address: order.purchasingEntity.location?.billingAddress ?? null, } : null; return { id: order.id, name: order.name, orderNumber: order.number, createdAt: order.createdAt, processedAt: order.processedAt, currencyCode: order.currencyCode, displayFinancialStatus: order.displayFinancialStatus, paymentGatewayNames: order.paymentGatewayNames ?? [], taxesIncluded: order.taxesIncluded, customer: order.customer, billingAddress: order.billingAddress, shippingAddress: order.shippingAddress, subtotalSet: order.subtotalPriceSet, totalTaxSet: order.totalTaxSet, totalPriceSet: order.totalPriceSet, taxLines: order.taxLines || [], discountCodes: order.discountCodes && order.discountCodes.length > 0 ? order.discountCodes : (order.discountCode ? [order.discountCode] : []), shippingLine: order.shippingLine ?? null, fulfillments: order.fulfillments ?? [], lineItems: (order.lineItems?.edges || []).map((e) => { const node = e.node as unknown as RawLineItem & { image?: { url: string | null } | null }; return { title: node.title, sku: node.sku, quantity: node.quantity, originalUnitPriceSet: node.originalUnitPriceSet, discountedUnitPriceSet: node.discountedUnitPriceSet ?? null, taxLines: node.taxLines, imageUrl: node.image?.url ?? null, }; }), purchasingEntity: { company: purchasingCompany }, }; }