Get unstuck

Hint 1: The client sends identifiers, not amounts

Only sku and qty mean anything in a line. The price comes from shop.catalog, and qty must pass Number.isInteger and the product’s limit.

Hint 2: Every coupon rule is a separate check

At most one code; a known code; the threshold before the discount; one use per account through shop.redeem; and no redemption until the whole cart has passed.

Reveal the reference patch and explanation
Fixedpricing.mjsJavaScript
export async function priceOrder(order, shop) {
  const reject = reason => ({status: 'rejected', reason});
  const lines = order?.lines, codes = order?.coupons ?? [];
  if (!Array.isArray(lines) || lines.length < 1 || lines.length > 20) return reject('cart size');
  let subtotal = 0;
  const seen = new Set();
  for (const {sku, qty} of lines) {
    const product = typeof sku === 'string' && Object.hasOwn(shop.catalog, sku) ? shop.catalog[sku] : null;
    if (!product || seen.has(sku)) return reject('unknown or repeated sku');
    if (!Number.isInteger(qty) || qty < 1 || qty > product.maxQty) return reject('quantity');
    seen.add(sku);
    subtotal += product.price * qty;
  }
  if (!Array.isArray(codes) || codes.length > 1) return reject('one coupon per order');
  let discount = 0;
  if (codes.length === 1) {
    const code = codes[0];
    const coupon = typeof code === 'string' && Object.hasOwn(shop.coupons, code) ? shop.coupons[code] : null;
    if (!coupon) return reject('unknown coupon');
    if (subtotal >= coupon.minSubtotal) {
      if (!(await shop.redeem(code, order.account))) return reject('coupon already used');
      discount = Math.floor(subtotal * coupon.percentOff / 100);
    }
  }
  return {status: 'priced', subtotal, discount, total: subtotal - discount};
}

With catalog prices hdmi 999, mount 4,999 and tv-55 89,900 cents, a TV line claiming price 1 still costs 89,900, and a line without any price costs the catalog price. Quantities −89, 0, 1.5, "2" and 3 TVs (limit 2) are rejected, and two lines of 2 TVs are rejected as a repeated SKU, which would otherwise bypass the limit. FAN10 (10% from 20,000) gives 8,990 off a TV but nothing off two cables, and is not redeemed there, so the same account can still use it on a qualifying order. FAN10 plus WELCOME5, or FAN10 twice, is stacking and is rejected, as is an unknown code. WELCOME5 a second time on the same account is rejected because shop.redeem returns false; another account can use it. An order with a bad line is rejected before redemption, so the coupon stays available.

Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.

Inspect the actual test harness

The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.

Download cases.mjs
How the browser sandbox runs your code

Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.

The same as in current Node.js:

  • ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag

Added to the sandbox so that it behaves like Node.js:

  • structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
  • URL and URLSearchParams, parsed by your browser’s own URL implementation
  • TextEncoder and TextDecoder for UTF-8
  • Intl.Collator and a locale-aware String.prototype.localeCompare
  • console.log and related methods, whose output appears with your results

Not available in the browser sandbox:

  • Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
  • Node.js modules and globals: import, require, process and Buffer
  • Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
  • Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
  • using declarations, Symbol.dispose and DisposableStack (explicit resource management)
  • Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace

Use the lesson in your own system

The shop object models server data and an atomic redemption (a unique constraint on coupon and account in a database). The lab does not model currencies, taxes, stock reservation, refunds, payment capture or the workflow state machine, and it prices the order once; a real checkout recomputes the amount whenever the cart changes and again from the stored order before payment. Limits on how often a valuable flow can be used, such as purchase caps per account and payment card, are a separate control.

Your next task: Where does your checkout take an amount, a quantity or a discount from the request, and which database constraint would still stop a negative total if that code were wrong?

OWASP Web Security Testing Guide: business logic testing

Related guide: Prevent business-logic flaws with server-side invariants