# VSR Replatform — Change Log

Active work log for INT-2682 replatform. Each entry records what was done, on which branch, and any decisions made that deviate from the plan.

---

## 2026-07-08

### Fix loose charge — bill the loose remainder, not the full quantity (`feature/INT-2851-cart-operations-rds`)

INT-2851. The loose surcharge was billing `$0.30 × full quantity`, ignoring the carton/bulk factor (`BULK_FACTOR_QT`) — overcharging any order of a full carton or more (qty 80 @ carton 80 billed $24 instead of $0). Now charges only the loose remainder: `$0.30 × (quantity % bulkFactor)`. Supersedes PR #26 (`rkordisch:feature/INT-2851-fix-loose-charge`, head `21dda9b`), integrating its code with one documented divergence. See `docs/DECISION_LOG.md` D011.

#### Implemented
- `pricing.ts` (server + frontend mirror): `computeLooseUnits` (`quantity % bulkFactor`, guards ≤0); `computeLooseCharge` takes `bulkFactor`; `CartItemCharges` gains `looseChargeQt`; `computeCartTotals` narrowed to the summed fields.
- Migration `005`: `cart_items` gains `bulk_factor_qt` / `loose_charge_qt` / `loose_charge_am`; `order_items` gains `bulk_factor_qt` (idempotent). Applied to local Docker + dev RDS.
- `RdsDataSource`: threads `bulk_factor_qt`; `refreshLooseChargeSnapshot` keeps `loose_charge_qt`/`_am` fresh on add/update; `order_items.loose_charge_qt` now stores the loose remainder.
- `addToCart` passes `product.bulk_factor_qt`; `{product,cart}.graphql` doc comments corrected; surcharge notes reworded to "per loose unit".

#### Divergence from PR #26
- `addCartItem` refreshes `bulk_factor_qt` on the `ON CONFLICT` re-add path (PR #26 leaves it stale — permanently `0` for pre-migration rows). Commented in `RdsDataSource`; see handoff §4.2.

#### Validation
- Server build clean; server 36 tests pass; frontend 24 pass; Biome clean. Carton cases 7→$2.10, 80→$0, 81→$0.30, 163→$0.90. Live smoke on dev RDS: qty 81 @ carton 80 → $0.30.

#### Also in this branch
- Full RDS schema (001–005) stood up on the freshly-provisioned dev RDS. `.gitignore` secret/PII hardening. `docs/cart-detail-divergences-INT-2851.html`. Follow-ups: migration-runner adoption; RDS↔legacy schema reconciliation (INT-2793).

---

## 2026-06-28

### Catalog browse on precomputed nav views — retire `V_PRODUCTS` (`feature/INT-2823-catalog-nav-views`)

INT-2823. Wired the GraphQL server + React frontend to the new precomputed Snowflake nav objects and retired `V_PRODUCTS`. Products are now keyed on `TRIM(ARTICLE_NO)`. See `docs/DECISION_LOG.md` D001–D005 for rationale.

#### Implemented

| File | Change |
| ---- | ------ |
| `packages/vsr-graphql-server/src/dataSources/SnowflakeDataSource.ts` | Re-pointed the product projection at raw `PRODUCT`/nav-view columns; `Product.id` = `TRIM(ARTICLE_NO)`; view-name resolution (`V_{ALL\|MUSIC\|VIDEO}_{VIEW}`); stock from `AVAILABLE_QT`; added nav row types + `getNavPrimary` / `getNavSecondary` / `getNavCategories` (`"COUNT"` quoted — reserved word) |
| `packages/vsr-graphql-server/src/resolvers/productResolvers.ts` | Mapping reads raw columns; `_rollupParentId` carries `SELECTION_ID` for the merch-rollup self-join; header comment de-referenced `V_PRODUCTS` |
| `packages/vsr-graphql-server/src/resolvers/cartOrderResolvers.ts` | `addToCart` updated to raw `ProductRow` column names (`selection_long_nm`, `deal_discount_1`, `todays_deal_waive_*`) |
| `packages/vsr-graphql-server/src/resolvers/navigationResolvers.ts` | New — assembles primary+secondary nav tree; maps `navCategories` (count→Number) |
| `packages/vsr-graphql-server/src/schema/navigation.graphql` | New — `NavPrimary` / `NavSecondary` / `NavCategory` types + `navigation` / `navCategories` queries |
| `packages/vsr-graphql-server/src/schema/product.graphql` | Removed `productFacets` query + `FacetBucket` type |
| `packages/vsr-graphql-server/src/index.ts` | Merged `navigationResolvers` |
| `apps/vsr/src/apollo/queries/navigationQueries.ts` | New — `GET_NAVIGATION`, `GET_NAV_CATEGORIES` |
| `apps/vsr/src/apollo/queries/{index,productQueries}.ts` | Removed `GET_PRODUCT_FACETS`; exported nav queries |
| `apps/vsr/src/components/VsrLayout.tsx` | Data-driven nav from `navigation`; logo + Home breadcrumb → `/`; removed hardcoded `TABS` |
| `apps/vsr/src/pages/CatalogPage.tsx` | Single `navCategories` sidebar (`SIDEBAR_CONFIG`); removed deals + 5 facet sidebars and the `V_PRODUCTS` empty-state copy |
| `apps/vsr/src/app.tsx` | Removed Dashboard route/import; added `LandingRedirect` (`/` → ALL PRODUCTS → New Releases → first category) |
| `infra/sql/snowflake-views.sql` | Mirrored DDL: replaced `V_PRODUCTS` with the 3 nav tables + 16 nav views; annotated `V_INVENTORY` as retired from the catalog stock path |
| `docs/DECISION_LOG.md` | New — D001–D005 |

#### Notes

- GraphQL client-facing field names were kept stable to minimize frontend churn.
- `DashboardPage.tsx` is left in the tree (unrouted) — explicit removal deferred pending approval. `GET_DEALS` query + deals resolver retained (harmless).
- `V_INVENTORY` retained for non-catalog consumers; the catalog no longer calls it.

#### Validation

- Backend: `pnpm build` clean, `pnpm lint` (pre-existing warnings only), `pnpm test` 21 passed.
- Frontend: typecheck clean, `pnpm lint` (pre-existing `any` warnings only), `pnpm test` 18 passed.
- End-to-end smoke against the live stack: `navigation`, `navCategories`, `products` (browse + category filter), `searchProducts`, `product` by `article_no`, `merchandiseRollup` all resolve; `productFacets` correctly rejected by the schema.

## 2026-06-10

### Visual layout restoration — meeting-driven catalog updates (`feature/INT-2682-visual-layout-restoration`)

#### Implemented

| File | Change |
| ---- | ------ |
| `apps/vsr/src/pages/CatalogPage.tsx` | Added New Releases street-date counts in the sidebar; added page-level quantity state and a single bottom `ADD SELECTED TO CART` action for New Releases using the existing `addToCart` mutation repeatedly; preserved per-item add-to-cart in other catalog views |
| `apps/vsr/src/components/ProductRow.tsx` | Switched quantity input default from `1` to blank, coercing to `1` only at submit time; added support for page-controlled quantity state in New Releases; moved stock/restock messaging into the order card |
| `apps/vsr/src/pages/ProductDetailPage.tsx` | Switched quantity input default from `1` to blank, coercing to `1` only at submit time; moved stock/restock messaging into the purchase card |
| `apps/vsr/src/styles/vsr-theme.scss` | Fixed sidebar search-input overflow with border-box sizing; added layout styles for catalog batch actions and card-scoped stock messaging |
| `docs/FOLLOW_UP_TICKETS.md` | Added follow-ups for unresolved `units percent` source mapping and pending restock-warning business rule |

#### Notes

- Existing `showStock` gating in `CatalogPage.tsx` already suppressed stock messages for future-dated New Releases, so this work preserved that behavior rather than re-implementing it.
- `priceCode` was already correctly mapped from `red_price_cd` in the GraphQL resolver path; no backend mapping change was needed.
- `configurationName`, `labelName`, `genreName`, and `returnsAllowed` were already present in the app and were preserved as part of the layout pass.

#### Validation

- File-level diagnostics on the touched frontend files reported no errors after each implementation slice.

## 2026-06-03

### Dev environment — LAN access fix (`feature/INT-2699-frontend-wiring`)

#### Issue

The dev stack was inaccessible from LAN devices (phones, tablets, other machines on the same network).

Two separate problems:

1. **Vite and the GraphQL stub both bound to `127.0.0.1` (loopback only).** Any request from a device other than the host machine was refused at the TCP level — the ports weren't listening on the network interface.

2. **Apollo client hardcoded `http://localhost:4000/graphql` as the GraphQL endpoint.** Even after fixing the bind address, this URL doesn't work on a LAN device — `localhost` on a remote browser resolves to *that device's* loopback, not the dev host. All GraphQL calls silently fail and the app renders with no data.

#### Fix

| File | Change |
| ---- | ------ |
| `apps/vsr/vite.config.ts` | Spread `baseConfig` and override `server.host: "0.0.0.0"` to bind Vite on all interfaces; add `server.proxy: { "/graphql": "http://localhost:4000" }` so the browser uses a relative path and Vite forwards the request server-side |
| `tools/vsr-graphql-stub/src/index.ts` | Add `host: "0.0.0.0"` to `startStandaloneServer` `listen` options |
| `apps/vsr/src/apollo/apolloClient.ts` | Change default `GRAPHQL_URL` fallback from `http://localhost:4000/graphql` to relative `/graphql` |

The proxy is the key piece: the browser fetches `/graphql` relative to wherever it loaded the page from (e.g. `http://192.168.5.146:3000/graphql`), and Vite resolves that server-side to `http://localhost:4000/graphql` — which is always correct from the host machine regardless of what network address the client used to reach it.

Docker Postgres (`"5432:5432"`) already binds to `0.0.0.0` by default — no change required there.

---

## 2026-04-14

### Phase 0 — Pre-work

#### 0.1 — Snowflake MCP wired to workspace
- **Branch:** `feature/INT-2722-monorepo-root`
- **File:** `.vscode/mcp.json`
- Pointed at `~/src/collab/lvona/snowflake-mcp/server.py` (SSO auth, `externalbrowser`)
- Default database: `VIRTUALSALESREP`, schema: `DEV`
- Enables live catalog DDL validation from this workspace without leaving VS Code

---

### Phase 1 — Monorepo Root + Lab Environment (INT-2722)

#### 1.1 — Feature branch created
- `feature/INT-2722-monorepo-root` off `master`
- Fork: `lealvona/virtual-sales-rep`
- Upstream: `theorchard/virtual-sales-rep` — push protected (`NO_PUSH`)

#### 1.2 — Docker Compose local dev environment
- **Files:** `docker/docker-compose.yml`, `docker/scripts/db-reset.sh`, `docker/.env.docker`
- `postgres:16-alpine` on port 5432
- `db-reset.sh` drops + recreates schema and runs all migrations in `infra/sql/`

#### 1.3 — Root monorepo configuration
- **Files:** `package.json`, `biome.json`, `tsconfig.base.json`, `.nvmrc`, `.npmrc`, `.editorconfig`
- pnpm workspace: `apps/*` + `packages/*`
- Pattern mirrors `orchard-suite` root exactly (hoist=false, strict-peer-dependencies=false)

#### 1.4 — apps/vsr/ package scaffold
- **Files:** `apps/vsr/package.json`, `apps/vsr/tsconfig.json`, `apps/vsr/vitest.config.ts`
- Deps: `@theorchard/suite-frontend`, `@theorchard/suite-apollo`, `@theorchard/suite-components`, React 18.2.0

#### 1.5 — apps/vsr/ entry files + DEV_MODE auth flag
- **Files:** `apps/vsr/src/index.tsx`, `apps/vsr/src/index.pug`, `apps/vsr/frontend.json`, `apps/vsr/.env.shadow`, `apps/vsr/.env.local` (gitignored)
- `DEV_MODE=true` in `.env.local` provides auth passthrough for local lab — no Auth0 round-trip

#### 1.6 — apps/vsr/ app.tsx + route stubs
- **Files:** `apps/vsr/src/app.tsx`, all stub page components in `apps/vsr/src/pages/`
- `initApplication()` from `@theorchard/suite-frontend` — exact orchard-suite pattern
- Left nav: Catalog, Cart, Order History, Order Tools, Account, Admin (guarded)

#### 1.7 — Apollo client setup
- **Files:** `apps/vsr/src/apollo/apolloClient.ts`, `apps/vsr/apollo.config.js`
- `createApolloClientOptions()` from `@theorchard/suite-apollo`

#### 1.8 — packages/vsr-graphql-server scaffold
- **Files:** `packages/vsr-graphql-server/package.json`, `src/index.ts` + dir structure
- `@theorchard/graphql-server` + `pg` (raw node-postgres, no ORM — matches connector-neo4j pattern)

#### 1.9 — tools/vsr-graphql-stub
- **Files:** `tools/vsr-graphql-stub/` — local mock GraphQL server for lab testing
- Not deployed, not in CI — lab helper only

#### 1.10 — Jenkinsfile seed
- **File:** `Jenkinsfile` at repo root

---

## Decisions Log Addendum
*(Full ADRs in `research/DECISION_LOG.md` — this section tracks deviations from plan)*

| # | Decision | Reason | Date |
|---|---------|--------|------|
| D-LAB-001 | Raw `pg` (node-postgres) for RDS access — no Prisma/Drizzle | No ORM exists in orchard-suite; matches explicit connector pattern of `connector-neo4j` | 2026-04-14 |
| D-LAB-002 | `exceljs` server-side only, never in frontend bundle | Org pattern: backend generates file → presigned S3 URL → `<a href download>` | 2026-04-14 |
| D-LAB-003 | Embla Carousel (~4kb) for CoverFlow spirit, CSS Scroll Snap for V1 | Zero transitive deps; no animation library in orchard-suite; lightest defensible addition | 2026-04-14 |
| D-LAB-004 | `DEV_MODE=true` auth passthrough via `.env.local` | Enables lab testing without Auth0; fully removable at Phase 10 | 2026-04-14 |
| D-LAB-005 | Snowflake MCP uses SSO (`externalbrowser`) — not service account | VSR_DEV_SVC not yet provisioned (Phase 0.2 pending); SSO is safe for exploration | 2026-04-14 |

---

## 2026-04-28

### Phase 2 — Frontend Visual Rewrite (INT-2699, `feature/INT-2699-frontend-wiring`)

#### 2.1 — VSR theme layer
- **File:** `apps/vsr/src/styles/vsr-theme.scss` (new)
- Full VSR colour palette extracted from live redb2b.com site:
  - Primary CTA: `#FF6600` (orange), hover `#e55c00`
  - Page bg: `#f5f5f5`, sidebar bg: `#f9f9f9`, white card areas
  - Nav chrome: `#1a1a1a`, nav text: white
  - Stock available: `#00aa00`, unavailable: `#cc0000`, deal price: `#c00`
- BEM classes for all shared elements: `vsr-header`, `vsr-tabs`, `vsr-subtabs`, `vsr-sidebar`, `vsr-content`, `vsr-product-table`, `vsr-price`, `vsr-deal-badge`, `vsr-stock`, `vsr-surcharge-note`, `vsr-totals`, `vsr-steps`, `vsr-breadcrumb`, `vsr-pagination`, `vsr-meta-dl`, `vsr-section-heading`
- Typography: Trebuchet MS → Verdana → Arial → sans-serif (matches legacy site)

#### 2.2 — VsrLayout shared chrome
- **File:** `apps/vsr/src/components/VsrLayout.tsx` (new)
- Header: REDB2B logo (left) + user greeting + cart shortcut + orders + account (right)
- 4 primary tabs: ALL PRODUCTS | MUSIC | VIDEO | ORDER TOOLS — router-backed (`/catalog/:tab/:subcategory`)
- Sub-category nav per tab: New Releases | On Deal | Best Sellers | Genre | Configuration | Exclusive (VIDEO tab omits Exclusive)
- Sidebar slot: accepts arbitrary filter panel content per page
- Breadcrumb trail below tabs, links to parent routes
- `hideTabs` prop for non-catalog pages (Cart, Checkout, Account, Orders)
- Uses `GET_ME` to personalise header greeting; gracefully degrades if no auth

#### 2.3 — CheckoutSteps component
- **File:** `apps/vsr/src/components/CheckoutSteps.tsx` (new)
- 3-step orange pill indicator (Cart → Shipping → Review), matching the redb2b.com checkout header
- Done steps rendered as clickable `<button>` for navigation; future steps rendered as static `<div>`
- Colour states: active = orange, done = green, pending = gray

#### 2.4 — ProductRow component
- **File:** `apps/vsr/src/components/ProductRow.tsx` (new)
- Full row: thumbnail (80×80), artist/title/config/genre/label/street date, UPC/carton factor, price block, stock status, qty input + ADD TO CART button
- Deal display: strikethrough original price + deal price (red) + `N% OFF — Deal Name` badge + expiry date
- Surcharge warnings inline per product (loose charge, min order charge)
- `compact` prop for dashboard and related-products contexts (thumbnail + name + format + price only)
- Calls `ADD_TO_CART` mutation directly; navigates to `/cart` on success

#### 2.5 — Route restructure
- **File:** `apps/vsr/src/app.tsx` (modified)
- `/catalog/:tab/:subcategory` — replaces flat `/catalog`; tab = `all|music|video`, subcategory = `new-releases|on-deal|best-sellers|genre|configuration|exclusive`
- `/product/:productId` — replaces `/catalog/:productId` for product detail; avoids route ambiguity with catalog tab params
- `/order-tools/:section?` — replaces flat `/order-tools`; section = `instructions|download|upload-excel|upload-po`
- Redirect chain: `/catalog` → `/catalog/all` → `/catalog/all/new-releases`
- All non-catalog pages (`/cart`, `/checkout/*`, `/orders/*`, `/account/*`, `/register`) unchanged

#### 2.6 — Page rewrites (all 11 pages)
All pages now use `VsrLayout` chrome and `vsr-theme.scss` classes. Business logic preserved unchanged.

| Page | Key changes |
|------|-------------|
| **DashboardPage** | Welcome heading + account badge; New Releases compact row table (8 items); Active Deals table with deal name/discount/period/count; Recent Orders table (5 rows). All with "View All" links. |
| **CatalogPage** | Left sidebar: search input, media filter (All/Music/Video), deal filter (on-deal view). Main: result count, full `ProductRow` table, page number pagination. URL-driven state (tab/subcategory/q/category/page params). |
| **CartPage** | Table columns: Line #, Artist/Title/UPC, Format, Unit Price, Qty (editable blur-to-update), Line Total, Surcharges, Remove. Right-aligned totals box: product + loose + min order + grand total. Three action buttons: Continue Shopping, Clear Cart, Proceed to Shipping. |
| **ShippingPage** | Ship-to dropdown populated from `user.shipToAccounts`; PO Number input (optional); cart item count + grand total summary. |
| **ReviewPage** | Order items table (Product, Qty, Unit Price, Line Total, Surcharges); ship-to + PO display; totals box; Place Order CTA (disabled if no ship-to). Success state shows Order ID/Status/Total + View Order History button. `sessionStorage` bridge from ShippingPage unchanged. |
| **ProductDetailPage** | 2-column layout (large image left, metadata right); metadata DL (Format, UPC, Cat#, Genre, Label, Street Date, Returns, Carton Factor); pricing block with deal display; surcharge warnings; qty + ADD TO CART; merchandise rollup if present; Related Products compact row table. |
| **OrderHistoryPage** | Status filter dropdown (all/pending/submitted/shipped/invoiced/cancelled); paginated table (Order ID, PO#, Status, Items, Total, Date, Tracking); clickable rows; prev/next pagination. |
| **OrderDetailPage** | Order metadata DL (Status, PO#, Ship To, Placed, Submitted, Shipped, CDS Ref, Tracking — conditional fields); 8-column item table; totals box. |
| **OrderToolsPage** | 4-tab sub-nav (Instructions, Download Excel, Upload Excel, Upload RecordTrak); instructions as ordered list; download as table (placeholder); upload forms with file chooser + status message (INT-2705 stub). |
| **ProfilePage** | Profile metadata DL; full name + packlist email checkbox form; ship-to accounts list; change password form with current + new PW inputs. |
| **RegisterPage** | Form fields (email, name, account#, password, confirm); client-side validation (required, min 8 chars, passwords match); success state. Stub — INT-2697. |

#### 2.7 — Verification
- `pnpm exec tsc --noEmit` → **0 errors**
- Original pages backed up: `.apocrypha/pages-v1.bak/`
- Pre-existing issues (not caused by this work):
  - `frontend start` fails — `@theorchard/frontend-cli-webpack` plugin missing (pre-dates this branch)
  - Vite build target issue — esbuild can't transform renamed destructuring for configured legacy targets (all of `react-bootstrap`, `suite-components` etc. hit the same error)

#### 2.8 — Decisions
| # | Decision | Reason |
|---|---------|--------|
| D-VIS-001 | Global SCSS via vsr-theme.scss instead of CSS Modules | CSS Modules per page creates duplicate theme values; BEM global classes are used consistently across VsrLayout, components, and pages — consistent with orchard-suite's pattern of global suite styles |
| D-VIS-002 | Custom HTML table rows + vsr-theme classes instead of GridTable from suite-components | GridTable requires column definitions incompatible with the variable-column layout of product rows (thumbnail, price block, inline CTA); plain `<table>` with theme classes gives full control over the data-dense layout matching redb2b.com |
| D-VIS-003 | `/product/:productId` instead of `/catalog/:productId` | New `/catalog/:tab/:subcategory` URL structure conflicts with a `:productId` param at the same depth; separate `/product/:id` route is unambiguous |
| D-VIS-004 | CoverFlow omitted | Dead feature requiring Flash/Java plugin — confirmed removed in replatform scope |
| D-VIS-005 | Product detail as full page, not modal | Shadowbox.js modal is a dead library; full page is simpler and more accessible; modal overlay can be layered on top in a follow-up ticket if UX review demands it |

---

## Next Tranche — Visual Verification & Query Gap Closure

*Scoped 2026-04-28. Not yet started. These items must be complete before the INT-2699 PR is marked ready.*

### Blocker: Dev server broken — `frontend-cli-webpack` not found
- **Root cause:** `@theorchard/frontend-cli` is resolving to a version that attempts to load a webpack plugin, but `frontend.json` specifies `"plugins": ["vite"]` and the installed dep is `@theorchard/frontend-cli-vite: ^2.3.0`. The cli wrapper is ignoring the plugin config and falling back to webpack.
- **Fix:** Pin `@theorchard/frontend-cli` to a version that correctly resolves the vite plugin, or wire `pnpm dev` directly to `frontend-cli-vite start`. Investigate root `node_modules/@theorchard/frontend-cli/` resolution chain.
- **Blocks:** All visual verification work.

### Gap 1 — `vsr-theme.scss` not imported at app entry point
- `apps/vsr/src/index.tsx` does not import the theme. VsrLayout imports it, but that means pages rendered before VsrLayout mounts won't have the correct `body`/`html` baseline styles.
- **Fix:** Add `import './styles/vsr-theme.scss'` to `index.tsx` so baseline styles apply globally from mount.

### Gap 2 — Query field mismatches to verify
Two fields used in pages are not confirmed in the stub server response:

| Page | Field used | Needs verification |
| ---- | ---------- | ------------------ |
| `CartPage` | `cart.cartId` (passed to `CLEAR_CART`) | Confirm `cartId` is returned in `CartFields` fragment |
| `OrderDetailPage` | `item.orderItemId` (used as React key) | Stub returns `lineNo` only — `orderItemId` may be null |

- **Fix:** Check `tools/vsr-graphql-stub/src/index.ts` mock data and add missing fields, or fall back to `lineNo` as key in `OrderDetailPage`.

### Gap 3 — Stub server: new filter params not wired
The stub server was written before the route restructure. New filter combinations need verification:

| Query | New param | Status |
| ----- | --------- | ------ |
| `GET_PRODUCTS` | `viewType: 'BEST_SELLERS'` | Likely falls through to default; verify |
| `GET_PRODUCTS` | `viewType: 'GENRE'` + `category` | Genre sidebar filter — confirm stub returns results |
| `GET_PRODUCTS` | `viewType: 'CONFIGURATION'` + `category` | Config sidebar filter — confirm |
| `GET_PRODUCTS` | `viewType: 'EXCLUSIVE'` | Exclusive tab — confirm |
| `SEARCH_PRODUCTS` | Works end-to-end | Re-verify with new URL param routing |

- **Fix:** Review `tools/vsr-graphql-stub/src/index.ts` resolver for `products()` and add case handling for all `viewType` values.

### Gap 4 — `genreName` field missing from `ProductRow` compact mode
The compact row in DashboardPage new releases table renders without genre. The `GET_PRODUCTS` query does return `genreName` but it's only used in the full row variant. Acceptable as-is for compact — no fix needed.

### Gap 5 — Route cross-references in other pages
Old `/catalog/:productId` links may exist in comments or test fixture strings. Confirm with grep:
```
grep -rn "/catalog/" apps/vsr/src/ --include="*.ts" --include="*.tsx" | grep -v "node_modules"
```
Expected: only `/catalog/:tab/:subcategory` patterns remain.

### Phase 3 — Visual Comparison (blocked by Gap: Dev server)
Once dev server is running, do a page-by-page visual comparison:

| Page | Screenshot reference | Key things to verify |
| ---- | -------------------- | -------------------- |
| Dashboard | `crawl-20260414-1400/001-new-releases.png` | Welcome heading, new releases table, deals table, recent orders |
| Catalog / New Releases | `crawl-20260414-1400/003-new-releases-list.png` | Product rows, sidebar filters, pagination |
| Catalog / On Deal | `crawl-20260414-1400/007-on-deal-list.png` | Deal name sidebar filter, deal price display |
| Product Detail | `crawl-20260414-1400/023-product-detail.png` | 2-col layout, metadata DL, pricing block, surcharge note, related products |
| Cart | `crawl-20260414-1400/024-cart-step1-view-update.png` | Table columns, totals box, 3-step header |
| Shipping | `crawl-20260414-1400/025-cart-step2-shipping.png` | Ship-to dropdown, PO input, cart summary |
| Review | `crawl-20260414-1400/026-cart-step3-review.png` | Items table, totals, Place Order button |
| Order History | `crawl-20260414-1400/027-order-history.png` | Status filter, paginated table |
| Order Detail | `crawl-20260414-1400/028-order-detail.png` | Metadata DL, items table, totals |
| Order Tools | `crawl-20260414-1400/018-order-tools-upload-excel.png` | 4-tab nav, upload form |
| Profile | `crawl-20260414-1400/020-profile.png` | Form fields, ship-to list, password section |

---

## 2026-07-07

### Deal-badge review follow-up (`feature/INT-2850-align-pricing-deal-fields`)

INT-2850 PR #25 review (@lealvona) follow-up. Display/robustness refinements on top of the approved pricing/deal-field alignment. See `docs/DECISION_LOG.md` D010 for the base decision.

#### Implemented

| File | Change |
| ---- | ------ |
| `packages/vsr-graphql-server/src/resolvers/productResolvers.ts` | Extracted the deal gate into an exported `deriveDealInfo(row)` pure helper; added a `>= 1%` floor on the derived-discount path so sub-1% rounding artifacts no longer render as deals (review #2) |
| `packages/vsr-graphql-server/src/__tests__/dealInfo.spec.ts` | New — 8 cases: ON_DEAL gate, bulk-vs-effective derivation, deal without programme name, `endsAt` from `TODAYS_DEFAULT_DEAL_END_DATE`, sub-1% floor, explicit `DEAL_DISCOUNT_1` (review #5) |
| `apps/vsr/src/components/DealBanner.tsx` | `Math.round()` the badge percent so derived fractional percents show a clean `15% OFF` (review #3); removed the dead `banner` variant + `variant` prop — all callers use the pill (review #4) |
| `apps/vsr/src/components/ProductRow.tsx`, `apps/vsr/src/pages/ProductDetailPage.tsx` | Dropped the now-removed `variant="pill"` prop |
| `apps/vsr/src/styles/vsr-theme.scss` | Removed the dead `.vsr-deal-banner` block |

#### Impact (live catalog, 37,591 PRODUCT rows)

- Review #2 (sub-1% floor): 0 rows affected today — latent-risk guard.
- Review #3 (rounded label): 1,700 deal rows currently render a fractional `% OFF`; now rounded. Example UPC `008333014022` (Majesty Crush): `15.1% OFF` → `15% OFF`.

