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
+38
View File
@@ -136,6 +136,7 @@ function buildAtB2BOrder(): RawOrderForInvoice {
displayFinancialStatus: "PENDING",
paymentGatewayNames: ["manual"],
taxesIncluded: false,
requiresShipping: true,
discountCodes: [],
customer: {
firstName: "Lukas",
@@ -329,6 +330,28 @@ function buildCategoryOnlyPickupOrder(): RawOrderForInvoice {
return o;
}
/** Pickup variant matching a REAL observed order on
* linumiq-dev.myshopify.com (#1032): Shopify's built-in "Shop location"
* rate. NO "pickup" string anywhere, deliveryCategory is `null`, and
* shippingAddress is also `null` — detection must rely on
* `requiresShipping && shippingAddress == null`. */
function buildShopLocationPickupOrder(): RawOrderForInvoice {
const o = buildAtB2BOrder();
o.shippingLine = {
title: "Shop location",
code: "Shop location",
source: "shopify",
carrierIdentifier: null,
deliveryCategory: null,
originalPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
discountedPriceSet: { shopMoney: { amount: "0.00", currencyCode: "EUR" } },
taxLines: [],
};
o.shippingAddress = null;
o.requiresShipping = true;
return o;
}
// ------------------------------------------------------------------
// Run assertions
// ------------------------------------------------------------------
@@ -655,6 +678,21 @@ async function main() {
assert("shippingMethod cleared in category-only pickup",
categoryPickupVm.shippingMethod == null);
// Real-world "Shop location" pickup (matches dev order #1032): no
// "pickup" keyword anywhere, deliveryCategory null, shippingAddress null.
// The only signal is `requiresShipping && !shippingAddress`.
const shopLocPickupVm = composeInvoice({
order: buildShopLocationPickupOrder(),
settings: settings as never,
invoiceNumber: "RE-1034",
});
assert("isPickup detected from missing shippingAddress (Shop location rate)",
shopLocPickupVm.isPickup);
assertEq("pickupLocationName from shippingLine.title for Shop location",
shopLocPickupVm.pickupLocationName, "Shop location");
assert("shippingMethod cleared for Shop location pickup",
shopLocPickupVm.shippingMethod == null);
// Fallback: when footerNoteEn is empty, English uses the German note.
console.log("• Footer note fallback (en → de when EN empty)");
const settingsNoEn = { ...(settings as object), footerNoteEn: "" } as never;