Quick order that handles invalid and unavailable products clearly

Validate pasted or imported quick-order rows, preserve duplicate SKU rows, and match each server result to the buyer's original input.

Znode 9PatternIntermediateGCG engineering guide
In this guide
  1. Pasted rows
  2. Bounded parse
  3. Server-owned context
  4. Per-line validation
  5. Cart mutation
  6. Row results
Each submitted row passes through parsing and contextual validation before the server records accepted cart changes and returns a distinct outcome.

Define a row contract before designing the screen

A quick-order batch often mixes valid products, mistyped SKUs, invalid quantities, and items unavailable to the selected account. Keep each row visible throughout parsing and submission so the buyer can tell what needs correction and what was added.

Use a stable source-row number rather than SKU as the result key. The same SKU can appear twice intentionally. The browser helper below separates formatting errors from valid rows, then aligns the server's cart outcomes with those source rows. Product visibility, purchasing permission, and final quantity rules remain server decisions.

  • accepted: the server confirms that the line was added.
  • correctable: the submitted line requires buyer input.
  • unavailable: the product cannot be purchased or disclosed in this context.
  • Request-level failures, such as expired authorization, belong to the HTTP error path rather than this per-row outcome type.

Keep validation contextual and deterministic

Resolve Store, account, shopper, order type, currency, and unit of measure from trusted server context. The browser can check input shape before a request, but a SKU and quantity alone do not establish that a product can be purchased.

normalizeRows accepts parsed grid or CSV rows, trims SKU whitespace while preserving case, and returns valid rows plus formatting errors. The example caps a batch at 200 rows, a SKU at 80 characters, and quantity at one million. These are application policy choices. Show errors for buyer review before sending the valid rows.

After the cart request completes, pass its mapped results to mergeCartResults. It requires exactly one recognized outcome per submitted source-row number and restores input order. Your HTTP adapter translates the installed cart response into that small contract and keeps missing and hidden products under the same unavailable outcome.

quick-order-rows.ts typescript

export interface OrderRow { row: number; sku: string; quantity: number }
export interface RowError { row: number; reason: "sku" | "quantity" }
export type CartOutcome = "accepted" | "correctable" | "unavailable";
export interface RowResult extends OrderRow { outcome: CartOutcome }

function object(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value))
    throw new TypeError("Expected a row object");
  return value as Record<string, unknown>;
}

export function normalizeRows(input: unknown): {
  valid: OrderRow[]; errors: RowError[]
} {
  if (!Array.isArray(input) || input.length < 1 || input.length > 200)
    throw new RangeError("Submit 1..200 rows");
  const valid: OrderRow[] = [], errors: RowError[] = [];
  input.forEach((value, index) => {
    const item = object(value), row = index + 1;
    const sku = typeof item.sku === "string" ? item.sku.trim() : "";
    if (!sku || sku.length > 80 || /[\x00-\x1f\x7f]/.test(sku)) {
      errors.push({ row, reason: "sku" }); return;
    }
    const raw = item.quantity;
    const quantity = typeof raw === "number" ? raw :
      typeof raw === "string" && /^[0-9]+$/.test(raw.trim()) ?
      Number(raw.trim()) : NaN;
    if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 1000000) {
      errors.push({ row, reason: "quantity" }); return;
    }
    valid.push({ row, sku, quantity });
  });
  return { valid, errors };
}

// Call after the server cart operation has completed. Preserve duplicate SKU rows.
export function mergeCartResults(rows: readonly OrderRow[], input: unknown): RowResult[] {
  if (!Array.isArray(input) || input.length !== rows.length)
    throw new Error("Expected one result per submitted row");
  const expected = new Set(rows.map(row => row.row));
  if (expected.size !== rows.length) throw new Error("Duplicate submitted row");
  const outcomes = new Map<number, CartOutcome>();
  for (const value of input) {
    const item = object(value);
    if (typeof item.row !== "number" || !expected.has(item.row) || outcomes.has(item.row))
      throw new Error("Unexpected or duplicate result row");
    if (item.outcome !== "accepted" && item.outcome !== "correctable" &&
        item.outcome !== "unavailable") throw new Error("Unknown outcome");
    outcomes.set(item.row, item.outcome);
  }
  return rows.map(row => ({
    row: row.row, sku: row.sku, quantity: row.quantity,
    outcome: outcomes.get(row.row)!
  }));
}

For two rows containing the same SKU, normalizeRows keeps row 1 and row 2 separately. If the server returns row 2 before row 1, mergeCartResults still returns them in their original order. A missing row result, duplicate result, or unknown outcome raises an error instead of silently marking the batch complete.

Choose atomic behavior deliberately

Decide whether the cart operation accepts valid lines or rejects the entire batch. If partial acceptance is supported, explain that choice before submission and display the final result beside every row afterward. Do not infer partial success from a timeout.

When the server contract supports a durable batch or request key, retain it and read the operation back after an uncertain response. Reuse that identity for an authorized retry. The browser helpers shown here do not implement server-side deduplication or recovery.

Cart acceptance is also separate from order placement. Refresh the authoritative cart totals and complete the normal checkout, pricing, and inventory checks before describing any line as ordered.

  • Disable repeated submission while the batch is pending.
  • Keep failed rows in the editor so the buyer can correct and retry them.
  • Refresh cart summaries from the authoritative response.
  • Make a retry idempotent through a server-owned request key when supported.

Build the regression matrix

The regression matrix should include one valid row, a mixed batch, duplicate rows, bad quantity, unavailable item, discontinued item, a product hidden from the current account, an expired context, and a request that times out after the server may have committed. Test both paste and import paths if both exist. Verify direct API calls too, because UI validation is not a security boundary.

Use a fictional product catalog, availability threshold, and account rule in examples. Pair it with a mock availability service and a clear version label. Review the regression matrix whenever the cart policy or platform patch changes.

Znode's product settings include quantity limits and out-of-stock policy. Reapply those native rules after this input parser succeeds. The helper deliberately stops at parsing and result alignment; the version-specific server cart adapter supplies the final accepted, correctable, or unavailable outcome.

Make the result easy to act on

Place the result next to each entered line, not in a generic toast. Use text, icon, and status rather than color alone. Move focus to a summary after submission, then provide links that take keyboard users directly to rows needing correction. Announce how many lines were accepted and how many require action.

The buyer should be able to remove an invalid row, correct it, or continue with accepted rows without guessing. That transparency is the practical difference between a convenient quick-order tool and an unreliable bulk cart shortcut.

References and further reading

Bring your next engineering question.

Need a reliable B2B ordering workflow?

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Znode 9 quote edits: invalidate shipping and totals correctly

Next
Next

Connect an external product or artwork workflow without duplicating state