From f16ef4e1032438f7588230a9a92e8500c3199204 Mon Sep 17 00:00:00 2001 From: Gerhard Scheikl Date: Fri, 15 May 2026 15:04:16 +0200 Subject: [PATCH] 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. --- app/services/invoice/composeInvoice.ts | 33 ++-- .../invoice/loadDraftOrderForOffer.server.ts | 1 - .../invoice/loadOrderForInvoice.server.ts | 40 ---- log.txt | 187 ++++++++++++++++++ scripts/render-sample.ts | 26 +-- 5 files changed, 209 insertions(+), 78 deletions(-) create mode 100644 log.txt diff --git a/app/services/invoice/composeInvoice.ts b/app/services/invoice/composeInvoice.ts index bc136c6..255dcb2 100644 --- a/app/services/invoice/composeInvoice.ts +++ b/app/services/invoice/composeInvoice.ts @@ -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 }; } /** diff --git a/app/services/invoice/loadDraftOrderForOffer.server.ts b/app/services/invoice/loadDraftOrderForOffer.server.ts index 837c815..e01f00f 100644 --- a/app/services/invoice/loadDraftOrderForOffer.server.ts +++ b/app/services/invoice/loadDraftOrderForOffer.server.ts @@ -163,7 +163,6 @@ export async function loadDraftOrderForOffer( paymentGatewayNames: [], shippingLine: null, fulfillments: [], - deliveryMethods: [], discountCodes: [], taxesIncluded: draft.taxesIncluded, customer: draft.customer, diff --git a/app/services/invoice/loadOrderForInvoice.server.ts b/app/services/invoice/loadOrderForInvoice.server.ts index fb70c15..c3bc7a5 100644 --- a/app/services/invoice/loadOrderForInvoice.server.ts +++ b/app/services/invoice/loadOrderForInvoice.server.ts @@ -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 { diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..c2a43c1 --- /dev/null +++ b/log.txt @@ -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 diff --git a/scripts/render-sample.ts b/scripts/render-sample.ts index c75bc98..6963ef5 100644 --- a/scripts/render-sample.ts +++ b/scripts/render-sample.ts @@ -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,