fix(invoice): drop fulfillmentOrders query (scope-denied), keep shippingLine pickup heuristic

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.
This commit is contained in:
Gerhard Scheikl
2026-05-15 15:04:16 +02:00
parent d742e75419
commit f16ef4e103
5 changed files with 209 additions and 78 deletions
+17 -16
View File
@@ -487,11 +487,18 @@ function mapTracking(order: RawOrderForInvoice): TrackingInfo[] {
}
/**
* Detects whether the order is a "local pickup" order. Primary signal is
* Shopify's `DeliveryMethodType` on the fulfillment order (`PICK_UP`),
* which is what the Shopify Local Pickup app sets. Falls back to a string
* heuristic on `shippingLine.source/code/title` for merchants who model
* pickup as a custom shipping rate named "Abholung"/"Pickup".
* Detects whether the order is a "local pickup" order. Pickup is detected
* heuristically from `shippingLine.{source,code,title,carrierIdentifier}`:
*
* - The Shopify Local Pickup app sets `shippingLine.code = "Pickup"` and
* uses the chosen pickup-location name as the shipping-line title — so
* the title doubles as the location name.
* - Merchants who model pickup as a custom shipping rate typically include
* "Abholung"/"Pickup" in the title or code.
*
* (We deliberately do NOT query `Order.fulfillmentOrders.deliveryMethod`
* here: that field requires the `read_merchant_managed_fulfillment_orders`
* scope, which would force every install to re-grant permissions.)
*
* Returns the pickup descriptor (with location name when known) or `null`
* when the order is a normal shipping order. Callers should not render the
@@ -500,23 +507,17 @@ function mapTracking(order: RawOrderForInvoice): TrackingInfo[] {
function detectPickup(
order: RawOrderForInvoice,
): { locationName: string | null } | null {
// Primary: DeliveryMethodType from fulfillment orders.
for (const dm of order.deliveryMethods ?? []) {
if (dm.methodType === "PICK_UP") {
return { locationName: dm.locationName };
}
}
// Fallback: legacy string heuristic on shippingLine.
const sl = order.shippingLine;
if (!sl) return null;
const haystack = [sl.source, sl.code, sl.title, sl.carrierIdentifier]
.filter(Boolean)
.join(" ")
.toLowerCase();
if (/pick[\s-]?up|abholung|abhol\b/.test(haystack)) {
return { locationName: sl.title?.trim() || null };
}
return null;
if (!/pick[\s-]?up|abholung|abhol\b/.test(haystack)) return null;
// For Shopify Local Pickup, `title` is the location name itself
// (e.g. "Lager Graz"). For custom-rate pickup ("Abholung im Lager"),
// it's a generic description — still better than nothing as a hint.
return { locationName: sl.title?.trim() || null };
}
/**
@@ -163,7 +163,6 @@ export async function loadDraftOrderForOffer(
paymentGatewayNames: [],
shippingLine: null,
fulfillments: [],
deliveryMethods: [],
discountCodes: [],
taxesIncluded: draft.taxesIncluded,
customer: draft.customer,
@@ -25,11 +25,6 @@ export interface RawOrderForInvoice {
taxLines: RawTaxLine[];
shippingLine: RawShippingLine | null;
fulfillments: RawFulfillment[];
/** Delivery methods declared on the order's fulfillment orders. Used to
* reliably detect local pickup (`methodType === "PICK_UP"`) and to
* surface the pickup-location name. May be empty for unfulfilled or
* digital orders. */
deliveryMethods: RawDeliveryMethod[];
/** 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. */
@@ -107,17 +102,6 @@ export interface RawFulfillment {
trackingInfo: RawTrackingInfo[];
}
/** Subset of Shopify's `DeliveryMethod` we care about. `methodType` is one
* of the enum values from `DeliveryMethodType` — e.g. `SHIPPING`,
* `PICK_UP`, `LOCAL`, `RETAIL`, `PICKUP_POINT`, `NONE`. */
export interface RawDeliveryMethod {
methodType: string | null;
/** Name of the location the customer chose to pick up from (when
* `methodType === "PICK_UP"`). Comes from the assigned location of the
* fulfillment order. */
locationName: string | null;
}
const QUERY = `#graphql
query OrderForInvoice($id: ID!) {
order(id: $id) {
@@ -189,18 +173,6 @@ const QUERY = `#graphql
company
}
}
fulfillmentOrders(first: 20) {
edges {
node {
deliveryMethod {
methodType
}
assignedLocation {
name
}
}
}
}
lineItems(first: 250) {
edges {
node {
@@ -268,14 +240,6 @@ interface RawAdminResponse {
discountCodes: string[] | null;
shippingLine: RawShippingLine | null;
fulfillments: RawFulfillment[] | null;
fulfillmentOrders: {
edges: {
node: {
deliveryMethod: { methodType: string | null } | null;
assignedLocation: { name: string | null } | null;
};
}[];
} | null;
lineItems: { edges: { node: RawLineItem }[] };
purchasingEntity: {
company?: { name: string } | null;
@@ -329,10 +293,6 @@ export async function loadOrderForInvoice(
: (order.discountCode ? [order.discountCode] : []),
shippingLine: order.shippingLine ?? null,
fulfillments: order.fulfillments ?? [],
deliveryMethods: (order.fulfillmentOrders?.edges ?? []).map((e) => ({
methodType: e.node.deliveryMethod?.methodType ?? null,
locationName: e.node.assignedLocation?.name ?? null,
})),
lineItems: (order.lineItems?.edges || []).map((e) => {
const node = e.node as unknown as RawLineItem & { image?: { url: string | null } | null };
return {
+187
View File
@@ -0,0 +1,187 @@
[2026-05-15T12:49:01.100Z] [shopify-app/INFO] Authenticating admin request | {shop: linumiq-dev.myshopify.com}
2026-05-15T12:49:01.351Z GET /app/invoices?embedded=1&hmac=aeac0281934ae196632ce4fd34138c825bc9deac1b11a5cb574575642eabf411&host=YWRtaW4uc2hvcGlmeS5jb20vc3RvcmUvbGludW1pcS1kZXY&id_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczpcL1wvbGludW1pcS1kZXYubXlzaG9waWZ5LmNvbVwvYWRtaW4iLCJkZXN0IjoiaHR0cHM6XC9cL2xpbnVtaXEtZGV2Lm15c2hvcGlmeS5jb20iLCJhdWQiOiJmYmMyNjNlNmNjMjhlOGRlMDMxODc4ZDJhMGYxNzQ0NCIsInN1YiI6IjE1ODc3NzQ0MjY1MyIsImV4cCI6MTc3ODg0OTQwMCwibmJmIjoxNzc4ODQ5MzQwLCJpYXQiOjE3Nzg4NDkzNDAsImp0aSI6IjkwMjg1NzAzLWQ2MTItNDMwNi1hMTA4LTgzYmYwMDA3MDY2MiIsInNpZCI6IjViOGM4NTExLWRiYTYtNDA1ZS1iYTJmLTc5NDg1Y2FmMDc4ZCIsInNpZyI6IjhiYjY4ZTM0MWVhMjlkZmQ1Njk5MTM0MTVhMGYwNmI1ZGQ5ZGE1MDY1NjkzNDQyMzhhZmI4MmQyYWVlMTg2YjQifQ.EMjRzLUIfXNAVQVCm23ue9xWsAkTp8CpmYayXNoHC5E&locale=de&session=82e350749722d1b0f547f4fd9e3a1fd2fc4906fdf369de5e3f79c556918bd8c8&shop=linumiq-dev.myshopify.com&timestamp=1778849340 200 - - 243.303 ms
[2026-05-15T12:49:07.062Z] [shopify-app/INFO] Authenticating admin request | {shop: null}
[2026-05-15T12:49:07.197Z] invoice action failed: GraphqlQueryError: Access denied for fulfillmentOrders field.
at throwFailedRequest (file:///app/node_modules/@shopify/shopify-api/dist/esm/lib/clients/common.mjs:88:15)
at NewGraphqlClient.request (file:///app/node_modules/@shopify/shopify-api/dist/esm/lib/clients/admin/graphql/client.mjs:70:13)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Object.query [as graphql] (file:///app/node_modules/@shopify/shopify-app-react-router/dist/esm/server/clients/admin/graphql.mjs:13:33)
at async loadOrderForInvoice (file:///app/build/server/index.js:862:20)
at async cancelAndReissue (file:///app/build/server/index.js:2530:17)
at async action$8 (file:///app/build/server/index.js:2688:23)
at async callRouteHandler (file:///app/node_modules/react-router/dist/development/chunk-YQSHRJWW.mjs:510:16)
at async file:///app/node_modules/react-router/dist/development/chunk-EVOBXE3Y.mjs:4865:19
at async callLoaderOrAction (file:///app/node_modules/react-router/dist/development/chunk-EVOBXE3Y.mjs:4917:16) {
response: Response {
status: 200,
statusText: 'OK',
headers: Headers {
date: 'Fri, 15 May 2026 12:49:07 GMT',
'content-type': 'application/json; charset=utf-8',
'transfer-encoding': 'chunked',
connection: 'keep-alive',
vary: 'Accept-Encoding,Sec-Fetch-Site',
'referrer-policy': 'origin-when-cross-origin',
'x-frame-options': 'DENY',
'cf-cache-status': 'DYNAMIC',
'x-stats-userid': '',
'x-stats-apiclientid': '353980645377',
'x-stats-apipermissionid': '1113553207645',
'x-shopify-api-version': '2025-10',
'content-language': 'en',
'alt-svc': 'h3=":443"; ma=86400',
'x-shopify-api-gql-engine': 'cardinal',
'x-shopify-api-deprecated-reason': 'Customer.email, CompanyLocation.taxRegistrationId',
'x-dc': 'gcp-europe-west1,gcp-europe-west1,gcp-europe-west1',
'x-request-id': 'e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347',
'content-encoding': 'gzip',
'strict-transport-security': 'max-age=7889238',
'server-timing': 'processing;dur=60, verdict_flag_enabled;desc="count=17";dur=0.547, graphql;desc="admin/query/other", _y;desc="1854685c-042f-4004-8d99-aa1349d9bf91", _s;desc="1bd42fd1-27d2-4dff-956a-6017e1e9b055"',
'report-to': '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=cT1vdtPL8i9M%2FCcvCkoDggc0C0ku0pMRXbl%2Fj%2BqAS2O3X2VlNrUzAftOH1%2FhVpeHezy52cHOWcubmklVVFcvMaydnp6QbPWQTIBeqCrEnVY6%2BHmsFLxzDmHla1huEymdNkDT3LFSrsWxayo%3D"}]}',
server: 'cloudflare',
nel: '{"report_to":"cf-nel","success_fraction":0.01,"max_age":604800}',
'content-security-policy': "default-src 'self' data: blob: 'unsafe-inline' 'unsafe-eval' https://* shopify-pos://*; block-all-mixed-content; child-src 'self' https://* shopify-pos://*; connect-src 'self' wss://* https://*; frame-ancestors 'none'; img-src 'self' data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.pci.shopifyinc.com https://checkout.pci.shopifyinc.com/build/04ed4e1/card_fields.js https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com 'self' 'unsafe-inline' 'unsafe-eval'; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347; report-to shopify-csp",
'x-content-type-options': 'nosniff',
'x-download-options': 'noopen',
'x-permitted-cross-domain-policies': 'none',
'x-xss-protection': '1; mode=block',
'reporting-endpoints': 'shopify-csp="/csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347"',
'cf-ray': '9fc242c358803830-FRA'
},
body: ReadableStream { locked: true, state: 'closed', supportsBYOB: true },
bodyUsed: true,
ok: true,
redirected: false,
type: 'basic',
url: 'https://linumiq-dev.myshopify.com/admin/api/2025-10/graphql.json'
},
headers: {
'Alt-Svc': [ 'h3=":443"; ma=86400' ],
'Cf-Cache-Status': [ 'DYNAMIC' ],
'Cf-Ray': [ '9fc242c358803830-FRA' ],
Connection: [ 'keep-alive' ],
'Content-Encoding': [ 'gzip' ],
'Content-Language': [ 'en' ],
'Content-Security-Policy': [
"default-src 'self' data: blob: 'unsafe-inline' 'unsafe-eval' https://* shopify-pos://*; block-all-mixed-content; child-src 'self' https://* shopify-pos://*; connect-src 'self' wss://* https://*; frame-ancestors 'none'; img-src 'self' data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.pci.shopifyinc.com https://checkout.pci.shopifyinc.com/build/04ed4e1/card_fields.js https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com 'self' 'unsafe-inline' 'unsafe-eval'; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347; report-to shopify-csp"
],
'Content-Type': [ 'application/json; charset=utf-8' ],
Date: [ 'Fri, 15 May 2026 12:49:07 GMT' ],
Nel: [
'{"report_to":"cf-nel","success_fraction":0.01,"max_age":604800}'
],
'Referrer-Policy': [ 'origin-when-cross-origin' ],
'Report-To': [
'{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=cT1vdtPL8i9M%2FCcvCkoDggc0C0ku0pMRXbl%2Fj%2BqAS2O3X2VlNrUzAftOH1%2FhVpeHezy52cHOWcubmklVVFcvMaydnp6QbPWQTIBeqCrEnVY6%2BHmsFLxzDmHla1huEymdNkDT3LFSrsWxayo%3D"}]}'
],
'Reporting-Endpoints': [
'shopify-csp="/csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347"'
],
Server: [ 'cloudflare' ],
'Server-Timing': [
'processing;dur=60, verdict_flag_enabled;desc="count=17";dur=0.547, graphql;desc="admin/query/other", _y;desc="1854685c-042f-4004-8d99-aa1349d9bf91", _s;desc="1bd42fd1-27d2-4dff-956a-6017e1e9b055"'
],
'Strict-Transport-Security': [ 'max-age=7889238' ],
'Transfer-Encoding': [ 'chunked' ],
Vary: [ 'Accept-Encoding,Sec-Fetch-Site' ],
'X-Content-Type-Options': [ 'nosniff' ],
'X-Dc': [ 'gcp-europe-west1,gcp-europe-west1,gcp-europe-west1' ],
'X-Download-Options': [ 'noopen' ],
'X-Frame-Options': [ 'DENY' ],
'X-Permitted-Cross-Domain-Policies': [ 'none' ],
'X-Request-Id': [ 'e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347' ],
'X-Shopify-Api-Deprecated-Reason': [ 'Customer.email, CompanyLocation.taxRegistrationId' ],
'X-Shopify-Api-Gql-Engine': [ 'cardinal' ],
'X-Shopify-Api-Version': [ '2025-10' ],
'X-Stats-Apiclientid': [ '353980645377' ],
'X-Stats-Apipermissionid': [ '1113553207645' ],
'X-Stats-Userid': [ '' ],
'X-Xss-Protection': [ '1; mode=block' ]
},
body: {
data: { order: null },
extensions: { cost: [Object] },
headers: Headers {
date: 'Fri, 15 May 2026 12:49:07 GMT',
'content-type': 'application/json; charset=utf-8',
'transfer-encoding': 'chunked',
connection: 'keep-alive',
vary: 'Accept-Encoding,Sec-Fetch-Site',
'referrer-policy': 'origin-when-cross-origin',
'x-frame-options': 'DENY',
'cf-cache-status': 'DYNAMIC',
'x-stats-userid': '',
'x-stats-apiclientid': '353980645377',
'x-stats-apipermissionid': '1113553207645',
'x-shopify-api-version': '2025-10',
'content-language': 'en',
'alt-svc': 'h3=":443"; ma=86400',
'x-shopify-api-gql-engine': 'cardinal',
'x-shopify-api-deprecated-reason': 'Customer.email, CompanyLocation.taxRegistrationId',
'x-dc': 'gcp-europe-west1,gcp-europe-west1,gcp-europe-west1',
'x-request-id': 'e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347',
'content-encoding': 'gzip',
'strict-transport-security': 'max-age=7889238',
'server-timing': 'processing;dur=60, verdict_flag_enabled;desc="count=17";dur=0.547, graphql;desc="admin/query/other", _y;desc="1854685c-042f-4004-8d99-aa1349d9bf91", _s;desc="1bd42fd1-27d2-4dff-956a-6017e1e9b055"',
'report-to': '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=cT1vdtPL8i9M%2FCcvCkoDggc0C0ku0pMRXbl%2Fj%2BqAS2O3X2VlNrUzAftOH1%2FhVpeHezy52cHOWcubmklVVFcvMaydnp6QbPWQTIBeqCrEnVY6%2BHmsFLxzDmHla1huEymdNkDT3LFSrsWxayo%3D"}]}',
server: 'cloudflare',
nel: '{"report_to":"cf-nel","success_fraction":0.01,"max_age":604800}',
'content-security-policy': "default-src 'self' data: blob: 'unsafe-inline' 'unsafe-eval' https://* shopify-pos://*; block-all-mixed-content; child-src 'self' https://* shopify-pos://*; connect-src 'self' wss://* https://*; frame-ancestors 'none'; img-src 'self' data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.pci.shopifyinc.com https://checkout.pci.shopifyinc.com/build/04ed4e1/card_fields.js https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com 'self' 'unsafe-inline' 'unsafe-eval'; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347; report-to shopify-csp",
'x-content-type-options': 'nosniff',
'x-download-options': 'noopen',
'x-permitted-cross-domain-policies': 'none',
'x-xss-protection': '1; mode=block',
'reporting-endpoints': 'shopify-csp="/csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347"',
'cf-ray': '9fc242c358803830-FRA'
},
errors: {
networkStatusCode: 200,
message: "GraphQL Client: An error occurred while fetching from the API. Review 'graphQLErrors' for details.",
graphQLErrors: [Array],
response: Response {
status: 200,
statusText: 'OK',
headers: Headers {
date: 'Fri, 15 May 2026 12:49:07 GMT',
'content-type': 'application/json; charset=utf-8',
'transfer-encoding': 'chunked',
connection: 'keep-alive',
vary: 'Accept-Encoding,Sec-Fetch-Site',
'referrer-policy': 'origin-when-cross-origin',
'x-frame-options': 'DENY',
'cf-cache-status': 'DYNAMIC',
'x-stats-userid': '',
'x-stats-apiclientid': '353980645377',
'x-stats-apipermissionid': '1113553207645',
'x-shopify-api-version': '2025-10',
'content-language': 'en',
'alt-svc': 'h3=":443"; ma=86400',
'x-shopify-api-gql-engine': 'cardinal',
'x-shopify-api-deprecated-reason': 'Customer.email, CompanyLocation.taxRegistrationId',
'x-dc': 'gcp-europe-west1,gcp-europe-west1,gcp-europe-west1',
'x-request-id': 'e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347',
'content-encoding': 'gzip',
'strict-transport-security': 'max-age=7889238',
'server-timing': 'processing;dur=60, verdict_flag_enabled;desc="count=17";dur=0.547, graphql;desc="admin/query/other", _y;desc="1854685c-042f-4004-8d99-aa1349d9bf91", _s;desc="1bd42fd1-27d2-4dff-956a-6017e1e9b055"',
'report-to': '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=cT1vdtPL8i9M%2FCcvCkoDggc0C0ku0pMRXbl%2Fj%2BqAS2O3X2VlNrUzAftOH1%2FhVpeHezy52cHOWcubmklVVFcvMaydnp6QbPWQTIBeqCrEnVY6%2BHmsFLxzDmHla1huEymdNkDT3LFSrsWxayo%3D"}]}',
server: 'cloudflare',
nel: '{"report_to":"cf-nel","success_fraction":0.01,"max_age":604800}',
'content-security-policy': "default-src 'self' data: blob: 'unsafe-inline' 'unsafe-eval' https://* shopify-pos://*; block-all-mixed-content; child-src 'self' https://* shopify-pos://*; connect-src 'self' wss://* https://*; frame-ancestors 'none'; img-src 'self' data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.pci.shopifyinc.com https://checkout.pci.shopifyinc.com/build/04ed4e1/card_fields.js https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com 'self' 'unsafe-inline' 'unsafe-eval'; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347; report-to shopify-csp",
'x-content-type-options': 'nosniff',
'x-download-options': 'noopen',
'x-permitted-cross-domain-policies': 'none',
'x-xss-protection': '1; mode=block',
'reporting-endpoints': 'shopify-csp="/csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=e39980c7-30a7-48ad-a982-f6ba6240fb0e-1778849347"',
'cf-ray': '9fc242c358803830-FRA'
},
body: ReadableStream { locked: true, state: 'closed', supportsBYOB: true },
bodyUsed: true,
ok: true,
redirected: false,
type: 'basic',
url: 'https://linumiq-dev.myshopify.com/admin/api/2025-10/graphql.json'
}
}
}
}
2026-05-15T12:49:07.201Z POST /api/orders/17307298627933/invoice.data 400 - - 138.897 ms
+5 -21
View File
@@ -137,7 +137,6 @@ function buildAtB2BOrder(): RawOrderForInvoice {
paymentGatewayNames: ["manual"],
taxesIncluded: false,
discountCodes: [],
deliveryMethods: [],
customer: {
firstName: "Lukas",
lastName: "Schmidhofer",
@@ -299,18 +298,14 @@ function buildDiscountedOrder(): RawOrderForInvoice {
function buildPickupOrder(): RawOrderForInvoice {
const o = buildAtB2BOrder();
o.shippingLine = {
title: "Local Pickup — Lager Graz",
code: "PICKUP",
source: "shopify-local-pickup",
title: "Lager Graz",
code: "Pickup",
source: "shopify",
carrierIdentifier: null,
originalPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
discountedPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
taxLines: [],
};
// Primary signal — what Shopify Local Pickup actually populates.
o.deliveryMethods = [
{ methodType: "PICK_UP", locationName: "Lager Graz" },
];
return o;
}
@@ -598,8 +593,8 @@ async function main() {
settings: settings as never,
invoiceNumber: "RE-1030",
});
assert("isPickup detected via DeliveryMethodType=PICK_UP", pickupVm.isPickup);
assertEq("pickupLocationName propagated", pickupVm.pickupLocationName, "Lager Graz");
assert("isPickup detected via shippingLine heuristic", pickupVm.isPickup);
assertEq("pickupLocationName propagated from shippingLine.title", pickupVm.pickupLocationName, "Lager Graz");
assert("shippingMethod cleared for pickup (renderer uses pickup row instead)",
pickupVm.shippingMethod == null);
assert(
@@ -616,17 +611,6 @@ async function main() {
!pickupText.includes("Lieferadresse"),
);
// Shopify-Local-Pickup-app fallback: methodType missing but shippingLine
// is named "Pickup at …".
const legacyPickupOrder = buildPickupOrder();
legacyPickupOrder.deliveryMethods = [];
const legacyPickupVm = composeInvoice({
order: legacyPickupOrder,
settings: settings as never,
invoiceNumber: "RE-1032",
});
assert("legacy heuristic still detects pickup from shippingLine", legacyPickupVm.isPickup);
// EN translation
const pickupEnVm = composeInvoice({
order: pickupOrder,