fix(invoice): detect pickup via missing shippingAddress (real signal for built-in 'Shop location' rate)

Reproduced against real dev order #1032: the built-in 'Shop location'
shipping rate sets neither a pickup keyword nor deliveryCategory:

  shippingLine: { title: 'Shop location', code: 'Shop location',
                  source: 'shopify', deliveryCategory: null }
  shippingAddress: null
  requiresShipping: true

So neither v2 (string regex on title/code) nor v3 (deliveryCategory)
caught it. The robust signal is 'requiresShipping && shippingAddress
== null': Shopify rejects checkout for a normal shipping order without
an address, so this combination is conclusive proof of pickup.

- Query Order.requiresShipping (only needs read_orders).
- detectPickup() now treats missing-address-but-requires-shipping as the
  primary signal; deliveryCategory + title/code regex remain as
  fallbacks for Local-Pickup-app installs and custom rates.
- New fixture buildShopLocationPickupOrder() in render-sample.ts
  mirrors order #1032 exactly so we never regress on this shape.
This commit is contained in:
Gerhard Scheikl
2026-05-15 15:24:43 +02:00
parent 720f508ec3
commit a2b3c14022
4 changed files with 75 additions and 26 deletions
@@ -13,6 +13,11 @@ export interface RawOrderForInvoice {
currencyCode: string;
displayFinancialStatus: string | null;
paymentGatewayNames: string[];
/** True when the order contains at least one shippable line item. For
* pickup orders this is `true` but `shippingAddress` is `null` — that
* combination is the most reliable pickup signal we have without
* hitting `read_merchant_managed_fulfillment_orders`. */
requiresShipping: boolean;
customer: {
firstName: string | null;
lastName: string | null;
@@ -118,6 +123,7 @@ const QUERY = `#graphql
displayFinancialStatus
paymentGatewayNames
taxesIncluded
requiresShipping
customer {
firstName
lastName
@@ -229,6 +235,7 @@ interface RawAdminResponse {
displayFinancialStatus: string | null;
paymentGatewayNames: string[] | null;
taxesIncluded: boolean;
requiresShipping: boolean | null;
customer: {
firstName: string | null;
lastName: string | null;
@@ -286,6 +293,7 @@ export async function loadOrderForInvoice(
displayFinancialStatus: order.displayFinancialStatus,
paymentGatewayNames: order.paymentGatewayNames ?? [],
taxesIncluded: order.taxesIncluded,
requiresShipping: order.requiresShipping ?? false,
customer: order.customer,
billingAddress: order.billingAddress,
shippingAddress: order.shippingAddress,