feat: establish auth listing and image vertical slices

Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
This commit is contained in:
2026-09-08 17:32:03 -04:00
commit c728016dec
128 changed files with 9788 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
# Accessibility
Target WCAG 2.2 Level AA.
## Required practices
- Semantic HTML.
- Full keyboard access.
- Visible focus indicators.
- Logical focus order.
- Accessible names for controls.
- Labels for all form inputs.
- Required/optional state communicated programmatically and visually.
- Accessible validation and error messages.
- No color-only meaning.
- Sufficient contrast.
- Responsive layouts.
- Support browser zoom and text resizing.
- Accessible dialogs and galleries.
- Focus management for dynamic UI.
- Appropriate live-region/status announcements for important asynchronous changes.
- Meaningful image alt text.
- Use ARIA only where native semantics are insufficient.
## Testing
Run automated accessibility testing in CI.
Serious accessibility regressions should fail CI.
Automated tests do not replace manual keyboard and assistive-technology review for important workflows.
+144
View File
@@ -0,0 +1,144 @@
# API
## Base
REST API under `/api/v1`.
Responses use:
```json
{ "data": {} }
```
Collections include pagination metadata.
## Public endpoints
```text
GET /api/v1/public/search
GET /api/v1/public/products/:slug
GET /api/v1/public/inventory
GET /api/v1/public/listings
GET /api/v1/public/sellers
GET /api/v1/public/currencies
GET /api/v1/public/exchange-rates
```
## Authentication
```text
POST /api/v1/auth/login
POST /api/v1/auth/logout
POST /api/v1/auth/forgot-password
POST /api/v1/auth/reset-password
GET /api/v1/auth/me
POST /api/v1/auth/mfa/enroll
POST /api/v1/auth/mfa/verify
POST /api/v1/auth/mfa/disable
GET /api/v1/auth/sessions
DELETE /api/v1/auth/sessions/:id
DELETE /api/v1/auth/sessions
```
`POST /api/v1/auth/forgot-password` always returns the same accepted response after validating an email-shaped request, whether or not an account exists. It sends an opaque, one-time reset token only through the configured mail adapter. `POST /api/v1/auth/reset-password` consumes that token, updates the password, and revokes all active sessions.
`GET /api/v1/auth/sessions` returns only the authenticated user's active sessions and marks the current session. It never returns session or CSRF token material. `DELETE /api/v1/auth/sessions/:id` revokes one session owned by the authenticated user. `DELETE /api/v1/auth/sessions` revokes all other active sessions while retaining the current session. Session revocation endpoints require CSRF validation.
## Admin endpoints
```text
GET /api/v1/admin/products
POST /api/v1/admin/products
GET /api/v1/admin/products/:id
PATCH /api/v1/admin/products/:id
GET /api/v1/admin/inventory
POST /api/v1/admin/inventory
GET /api/v1/admin/inventory/:id
PATCH /api/v1/admin/inventory/:id
POST /api/v1/admin/inventory/:id/list
POST /api/v1/admin/inventory/:id/archive
POST /api/v1/admin/inventory/:id/relist
POST /api/v1/admin/inventory/:id/pricing/refresh
GET /api/v1/admin/listings
GET /api/v1/admin/listings/:id
PATCH /api/v1/admin/listings/:id
POST /api/v1/admin/listings/:id/publish
POST /api/v1/admin/listings/:id/unpublish
POST /api/v1/admin/listings/:id/pending
POST /api/v1/admin/listings/:id/archive
POST /api/v1/admin/pricing/refresh
GET /api/v1/admin/jobs/:id
POST /api/v1/admin/inventory/:id/images
PATCH /api/v1/admin/inventory/:id/images/order
POST /api/v1/admin/images/:id/primary
DELETE /api/v1/admin/images/:id
POST /api/v1/admin/sales
POST /api/v1/admin/bulk
GET /api/v1/admin/dashboard
GET /api/v1/admin/saved-views
POST /api/v1/admin/saved-views
PATCH /api/v1/admin/saved-views/:id
DELETE /api/v1/admin/saved-views/:id
```
Listing creation and lifecycle writes require an authenticated admin session and
the session-bound CSRF header. Publishing is seller-scoped and checks the
persisted product, category, condition, asking price, physical inventory, and
image prerequisites before changing a draft to `AVAILABLE`; sold and archived
physical inventory cannot be published. Unpublish changes
an available listing to `CLOSED`; pending changes are available from draft or
available listings.
`POST /api/v1/admin/inventory/:id/images` stores validated image metadata for an
owned physical inventory item. File bytes are handled by the future
`ImageStorage` adapter; publication counts persisted inventory images rather
than trusting a listing request field.
## Provider matching
The API must expose Product-level operations for:
- searching configured providers for candidate matches
- confirming a provider mapping
- manually searching/refining a provider query
- marking a provider not applicable
- disabling/replacing a mapping
Exact route naming may be finalized during API implementation, but these operations must remain Product-scoped.
## Validation
Validate body, query, path, and response schemas.
Business validation occurs after request-shape validation.
Allowlist sortable fields.
## Errors
Use machine-readable codes, including:
- `PRODUCT_NOT_FOUND`
- `INVENTORY_NOT_FOUND`
- `INVENTORY_EDIT_LOCKED`
- `INVALID_STATUS_TRANSITION`
- `LISTING_REQUIREMENTS_NOT_MET`
- `SALE_PRICE_REQUIRED`
- `SALE_ITEM_NOT_AVAILABLE`
- `PRICING_PROVIDER_FAILED`
- `IMAGE_LIMIT_EXCEEDED`
- `UNAUTHORIZED`
- `FORBIDDEN`
## Privacy
Public DTOs must never expose private acquisition, sale, buyer, internal, provider-debugging, pricing-history, or audit information.
## Pagination and query state
Large collections use pagination. Public/admin filters and searches should be represented in URL query state where useful for shareability, refresh, and back/forward navigation.
+87
View File
@@ -0,0 +1,87 @@
# Architecture
## Architectural style
Modular monolith.
Do not split domains into microservices unless a future architecture decision explicitly requires it.
## Runtime topology
```text
Public Web
↓ HTTPS
Reverse Proxy
↓
Fastify API ───── PostgreSQL
│
├────────────── Redis / BullMQ
│ ↓
│ Worker
│
└────────────── Image Storage
```
Docker Compose is the baseline deployment mechanism.
## Applications
### web
Vue 3/Vite public and admin frontend.
### api
Fastify REST API. Owns authentication, authorization, domain use cases, persistence, and synchronous integrations.
### worker
Processes asynchronous jobs including bulk pricing, image processing, and large exports.
## Modules
API modules:
- auth
- users
- sellers
- products
- inventory
- listings
- pricing
- sales
- images
- search
- settings
- audit
## Layering
`Vue Component → Composable → API Service → HTTP → Fastify Route → Validation → Application Service → Domain Logic → Repository → PostgreSQL`
## External boundaries
Pricing providers implement `PricingProvider`.
Image storage implements `ImageStorage`.
Search is accessed through `SearchService`.
Currency conversion is accessed through a currency/exchange-rate service.
## Shared packages
Only genuinely shared material belongs in `packages/*`:
- API contracts
- validation schemas
- safe utilities
Server-side domain logic stays on the server.
## Search
PostgreSQL-backed initially. Keep search behind an abstraction so OpenSearch/Elasticsearch can be introduced later without changing product-facing behavior.
## Authentication
Use server-side sessions with secure HttpOnly SameSite cookies. Authentication tokens must not be stored in localStorage.
## Caching
Use selective caching for stable/repeated data such as exchange rates, public metadata, and configuration. Avoid aggressive caching of mutations, financial calculations, auth state, and sale state.
+201
View File
@@ -0,0 +1,201 @@
# Data Model
## Core relationship
```text
User → Seller → Product → Inventory Item → Listing
│
├── Images
├── Acquisition
├── Pricing Observations
├── Market Valuations
└── Activity
Sale → Sale Items → Inventory Items
```
## Product
Represents what the item is, independent of physical ownership.
Examples:
- Charizard ex — SV151 — #199/165
- Pokémon Center Worlds 2025 Pikachu Plush
Products persist after all inventory is sold.
## Inventory Item
Represents one physical copy.
Each copy has its own:
- condition
- images
- acquisition
- asking price/listing
- market valuation
- notes
- status
Do not model multiple physical copies as a quantity field on one Inventory Item.
## Listing
Represents how an Inventory Item is currently offered publicly.
A Product can have many physical Inventory Items with different conditions and asking prices.
## Sale
A manual external transaction. One Sale can contain multiple Sale Items.
## Provider Product Mapping
Provider mappings are Product-level relationships.
Recommended fields:
- id
- product_id
- provider_id
- mapping_type (`PRODUCT` or `SEARCH`)
- external_identifier
- search_query / search parameters
- source_url
- status
- metadata
- created_at
- updated_at
- disabled_at
A provider can support one or both mapping types.
eBay supports both:
- specific product/catalog mappings where appropriate
- search/query mappings where no canonical product is appropriate
Mappings can be replaced without destroying historical records.
Only one active/default mapping per provider should be used for normal pricing unless a provider integration explicitly supports multiple active sources.
## Suggested pricing tables
### pricing_providers
`id, name, code, is_enabled, priority, configuration, created_at, updated_at`
### product_pricing_providers
`product_id, provider_id, is_enabled`
### pricing_provider_mappings
As described above.
### pricing_observations
`id, product_id, inventory_item_id, provider_id, source_identifier, source_url, condition_id, price_minor, currency, observed_at, quality_score, is_outlier, outlier_reason, raw_reference`
### market_valuations
`id, inventory_item_id, value_minor, currency, strategy, confidence, source_count, calculated_at`
### market_value_overrides
`id, inventory_item_id, value_minor, currency, reason, created_by, created_at, removed_at`
### pricing_jobs
`id, job_type, status, requested_by, started_at, completed_at, total_items, processed_items, successful_items, failed_items, error_summary, created_at`
## Conditions
`condition_profiles`:
- id
- category_id
- name
- active
- sort
`conditions`:
- id
- condition_profile_id
- name
- code
- quality_rank
- description
- active
- sort
## Images
`inventory_images`:
- id
- inventory_item_id
- storage_key
- original_filename
- mime_type
- file_size
- width
- height
- alt_text
- sort_order
- is_primary
- processing_status
- created_at
- updated_at
## Acquisitions
`acquisitions`:
- id
- inventory_item_id
- method
- cost_minor
- currency
- date
- source
- location
- shipping_minor
- tax_minor
- other_fees_minor
- estimated_trade_value_minor
- notes
- created_at
- updated_at
## Sales
`sales`:
- id
- seller_id
- sale_date
- sale_location
- sold_price_minor
- currency
- platform_fees_minor
- payment_processing_fees_minor
- shipping_cost_minor
- taxes_minor
- other_fees_minor
- buyer_name
- buyer_contact
- notes
- created_by
- created_at
- updated_at
`sale_items`:
- id
- sale_id
- inventory_item_id
- sold_price_minor
- cost_basis_minor
## Activity
Activity records retain:
- affected Product/Inventory Item
- event
- previous value where applicable
- new value
- actor
- timestamp
Security events are stored separately.
## Money
Store money as integer minor units. Negative values are prohibited; zero is valid. USD is canonical for stored/calculated market values unless a documented external representation requires another currency.
+24
View File
@@ -0,0 +1,24 @@
# Architecture Decision: Provider Product Mapping
## Status
Accepted
## Decision
Provider mappings are first-class Product-level records.
During new Product creation, configured pricing providers are searched for candidate matches. The admin confirms the appropriate match, refines a search manually when necessary, or marks a provider Not Applicable.
Mappings are reused by all physical Inventory Items belonging to the Product.
## Mapping types
- `PRODUCT`: specific canonical provider/catalog entity.
- `SEARCH`: provider search/query definition.
eBay supports both mapping types.
## Rationale
A provider relationship describes the identity of the Product, not a particular physical copy. Keeping it at Product level avoids repeating provider discovery for every copy while supporting multiple conditions and future copies.
A search mapping is necessary for providers such as eBay where a canonical product may not exist for every collectible or merchandise item.
+58
View File
@@ -0,0 +1,58 @@
# Deployment
## Environments
- development
- test/CI
- production
## Docker Compose
Baseline services:
- frontend
- API
- worker
- PostgreSQL
- Redis
- reverse proxy
Persistent data:
- PostgreSQL data
- Redis data where needed
- image storage
## Reverse proxy
Responsible for:
- HTTPS/TLS
- routing
- compression
- security headers
- rate limiting where appropriate
## Configuration
Environment variables/secrets:
- database connection
- Redis connection
- authentication/session secrets
- provider credentials
- email credentials
- storage credentials
Application settings such as pricing strategy and site settings belong in the database.
## Backups
Back up:
- PostgreSQL
- image storage
- required environment/configuration information
Retention policy and restore procedures must be documented for production.
Disaster recovery should allow reconstruction elsewhere from database backup, image backup, and deployment/environment configuration.
## Future hosting
Initial deployment targets a home server/NAS. The architecture should allow later movement to an online server or managed infrastructure without redesigning the domain model.
+43
View File
@@ -0,0 +1,43 @@
# Design System Direction
## Principles
- Minimal
- Clean
- Information-dense without clutter
- Accessible
- Responsive
- Consistent
- Component-driven
## Component architecture
Build atomic/reusable Vue components.
Examples:
- SearchBar
- ProductCard
- InventoryGroup
- InventoryCopyRow
- PriceDisplay
- ConditionBadge
- StatusBadge
- ImageGallery
- FilterPanel
- SortControl
- Modal/Dialog
- FormField
- ValidationMessage
- DataTable
- EmptyState
- LoadingState
- ErrorState
- ActivityTimeline
Components should have clear responsibilities and avoid embedding domain business rules.
## Visual decisions
Exact color palette, typography, spacing tokens, and final visual styling can evolve during implementation, but must preserve accessibility and the documented information hierarchy.
No color-only indication of state or condition.
+95
View File
@@ -0,0 +1,95 @@
# Image Management
## Requirements
Multiple images are core functionality.
Use cases include:
- card front/back
- corners
- edges/whitening
- holographic details
- merchandise angles
- defects/details
## Workflow
`Upload → Validate → Store Original → Queue Processing → Generate Derivatives → Complete`
Support:
- multiple-file upload
- drag/drop
- preview
- reorder
- primary image
- delete
- add images later
Image count is configurable.
## Storage
Images are not stored in PostgreSQL.
Use an `ImageStorage` abstraction.
Initial:
- Docker-mounted local storage
Future:
- S3-compatible object storage
The API uses an `ImageStorage` contract with `put`, `get`, and `delete`
operations. The current local adapter rejects absolute and traversal paths and
does not overwrite an existing key. It stores bytes outside PostgreSQL; image
metadata remains in `inventory_images`.
## Processing
Validate:
- MIME type
- file signature where practical
- size
- image dimensions
- configured image count
Generate:
- thumbnail
- medium
- large/responsive derivatives
Modern formats such as WebP/AVIF may be used where appropriate.
Retain originals where practical.
## Metadata
Store:
- storage key
- original filename
- MIME type
- file size
- dimensions
- alt text
- order
- primary flag
- processing status
- timestamps
## Alt text
Generate an initial suggestion automatically, but require the seller to be able to review/edit it.
Do not invent details not visible or supplied by the seller.
## Accessibility
Public galleries must support:
- keyboard operation
- accessible controls
- visible focus
- meaningful names
- focus management
- screen-reader-friendly state
Primary images power cards, search results, homepage previews, and social/share metadata.
+156
View File
@@ -0,0 +1,156 @@
# Implementation Status
Last updated: 2026-09-08
## Resume Here
Before changing code, read this document, `AGENTS.md`, and the linked source-of-truth documentation. Then confirm the current baseline with:
```sh
npm test
```
For persistence or Redis-backed changes, also run the isolated integration suite after setting `TEST_DATABASE_URL` to the `*_test` database and `TEST_REDIS_URL` to the test Redis instance:
```sh
TEST_DATABASE_URL='postgresql://...' TEST_REDIS_URL='redis://...' npm run test:integration
```
The current local container runtime is OrbStack. If Docker commands point at a stale host, run:
```sh
unset DOCKER_HOST
docker context use orbstack
```
## Completed
### Workspace And Database
- Npm workspace with the API and shared contracts/validation packages.
- PostgreSQL Compose service on local port `5433`; API port `3000` is registered in `.local/project-ports.json`.
- Redis Compose service on local port `6380` supports distributed login throttling.
- Drizzle schema and migrations:
- `0000_core_inventory`: users, sellers, categories, condition profiles, conditions, products, and physical inventory items.
- `0001_auth_sessions`: hashed server-side sessions, expiry, revocation, IP address, and user agent.
- `0002_session_csrf_tokens`: session-bound hashed CSRF tokens.
- `0003_security_audit_events`: private append-only authentication security events.
- `0004_password_reset_tokens`: hashed, one-time password recovery tokens.
- `0005_mfa_methods`: protected TOTP method records and lifecycle state.
- `0006_listings`: listing records and lifecycle enum.
- `0007_listing_image_count`: persisted image prerequisite for publication.
- `0008_inventory_images`: item-owned image metadata and processing state.
- Isolated integration tests require a database name ending in `_test` and truncate only that database's tables.
- Local-only `npm run provision:admin` creates the first Admin User and linked Seller atomically. It requires `INITIAL_ADMIN_EMAIL`, `INITIAL_ADMIN_PASSWORD`, and `INITIAL_SELLER_NAME`, hashes the password with scrypt, and refuses once any User exists.
### Domain And Persistence
- Product identity requires name, category, and seller.
- Products receive seller-scoped, normalized slugs; duplicate names receive a numeric suffix.
- Each Inventory Item is a separate physical copy.
- Inventory condition must belong to the Product category.
- Inventory lifecycle supports `ACTIVE`, `SOLD`, and `ARCHIVED`; sold and archived items are locked against ordinary edits.
- Repositories for products, inventory items, conditions, users, and sessions use Drizzle/PostgreSQL.
- Listing persistence supports draft creation, seller-scoped lookup, status updates, and inventory-item history.
### API And Authentication
- Fastify app bootstrap and `GET /health` response.
- Admin Product endpoints:
- `GET /api/v1/admin/products`
- `POST /api/v1/admin/products`
- Admin Inventory endpoint:
- `POST /api/v1/admin/inventory`
- Authentication endpoints:
- `POST /api/v1/auth/login`
- `POST /api/v1/auth/logout`
- `GET /api/v1/auth/me`
- Login uses asynchronous scrypt password verification. Session tokens are random, stored only as SHA-256 hashes, and never returned in JSON.
- Session cookie settings are `HttpOnly`, `SameSite=Strict`, path `/`, and `Secure` in production.
- Login is limited to five attempts per client IP every 15 minutes and returns `429 AUTH_RATE_LIMITED` with `Retry-After` once exhausted. Redis atomically coordinates the limit across API processes.
- Login sets a separate same-site CSRF cookie. Authenticated state-changing routes require its matching `x-csrf-token` header, validated against the session-bound hash.
- `GET /api/v1/auth/sessions` returns safe metadata for the current user's active sessions and marks the current session. `DELETE /api/v1/auth/sessions/:id` revokes an owned session; `DELETE /api/v1/auth/sessions` revokes all other sessions while retaining the current one. Revoking the current session clears both cookies.
- Active sessions renew `last_activity_at` after five minutes, avoiding a database write on every authenticated request.
- Authentication events are retained separately from inventory activity. `LOGIN_SUCCESS`, `LOGIN_FAILURE`, `SESSION_CREATED`, and `SESSION_REVOKED` include the known actor where available, plus safe request context. Passwords and session/CSRF tokens or hashes are never recorded.
- Password recovery uses generic `POST /api/v1/auth/forgot-password` responses to prevent account enumeration. Opaque tokens are delivered only through the SMTP adapter, stored only as hashes, expire after one hour, are consumed once, and revoke existing sessions after a password reset.
- API startup requires `SMTP_URL`, `SMTP_FROM`, and `PASSWORD_RESET_URL`; configure these local values before starting the server.
- Existing admin routes validate the authenticated session and derive seller ownership server-side.
- Request schemas live in `packages/validation`; admin response DTOs live in `packages/contracts`.
- MFA enrollment, verification, disable, protected secret handling, login verification, and audit events are implemented.
- Admin listing creation and lifecycle routes are implemented:
- `POST /api/v1/admin/listings`
- `GET /api/v1/admin/listings`
- `GET /api/v1/admin/listings/:id`
- `POST /api/v1/admin/listings/:id/publish`
- `POST /api/v1/admin/listings/:id/unpublish`
- `POST /api/v1/admin/listings/:id/pending`
- `POST /api/v1/admin/listings/:id/archive`
- Listing publication is seller-scoped, CSRF-protected, and gated by persisted identity, price, physical inventory, condition, and image prerequisites.
- Listing publication rejects physical inventory in `SOLD` or `ARCHIVED` status with a conflict response.
- Listing reads are seller-scoped and return only admin listing DTO fields.
- Item-owned image metadata can be added through a seller-scoped, CSRF-protected admin route; publication uses the persisted image count.
- `ImageStorage` and a traversal-safe local filesystem adapter are implemented; upload transport, signature validation, and derivative processing remain.
### Verified Baseline
| Suite | Current coverage |
| --- | --- |
| Domain | Product requirements and inventory lifecycle rules |
| Application | Product slug allocation, condition compatibility, password verification/recovery, session issuance, activity renewal, management, and audit events |
| HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management |
| Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling |
Latest result: `npm test` passed 62 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis.
## Next Milestone
Complete the inventory/listing integrity boundary before starting pricing and acquisition.
1. Add upload transport, MIME/signature/size validation, and processing/derivative lifecycle for persisted image metadata.
2. Remove the temporary listing image-count snapshot after storage-backed image counts are fully established.
3. Begin acquisition and pricing foundations after the listing/image boundary is complete.
Acceptance criteria: listing reads and lifecycle writes preserve seller ownership, CSRF, inventory status, and publication prerequisites.
## Overall Roadmap
Work through one bounded vertical slice at a time. The linked documentation remains authoritative when this roadmap is less detailed.
1. Authentication hardening.
2. Listings and image prerequisites: keep Listing separate from Inventory Item; enforce publish requirements from [REQUIREMENTS.md](REQUIREMENTS.md).
3. Acquisition, pricing provider mappings, observations, valuations, and private/public DTO boundaries from [DATA-MODEL.md](DATA-MODEL.md) and [PRICING.md](PRICING.md).
4. Sales workflow, sold-item locking, corrections, cost basis, and audit history.
5. SearchService, public catalog/search endpoints, filtering, pagination, and public product URLs.
6. Image storage abstraction, upload validation, processing worker, responsive variants, and alt-text editing.
7. Vue admin workflows and public catalog, following [DESIGN.md](DESIGN.md), [UX.md](UX.md), and [ACCESSIBILITY.md](ACCESSIBILITY.md).
8. Dashboard, bulk operations, exports, settings, workers, and provider adapters.
9. CI, Docker deployment completion, backups, end-to-end/accessibility coverage, and operational documentation.
## Known Gaps
- No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables.
- Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing and removal of the legacy listing snapshot remain.
- Sales, acquisitions, pricing, image processing, search, worker jobs, web UI, CI, and deployment services remain unimplemented.
- `npm install` currently reports dependency audit findings. Review and remediate them before production deployment; do not use `npm audit fix --force` without reviewing its changes.
## Local Commands
```sh
npm install
npm run db:up
npm run db:migrate
npm run provision:admin
npm test
```
The Compose database requires a local `.env` created from `.env.example`. To prepare the test database once after bringing PostgreSQL up:
```sh
docker compose exec -T postgres sh -c 'createdb -U "$POSTGRES_USER" pokemon_inventory_test'
```
Then migrate it using its `TEST_DATABASE_URL` value as `DATABASE_URL`, and run integration tests with both `TEST_DATABASE_URL` and `TEST_REDIS_URL` set. See [README.md](../README.md) for the environment-file prerequisites.
## Suggested Resume Prompt
`Read docs/IMPLEMENTATION-STATUS.md and continue the next bounded milestone. Verify the existing baseline first, follow AGENTS.md, and keep the change focused with tests.`
+126
View File
@@ -0,0 +1,126 @@
# Pricing Architecture
## Goals
Provide a defensible calculated market value while retaining provider provenance and uncertainty.
## Provider abstraction
All providers implement a common conceptual interface:
`PricingProvider`
A provider returns normalized observations containing:
- provider/source
- Product or Inventory Item context
- price
- currency
- condition
- timestamp
- source URL/identifier
- quality/confidence information
Provider adapters own provider-specific API behavior and normalization.
Use officially supported APIs or permitted sources. Do not invent APIs or bypass restrictions.
## Provider Product Mapping
Mapping occurs during new Product creation.
Workflow:
`Create Product → Search Providers → Candidate Matches → Admin Confirms Mapping`
The admin can also search/refine manually.
Mappings are persistent Product-level records and are reused for future physical copies.
Provider mapping is not required.
A provider can be:
- linked to a canonical product
- linked to a search/query
- marked Not Applicable
- left unlinked
### Mapping types
`PRODUCT`
- specific provider/catalog entity
`SEARCH`
- provider search/query definition
eBay supports both.
## Pricing pipeline
```text
Raw Provider Data
↓
Normalized Observations
↓
Quality / Confidence Analysis
↓
Valuation Strategy
↓
Calculated Market Value
↓
Optional Manual Override
↓
Display Currency Conversion
```
## Valuation
Default strategy: median.
Supported strategies should include:
- average
- median
- weighted average
The strategy is configurable.
Outliers remain in the underlying observations and are flagged with a reason. The valuation engine may account for outlier status according to the selected strategy, but raw observations are not silently discarded.
One valid source can produce a market value with low confidence.
No reliable condition-specific value means unavailable; never fabricate a value.
## Refresh
Refresh:
- one Inventory Item
- Product-level copies
- bulk selected inventory
Bulk refresh runs through BullMQ in the worker.
Provider failure should not invalidate other valid observations. The failed provider is recorded for admin diagnostics.
## Overrides
Admin can manually override market value.
Override requires:
- value
- currency
- private reason
- actor
- timestamp
Overrides are never exposed publicly as provider-level information.
## Currency
Canonical stored/calculated market values use USD.
Visitor currency conversion occurs at presentation time using cached exchange rates.
The original USD value remains authoritative.
## Staleness
Pricing freshness is configurable. Dashboard and admin inventory can identify stale values. The exact default threshold is a configuration decision during implementation, not a hard-coded domain invariant.
+36
View File
@@ -0,0 +1,36 @@
# Product Definition
## Purpose
Provide a personal catalog for Pokémon products the seller owns, allowing the seller to maintain inventory, understand market pricing, publish asking prices, and record completed external sales.
## Primary users
### Public visitor
Can search and browse available inventory, inspect item details, view calculated market value and asking price, and contact the seller.
### Admin/seller
Can manage products, physical inventory, listings, images, pricing, acquisitions, sales, settings, and history.
MVP has one seller/admin. The model is multi-seller ready.
## Non-goals
The application does not provide:
- shopping cart
- checkout
- payment processing
- shipping fulfillment
- customer accounts
- order management
- in-app e-commerce transactions
## Product principles
1. Inventory accuracy over convenience.
2. Preserve history.
3. Keep private seller data private.
4. Automate repetitive pricing work without hiding its provenance.
5. Allow unusual products that have no provider match.
6. Prefer reusable domain services over duplicated behavior.
7. Accessibility is a product requirement, not a later enhancement.
+176
View File
@@ -0,0 +1,176 @@
# Functional Requirements
## Public
### Homepage
- Minimal, search-first landing page.
- Search updates as the visitor types.
- Recently added available items appear below search.
- No-match state: `No relevant matches found.`
### Search
Search may include product name, Pokémon name, set, card number, category, description, event, year, brand, and tags.
Public search must use the same underlying search abstraction as admin search while respecting public visibility.
### Inventory
Filters include status, newly added, category, set, condition, price range, and event. Available filter values are derived from current data where appropriate.
Sorting includes:
- Recently Added
- Recently Updated
- Name A–Z
- Name Z–A
- Price Low→High
- Price High→Low
- Market Value Low→High
- Market Value High→Low
Sold is hidden by default but can be included through an explicit public filter/search.
Pending listings are public and clearly marked.
### Item details
Show images, title, product details, condition, asking price, market value where available, market update time, seller description, and configured seller contact methods.
Public pages have stable, shareable URLs.
## Inventory creation
Guided flow:
`Find/Create Product → Link Market Sources → Inventory Details → Images → Pricing → Review → List`
Required for an item to become Available:
- Product name
- Category
- Asking price
- Images
- Physical inventory record
- Condition
Provider matching is not mandatory.
### Existing Product
Admin can add another physical copy, reuse prior item data as a starting point, or create a new physical record after a previous copy sold.
### Provider matching
When creating a new Product, the system searches applicable configured providers and presents candidate matches. Admin can refine/search manually and confirm a mapping.
Mappings belong to Products and are reused by physical copies.
A provider may be marked Not Applicable. A Product may exist without any provider mapping.
## Inventory lifecycle
Inventory status:
- `ACTIVE`
- `SOLD`
- `ARCHIVED`
Listing status:
- `DRAFT`
- `AVAILABLE`
- `PENDING`
- `CLOSED`
- `ARCHIVED`
Owned items may have no active listing and may be designated for sale, personal collection, display, or other purposes.
Pending and sold items are locked against ordinary edits. Controlled corrections remain possible with audit history.
Relisting creates a new physical Inventory Item rather than reopening the sold physical record.
## Conditions
Card profile:
- NM
- LP
- MP
- HP
- Damaged
Merchandise profile:
- New
- Like New
- Used
- Damaged
Profiles and labels are configurable.
## Pricing
- Multiple providers supported.
- Median is the default valuation strategy.
- Outliers are retained and flagged.
- One valid source may produce a low-confidence value.
- Manual market-value overrides require a private reason.
- Provider errors are retained for admin visibility.
- Public sees only calculated/overridden market value, not source-level observations.
- Canonical currency is USD.
- Condition-specific values are used only when reliable data exists.
- No fabricated values.
- Pricing can refresh by item, Product, or bulk.
- Bulk refresh is asynchronous.
## Images
- Multiple images per physical item.
- Multiple-file upload and drag/drop.
- Reorder, primary selection, deletion, and later additions.
- Configurable image count limit.
- Automatic validation and optimization.
- Original retained where practical.
- Responsive derivatives generated.
- Seller can edit generated alt-text suggestions.
## Acquisition
Private acquisition information includes cost, date, source, location, shipping, tax, fees, trade value, and notes.
Total cost basis is calculated from component costs.
## Sales
Record Sale requires sold price and supports date, location/platform, fees, shipping, taxes, buyer information, and notes.
Multiple inventory items may belong to one sale.
Sale correction/undo is controlled and audited.
## Bulk operations
Supported:
- status change
- archive
- pricing refresh
- tags
Mark Sold uses the dedicated sale workflow and cannot bypass sold-price validation.
## Dashboard
Dashboard should meaningfully present:
- inventory count
- available listings
- sold count
- total acquisition cost
- total asking value
- total market value
- potential profit
- recently added items
- stale pricing
- missing images
- missing pricing
- provider errors
- recent activity
Actionable alerts link to filtered management views.
## Exports
Admin-only CSV, Excel, and JSON exports. Large exports may run asynchronously.
## Settings
General, inventory, pricing, and security settings are configurable as described in the supporting documentation.
+95
View File
@@ -0,0 +1,95 @@
# Security
## Authentication
MVP:
- email/password
- MFA using TOTP or passkey-capable architecture
- password recovery
- remember-me option
- configurable session expiry
- active session/device management
- revoke one or all sessions
Use secure server-side sessions with:
- HttpOnly
- Secure in production
- appropriate SameSite policy
Never store auth tokens in localStorage.
## Authorization
MVP has Admin role.
Design authorization so future roles can be added without rewriting domain logic.
All authorization is enforced server-side.
## CSRF
Implement an appropriate CSRF defense for cookie-based authentication.
## Input and API security
- Validate all inputs.
- Parameterize database queries through Drizzle.
- Allowlist sort fields.
- Apply pagination.
- Rate-limit sensitive/auth endpoints.
- Use security headers.
- Do not leak private fields through serialization.
## Upload security
- Validate MIME type and file signatures where practical.
- Limit size and dimensions.
- Limit image count.
- Process in controlled jobs.
- Treat filenames as untrusted.
- Do not execute uploaded content.
## Sessions
Suggested fields:
`id, user_id, session_token_hash, created_at, expires_at, last_activity_at, ip_address, user_agent, revoked_at`
## MFA
Suggested fields:
`id, user_id, type, secret_reference, name, is_enabled, created_at, last_used_at`
Secrets must be stored using appropriate secret protection; do not expose raw MFA secrets in logs.
## Security audit log
Separate from inventory activity.
Events include:
- LOGIN_SUCCESS
- LOGIN_FAILURE
- PASSWORD_RESET
- MFA_ENROLLED
- MFA_FAILURE
- SESSION_CREATED
- SESSION_REVOKED
- ACCOUNT_UPDATED
## Secrets
Credentials, API keys, database URLs, and encryption secrets belong in environment/secret management, never in source control.
## Privacy boundary
Private:
- acquisition data
- sale data
- buyer information
- profit
- internal notes
- inventory IDs
- provider observations and errors
- pricing methodology/confidence/history
- activity/audit logs
Public data must be exposed through purpose-built DTOs.
+61
View File
@@ -0,0 +1,61 @@
# Testing Strategy
Testing begins with the first implementation.
## Unit
Test domain rules and pure services:
- status transitions
- listing requirements
- cost basis
- profit
- valuation strategies
- outlier handling
- condition rules
## API / integration
Test:
- authentication
- authorization
- repositories
- API validation
- Product/provider mapping
- inventory lifecycle
- sales transactions
- public/private DTO boundaries
## Component
Test important Vue components and composables.
## E2E
Core workflows:
1. Login
2. Add Product
3. Find/link provider mappings
4. Add Inventory Item
5. Upload images
6. Calculate pricing
7. Publish listing
8. Search public site
9. Record sale
10. View dashboard
## Accessibility
Run automated accessibility checks and maintain manual keyboard/assistive technology coverage for critical workflows.
## CI
GitHub Actions should run:
- linting
- unit tests
- integration/API tests
- component tests
- E2E tests as appropriate
- accessibility checks
- build validation
Do not weaken or skip tests to obtain a passing build.
+78
View File
@@ -0,0 +1,78 @@
# UX Requirements
## Design direction
Clean, minimal, catalog-focused, and responsive.
The homepage is intentionally search-first and visually similar in simplicity to a search engine landing page.
## Public information hierarchy
For cards:
1. Primary image
2. Product/item name
3. Market value
4. Asking price
The asking price is the primary seller decision value; market value provides context.
## Product grouping
Multiple physical copies of one Product are grouped together.
Example:
```text
Charizard ex #199/165
Near Mint $115 asking $120 market
Lightly Played $95 asking $90 market
+ 1 More Available
```
Copies are sorted by condition quality, best first.
Selecting the group can reveal all copies.
Do not create separate Product groups solely because condition differs.
## Inventory management
Admin uses search plus filters and saved views.
Recommended saved views:
- Needs Attention
- Recently Added
- High Value
- Sold This Month
Bulk selection should be available where safe.
## Guided creation
`Find/Create Product → Link Market Sources → Inventory Details → Images → Pricing → Review → List`
The workflow should make it clear whether the user is editing the Product or the physical Inventory Item.
## Locked states
Pending and Sold items are visibly locked against ordinary edits. Controlled actions such as correcting a sale remain available through explicit workflows.
## Public sold items
Sold items show Sold status and no asking price.
## Responsive behavior
Support desktop, tablet, and mobile. Layouts should adapt rather than simply shrink desktop controls.
## Feedback
Dynamic actions must provide accessible status feedback, including:
- loading
- success
- validation error
- provider failure
- image processing
- background job progress
Avoid relying on color alone.