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:
@@ -487,11 +487,18 @@ function mapTracking(order: RawOrderForInvoice): TrackingInfo[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects whether the order is a "local pickup" order. Primary signal is
|
* Detects whether the order is a "local pickup" order. Pickup is detected
|
||||||
* Shopify's `DeliveryMethodType` on the fulfillment order (`PICK_UP`),
|
* heuristically from `shippingLine.{source,code,title,carrierIdentifier}`:
|
||||||
* which is what the Shopify Local Pickup app sets. Falls back to a string
|
*
|
||||||
* heuristic on `shippingLine.source/code/title` for merchants who model
|
* - The Shopify Local Pickup app sets `shippingLine.code = "Pickup"` and
|
||||||
* pickup as a custom shipping rate named "Abholung"/"Pickup".
|
* 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`
|
* Returns the pickup descriptor (with location name when known) or `null`
|
||||||
* when the order is a normal shipping order. Callers should not render the
|
* when the order is a normal shipping order. Callers should not render the
|
||||||
@@ -500,24 +507,18 @@ function mapTracking(order: RawOrderForInvoice): TrackingInfo[] {
|
|||||||
function detectPickup(
|
function detectPickup(
|
||||||
order: RawOrderForInvoice,
|
order: RawOrderForInvoice,
|
||||||
): { locationName: string | null } | null {
|
): { 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;
|
const sl = order.shippingLine;
|
||||||
if (!sl) return null;
|
if (!sl) return null;
|
||||||
const haystack = [sl.source, sl.code, sl.title, sl.carrierIdentifier]
|
const haystack = [sl.source, sl.code, sl.title, sl.carrierIdentifier]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
if (/pick[\s-]?up|abholung|abhol\b/.test(haystack)) {
|
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 };
|
return { locationName: sl.title?.trim() || null };
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks the delivery date for §11 UStG: the latest fulfillment timestamp
|
* Picks the delivery date for §11 UStG: the latest fulfillment timestamp
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ export async function loadDraftOrderForOffer(
|
|||||||
paymentGatewayNames: [],
|
paymentGatewayNames: [],
|
||||||
shippingLine: null,
|
shippingLine: null,
|
||||||
fulfillments: [],
|
fulfillments: [],
|
||||||
deliveryMethods: [],
|
|
||||||
discountCodes: [],
|
discountCodes: [],
|
||||||
taxesIncluded: draft.taxesIncluded,
|
taxesIncluded: draft.taxesIncluded,
|
||||||
customer: draft.customer,
|
customer: draft.customer,
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ export interface RawOrderForInvoice {
|
|||||||
taxLines: RawTaxLine[];
|
taxLines: RawTaxLine[];
|
||||||
shippingLine: RawShippingLine | null;
|
shippingLine: RawShippingLine | null;
|
||||||
fulfillments: RawFulfillment[];
|
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
|
/** Discount codes applied to the cart (e.g. `["SUMMER10"]`). Empty when
|
||||||
* no codes were used. Manual / automatic discounts without a code are
|
* no codes were used. Manual / automatic discounts without a code are
|
||||||
* not exposed here. */
|
* not exposed here. */
|
||||||
@@ -107,17 +102,6 @@ export interface RawFulfillment {
|
|||||||
trackingInfo: RawTrackingInfo[];
|
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
|
const QUERY = `#graphql
|
||||||
query OrderForInvoice($id: ID!) {
|
query OrderForInvoice($id: ID!) {
|
||||||
order(id: $id) {
|
order(id: $id) {
|
||||||
@@ -189,18 +173,6 @@ const QUERY = `#graphql
|
|||||||
company
|
company
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fulfillmentOrders(first: 20) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
deliveryMethod {
|
|
||||||
methodType
|
|
||||||
}
|
|
||||||
assignedLocation {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lineItems(first: 250) {
|
lineItems(first: 250) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@@ -268,14 +240,6 @@ interface RawAdminResponse {
|
|||||||
discountCodes: string[] | null;
|
discountCodes: string[] | null;
|
||||||
shippingLine: RawShippingLine | null;
|
shippingLine: RawShippingLine | null;
|
||||||
fulfillments: RawFulfillment[] | null;
|
fulfillments: RawFulfillment[] | null;
|
||||||
fulfillmentOrders: {
|
|
||||||
edges: {
|
|
||||||
node: {
|
|
||||||
deliveryMethod: { methodType: string | null } | null;
|
|
||||||
assignedLocation: { name: string | null } | null;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} | null;
|
|
||||||
lineItems: { edges: { node: RawLineItem }[] };
|
lineItems: { edges: { node: RawLineItem }[] };
|
||||||
purchasingEntity: {
|
purchasingEntity: {
|
||||||
company?: { name: string } | null;
|
company?: { name: string } | null;
|
||||||
@@ -329,10 +293,6 @@ export async function loadOrderForInvoice(
|
|||||||
: (order.discountCode ? [order.discountCode] : []),
|
: (order.discountCode ? [order.discountCode] : []),
|
||||||
shippingLine: order.shippingLine ?? null,
|
shippingLine: order.shippingLine ?? null,
|
||||||
fulfillments: order.fulfillments ?? [],
|
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) => {
|
lineItems: (order.lineItems?.edges || []).map((e) => {
|
||||||
const node = e.node as unknown as RawLineItem & { image?: { url: string | null } | null };
|
const node = e.node as unknown as RawLineItem & { image?: { url: string | null } | null };
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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×tamp=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
|
||||||
@@ -137,7 +137,6 @@ function buildAtB2BOrder(): RawOrderForInvoice {
|
|||||||
paymentGatewayNames: ["manual"],
|
paymentGatewayNames: ["manual"],
|
||||||
taxesIncluded: false,
|
taxesIncluded: false,
|
||||||
discountCodes: [],
|
discountCodes: [],
|
||||||
deliveryMethods: [],
|
|
||||||
customer: {
|
customer: {
|
||||||
firstName: "Lukas",
|
firstName: "Lukas",
|
||||||
lastName: "Schmidhofer",
|
lastName: "Schmidhofer",
|
||||||
@@ -299,18 +298,14 @@ function buildDiscountedOrder(): RawOrderForInvoice {
|
|||||||
function buildPickupOrder(): RawOrderForInvoice {
|
function buildPickupOrder(): RawOrderForInvoice {
|
||||||
const o = buildAtB2BOrder();
|
const o = buildAtB2BOrder();
|
||||||
o.shippingLine = {
|
o.shippingLine = {
|
||||||
title: "Local Pickup — Lager Graz",
|
title: "Lager Graz",
|
||||||
code: "PICKUP",
|
code: "Pickup",
|
||||||
source: "shopify-local-pickup",
|
source: "shopify",
|
||||||
carrierIdentifier: null,
|
carrierIdentifier: null,
|
||||||
originalPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
|
originalPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
|
||||||
discountedPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
|
discountedPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
|
||||||
taxLines: [],
|
taxLines: [],
|
||||||
};
|
};
|
||||||
// Primary signal — what Shopify Local Pickup actually populates.
|
|
||||||
o.deliveryMethods = [
|
|
||||||
{ methodType: "PICK_UP", locationName: "Lager Graz" },
|
|
||||||
];
|
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,8 +593,8 @@ async function main() {
|
|||||||
settings: settings as never,
|
settings: settings as never,
|
||||||
invoiceNumber: "RE-1030",
|
invoiceNumber: "RE-1030",
|
||||||
});
|
});
|
||||||
assert("isPickup detected via DeliveryMethodType=PICK_UP", pickupVm.isPickup);
|
assert("isPickup detected via shippingLine heuristic", pickupVm.isPickup);
|
||||||
assertEq("pickupLocationName propagated", pickupVm.pickupLocationName, "Lager Graz");
|
assertEq("pickupLocationName propagated from shippingLine.title", pickupVm.pickupLocationName, "Lager Graz");
|
||||||
assert("shippingMethod cleared for pickup (renderer uses pickup row instead)",
|
assert("shippingMethod cleared for pickup (renderer uses pickup row instead)",
|
||||||
pickupVm.shippingMethod == null);
|
pickupVm.shippingMethod == null);
|
||||||
assert(
|
assert(
|
||||||
@@ -616,17 +611,6 @@ async function main() {
|
|||||||
!pickupText.includes("Lieferadresse"),
|
!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
|
// EN translation
|
||||||
const pickupEnVm = composeInvoice({
|
const pickupEnVm = composeInvoice({
|
||||||
order: pickupOrder,
|
order: pickupOrder,
|
||||||
|
|||||||
Reference in New Issue
Block a user