f16ef4e103
Querying Order.fulfillmentOrders.deliveryMethod requires the
read_merchant_managed_fulfillment_orders scope (not read_orders, despite
what shopify.dev claims) and was failing with 'Access denied for
fulfillmentOrders field' against real stores.
Adding that scope would force every install to re-grant permissions, so
instead we rely on shippingLine alone:
- Shopify Local Pickup app: shippingLine.code = 'Pickup' (caught by the
regex) and shippingLine.title is the chosen location name itself (e.g. 'Lager Graz') \u2014 perfect as pickupLocationName.
- Custom-rate pickup ('Abholung im Lager'): regex matches title/code,
title is used as the location hint.
Removes RawDeliveryMethod, the deliveryMethods field on RawOrderForInvoice,
and the fulfillmentOrders edges from RawAdminResponse.
190 lines
5.1 KiB
TypeScript
190 lines
5.1 KiB
TypeScript
import type { AdminApiContext } from "@shopify/shopify-app-react-router/server";
|
|
|
|
import type {
|
|
RawAddress,
|
|
RawLineItem,
|
|
RawMoney,
|
|
RawOrderForInvoice,
|
|
RawTaxLine,
|
|
} from "./loadOrderForInvoice.server";
|
|
|
|
/**
|
|
* Loads a Shopify DraftOrder and adapts it to the same `RawOrderForInvoice`
|
|
* shape used for completed orders, so the rest of the pipeline (composer,
|
|
* PDF, etc.) doesn't need to know whether it's rendering an invoice or an
|
|
* offer.
|
|
*
|
|
* Drafts have no `processedAt` (we use createdAt) and no
|
|
* `displayFinancialStatus` (we treat them as not paid).
|
|
*/
|
|
const QUERY = `#graphql
|
|
query DraftOrderForOffer($id: ID!) {
|
|
draftOrder(id: $id) {
|
|
id
|
|
name
|
|
createdAt
|
|
currencyCode
|
|
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 } }
|
|
}
|
|
lineItems(first: 250) {
|
|
edges {
|
|
node {
|
|
title
|
|
sku
|
|
quantity
|
|
originalUnitPriceSet { 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?: {
|
|
draftOrder?: {
|
|
id: string;
|
|
name: string;
|
|
createdAt: string;
|
|
currencyCode: string;
|
|
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[];
|
|
lineItems: { edges: { node: RawLineItem & { image?: { url: string | null } | null } }[] };
|
|
purchasingEntity: {
|
|
company?: { name: string } | null;
|
|
location?: {
|
|
taxRegistrationId: string | null;
|
|
billingAddress: RawAddress | null;
|
|
} | null;
|
|
} | null;
|
|
} | null;
|
|
};
|
|
}
|
|
|
|
export async function loadDraftOrderForOffer(
|
|
admin: AdminApiContext,
|
|
draftOrderGid: string,
|
|
): Promise<RawOrderForInvoice> {
|
|
const response = await admin.graphql(QUERY, { variables: { id: draftOrderGid } });
|
|
const json = (await response.json()) as RawAdminResponse;
|
|
const draft = json.data?.draftOrder;
|
|
if (!draft) {
|
|
throw new Error(`Draft order ${draftOrderGid} not found.`);
|
|
}
|
|
|
|
const purchasingCompany = draft.purchasingEntity?.company
|
|
? {
|
|
name: draft.purchasingEntity.company.name,
|
|
vatId: draft.purchasingEntity.location?.taxRegistrationId ?? null,
|
|
address: draft.purchasingEntity.location?.billingAddress ?? null,
|
|
}
|
|
: null;
|
|
|
|
// Drafts don't have a numeric "order number" — use a hash of the GID as a
|
|
// numeric proxy for the invoice-counter signature (not actually used when
|
|
// generating offers, but kept non-zero to satisfy downstream code).
|
|
const orderNumber = parseInt(draft.id.replace(/[^0-9]/g, "").slice(-9), 10) || 0;
|
|
|
|
return {
|
|
id: draft.id,
|
|
name: draft.name,
|
|
orderNumber,
|
|
createdAt: draft.createdAt,
|
|
processedAt: null,
|
|
currencyCode: draft.currencyCode,
|
|
displayFinancialStatus: null,
|
|
paymentGatewayNames: [],
|
|
shippingLine: null,
|
|
fulfillments: [],
|
|
discountCodes: [],
|
|
taxesIncluded: draft.taxesIncluded,
|
|
customer: draft.customer,
|
|
billingAddress: draft.billingAddress,
|
|
shippingAddress: draft.shippingAddress,
|
|
subtotalSet: draft.subtotalPriceSet,
|
|
totalTaxSet: draft.totalTaxSet,
|
|
totalPriceSet: draft.totalPriceSet,
|
|
taxLines: draft.taxLines || [],
|
|
lineItems: (draft.lineItems?.edges || []).map((e) => {
|
|
const node = e.node;
|
|
return {
|
|
title: node.title,
|
|
sku: node.sku,
|
|
quantity: node.quantity,
|
|
originalUnitPriceSet: node.originalUnitPriceSet,
|
|
discountedUnitPriceSet: null,
|
|
taxLines: node.taxLines,
|
|
imageUrl: node.image?.url ?? null,
|
|
};
|
|
}),
|
|
purchasingEntity: { company: purchasingCompany },
|
|
};
|
|
}
|