Skip to content
Tyler Marshall

Safe reorder routing for transformed bundles

A merchant needs customers to reorder ordinary camping accessories without replaying transformed set components as loose, incorrectly priced merchandise.

Known limits

Selection state
Protected sets reopen as a clean configurator and never restore their historical selections.
Account surface
The order action is available only in Shopify's new customer accounts, not classic account templates.
Handoff trust
The URL fragment handoff is validated but not authenticated, so it cannot carry privileged variants, prices, or discounts.
Availability
Catalog availability checked on the order page is not an inventory reservation and can change before Cart Ajax runs.
Storefront origin
Configurator destinations must share the primary storefront origin.

Original reference implementation. Not client source code.

Why a Customer Account order action

A commerce Function has no role on an authenticated historical order page. Native reorder can replay merchandise lines, but it does not know that transformed set components require a carrier and configurator contract. A theme can receive safe cart additions, yet it should not be the first place where private order attributes are interpreted.

The Customer Account order action owns that decision. Shopify supports paired menu item and modal targets with Order API access, so the extension can choose a configurator link, an ordinary item replay, an explanatory modal, or no action. A small theme adapter performs only the final cart mutation on the same origin. The operational constraints are on the plate above.

Design

One claimed marker protects its complete native order group, including malformed groups that must never fall through to ordinary replay.

src/orderClassifier.ts
export function classifyGroup(group: OrderGroup): GroupDecision {
  const claimed = group.lines.some((line) => line.setMarker !== null);
  if (!claimed) return { kind: "replayable", lines: group.lines };
 
  const parsed = group.lines.map((line) => ({
    line,
    marker: parseSetMarker(line.setMarker),
  }));
  if (parsed.some((item) => !item.marker.ok)) {
    return { kind: "blocked", reason: "marker_invalid" };
  }
 
  const receipt = assembleReceipt(parsed);
  if (!receipt.ok || !matchesProtectedSet(group, receipt.value)) {
    return { kind: "blocked", reason: "protected_set_invalid" };
  }
 
  return { kind: "protected", groupId: group.id };
}

Ordinary lines keep visible properties, lose every private property, and retain variant IDs as strings.

src/replayPayload.ts
export function prepareReplayLine(line: OrderLine): ReplayLineResult {
  if (!line.purchasable || !/^\d+$/.test(line.variantId)) {
    return { ok: false, reason: "variant_unavailable" };
  }
  if (!Number.isInteger(line.quantity) || line.quantity < 1 || line.quantity > 99) {
    return { ok: false, reason: "quantity_invalid" };
  }
 
  const properties: Record<string, string> = {};
  for (const [rawKey, rawValue] of Object.entries(line.properties)) {
    const key = rawKey.trim();
    if (!key || key.startsWith("_")) continue;
    if (key.length > 120 || rawValue.length > 500) {
      return { ok: false, reason: "property_too_large" };
    }
    properties[key] = rawValue;
  }
 
  return { ok: true, item: {
    id: line.variantId,
    quantity: line.quantity,
    properties: sortProperties(properties),
  } };
}

The action policy refuses partial replay when protected, blocked, unavailable, and ordinary content would give one order several meanings.

src/actionPolicy.ts
export function selectOrderAction(order: PreparedOrder): OrderAction {
  if (order.blockedCount > 0 || order.unavailableCount > 0) {
    return { kind: "modal" };
  }
 
  const hasProtected = order.protectedGroups.length > 0;
  const hasOrdinary = order.replay !== null;
  if (hasProtected && hasOrdinary) return { kind: "modal" };
 
  if (order.protectedGroups.length === 1 && order.builderHref) {
    return { kind: "configure", href: order.builderHref };
  }
  if (!hasProtected && order.replay) {
    return { kind: "replay", fragment: order.replay.fragment };
  }
  return order.protectedGroups.length > 0
    ? { kind: "modal" }
    : { kind: "none" };
}

The storefront clears the fragment before decoding and independently validates the payload before one Cart Ajax call.

src/handoff.ts
export async function consumeHandoff(
  hash: string,
  browser: BrowserPort,
  cart: CartPort,
): Promise<HandoffResult> {
  const encoded = readReplayFragment(hash);
  if (encoded === null) return { kind: "absent" };
 
  browser.clearFragment();
  if (encoded.length > 8_000) return { kind: "invalid" };
 
  const decoded = decodeBase64UrlJson(encoded);
  if (!decoded.ok) return { kind: "invalid" };
 
  const replay = parseReplayPayload(decoded.value);
  if (!replay.ok) return { kind: "invalid" };
 
  const response = await cart.add({ items: replay.value.items });
  return response.ok
    ? { kind: "added", itemCount: replay.value.items.length }
    : { kind: "failed" };
}

What the tests cover

The suite runs without a store and covers complete and malformed protected groups, native group claim propagation, duplicate identities, current availability, private property removal, aggregation by visible properties, lossless large IDs, payload guards, URLs confined to the storefront origin, broken pagination, action selection, Unicode encoding, fragment clearing, independent handoff validation, and cart transport failure. Generated fixtures enforce the central invariant that no protected or blocked variant ID can appear in a replay payload.

This test pins the deliberate clean configurator result and shows that historical selections never enter the destination.

src/orderClassifier.test.ts
it("opens a clean builder for a historical protected set", () => {
  const group = protectedGroup("trip44", [
    component("stove", { Finish: "Blue" }),
    component("cookware", { Material: "Steel" }),
    component("storage"),
  ]);
 
  const order = prepareOrder([group], {
    builderUrl: "/pages/camp-kitchen-builder",
    storefrontOrigin: "https://merchant.example",
  });
  const action = selectOrderAction(order);
 
  expect(action).toEqual({
    kind: "configure",
    href: "https://merchant.example/pages/camp-kitchen-builder",
  });
  expect(JSON.stringify(action)).not.toContain("Blue");
  expect(JSON.stringify(action)).not.toContain("Steel");
});

Reading the source

Start with src/orderClassifier.ts for the closed claim boundary, then follow src/replayPayload.ts and src/actionPolicy.ts to the final action. Read src/handoff.ts as a separate trust boundary. The TSX menu and modal targets only render these decisions.

Want to see how this holds up in production?

The case studies are the same problems with a client, a deadline, and a legacy codebase attached.