Price an order without trusting the client

A checkout trusts the price and quantity each cart line sends, adds every coupon code it recognizes, and never limits redemptions. Customers can choose their price, order negative quantities and stack discounts. Repair the pricing function so every amount comes from server data and every coupon rule holds.

Requires Node.js 22 or newer. No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit pricing.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Reproduce the tampering
Run the starter. The price-tamper case buys the TV for one cent because the line’s price comes from the client, and the negative-quantity case subtracts cables from the total.

2. Recompute from server data
Look each SKU up in shop.catalog and use its price. Reject unknown and repeated SKUs, and quantities that are not integers from 1 to maxQty: Number.isInteger rejects 1.5, "2" and true.

3. Apply one coupon by its rules
Reject two or more codes and unknown codes. Compare the subtotal with minSubtotal; only when it qualifies, await shop.redeem(code, order.account), reject on false, and compute the discount with Math.floor in cents.

4. Keep legitimate orders working
Ordinary orders, a qualifying coupon and a second account using the same coupon must all be priced. Redeem only after the cart is valid, so a rejected order does not use up the customer’s coupon.

SUPPLIED CONTRACT
priceOrder(order, shop) is async and returns {status: "priced", subtotal, discount, total} in integer cents, or {status: "rejected"} (a reason field is optional). order comes from the client: {account, lines: [{sku, qty, price?}], coupons: [code, ...]}. shop is server data: catalog[sku] = {price, maxQty}, coupons[code] = {percentOff, minSubtotal}, and await shop.redeem(code, account), an atomic operation that records one use of a coupon by an account and returns true the first time and false after. Rules: ignore any client price; reject an order with no lines or more than 20, an unknown or repeated SKU, or a quantity that is not an integer from 1 to the product’s maxQty; reject more than one coupon code and an unknown code. A coupon applies only when the subtotal is at least its minSubtotal; then redeem it, reject the order if the account already used it, and set discount = Math.floor(subtotal × percentOff / 100). Below the threshold the discount is 0 and the coupon is not redeemed. Validate the whole cart before redeeming anything. The order is untrusted JSON: lines and coupons must be lists, each SKU and coupon code a string, and look them up with Object.hasOwn so a name every object inherits, such as constructor, is unknown.

REFERENCE
reference.mjs contains an explained solution: 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.

SCOPE
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.

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?

SOURCE
https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/10-Business_Logic_Testing/
