# Bluepine Supply — Product Requirements (v2.5)

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.

## Users
- **Office buyer** — Find items, see an exact total with tax and discount, and place an order with clear confirmation.
- **Accounts payable contact** — Keep a billing email on file and look back at past orders.
- **Integrator** — 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.

## Sign-in (AUTH)

Buyer login and logout.

- **FR-AUTH-01** (Must) A valid sign-in opens the Catalog with all eight products and the buyer’s name in the header.
  - 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"
- **FR-AUTH-02** (Must) A failed sign-in shows one generic message for a wrong password or a wrong-case username, and the form stays.
  - Alert reads exactly "Incorrect username or password."
  - Usernames are case-sensitive
- **FR-AUTH-03** (Could) Sign out returns to the sign-in form and the session does not survive a reload.

## Catalog (CAT)

Eight products with category filter, price sort and a support link.

- **FR-CAT-01** (Must) The Category filter shows exactly the products of the chosen category.
  - 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
- **FR-CAT-02** (Must) Price sorts compare prices numerically and "high to low" is the exact reverse of "low to high".
  - 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
- **FR-CAT-03** (Could) 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.

## Cart, promo code and tax (CRT)

Quantities, line totals, SAVE10 promo code and 8% tax.

- **FR-CRT-01** (Must) The navigation badge counts units (not lines) and updates immediately on every page, including the Catalog.
  - 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
- **FR-CRT-02** (Must) 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.
  - 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)
- **FR-CRT-03** (Must) Promo code SAVE10 takes 10% off the sub-total before tax; codes are case-insensitive.
  - Entering "save10" in the promo-code field on the $50.98 basket: Discount (SAVE10) -$5.10, Tax (8%) $3.67, Total $49.55
- **FR-CRT-04** (Should) An unknown promo code is rejected and removes any discount already applied.
  - "SAVE50" shows an alert of the form "That <promo> code is not valid."; the Discount row disappears and Total returns to $55.06
- **FR-CRT-05** (Should) Changing a quantity or removing a line recalculates everything; quantity has a minimum of 1.
  - 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)"
- **FR-CRT-06** (Could) The cart is kept for the browser tab across reloads (session storage) while the user stays signed in.

## Checkout and confirmation (CHK)

Validate delivery details, place the order, confirm.

- **FR-CHK-01** (Must) Checkout requires full name, a well-formed email and a delivery address, reporting each separately, and creates no order while any is invalid.
  - 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."
- **FR-CHK-02** (Must) A valid order shows an "Order confirmed" page, empties the cart and appears first in Orders.
  - 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"
- **FR-CHK-03** (Could) Direct visits to confirmation or checkout without an order or items are safe.
  - #/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
- **FR-CHK-04** (Could) Orders are placed without payment; no payment details are collected.

## Order history (ORD)

Twelve seeded orders, newest first, five per page.

- **FR-ORD-01** (Must) Orders list newest first, five per page, and each page shows its own rows.
  - 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

## Settings (SET)

Display name and billing email.

- **FR-SET-01** (Should) Saving settings confirms, updates the header greeting and persists across reloads.
  - Defaults: Display name "Buyer" and a seeded billing email
  - Saving "Ada" shows "Settings saved." and header "Hi, Ada"; after reload both values are kept
- **FR-SET-02** (Should) Billing email accepts up to 40 characters, in the field and in the API.
  - 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."

## REST API (API)

Bearer-token API mirroring the website.

- **FR-API-01** (Should) Protected endpoints require a bearer token; login rejects bad credentials; catalog filters and sorts server-side.
  - 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
- **FR-API-02** (Must) The cart-quote and promo-validation endpoints use integer cents and agree with the UI.
  - 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
- **FR-API-03** (Must) Order endpoints page the history, validate input and number new orders per session.
  - 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

## Non-functional requirements

- **NFR-01** (Must) All API amounts are integer cents and displayed amounts are dollars with exactly two decimals; both always agree.
- **NFR-02** (Should) Invalid checkout fields are exposed to assistive technology with aria-invalid and the current pagination page with aria-current="page".

## Glossary and rules

- **SAVE10** — The only valid promo code; 10% off the sub-total, applied before tax.
- **Units** — Sum of quantities across cart lines; this is what the Cart badge shows.

## Out of scope

- Payment: orders are placed without one
- Email delivery
