Quick order that handles invalid and unavailable products clearly
Treat each uploaded or pasted quick-order row as its own validated outcome, while preserving the buyer's valid work and explaining what needs correction.
In this guide
Define a row contract before designing the screen
Quick order often starts as a textarea, CSV import, or SKU and quantity grid. The difficult case is mixed input: one row is valid, another SKU does not exist, another quantity breaks a package rule, and a fourth product is unavailable for the current buying context. A single all-or-nothing error wastes a buyer's valid work. A silent partial add is worse because it obscures what will actually be ordered.
Return a stable result for each submitted row. The result should preserve the row number, submitted display value, normalized product identity only when safe to show, requested quantity, outcome, human-readable reason, and next action. The server decides availability and permissions. The browser can help a user correct formatting, but cannot establish that an item can be purchased.
- Accepted: line was added with the validated quantity.
- Correctable: SKU, quantity, unit, or configuration requires input.
- Unavailable: the requested product cannot be purchased or disclosed in this context.
- Rejected: caller lacks permission or request limits were exceeded.
Keep validation contextual and deterministic
A SKU is not a complete buying request. The selected account, store, shopper, order type, currency, unit of measure, and quantity can change the answer. Resolve that context on the server from the authenticated session or controlled server-side selection. Do not accept an arbitrary account identifier from an import and use it as authorization.
Validate format and limits before expensive lookups, then resolve products in bounded batches. Apply the same package and quantity policy to quick order and ordinary cart updates so a buyer cannot reach inconsistent rules by choosing another UI. Use neutral examples such as packages of five rather than copying a customer's order-type behavior.
The TypeScript below accepts parsed CSV or grid rows as unknown input, preserves SKU case, and keeps duplicate SKUs as separate source rows. The 200-row, 80-character SKU, and one-million quantity caps are explicit sample policy choices. Invalid row formats return correction reasons; malformed batch structure rejects the request. The caller sends only valid rows after the buyer reviews the errors, and merges final cart results by stable row number rather than response order. Unavailable covers missing and hidden products without confirming their existence.
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)!
}));
}
Choose atomic behavior deliberately
Decide whether the endpoint accepts valid rows while returning failures, or rejects the entire batch. For B2B quick order, partial acceptance is often more useful, but it must be visible before the cart changes. Return a batch identifier and final per-line outcomes so an interrupted client can read back the result instead of resubmitting blindly.
If inventory or price is calculated asynchronously, distinguish initial acceptance from a later recalculation. Never label a product as successfully ordered merely because its text was parsed. An accepted cart change still needs the normal checkout, pricing, and inventory verification paths.
- 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?
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.