The living Scrum backlog for this repository is docs/scrum-backlog.md.
Agents must use it as the primary planning artifact for implementation work:
docs/scrum-backlog.md when work is selected, split, deferred, blocked, or completedIf implementation order changes to unblock work, update docs/scrum-backlog.md to reflect that decision and keep the reasoning brief and explicit.
Agents must also consult:
docs/decision-log.md for accepted product and delivery decisionsdocs/release-mvp-checklist.md for the current MVP completion criteriadocs/prompt-process.md for the standard prompt format and prompt-repair workflowThe full CartWise v1 vision in this file remains the long-term target architecture and product direction.
However, the current active delivery target is a local MVP by September 10, 2026 for a solo founder + AI workflow.
For the current MVP, prioritize only the smallest launchable grocery-memory slice:
For the current MVP, these are explicitly not launch blockers unless the user reprioritizes them:
When this file and docs/scrum-backlog.md differ on near-term execution priority, use docs/scrum-backlog.md as the active delivery plan and keep this file as the broader product specification.
If a substantial user prompt does not follow the standard structure in docs/prompt-process.md, agents should still help, but should also briefly show a corrected prompt in the recommended format and then proceed using that repaired structure.
Build CartWise v1, a mobile-first grocery companion web application using ASP.NET Core MVC, Razor Views, HTML5, CSS3, jQuery, and vanilla JavaScript.
CartWise v1 is not a grocery delivery platform and is not tied to any retailer. Its first job is to become the shopper's grocery memory:
Do not expand v1 into meal planning, coupon clipping, universal retailer checkout, indoor navigation, or full cross-retailer real-time price comparison unless explicitly requested.
Use the simplest architecture that keeps domain rules testable and controllers thin.
CartWise.sln
src/
CartWise.Web/
Controllers/
ViewModels/
Views/
wwwroot/
css/
js/
pages/
services/
components/
utils/
images/
Program.cs
appsettings.json
CartWise.Application/
Interfaces/
Services/
DTOs/
Results/
Validation/
CartWise.Domain/
Entities/
Enums/
ValueObjects/
Rules/
CartWise.Infrastructure/
Data/
CartWiseDbContext.cs
Configurations/
Migrations/
Integrations/
OpenFoodFacts/
USDA/
Retailers/
Services/
tests/
CartWise.Domain.Tests/
CartWise.Application.Tests/
CartWise.Web.Tests/
Owns:
Must not contain pricing rules, replenishment rules, or database queries scattered through controllers.
Owns:
Owns:
Must not reference ASP.NET Core, EF Core, HTTP, external APIs, or Razor.
Owns:
Use ASP.NET Core Identity for authentication unless the repository explicitly specifies another authentication system.
Extend IdentityUser only with fields that are truly needed.
Suggested fields:
Do not duplicate Email, PasswordHash, etc. already supplied by Identity.
HouseholdId UUID PK
Name varchar(120) not null
CreatedUtc timestamptz not null
CreatedByUserId FK ApplicationUser
HouseholdId UUID PK/FK
UserId string PK/FK
Role smallint not null
JoinedUtc timestamptz not null
Roles:
Relationships:
Every household-scoped query must enforce membership authorization.
Represents what the shopper means generically.
Examples:
Fields:
GroceryConceptId UUID PK
Name varchar(160) not null
NormalizedName varchar(160) not null
CategoryId UUID nullable FK
CreatedUtc timestamptz not null
Indexes:
BrandId UUID PK
Name varchar(160) not null
NormalizedName varchar(160) not null
Use a simple adjacency list if hierarchy is needed.
ProductCategoryId UUID PK
Name varchar(160) not null
ParentCategoryId UUID nullable FK ProductCategory
Represents a specific sellable/package-level product.
ProductId UUID PK
GroceryConceptId UUID nullable FK
BrandId UUID nullable FK
Name varchar(240) not null
NormalizedName varchar(240) not null
SizeValue numeric(12,3) nullable
SizeUnit varchar(32) nullable
ImageUrl text nullable
SourceType smallint not null
SourceExternalId varchar(200) nullable
CreatedUtc timestamptz not null
UpdatedUtc timestamptz not null
SourceType examples:
Do not use UPC as Product PK.
ProductIdentifierId UUID PK
ProductId UUID FK not null
IdentifierType smallint not null
Value varchar(64) not null
NormalizedValue varchar(64) not null
IdentifierType:
Indexes:
Relationship:
Maps a generic concept to household preferences.
HouseholdProductPreferenceId UUID PK
HouseholdId UUID FK not null
GroceryConceptId UUID FK not null
PreferredProductId UUID nullable FK Product
SubstitutionLevel smallint not null
PreferredQuantity numeric(12,3) nullable
PreferredUnit varchar(32) nullable
UpdatedUtc timestamptz not null
SubstitutionLevel: 0 = ExactProductOnly 1 = SameBrand 2 = SimilarPreferredBrands 3 = AnyEquivalent 4 = CheapestAcceptable
Unique index:
ShoppingListId UUID PK
HouseholdId UUID FK not null
Name varchar(160) not null
Status smallint not null
CreatedUtc timestamptz not null
CompletedUtc timestamptz nullable
CreatedByUserId FK ApplicationUser not null
Status:
v1 rule: a household should have at most one default active list unless multi-list functionality is explicitly added.
ShoppingListItemId UUID PK
ShoppingListId UUID FK not null
GroceryConceptId UUID nullable FK
PreferredProductId UUID nullable FK
DisplayName varchar(240) not null
Quantity numeric(12,3) nullable
Unit varchar(32) nullable
Status smallint not null
SortOrder int not null
AddedByUserId FK ApplicationUser not null
AddedUtc timestamptz not null
PurchasedUtc timestamptz nullable
Notes varchar(500) nullable
RowVersion/version concurrency token
Status:
Rules:
RetailerId UUID PK
Name varchar(160) not null
NormalizedName varchar(160) not null
WebsiteUrl text nullable
StoreLocationId UUID PK
RetailerId UUID FK not null
ExternalStoreId varchar(120) nullable
Name varchar(200) not null
AddressLine1 varchar(200) nullable
AddressLine2 varchar(200) nullable
City varchar(120) nullable
Region varchar(80) nullable
PostalCode varchar(24) nullable
CountryCode char(2) nullable
Latitude numeric(9,6) nullable
Longitude numeric(9,6) nullable
CreatedUtc timestamptz not null
UpdatedUtc timestamptz not null
Do not require PostGIS in first migration. Latitude/Longitude are enough initially.
Represents a shopping transaction.
PurchaseId UUID PK
HouseholdId UUID FK not null
StoreLocationId UUID nullable FK
PurchasedByUserId FK ApplicationUser not null
PurchasedUtc timestamptz not null
Subtotal numeric(12,2) nullable
Tax numeric(12,2) nullable
Total numeric(12,2) nullable
SourceType smallint not null
CreatedUtc timestamptz not null
Purchase SourceType:
PurchaseItemId UUID PK
PurchaseId UUID FK not null
ProductId UUID nullable FK
GroceryConceptId UUID nullable FK
ShoppingListItemId UUID nullable FK
Description varchar(240) not null
Quantity numeric(12,3) not null default 1
Unit varchar(32) nullable
LinePrice numeric(12,2) not null
UnitPrice numeric(12,4) nullable
CreatedUtc timestamptz not null
This is append-only history.
PriceObservationId UUID PK
HouseholdId UUID nullable FK
ProductId UUID not null FK
StoreLocationId UUID nullable FK
Price numeric(12,2) not null
SalePrice numeric(12,2) nullable
UnitPrice numeric(12,4) nullable
CurrencyCode char(3) not null default 'USD'
ObservedUtc timestamptz not null
SourceType smallint not null
SourceReference varchar(200) nullable
ConfidenceScore numeric(5,4) not null
CreatedUtc timestamptz not null
SourceType:
Indexes:
Never update an old observation merely because a newer price appears. Add another row.
Do not create an elaborate ML model in v1.
A service can calculate replenishment dynamically from PurchaseItem history. If caching later becomes necessary, introduce a derived table.
Suggested v1 calculation:
Never automatically add predicted items to a list in v1. Suggest them.
ApplicationUser
M:N Household through HouseholdMember
Household
1:N ShoppingList
1:N Purchase
1:N HouseholdProductPreference
ShoppingList
1:N ShoppingListItem
GroceryConcept
1:N Product
1:N ShoppingListItem
1:N HouseholdProductPreference
Brand
1:N Product
Product
1:N ProductIdentifier
1:N PurchaseItem
1:N PriceObservation
Retailer
1:N StoreLocation
StoreLocation
1:N Purchase
1:N PriceObservation
Purchase
1:N PurchaseItem
Avoid cascade delete where deletion would destroy historical purchase or price data. Prefer restrict/no-action for historical relationships.
Use explicit IEntityTypeConfiguration classes in Infrastructure/Data/Configurations.
Examples:
HouseholdConfiguration.cs
ShoppingListConfiguration.cs
ShoppingListItemConfiguration.cs
ProductConfiguration.cs
ProductIdentifierConfiguration.cs
PurchaseConfiguration.cs
PurchaseItemConfiguration.cs
PriceObservationConfiguration.cs
Guidelines:
AsNoTracking() for read-only queries.Create focused services. Avoid “God services”.
Responsibilities:
Responsibilities:
Responsibilities:
Coordinates external data without exposing provider details to controllers.
Responsibilities:
Responsibilities:
Responsibilities:
Responsibilities:
Responsibilities:
Define:
public interface IProductDataProvider
{
string Name { get; }
Task<ProductLookupResult?> FindByBarcodeAsync(
string barcode,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<ProductSearchResult>> SearchAsync(
string query,
CancellationToken cancellationToken = default);
}
Potential implementations:
OpenFoodFactsProductDataProviderUsdaProductDataProviderProvider data must be normalized into CartWise domain/application DTOs before persistence.
Never bind Razor pages directly to provider-specific response models.
Barcode received
-> normalize barcode
-> local ProductIdentifier query
-> if found, return local product
-> if missing, query configured provider(s)
-> normalize result
-> persist Product + ProductIdentifier if acceptable
-> return product
Respect provider rate limits. Cache imported products locally.
Retailer integrations are not required for initial launch, but design an interface so they can be added later.
[Flags]
public enum RetailerCapabilities
{
None = 0,
StoreSearch = 1,
ProductSearch = 2,
Pricing = 4,
Inventory = 8,
Promotions = 16,
AisleLocation = 32
}
public interface IRetailerProvider
{
string Name { get; }
RetailerCapabilities Capabilities { get; }
}
Do not fill v1 with fake methods that no provider can support yet. Add capability-specific interfaces when implementing real integrations.
Never write controller/domain logic such as if (retailer.Name == "Meijer").
Controllers must be thin and delegate to services.
Routes:
GET /
GET /Home/Index
Responsibilities:
Routes:
GET /household
GET /household/create
POST /household/create
GET /household/members
POST /household/members/invite [future/simple v1 if desired]
POST /household/members/remove
Prefer clean route attributes.
Routes:
GET /list
POST /list/items
POST /list/items/{id}/toggle
POST /list/items/{id}/skip
POST /list/items/{id}/delete
POST /list/items/{id}/resolve
For progressive enhancement:
Routes:
GET /products/{id}
GET /products/search?q=milk
GET /products/barcode/{barcode}
If live search needs JSON:
GET /api/products/search?q=milk
GET /api/products/barcode/{barcode}
Only introduce /api endpoints for actual client-side needs.
Routes:
GET /shopping/start
POST /shopping/items/{id}/purchase
POST /shopping/items/{id}/skip
POST /shopping/items/{id}/unavailable
POST /shopping/items/{id}/substitute
POST /shopping/complete
Routes:
GET /prices
GET /prices/product/{productId}
POST /prices/product/{productId}/observe
Routes:
GET /purchases
GET /purchases/{id}
Manual purchase edit screens can be added if needed but are not core first-pass scope.
Routes:
GET /scan
The scan page uses camera/barcode JS and requests product resolution through the Product endpoint.
Never pass EF Core entities directly to views.
HouseholdName
ActiveListItemCount
RunningLowSuggestions[]
RecentPriceInsights[]
ShoppingListId
Name
Items[]
RunningLowSuggestions[]
Id
DisplayName
QuantityDisplay
Status
ProductId?
ProductImageUrl?
IsResolved
PriceHint?
ConcurrencyToken
ProductId
Name
BrandName
ConceptName
PackageDisplay
BarcodeIdentifiers[]
ImageUrl
LatestPrice?
AverageHouseholdPrice?
LowestHouseholdPrice?
UnitPrice?
PriceHistory[]
ShoppingListId
StoreLocation?
RemainingItemCount
PurchasedItemCount
Groups[]
Name
SortOrder
Items[]
Group names in v1 may be category-based rather than real store aisles:
ProductId
ProductName
Observations[]
AveragePaid
MedianPaid
LowestPaid
LatestPaid
GroceryConceptId
Name
LastPurchasedUtc
TypicalIntervalDays
EstimatedDueUtc
Confidence
Reason
Views/
Shared/
_Layout.cshtml
_MobileBottomNav.cshtml
_ValidationScriptsPartial.cshtml
_ProductImage.cshtml
Error.cshtml
Home/
Index.cshtml
Household/
Index.cshtml
Create.cshtml
Members.cshtml
ShoppingList/
Index.cshtml
_ListItem.cshtml
_RunningLowSuggestion.cshtml
Product/
Details.cshtml
Search.cshtml
_SearchResults.cshtml
Shopping/
Start.cshtml
_ShoppingItem.cshtml
Price/
Index.cshtml
Product.cshtml
Purchase/
Index.cshtml
Details.cshtml
Scan/
Index.cshtml
Use partials for server-rendered fragments that JavaScript may refresh.
wwwroot/js/
pages/
shopping-list.js
shopping-mode.js
product-search.js
scan.js
price-history.js
services/
http.js
barcode.js
offline.js # later
components/
toast.js
dialog.js
utils/
dom.js
money.js
dates.js
Rules:
navigator.mediaDevices, Service Workers, IndexedDB, Web APIs, and small focused logic where jQuery provides no benefit$.ajax, $.get, or $.post for jQuery-oriented asynchronous requests; fetch is also allowed when native code is clearerapp.jsdata-* attributes rather than brittle DOM traversalShow:
Show:
Show:
Show:
Show:
Show:
Show:
Implement deterministic functions first.
If price and package size are known:
unitPrice = effectivePrice / packageSize
Keep unit labels. Do not compare incompatible units without an explicit conversion service.
Use purchase/observed history for the household and product.
Prefer both:
Only display if minimum history threshold is met.
Example:
if current <= 10th/20th percentile or materially below median
label = Good Price
Do not hardcode an arbitrary universal threshold without tests.
Display:
Do not display “current price” for stale user observations. Prefer “last observed”.
Mandatory:
Use policies or a centralized household authorization service rather than repeating fragile checks.
Examples:
Return user-friendly validation messages. Log technical details server-side.
Use ILogger<T> structured logging.
Log:
Do not log:
Include correlation/request IDs through normal ASP.NET logging.
Test:
Test:
Use WebApplicationFactory where useful.
Test:
Do not spend early effort on pixel-perfect browser automation before domain workflows stabilize.
Create development-only seed data:
Never seed production with fake price data.
Suggested concepts:
Agents must follow this order unless a blocking dependency requires a small adjustment.
For the current local MVP target, execute in this order unless the user explicitly reprioritizes:
Post-MVP resumes after that in the backlog-defined order, especially product catalog/barcode work and dedicated shopping mode.
CartWise.sln and the four projects.Exit condition: authenticated MVC shell runs against PostgreSQL. For the current MVP, an authenticated MVC shell running correctly against SQLite is sufficient.
Exit condition: a user can create a household and all household data is access-controlled.
Exit condition: household users can maintain a useful shared grocery list even with zero product catalog data.
IProductDataProvider.Exit condition: scan known barcode -> local or external lookup -> product persisted -> viewable/addable to list.
Exit condition: user records a purchase price and later sees accurate personal price history.
Exit condition: a shopper can use the app comfortably during a real grocery trip.
Exit condition: app can suggest likely-needed repeat items without ML or automatic list mutation.
The current MVP is done when an authenticated household user can:
For the current MVP, use docs/release-mvp-checklist.md as the practical release gate.
CartWise v1 is done when an authenticated household can:
Unless the user explicitly changes scope, do not build:
Architectural seams may be left for later, but do not build speculative systems.
docs/scrum-backlog.md before starting implementation work.docs/scrum-backlog.md as work starts, changes, and completes.docs/decision-log.md before changing near-term MVP scope or delivery assumptions.docs/prompt-process.md for substantial prompts and show repaired prompt examples when a user request is too informal for reliable execution.Product, PurchaseItem).<Resource>Controller.I<Resource>Service + <Resource>Service.ShoppingModeViewModel).Async.Utc.<Entity>NameId except Identity's standard Id where appropriate._.If an agent is asked to “start building CartWise,” do not scaffold every feature simultaneously.
Build this first vertical slice end-to-end:
Register/Login
-> Create Household
-> Open Active Grocery List
-> Add “Milk”
-> Toggle Purchased
-> Reload and verify persisted state
Then extend it in the implementation order above.
When making tradeoffs, remember:
CartWise is the shopper's grocery memory.
The first release should make it effortless to remember what the household needs, what it buys, and what it normally pays. Retailer integrations are enhancements, not foundations.
Powered by TurnKey Linux.