fix(locations): drop Shopify's literal "null" city from imported addresses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-16 07:17:43 -04:00
parent 0a979becc3
commit a5941a5149
3 changed files with 27 additions and 2 deletions

View File

@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { cleanAddress } from "../format";
describe("cleanAddress", () => {
it("drops Shopify's literal null city", () => {
expect(cleanAddress("null ON, Canada")).toBe("ON, Canada");
});
it("drops parts that were only null", () => {
expect(cleanAddress("null, Toronto, Canada")).toBe("Toronto, Canada");
});
it("leaves real addresses alone, including words containing null", () => {
expect(cleanAddress("12 Nullarbor Rd, Waterloo ON N2V 1A1, Canada")).toBe("12 Nullarbor Rd, Waterloo ON N2V 1A1, Canada");
});
});

View File

@ -30,3 +30,12 @@ export const STATUS_TONE: Record<string, "success" | "critical" | "neutral" | "i
export const METHOD_LABEL: Record<string, string> = { pickup: "Pickup", delivery: "Delivery", shipping: "Shipping" }; export const METHOD_LABEL: Record<string, string> = { pickup: "Pickup", delivery: "Delivery", shipping: "Shipping" };
export const rangesToText = (r?: { start: string; end: string }[]) => (r ?? []).map((x) => `${x.start}-${x.end}`).join(", "); export const rangesToText = (r?: { start: string; end: string }[]) => (r ?? []).map((x) => `${x.start}-${x.end}`).join(", ");
/** Shopify's formatted address prints a missing city as the word "null" (e.g. "null ON, Canada"). */
export function cleanAddress(address: string) {
return address
.split(",")
.map((part) => part.replace(/\bnull\b/g, "").replace(/\s+/g, " ").trim())
.filter(Boolean)
.join(", ");
}

View File

@ -4,6 +4,7 @@
import type { Location, PeakDay as PeakRow, ProductRule as RuleRow, Shop } from "@prisma/client"; import type { Location, PeakDay as PeakRow, ProductRule as RuleRow, Shop } from "@prisma/client";
import prisma from "../db.server"; import prisma from "../db.server";
import type { LocationConfig, Method, PeakDay, ProductRule } from "../engine/types"; import type { LocationConfig, Method, PeakDay, ProductRule } from "../engine/types";
import { cleanAddress } from "../lib/format";
import { restoreCheckoutFunctions } from "../lib/guard.server"; import { restoreCheckoutFunctions } from "../lib/guard.server";
import { type AppSettings, withDefaults } from "./settings"; import { type AppSettings, withDefaults } from "./settings";
@ -21,7 +22,7 @@ export function toLocationConfig(l: Location): LocationConfig {
id: l.id, id: l.id,
shopifyLocationId: l.shopifyLocationId ?? undefined, shopifyLocationId: l.shopifyLocationId ?? undefined,
name: l.name, name: l.name,
address: l.address, address: cleanAddress(l.address),
timezone: l.timezone, timezone: l.timezone,
lat: l.lat ?? undefined, lat: l.lat ?? undefined,
lng: l.lng ?? undefined, lng: l.lng ?? undefined,
@ -102,6 +103,7 @@ type AdminGraphql = { graphql: (q: string, o?: { variables?: Record<string, unkn
const DEFAULT_HOURS = { start: "10:00", end: "18:00" }; const DEFAULT_HOURS = { start: "10:00", end: "18:00" };
/** /**
* First install: create the shop row and import Shopify locations as pickup-ready * First install: create the shop row and import Shopify locations as pickup-ready
* (disabled until the merchant reviews hours). Re-install: clear uninstalledAt and restore checkout functions. * (disabled until the merchant reviews hours). Re-install: clear uninstalledAt and restore checkout functions.
@ -144,7 +146,7 @@ export async function ensureShop(domain: string, admin: AdminGraphql) {
.map((l, i) => ({ .map((l, i) => ({
shopifyLocationId: l.id, shopifyLocationId: l.id,
name: l.name, name: l.name,
address: l.address.formatted.join(", "), address: cleanAddress(l.address.formatted.join(", ")),
timezone: data.shop.ianaTimezone, timezone: data.shop.ianaTimezone,
lat: l.address.latitude, lat: l.address.latitude,
lng: l.address.longitude, lng: l.address.longitude,