{"ns":"bluepine","title":"Bluepine Supply","version":"2.5","summary":"This product is a B2B office-supply store. A buyer signs in, browses an eight-product catalog by category and price, builds a cart with an optional promo code, checks out with a name, email and delivery address, reviews paginated order history and manages account settings. The same pricing and order rules are exposed through a REST API.","problem":"Procurement buyers need to order everyday supplies quickly and trust that every price, discount, tax amount and order number on screen is exact.","personas":[{"name":"Office buyer","need":"Find items, see an exact total with tax and discount, and place an order with clear confirmation."},{"name":"Accounts payable contact","need":"Keep a billing email on file and look back at past orders."},{"name":"Integrator","need":"Price carts and place orders through an API that agrees with the website to the cent."}],"goals":["Line totals, sub-total, discount, tax and total always reconcile to the cent, in the UI and the API.","Invalid input is rejected with a specific, correct message before an order is created.","Browsing (filter, sort, pages) always shows exactly the items the control promises."],"features":[{"id":"AUTH","title":"Sign-in","summary":"Buyer login and logout.","reqs":[{"id":"FR-AUTH-01","weight":"Must","text":"A valid sign-in opens the Catalog with all eight products and the buyer’s name in the header.","accept":["The sign-in page shows the store name as its heading and a hint about the demo account","After sign-in: heading \"Catalog\", header \"Hi, Buyer\", navigation Catalog / Cart (0) / Orders / Settings","8 product cards, each with name, \"<Category> · <SKU>\", price and \"Add to cart\""]},{"id":"FR-AUTH-02","weight":"Must","text":"A failed sign-in shows one generic message for a wrong password or a wrong-case username, and the form stays.","accept":["Alert reads exactly \"Incorrect username or password.\"","Usernames are case-sensitive"]},{"id":"FR-AUTH-03","weight":"Could","text":"Sign out returns to the sign-in form and the session does not survive a reload."}]},{"id":"CAT","title":"Catalog","summary":"Eight products with category filter, price sort and a support link.","reqs":[{"id":"FR-CAT-01","weight":"Must","text":"The Category filter shows exactly the products of the chosen category.","accept":["Paper (3): A4 copy paper, Sticky notes, Spiral notebook","Pens (2): Gel pens 10 pack, Highlighters 6 colours","Desk (3): Desk stapler, Monitor stand, Desk organiser","All: 8 products"]},{"id":"FR-CAT-02","weight":"Must","text":"Price sorts compare prices numerically and \"high to low\" is the exact reverse of \"low to high\".","accept":["Low to high: Sticky notes $4.25, Highlighters $5.75, Gel pens $6.99, A4 copy paper $9.49, Spiral notebook $12.50, Desk stapler $14.99, Desk organiser $21.50, Monitor stand $32.00","Desk with high to low: Monitor stand, Desk organiser, Desk stapler"]},{"id":"FR-CAT-03","weight":"Could","text":"The footer \"Email support\" link opens a message to the support address (support@ at the store own example.com domain); the API reports the same address as supportEmail."}]},{"id":"CRT","title":"Cart, promo code and tax","summary":"Quantities, line totals, SAVE10 promo code and 8% tax.","reqs":[{"id":"FR-CRT-01","weight":"Must","text":"The navigation badge counts units (not lines) and updates immediately on every page, including the Catalog.","accept":["Adding \"Gel pens, 10 pack\" shows status \"Gel pens, 10 pack added to cart.\" and \"Cart (1)\"","Adding it again and then \"Desk stapler\" shows \"Cart (3)\" while still on Catalog"]},{"id":"FR-CRT-02","weight":"Must","text":"Each line total is price × quantity, the Sub-total row sums every line, tax is 8% of the discounted sub-total rounded to the nearest cent, and Total = Sub-total − Discount + Tax.","accept":["2 × A4 copy paper $9.49 + 1 × Monitor stand $32.00: lines $18.98 and $32.00, Sub-total $50.98, Tax (8%) $4.08, Total $55.06","Tax on a $45.88 amount is $3.67 (not rounded up)"]},{"id":"FR-CRT-03","weight":"Must","text":"Promo code SAVE10 takes 10% off the sub-total before tax; codes are case-insensitive.","accept":["Entering \"save10\" in the promo-code field on the $50.98 basket: Discount (SAVE10) -$5.10, Tax (8%) $3.67, Total $49.55"]},{"id":"FR-CRT-04","weight":"Should","text":"An unknown promo code is rejected and removes any discount already applied.","accept":["\"SAVE50\" shows an alert of the form \"That <promo> code is not valid.\"; the Discount row disappears and Total returns to $55.06"]},{"id":"FR-CRT-05","weight":"Should","text":"Changing a quantity or removing a line recalculates everything; quantity has a minimum of 1.","accept":["A4 copy paper quantity 1: line $9.49, Sub-total $41.49, Tax $3.32, Total $44.81, \"Cart (2)\"","Quantity 0 snaps back to 1","Removing every line shows \"Your cart is empty.\" and \"Cart (0)\""]},{"id":"FR-CRT-06","weight":"Could","text":"The cart is kept for the browser tab across reloads (session storage) while the user stays signed in."}]},{"id":"CHK","title":"Checkout and confirmation","summary":"Validate delivery details, place the order, confirm.","reqs":[{"id":"FR-CHK-01","weight":"Must","text":"Checkout requires full name, a well-formed email and a delivery address, reporting each separately, and creates no order while any is invalid.","accept":["Checkout shows \"3 item(s) · $55.06 including tax\"","Empty form: \"Enter your full name.\", \"Enter a valid email address.\", \"Enter a delivery address.\"; fields are aria-invalid=\"true\"","Email \"ada@example\" (no domain dot) leaves only \"Enter a valid email address.\""]},{"id":"FR-CHK-02","weight":"Must","text":"A valid order shows an \"Order confirmed\" page, empties the cart and appears first in Orders.","accept":["Confirmation reads \"Order BP-1241 was placed: 3 item(s), $49.55.\" for the promo-code basket","Navigation reads \"Cart (0)\"","Orders first row: BP-1241 · 2026-10-02 · 3 · $49.55, \"Page 1 of 3\""]},{"id":"FR-CHK-03","weight":"Could","text":"Direct visits to confirmation or checkout without an order or items are safe.","accept":["#/confirmation: \"There is no recent order to show.\" with \"Continue shopping\"","#/checkout with an empty cart: \"Your cart is empty.\"","Unknown routes show the Catalog"]},{"id":"FR-CHK-04","weight":"Could","text":"Orders are placed without payment; no payment details are collected."}]},{"id":"ORD","title":"Order history","summary":"Twelve seeded orders, newest first, five per page.","reqs":[{"id":"FR-ORD-01","weight":"Must","text":"Orders list newest first, five per page, and each page shows its own rows.","accept":["Page 1: BP-1240 2026-09-28 1 $19.99; BP-1239 2026-09-26 2 $27.34; BP-1238 3 $34.69; BP-1237 4 $42.04; BP-1236 1 $49.39; \"Page 1 of 3\"; \"Previous\" disabled","Page 2: BP-1235 … BP-1231, button 2 has aria-current=\"page\"","Page 3: BP-1230 and BP-1229 only; \"Next\" disabled"]}]},{"id":"SET","title":"Settings","summary":"Display name and billing email.","reqs":[{"id":"FR-SET-01","weight":"Should","text":"Saving settings confirms, updates the header greeting and persists across reloads.","accept":["Defaults: Display name \"Buyer\" and a seeded billing email","Saving \"Ada\" shows \"Settings saved.\" and header \"Hi, Ada\"; after reload both values are kept"]},{"id":"FR-SET-02","weight":"Should","text":"Billing email accepts up to 40 characters, in the field and in the API.","accept":["A 31-character address is saved in full","The field keeps at most 40 characters (maxlength 40)","API: 40 characters returns 200; 41 returns 422 \"Billing email must be at most 40 characters.\""]}]},{"id":"API","title":"REST API","summary":"Bearer-token API mirroring the website.","reqs":[{"id":"FR-API-01","weight":"Should","text":"Protected endpoints require a bearer token; login rejects bad credentials; catalog filters and sorts server-side.","accept":["GET /orders without a token returns 401","POST /auth/login with a wrong password returns 401 \"Invalid credentials\"; a valid login returns 200 with token and user","GET /products?category=Desk&sort=price-desc returns BP-302, BP-303, BP-301 and supportEmail"]},{"id":"FR-API-02","weight":"Must","text":"The cart-quote and promo-validation endpoints use integer cents and agree with the UI.","accept":["POST /cart/quote with BP-101 ×2, BP-302 ×1 and promo code \"save10\": items 3, sub-total 5098, discount 510, tax 367, total 4955","Promo code \"SAVE50\" returns 422 with the same \"... code is not valid.\" message as the website","Validating \" save10 \" (padded, lower-case) returns valid true, code \"SAVE10\", percent 10"]},{"id":"FR-API-03","weight":"Must","text":"Order endpoints page the history, validate input and number new orders per session.","accept":["GET /orders: total 12, pages 3, pageSize 5, first BP-1240 placed 2026-09-28, items 1, total 1999; page=3 gives BP-1230, BP-1229","POST /orders with empty fields returns 422 with errors for name, email, address and lines (\"Your cart is empty.\")","A valid order returns 201 id BP-1241, placed 2026-10-02, items 3, total 4955","GET /orders/BP-1241 returns 200; /orders/BP-9999 returns 404 not_found"]}]}],"nfr":[{"id":"NFR-01","weight":"Must","text":"All API amounts are integer cents and displayed amounts are dollars with exactly two decimals; both always agree."},{"id":"NFR-02","weight":"Should","text":"Invalid checkout fields are exposed to assistive technology with aria-invalid and the current pagination page with aria-current=\"page\"."}],"glossary":[{"term":"SAVE10","meaning":"The only valid promo code; 10% off the sub-total, applied before tax."},{"term":"Units","meaning":"Sum of quantities across cart lines; this is what the Cart badge shows."}],"outOfScope":["Payment: orders are placed without one","Email delivery"]}