# 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 `.gitignore`s 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 `DELETE` → `204 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 |