fix(invoice): detect pickup via DeliveryMethodType and show 'Abholort: <location>' meta row
- Use Order.fulfillmentOrders.deliveryMethod.methodType === 'PICK_UP' as the primary signal (Shopify Local Pickup app exposes this reliably; the shippingLine title is just the location name with no 'pickup' keyword). Keep the legacy shippingLine string heuristic as a fallback for custom shipping rates merchants name 'Abholung'/'Pickup'. - Surface assignedLocation.name as pickupLocationName on the view model. - Replace the 'Versandart: <location name>' row with 'Abholort: <location>' (DE) / 'Pick-up location: <location>' (EN); falls back to plain 'Abholung'/'Pick-up' when the location name is unavailable.
This commit is contained in:
@@ -62,10 +62,14 @@ export function composeInvoice({
|
||||
});
|
||||
let notices = deriveNotices({ order, settings, isB2B });
|
||||
|
||||
const isPickup = detectPickup(order.shippingLine);
|
||||
const pickupInfo = detectPickup(order);
|
||||
const isPickup = pickupInfo != null;
|
||||
const separateShippingAddress = isPickup ? undefined : mapSeparateShippingAddress(order);
|
||||
// For shipping orders we surface the carrier label (e.g. "Standardversand").
|
||||
// For pickup orders the meta row uses a different label entirely
|
||||
// ("Abholort: <location>") — see the renderer.
|
||||
const shippingMethod = isPickup
|
||||
? strings.pickupLabel
|
||||
? undefined
|
||||
: order.shippingLine?.title?.trim() || undefined;
|
||||
const tracking = mapTracking(order);
|
||||
|
||||
@@ -135,6 +139,7 @@ export function composeInvoice({
|
||||
tracking,
|
||||
discountCodes: order.discountCodes ?? [],
|
||||
isPickup,
|
||||
pickupLocationName: pickupInfo?.locationName ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -482,24 +487,36 @@ function mapTracking(order: RawOrderForInvoice): TrackingInfo[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristically detects whether the order's shipping line is a "local
|
||||
* pickup" line. Shopify exposes pickup either via the dedicated Local
|
||||
* Pickup app (source contains "pickup") or via a custom rate the merchant
|
||||
* named "Abholung"/"Pickup". When detected, callers should NOT render the
|
||||
* 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".
|
||||
*
|
||||
* Returns the pickup descriptor (with location name when known) or `null`
|
||||
* when the order is a normal shipping order. Callers should not render the
|
||||
* pickup-location address as a separate "delivery address".
|
||||
*/
|
||||
function detectPickup(shippingLine: RawShippingLine | null): boolean {
|
||||
if (!shippingLine) return false;
|
||||
const haystack = [
|
||||
shippingLine.source,
|
||||
shippingLine.code,
|
||||
shippingLine.title,
|
||||
shippingLine.carrierIdentifier,
|
||||
]
|
||||
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();
|
||||
return /pick[\s-]?up|abholung|abhol\b/.test(haystack);
|
||||
if (/pick[\s-]?up|abholung|abhol\b/.test(haystack)) {
|
||||
return { locationName: sl.title?.trim() || null };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,9 @@ export interface InvoiceStrings {
|
||||
shippingItemPrefix: string;
|
||||
discountCodeLabel: string;
|
||||
pickupLabel: string;
|
||||
/** Used as the meta-row label when the order is a local pickup. The row
|
||||
* value is then the pickup location name (e.g. "Lager Graz"). */
|
||||
pickupLocationLabel: string;
|
||||
}
|
||||
|
||||
/** Status displayed for the order's payment, derived from Shopify's
|
||||
@@ -167,6 +170,7 @@ const de: InvoiceStrings = {
|
||||
shippingItemPrefix: "Versand",
|
||||
discountCodeLabel: "Rabattcode",
|
||||
pickupLabel: "Abholung",
|
||||
pickupLocationLabel: "Abholort",
|
||||
};
|
||||
|
||||
const en: InvoiceStrings = {
|
||||
@@ -236,6 +240,7 @@ const en: InvoiceStrings = {
|
||||
shippingItemPrefix: "Shipping",
|
||||
discountCodeLabel: "Discount code",
|
||||
pickupLabel: "Pick-up",
|
||||
pickupLocationLabel: "Pick-up location",
|
||||
};
|
||||
|
||||
// Locale → invoice language. We only render in German (`de`) when the
|
||||
|
||||
@@ -163,6 +163,7 @@ export async function loadDraftOrderForOffer(
|
||||
paymentGatewayNames: [],
|
||||
shippingLine: null,
|
||||
fulfillments: [],
|
||||
deliveryMethods: [],
|
||||
discountCodes: [],
|
||||
taxesIncluded: draft.taxesIncluded,
|
||||
customer: draft.customer,
|
||||
|
||||
@@ -25,6 +25,11 @@ 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. */
|
||||
@@ -102,6 +107,17 @@ 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) {
|
||||
@@ -173,6 +189,18 @@ const QUERY = `#graphql
|
||||
company
|
||||
}
|
||||
}
|
||||
fulfillmentOrders(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
deliveryMethod {
|
||||
methodType
|
||||
}
|
||||
assignedLocation {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lineItems(first: 250) {
|
||||
edges {
|
||||
node {
|
||||
@@ -240,6 +268,14 @@ 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;
|
||||
@@ -293,6 +329,10 @@ 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 {
|
||||
|
||||
@@ -329,7 +329,14 @@ export function InvoiceDocument({ invoice }: DocProps) {
|
||||
<Text style={styles.metaValue}>{invoice.discountCodes.join(", ")}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{invoice.kind === "invoice" && invoice.shippingMethod ? (
|
||||
{invoice.kind === "invoice" && invoice.isPickup ? (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.pickupLocationLabel}</Text>
|
||||
<Text style={styles.metaValue}>
|
||||
{invoice.pickupLocationName ?? t.pickupLabel}
|
||||
</Text>
|
||||
</View>
|
||||
) : invoice.kind === "invoice" && invoice.shippingMethod ? (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.shippingMethodLabel}</Text>
|
||||
<Text style={styles.metaValue}>{invoice.shippingMethod}</Text>
|
||||
|
||||
@@ -70,6 +70,10 @@ export interface InvoiceViewModel {
|
||||
/** True when the customer chose local pickup (so we shouldn't render the
|
||||
* pickup-location address as a "delivery address"). */
|
||||
isPickup: boolean;
|
||||
|
||||
/** Name of the pickup location (e.g. "Lager Graz"). Set only when
|
||||
* `isPickup` is true and the location name was available. */
|
||||
pickupLocationName?: string;
|
||||
}
|
||||
|
||||
export interface TrackingInfo {
|
||||
|
||||
Reference in New Issue
Block a user