Skip to content

Latest commit

 

History

History
51 lines (43 loc) · 15.6 KB

File metadata and controls

51 lines (43 loc) · 15.6 KB

Decision Log

Numbered, durable decisions. Status: LOCKED (agreed) / OPEN (deferred). A decision that changes the spec or design updates the relevant doc before code is written.

ID Decision Why Status
D1 Monorepo of independent codebases. Root holds shared artifacts; each impl in its own folder: estore-laravel/ (backend API), estore-nuxt/ (frontend), future estore-springboot/, estore-dotnet/. Plus contract/, docs/, process/, temp/. One workspace, many comparable implementations of one contract. LOCKED
D2 Strict, root-owned OpenAPI 3 contract is the single source of truth (contract/). Every backend language impl conforms to the same contract; the one Nuxt frontend targets the contract, not any single impl. Fair cross-language comparison; one frontend works against any backend unchanged. LOCKED
D3 v1 backend scope = Catalog + Cart + Checkout + Accounts/Auth + Admin/Inventory. The core e-commerce loop plus store management. LOCKED
D4 No online payments in v1. Checkout creates an order with payment status pending (no gateway). User: "first not pay online." Payment integration deferred. LOCKED (deferred → B1)
D5 Actors/roles = Guest, Customer, Admin. Anonymous browsing, registered ordering, store management. LOCKED
D6 PostgreSQL in Docker Compose, shared baseline across all language impls. Parity baseline for a fair comparison; reproducible env. LOCKED
D7 Backend = modular monolith, 8 modules: Core, Identity, Access, Catalog, Pricing, Inventory, Cart, Ordering (docs/architecture/system-design.md). One-directional deps; admin ops live in their owning module (no god Admin module); Ordering is the sole checkout orchestrator owning the atomic tx. Microservices unjustified; boundaries = comparison unit; doctrine (separate catalog from fulfillment). LOCKED
D8 Package strategy: prefer mature, well-maintained Laravel ecosystem packages over hand-rolling. Research (local laravel_docs/ first, then web), propose each with rationale; explicit approval required before adding any dependency. v1 list APPROVED: prod — nwidart/laravel-modules, laravel/sanctum, spatie/laravel-permission, spatie/laravel-query-builder, spatie/laravel-medialibrary, spatie/laravel-data, brick/money; dev — pestphp/pest, hotmeteor/spectator. Contract is hand-authored & validated (Spectator), not generated (no Scramble). User steer + standing rule; list approved 2026-06-25. LOCKED
D9 Frontend = Nuxt 3 / Vue 3, SSR (universal) mode. Pinia for state; a type-safe API client generated from the root OpenAPI (so one client targets any backend). SEO on storefront/product pages matters; generated client keeps the frontend contract-bound. LOCKED
D10 Offers = first-class entity (admin-managed): discount type (percentage/fixed via a registry), value, date window, linked to products (many-to-many). Effective price = best active applicable offer. User-entered coupon codes still deferred (B2). User chose "separate Offer entity" over a price field. LOCKED
D14 Cart: guest cart via anonymous cart token; merges into the account on login; must be logged in to place an order (order always has an owner). User choice. LOCKED
D15 Inventory: track stock qty per product; checkout validates + decrements atomically in one DB transaction; overselling blocked (fail loud). Order cancellation restocks. User choice + money/integrity-atomic doctrine. LOCKED
D16 Categories: flat (non-nested), many-to-many with products. Nesting → backlog. User choice. LOCKED
D17 Error handling: typed domain-exception hierarchy thrown by services, rendered by the framework-native central exception handler (no per-controller try/catch); single error shape = RFC 9457 Problem Details (application/problem+json, successor to 7807). User: "handle requests via a custom Exception class"; doctrine: framework-native + portable across Laravel/Spring/.NET. LOCKED
D18 Error-code scheme: every error carries a stable code (numeric, one 100-block per module: common 1000 · identity 1100 · access 1200 · catalog 1300 · pricing 1400 · inventory 1500 · cart 1600 · ordering 1700) and a code_name = module.ErrorName; type URI derived from code_name. A single error catalog (contract/error-catalog.md, Stage 2) is the source of truth; adding an error = next number in the block (OCP, never a renumber). User: "code number + code-name module.ErrorName"; aligned to RFC 9457 + gRPC numeric + Stripe string-code conventions. LOCKED
D11 Adopt the user's system-design-components methodology + base/ template for Stage 2 module designs (context, user-stories, use-case, CRC cards, domain-model, data-model, glossary → modular-monolith architecture/sequence/pseudocode/tradeoffs). User-supplied, matches our doctrine and architecture exactly. LOCKED
D12 Reuse the existing auth + rate-limiting modular-monolith component designs as-is for the Identity/Auth module and the rate-limiting NFR. Already designed at our target level; DRY. LOCKED
D13 Use examples/vip_backend as the primary structural + package reference for estore-laravel (modular monolith via nwidart/laravel-modules). Drives the D8 package shortlist proposed for approval in Stage 2. Proven real-world Laravel 13 modular monolith. LOCKED (ref); package list OPEN

| D19 | No Docker for local dev. Dockerfile + docker-compose.yml moved to deploy/ (outside the code, handled last). Dev runs on the host toolchain against host PostgreSQL (127.0.0.1:5432, role deployer, DBs estore + estore_test) and host Redis (optional — dev uses the database cache/queue drivers). Postgres stays the locked DB (D6); containerization is a deploy-time concern. | User: "not use docker for now… deploy folder handles it." | LOCKED | | D20 | Version control = one git repo per code folder. estore-laravel is its own repo (branch main); estore-nuxt gets its own when scaffolded. The estore/ root is local-only (no VCS for now). A future umbrella repo may be added later to link the docs + the per-code repos. Framework .gitignores handle vendor//node_modules//.env. | User: "git init on backend code, another on frontend; root local only; maybe a monorepo later." | LOCKED |

| D21 | Contract amendment (post-freeze): added POST /admin/products/{publicId}/images (multipart upload) to contract/openapi.yaml and wired spatie/medialibrary on Product (images collection); Product.images now resolves to real URLs. Image deletion deferred (B9). | Frozen contract under-covered the spec (FR-A1/FR-C3 want product images); user chose to amend + wire medialibrary. | LOCKED |

Future language implementations (Spring Boot, .NET) are siblings, not current scope — see backlog B3/B4. AEKL (Spring Boot + Angular) feeds B3. Post-freeze contract changes are logged here (e.g. D21).

| D22 | QA hardening (Stage 5). Applied security + code-review fixes: order address encrypted:array (PII at rest; column → text), redacted identifiers in auth logs, finite Sanctum token TTL, two-bucket rate limiting (throttle:api/auth), per_page clamp, exact-decimal price filters, currency allowlist, removed dead request() coupling. Contract change: removed two unreachable error codes — inventory.StockNotFound (1500) and cart.NotFound (1600) — from contract/error-catalog.md (they were never thrown). | security-reviewer + code-reviewer audits; fix real findings, keep the contract honest. | LOCKED | | — | Accepted-by-design (not changed): register returns 409 IdentifierInUse (auth component accepts registration enumeration; login stays generic); password min 8 / no composition (auth component default); offers don't validate product existence (would create the Catalog↔Pricing cycle); order-number collision retry left to the unique constraint (≈62^12 entropy); cart holds an item even if later unpublished (soft-hold; checkout decrement is the hard guard). | | INFO | | D23 | Frontend scaffold (SG-10) — Nuxt 4. Amends D9: Nuxt 3 → Nuxt 4.4.8 (current stable; still Vue 3, SSR/universal, Pinia — everything D9 intended, newer major + longest support). Stack: pnpm; @pinia/nuxt (stores auth+cart); openapi-typescript generates app/types/api.ts from the root contract/openapi.yaml, consumed via a thin typed wrapper over Nuxt's native $fetch/useFetch (no extra runtime client — idiomatic SSR data layer). SSR-safe Sanctum bearer + guest cart tokens stored in cookies (useTokens); errors read via getProblem (RFC 9457). New app/ srcDir layout. estore-nuxt is its own git repo (branch main). Dev API base http://localhost:8000/api/v1 (overridable via NUXT_PUBLIC_API_BASE). | User chose current-stable Nuxt + idiomatic-SSR client (vs. the older locked major / a heavier client). Approved 2026-06-25. | LOCKED | | D24 | Contract clarification (post-freeze): Address.country is an ISO 3166-1 alpha-2 code (2 chars, e.g. US). The backend order validation already enforced country ≤ 2 chars, but contract/openapi.yaml only declared type: string — surfaced via a 422 when the Nuxt checkout sent full country names. Added minLength/maxLength: 2 + description/example to the contract; the frontend checkout now submits ISO codes. | Contract must document the real constraint so every impl (Laravel/Spring/.NET) and the frontend agree; found during SG-10b checkout wiring. | LOCKED | | D25 | Contract additions (post-freeze, frontend-driven): offer listing + product image delete. Added GET /admin/offers (→ Offer[]) so admins can list/edit/delete offers (was create-only), and GET /admin/products/{id}/images (media list with ids) + DELETE /admin/products/{id}/images/{mediaId} for image management (closes B9). Backend built TDD (140 tests green, Pint clean); frontend offer-management + per-image delete wired. | The two UX gaps needed these endpoints; implemented TDD on Laravel and updated the contract to stay authoritative for all impls. | LOCKED | | D26 | Umbrella monorepo realized — fulfils D20. The cloned GitHub repo aristonis/estore (at estore/, branch main) is now the umbrella/meta repo and the canonical home of the shared cross-language artifacts: contract/ (OpenAPI + error-catalog — the single source of truth, D2) and docs/ were moved in from the local-only workspace root. Per-language code repos (estore-laravel, estore-nuxt, future Spring Boot/.NET) stay separate repos, linked from the README Repositories index (→ promote to git submodules once they have remotes). Both contract consumers repointed ../contract../estore/contract: Laravel (config/spectator.php + 8 module error-catalog tests), Nuxt (gen:api + doc refs). Verified: Laravel 140/140, Nuxt types regenerate byte-identical, typecheck + build green. The workspace root (projects/estore/) stays local-only progress scaffolding (.progress/, memory/, backlog.md). Split (refined): the monorepo docs/ holds only product docs (requirements/spec.md, architecture/system-design.md, decision-log.md); process/work-progress docs — stage-gates/, work-log.md, references.md (local filesystem pointers) — moved to the workspace process/, since they describe how the build was run, not what the system is. | User: "make new folder for monorepo that contains docs, api_contracts, links to other repos"; chose canonical-home + rewire over a published copy; later asked to keep only true product artifacts in the repo. | LOCKED |

Sprint 1 — Social Proof (2026-06-26) — reviews & ratings + wishlist + related products. Spec docs/requirements/sprint-1-social-proof.md, design docs/architecture/sprint-1-social-proof-design.md. Pulls backlog B6 (reviews/wishlist) into scope.

| D27 | Reviews = new sealed module + event-driven rating mirror (cycle break). New Reviews module (error block 1800s) owns reviews, status (published/hidden), per-review verified-purchase, aggregates, and admin moderation. Ratings surface on the Catalog Product payload via a derived product_ratings read-model that a Catalog listener updates on Core\Events\ProductRatingChanged (emitted by Reviews on every mutation). Catalog never imports Reviews — a direct read would form Catalog→Reviews→Ordering→Catalog. Verified-purchase = live Ordering.hasPurchased() at write. | Acyclic one-directional deps (same trap dodged for Pricing, D22 INFO); doctrine "auto-mirror, single source of truth" (truth = reviews, mirror = derived); live check needs no backfill. | LOCKED | | D28 | Dual rating on the Product payload (resolves spec OQ-1). Reviews are open to all logged-in customers (purchase not required); each review shows a verified-purchase badge. Product carries two averages + counts: overall (all published reviews) and verified-buyers-only (average_rating/review_count + verified_average_rating/verified_review_count), all present (counts default 0, averages null). Moderation = auto-publish + admin hide/delete (OQ-2). | User: "show diff rating between all vs purchased-users together"; demoable while still exercising the Ordering service-to-service check. | LOCKED | | D29 | Wishlist = new sealed module, customers-only (reserves error block 1900s; uses none yet — reuses catalog.ProductNotFound). Per-account saved products; idempotent add/remove; presented like the catalog (Pricing + Inventory + rating mirror). New Permission::ManageReviews ('manage-reviews') auto-granted to admin by the existing RoleSeeder (enum insert, OCP). Guest wishlist deferred (OQ-3 → backlog). | User chose customers-only; reuse over new error codes (KISS); RBAC stays data-driven. | LOCKED | | — | Contract amendments (Sprint 1, applied in Stage 4 SG-R0, Spectator-gated): + GET/POST /products/{id}/reviews, PATCH/DELETE /products/{id}/reviews/{reviewId}, GET /products/{id}/related, GET /wishlist, POST /wishlist/items, DELETE /wishlist/items/{productId}, GET/PATCH/DELETE /admin/reviews[/{reviewId}]; Product schema gains the four rating fields; new Review/ReviewInput/ReviewModerationInput/RatingSummary/ReviewPage/Wishlist/WishlistItemInput schemas. SG-R4 refinement: GET /admin/reviews returns a dedicated AdminReviewPage (page + data, no summary) — a cross-product moderation list has no meaningful single rating summary. | Contract stays the source of truth for every impl; logged per the post-freeze rule. | LOCKED | | D30 | Sprint 1 QA/acceptance hardening. security-reviewer + code-reviewer fan-out; triaged + fixed real findings: review mutations wrapped in a DB::transaction with the synchronous mirror recompute (integrity-atomic — no half-written review / stale aggregate on a transient failure); own-review edit/delete scoped by product_public_id (REST-correct, closes a wrong-product-URL acceptance + minor IDOR); review DELETE204 noContent; frontend my-review cookie persisted (maxAge 1y). Accepted/backlog: review pagination (B10), CSP + config/cors.php deploy hardening (B11), Review status/is_verified_purchase $fillable defense-in-depth (not exploitable — explicit/allow-listed assignment), rebuild-ratings chunking (YAGNI). No new dependency; backend 191/191, frontend 50/50. | Reviewers proactively after the chunk; fix real findings, keep the contract honest; defer non-regressions. | LOCKED |