From 6fd802fc2759d97c33bf3f4bc159b49b02c374b3 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Mon, 10 Aug 2026 15:05:22 -0400 Subject: [PATCH] CW-STORY-04.5: build scan page; complete CW-EPIC-04 Camera-based barcode scan page using the native BarcodeDetector API (no bundler in this project, so no vendored scanning library) with a manual-entry fallback for unsupported browsers. Adds the /products/barcode/{barcode} and /api/products/barcode/{barcode} routes ProductController was missing, plus an "Add to my list" action on product details. Verified live against the real Open Food Facts API end to end. "Record price" deferred to CW-EPIC-05, which doesn't exist yet. Co-Authored-By: Claude Sonnet 5 --- .claude/settings.json | 6 +- README.md | 10 ++- docs/scrum-backlog.md | 18 ++-- .../Controllers/ProductController.cs | 29 +++++++ .../Controllers/ScanController.cs | 12 +++ src/CartWise.Web/Views/Product/Details.cshtml | 7 +- src/CartWise.Web/Views/Scan/Index.cshtml | 31 +++++++ src/CartWise.Web/Views/Shared/_Layout.cshtml | 3 + src/CartWise.Web/wwwroot/js/pages/scan.js | 86 +++++++++++++++++++ .../CartWise.Web.Tests/ScanAndBarcodeTests.cs | 62 +++++++++++++ 10 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 src/CartWise.Web/Controllers/ScanController.cs create mode 100644 src/CartWise.Web/Views/Scan/Index.cshtml create mode 100644 src/CartWise.Web/wwwroot/js/pages/scan.js create mode 100644 tests/CartWise.Web.Tests/ScanAndBarcodeTests.cs diff --git a/.claude/settings.json b/.claude/settings.json index ea2f793..c1dd4cb 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -32,7 +32,11 @@ "Bash(curl -s http://127.0.0.1:5187/css/site.css)", "PowerShell(\"done\")", "Bash(grep -n \"^### \\\\`CW-STORY\\\\|^## \\\\`CW-EPIC\" docs/scrum-backlog.md)", - "Bash(cd \"g:/development/C Sharp AI/CartWise\" && rm -f verify3.db* && dotnet ef database update --project src/CartWise.Infrastructure/CartWise.Infrastructure.csproj --startup-project src/CartWise.Web/CartWise.Web.csproj --connection \"Data Source=verify3.db\" 2>&1 | tail -15 && rm -f verify3.db*)" + "Bash(cd \"g:/development/C Sharp AI/CartWise\" && rm -f verify3.db* && dotnet ef database update --project src/CartWise.Infrastructure/CartWise.Infrastructure.csproj --startup-project src/CartWise.Web/CartWise.Web.csproj --connection \"Data Source=verify3.db\" 2>&1 | tail -15 && rm -f verify3.db*)", + "Bash(curl -s -m 8 \"https://world.openfoodfacts.org/api/v2/product/3017620422003.json\")", + "Bash(curl -s -b /tmp/cw5.txt -m 15 http://127.0.0.1:5187/api/products/barcode/3017620422003)", + "Bash(curl -s -b /tmp/cw5.txt http://127.0.0.1:5187/products/0ce5df7a-6c8d-489d-ba9f-8d41699f0705 -o /tmp/product.html -w \"status=%{http_code}\\\\n\")", + "Bash(sed -n '/

/,/Add to my list/p' /tmp/product.html)" ], "additionalDirectories": [ "G:\\development\\C Sharp AI\\CartWise" diff --git a/README.md b/README.md index e20f5ba..48b19cb 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,15 @@ Progress so far (see `docs/scrum-backlog.md` for the authoritative task-level st - [x] `CW-STORY-03.4` Toggle purchased/skip/delete with optimistic concurrency handling and jQuery progressive enhancement - [x] `CW-STORY-03.5` Mobile-first/touch-friendly polish pass (not visually verified in a browser — no browser-automation tool available in this environment; recommend a quick manual check) -Next up: `CW-EPIC-05` Stores, Purchases, and Price Intelligence (`CW-EPIC-04` is post-MVP). +`CW-EPIC-04` Product Catalog and Barcode Resolution is complete — built ahead of the MVP-tagged epics per `DEC-010` (explicit founder direction to work epics in strict numeric order rather than MVP-first): + +- [x] `CW-STORY-04.1` Brand/Product/ProductIdentifier/HouseholdProductPreference entities, EF configuration, migration +- [x] `CW-STORY-04.2` Local product search and details pages +- [x] `CW-STORY-04.3` Local-first barcode lookup flow with provider fallback (`IProductDataProvider`) +- [x] `CW-STORY-04.4` Real `OpenFoodFactsProductDataProvider` (typed `HttpClient`, no API key required) +- [x] `CW-STORY-04.5` Camera-based scan page (native `BarcodeDetector` API, manual-entry fallback) with add-to-list — verified live against the real Open Food Facts API + +Next up: `CW-EPIC-05` Stores, Purchases, and Price Intelligence. Next up: `CW-EPIC-03` Smart Shopping List. diff --git a/docs/scrum-backlog.md b/docs/scrum-backlog.md index 415b192..0d42035 100644 --- a/docs/scrum-backlog.md +++ b/docs/scrum-backlog.md @@ -520,11 +520,19 @@ Two more real bugs found by tests (bringing the running total for `CW-EPIC-03` t - User can add scanned product to the list or record a price **Tasks** -- [ ] Create `ScanController` -- [ ] Build `Views/Scan/Index.cshtml` -- [ ] Add camera permission flow -- [ ] Add barcode JS integration -- [ ] Add add-to-list and record-price actions +- [x] Create `ScanController` +- [x] Build `Views/Scan/Index.cshtml` +- [x] Add camera permission flow +- [x] Add barcode JS integration +- [x] Add add-to-list and record-price actions + +**Status:** Done, with one deliberate partial: "record-price" isn't wired up because `CW-EPIC-05` (Purchases/Price Intelligence) hasn't been built yet — a direct consequence of `DEC-010`'s strict-numeric-order choice reaching `04.5` before `05` exists. "Add to list" is fully wired instead, which satisfies the acceptance criterion's "add ... to the list **or** record a purchase/price." Will revisit once `CW-EPIC-05`'s `PriceController` exists. + +Also added `GET /products/barcode/{barcode}` and `GET /api/products/barcode/{barcode}` to `ProductController` (per AGENTS.md §11 — these were specified but not yet built in `CW-STORY-04.2`/`04.3`, since neither of those stories' task lists mentioned a controller route; the scan page is what actually needed them). The JSON route is the "actual client-side need" AGENTS.md §11 requires before introducing an `/api` route — the scan page's JS calls it via `fetch`/`$.getJSON` to resolve a detected barcode without a full page reload. + +No barcode-scanning JS library is vendored — this project has no bundler/npm step (`wwwroot` is plain static files, matching the existing jQuery/Bootstrap vendoring pattern), and pulling in a third-party scanning library's browser build wasn't practical to do well without one. Instead, `wwwroot/js/pages/scan.js` uses the native browser `BarcodeDetector` API (Chrome/Edge/Android WebView) with graceful degradation: browsers lacking it get a clear "not supported" message and a manual barcode-entry text field instead, which also doubles as the non-JS fallback path per AGENTS.md §14. + +Verified live end-to-end with a **real** barcode against the **real** Open Food Facts API (not mocked): `GET /api/products/barcode/3017620422003` (Nutella) → provider fallback fires → product persists locally with the correct name/brand/identifier → `/products/{id}` renders it → "Add to my list" persists it onto the household's shopping list. Also confirmed a genuinely-unknown barcode correctly 404s (HTML route) / returns `{"found":false}` (JSON route) after the provider fallback misses too. --- diff --git a/src/CartWise.Web/Controllers/ProductController.cs b/src/CartWise.Web/Controllers/ProductController.cs index 6068101..8c4b121 100644 --- a/src/CartWise.Web/Controllers/ProductController.cs +++ b/src/CartWise.Web/Controllers/ProductController.cs @@ -60,4 +60,33 @@ public class ProductController : Controller return View(viewModel); } + + [HttpGet("products/barcode/{barcode}")] + public async Task Barcode(string barcode) + { + var result = await _productService.ResolveBarcodeAsync(barcode); + if (!result.IsSuccess) + { + return NotFound(result.Error); + } + + return RedirectToAction(nameof(Details), new { id = result.Value!.ProductId }); + } + + [HttpGet("api/products/barcode/{barcode}")] + public async Task BarcodeJson(string barcode) + { + var result = await _productService.ResolveBarcodeAsync(barcode); + if (!result.IsSuccess) + { + return Ok(new { found = false, error = result.Error }); + } + + return Ok(new + { + found = true, + productId = result.Value!.ProductId, + redirectUrl = Url.Action(nameof(Details), new { id = result.Value.ProductId }) + }); + } } diff --git a/src/CartWise.Web/Controllers/ScanController.cs b/src/CartWise.Web/Controllers/ScanController.cs new file mode 100644 index 0000000..3b2ad1a --- /dev/null +++ b/src/CartWise.Web/Controllers/ScanController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CartWise.Web.Controllers; + +[Authorize] +[Route("scan")] +public class ScanController : Controller +{ + [HttpGet("")] + public IActionResult Index() => View(); +} diff --git a/src/CartWise.Web/Views/Product/Details.cshtml b/src/CartWise.Web/Views/Product/Details.cshtml index 81bc92c..c4eb10e 100644 --- a/src/CartWise.Web/Views/Product/Details.cshtml +++ b/src/CartWise.Web/Views/Product/Details.cshtml @@ -29,4 +29,9 @@ } -

Price history and household insights will appear here once purchase tracking is available.

+
+ + +
+ +

Price history and recording a purchase will appear here once purchase tracking is available.

diff --git a/src/CartWise.Web/Views/Scan/Index.cshtml b/src/CartWise.Web/Views/Scan/Index.cshtml new file mode 100644 index 0000000..356fd94 --- /dev/null +++ b/src/CartWise.Web/Views/Scan/Index.cshtml @@ -0,0 +1,31 @@ +@{ + ViewData["Title"] = "Scan a barcode"; +} + +

Scan a barcode

+ + + +
+ +
+ + + +
+
+ + +
+
+ +
+
+ +
+ +@section Scripts { + +} diff --git a/src/CartWise.Web/Views/Shared/_Layout.cshtml b/src/CartWise.Web/Views/Shared/_Layout.cshtml index 2a726ca..3ef92ce 100644 --- a/src/CartWise.Web/Views/Shared/_Layout.cshtml +++ b/src/CartWise.Web/Views/Shared/_Layout.cshtml @@ -31,6 +31,9 @@ + }