From bdb3a083acb025a55ae09e3d2aa2d0f8e5c8ba4b Mon Sep 17 00:00:00 2001 From: AdilAzhariOmsan Date: Tue, 3 Nov 2026 20:53:14 +0800 Subject: [PATCH 1/5] docs(discovery): add Phase 0 assessment and Phase 1 review --- .../discovery/PHASE-0-TECHNICAL-ASSESSMENT.md | 1285 +++++++++++++++++ docs/discovery/PHASE-1-REVIEW.md | 158 ++ 2 files changed, 1443 insertions(+) create mode 100644 docs/discovery/PHASE-0-TECHNICAL-ASSESSMENT.md create mode 100644 docs/discovery/PHASE-1-REVIEW.md diff --git a/docs/discovery/PHASE-0-TECHNICAL-ASSESSMENT.md b/docs/discovery/PHASE-0-TECHNICAL-ASSESSMENT.md new file mode 100644 index 0000000..afa04a0 --- /dev/null +++ b/docs/discovery/PHASE-0-TECHNICAL-ASSESSMENT.md @@ -0,0 +1,1285 @@ +# Nexo 2.0 — Phase 0: Discovery and Technical Assessment + +| | | +|---|---| +| **Status** | Draft for owner review | +| **Date** | 2026-11-01 | +| **Scope** | Read-only assessment of the existing codebase. No application code, configuration, migrations or dependencies were changed. | +| **Repository assessed** | `AdilAzhari/nexo-ecommerce` → local `C:\laragon\www\modular-ecommerce-system` (its `origin` remote is `Sentients-AI/nexo-ecommerce`) | +| **Working tree assessed** | Branch `redesign` @ `774d131`, **plus 44 uncommitted files** (33 modified, 11 untracked). `redesign` is 4 commits ahead of / 2 behind `main`. | +| **Also inspected** | `C:\laragon\www\Nexo` (`Sentients-AI/Nexo`): a separate, earlier restart on Laravel 13 + React 19 (see §16, Q2) | + +### How to read this report + +Every finding is tagged with: + +- **Severity:** Critical / High / Medium / Low / Info. +- **Kind:** + - **Defect:** behaviour is wrong today. + - **Risk:** an architectural concern that will cause defects. + - **Opportunity:** an improvement, not a fix. +- **Evidence level:** + - **Verified:** executed or observed (tests, type-check, command output). + - **Code-confirmed:** traced through source, not executed. + - **Documented:** claimed by docs and not verified. + - **Assumption:** an inference that still needs confirming. + +All paths are relative to the repository root unless stated otherwise. + +--- + +## 1. Executive summary + +**What exists.** The repository is a large Laravel 12 + Filament 5 + Vue 3/Inertia 2 application: + +- 621 PHP files in `app/` across 29 `app/Domain/*` folders. +- 109 migrations. +- 81 Vue single-file components. +- ~1,314 Pest test cases in 135 files. + +It was built as a **multi-tenant SaaS** (one isolated store per subdomain). On 2026-08-30 it was partly re-documented as a **single multi-vendor marketplace** for the Sudan market (`docs/product/*`, ADR-0002..0004). The code still implements the SaaS model. The marketplace pivot exists on paper plus a small WIP on `redesign`, and that WIP takes a direction (a session-scoped "shopping tenant") that contradicts its own ADR-0004. + +**The headline.** The codebase *looks* far more sophisticated than it *behaves*. It has DDD folder structure, specifications, guards, value-object IDs, a domain-event table, projections, metrics and an `INVARIANTS.md`. But several load-bearing paths are broken or never wired: + +1. **Payments never mark an order as paid** (Critical, code-confirmed). + - No production code dispatches `PaymentSucceeded`, so the `MarkOrderPaid` listener never runs. + - The webhook success path also calls Stripe's *confirm* API on a PaymentIntent the browser has already confirmed. Stripe rejects that call, and the job then fails and rolls back. +2. **The domain-event "outbox" is write-only** (Critical, code-confirmed). `DomainEventRecorder::record()` writes to `domain_events`. Nothing ever schedules or dispatches the job that reads it (`DispatchDomainEventsJob`). As a result: + - `RefundSucceeded` never reaches `UpdateOrderRefundState` or `ReleaseStockOnRefund`. + - `PaymentFailed` never reaches its listeners. +3. **Inventory arithmetic is internally inconsistent** (Critical, code-confirmed). + - Reserving stock subtracts it twice: it decrements `quantity_available` *and* `isAvailable()` subtracts `quantity_reserved` again. + - Release and cancel paths ignore `variant_id`, and use three different, contradictory semantics. +4. **Vendor dashboard has no role authorization** (Critical, code-confirmed). + - Any logged-in user whose `tenant_id` matches the store can use `/vendor/*`. Storefront customers get the store's `tenant_id` automatically on registration. + - Such a user can set any order to any status (bypassing the state machine), edit products and prices, and adjust stock. +5. **Staging deploys the `redesign` branch against the production database** (High, documented in the workflow itself). This conflicts directly with the brief's rule never to connect experiments to production. +6. **The "concurrency/torture" tests do not test concurrency.** They run sequentially on SQLite, where `lockForUpdate()` is a no-op, and their assertions are loose enough to pass with the double-subtraction bug present. + +**Assessment.** The *ideas* are mostly sound and worth keeping: integer-cents money, idempotency keys, snapshotted order lines, a state-machine enum, an `OrderGroup` + per-vendor `Order` split (ADR-0004) and context-dependent vendor scoping (ADR-0003). The *implementation* is not a safe base to evolve. The money, inventory and authorization paths need re-implementation against a fresh schema, not patching. This supports the brief's hybrid-rebuild strategy: + +- Reuse concepts, enums, value objects, specifications and selected UI pieces as references. +- Re-implement the transactional core with real tests (MySQL, real concurrency). + +**The single most important owner decision (Q1)** is the product context. The ratified docs target the **Sudan market with Stripe + Stripe Connect**. The new brief targets a **portfolio-grade, Lazada/Shopee-style general marketplace (EN/AR/MS)**. Stripe does not support Sudan-based businesses or Connect accounts *(assumption, to verify against Stripe's current country list)*, so the documented V1 money flow cannot work for the documented market. This decision shapes ADR-0005 (payments), currency, demo data and copy. + +--- + +## 2. Repository inventory + +### 2.1 Stack (verified from `composer.json`, `package.json`, `composer.lock`) + +| Layer | Declared | Notes | +|---|---|---| +| PHP | `^8.2` (README says 8.4; CI uses 8.4; local CLI is 8.5.8) | Constraint is looser than reality | +| Laravel | `^12.0` | | +| Admin | `filament/filament ^5.3` | Single panel `control-plane` (`app/Providers/Filament/AdminPanelProvider.php`) | +| Frontend | Vue 3.5, `@inertiajs/vue3 ^2.3`, TypeScript 5.9, Tailwind 4, Vite 7 | `vue-tsc` installed but **not run in CI** | +| Payments | `laravel/cashier ^16.1` **and** `stripe/stripe-php: "*"` (unpinned) | Cashier used only for the (dormant) subscription feature | +| Realtime | `laravel/reverb: "*"` (unpinned), laravel-echo, pusher-js | | +| Search | `laravel/scout ^11.1`, `typesense/typesense-php ^6.0` | | +| Auth | Sanctum 4, Socialite (Google) | | +| Misc | `nativephp/electron: "*"`, `amphp/parallel`, `barryvdh/laravel-dompdf`, `fakerphp/faker` (in **require**, not dev) | `nativephp/electron` has no visible use in a web marketplace; unpinned `*` constraints are a supply-chain/reproducibility risk | +| Quality | Pint, Rector, `carthage-software/mago 1.1.0`, Pest 4, `laravel/boost` | **No PHPStan/Larastan.** Mago is installed but not wired into CI | +| Composer audit | `config.audit.ignore: GHSA-vvj3-c3rp-c85p` | Ignored advisory must be justified (Q9) | + +### 2.2 Top-level layout + +``` +app/Application/ UseCases (Checkout, ProcessPayment, RequestRefund) + Request/Response DTOs +app/Domain/ 29 folders (see §4) +app/Infrastructure/Payment/Stripe/PaymentGatewayService.php (only infrastructure adapter) +app/Filament/ ~23 resources, 8 pages, 17 widgets +app/Http/ 36 web controllers, 17 API v1 controllers, 9 middleware, 10 policies +app/Shared/ Alerting, Metrics, Performance, Specifications, ValueObjects, Domain events, AuditLog +app/Jobs/ HandleStripeEventJob, ConfirmPaymentIntentJob, DispatchDomainEventsJob +resources/js/ Pages (49), Components (29), Composables (13), Layouts (3), types (4) +lang/{en,ar,ms}/ui.php 117 lines each +tests/ Feature (Api, Domains, Journeys, MultiTenancy, Torture, Web, Filament…), Unit, Browser (3) +.github/workflows/ ci.yml, pr-checks.yml, format.yml, codeql.yml, deploy.yml, deploy-staging.yml +``` + +### 2.3 Documentation inventory and its reliability + +| Document | Describes | Reliability | +|---|---|---| +| `README.md` | Feature list (≈40 features), SaaS multi-tenancy | **Overstates.** It lists e.g. "Order Processing — full lifecycle (pending → paid → …)", which the payment defect breaks (§7.1). The 2026-08-30 banner admits it is mid-pivot. | +| `docs/product/{vision,goals,personas,problem-statement,scope}.md` | Sudan-market marketplace, Stripe Connect, V1 success criteria | Coherent, but is *intent*. Contradicted by the new brief on market and audience. Untracked (uncommitted). | +| `docs/architecture/adr/0002..0004` | Vendor = tenant record; explicit vendor scoping; OrderGroup + per-vendor Order | Status "Accepted" but **not implemented**. ADR-0005 (payments) referenced, never written. Untracked. | +| `docs/architecture/marketplace-migration.md` | Phases A–G of SaaS → marketplace pivot | Plan only | +| `INVARIANTS.md` | Invariants, state machines, guards | Partly aspirational. Its own "Known Gaps" section admits incomplete guards and a webhook bug. Claims "Order Pending → Paid via `PaymentSucceeded` event via webhook", which is not wired (§7.1). | +| `PRODUCTION_READINESS_REVIEW.md`, `DATABASE_SCHEMA.md`, `API.md`, `docs/COMPREHENSIVE_CODEBASE_GUIDE.md`, `docs/DECISIONS.md`, `docs/ARCHITECTURE_DECISIONS.md` | SaaS-era deep dives | Out of date relative to the pivot. Treat as historical. | +| `docs/REDESIGN_BRIEF.md`, `redesign.md`, `Check.md` | Earlier visual-redesign briefs ("Nile Market" palette) | Superseded by the Nexo 2.0 brief | +| `CLAUDE.md` (git-ignored) | Agent instructions | Not part of the shared repo | + +--- + +## 3. Current architecture + +### 3.1 Runtime shape + +``` +Browser (Vue/Inertia SPA) Filament /control-plane (Livewire) + │ Inertia pages /{locale}/… │ + │ JSON /api/v1/* (session+Sanctum) │ + ▼ ▼ +routes/web.php ─ middleware: locale, tenant.subdomain | tenant.user, auth +routes/api/v1.php ─ api group + EnsureFrontendRequestsAreStateful + ResolveTenantFromUser + │ +Controllers ──(some)──► Application/UseCases ──► Domain/*/Actions ──► Eloquent models + │ (48 of ~60 controllers query Eloquent directly) + ▼ +MySQL/MariaDB ◄── Context('tenant_id') global TenantScope on 46 models +Queue: HandleStripeEventJob; Scout→Typesense; Reverb broadcasts; Schedule: 3 commands +``` + +### 3.2 Layering in practice + +- **Application layer is thin and partial.** Only three use cases exist: `CheckoutUseCase`, `ProcessPaymentUseCase` and `RequestRefundUseCase`. Most behaviour lives in controllers or Filament pages calling models or actions directly. +- **Domain is not independent of infrastructure.** Domain models are Eloquent models, and actions call `DB::transaction`, `Context`, `config()` and `MetricsRecorder` directly. 11 domain files import `App\Events\*` (broadcast events defined outside the domain). This is normal for Laravel, but it is not the "domain independent of infrastructure" the brief asks for. +- **Two parallel event mechanisms:** + 1. Laravel events (`X::dispatch`, auto-discovered listeners in `app/Domain/*/Listeners`, see `bootstrap/app.php`). + 2. `DomainEventRecorder` → `domain_events` table, which has no consumer (§7.2). + + Which mechanism is used varies per action, and that is the root cause of several defects. +- **Duplicated actions:** + - `Inventory/Actions/ReserveStock` and `ReserveStockAction` are near-identical, but only one filters by variant. + - `Order/Actions/CheckoutAction` is used only by tests. + - `Payment/Webhooks/HandlePaymentWebhookAction.php` is empty. + +### 3.3 Domain dependency graph (code-confirmed, from `use App\Domain\…` imports) + +``` +Order -> Bundle Cart Currency GiftCard Inventory Payment Product Promotion Refund Shipping Tax Tenant User (14) +Product -> Category Inventory Order Question Review Tenant User +Refund -> Inventory Order Payment Tenant User +Payment -> Order Tenant +Inventory-> Order Product Tenant User +Promotion-> Cart Category Order Product Tenant User +Tenant -> Product Review Role User +User -> Order Role Tenant +``` + +**Cycles:** + +- Order↔Payment +- Order↔Inventory +- Order↔Refund +- Order↔Product +- Order↔Promotion +- Product↔Inventory +- Tenant↔User + +No tooling enforces boundaries: no architecture tests, no deptrac, no PHPStan rules. + +--- + +## 4. Current domain map + +| Folder | Files | Scope tag in `docs/product/scope.md` | Observed state | +|---|---|---|---| +| Refund | 32 | Core | Rich model, broken event delivery (§7.2), race conditions (§7.5) | +| Order | 29 | Core | State-machine enum good; bypassed in several places (§7.4) | +| Inventory | 23 | Core | Arithmetic inconsistent (§7.3) | +| Product | 22 | Core | Variants, price history, downloads | +| Payment | 22 | Core | Success path broken (§7.1) | +| Promotion | 18 | Extended | BOGO/tiered/bundle/flash/code + experiments | +| User | 15 | Core | Is tenant-scoped (`BelongsToTenant`) | +| Review, Referral, Loyalty | 13/13/12 | Extended | | +| Cart | 12 | Core | One cart = one tenant | +| Chat, Question, GiftCard, Subscription, Bundle, Content, FeatureFlag, Config | 2–9 each | Experimental/Extended | Candidates for retirement | +| Projections | 7 | — | Refund/order-financial projections + replay; depends on the unconsumed event table | +| Tenant | 5 | Core ("Vendor") | Subdomain resolution + global scope | +| Tax, Shipping, Currency, Idempotency, Role, Notification, Category, Shared | 1–7 each | Core/Extended | | + +**Key observation.** "Domain" here means *folder*, not *bounded context*. Ownership of state transitions is spread across controllers, Filament pages, listeners and actions. For example, `Order.status` is written from: + +- `VendorOrderController::updateStatus` +- `CancelOrder` +- `CompensateStockOnCancelAction` +- `HandleStripeEventJob::handleFailure` +- `MarkOrderPaid` +- `RetryOrderFinalizationAction` +- Filament `ViewOrder` + +--- + +## 5. Current database design + +### 5.1 Overview (code-confirmed from `database/migrations/*`) + +- 109 migrations. 41 of them alter existing tables, and many are "add_x_to_y" patches. This is a normal history, but it is a strong signal for the brief's fresh-schema decision. +- Money is stored as **integer cents** (`bigInteger …_cents`). This is good and should be kept. `decimal` is used only for `exchange_rate (10,6)`, tax `rate (5,4)` and metrics. +- `tenant_id` was retro-fitted to ~45 tables by one migration, `2026_02_07_000002_add_tenant_id_to_tables.php`. It is nullable, with `cascadeOnDelete`. + +### 5.2 Integrity findings + +| # | Finding | Evidence | Sev / Kind | +|---|---|---|---| +| D1 | Deleting a **tenant cascades** to orders, payment intents, refunds, stock movements and every other tenant-owned table. A vendor deletion would destroy financial history. | `add_tenant_id_to_tables.php` (`->cascadeOnDelete()`) | High / Risk | +| D2 | Deleting a **user cascades** to their orders; deleting a **product cascades** to historical `order_items`. | `create_orders_table.php:18`, `create_order_items_table.php:19` | High / Risk | +| D3 | `stocks`: unique only on `variant_id`. Product-level stock rows (`variant_id NULL`) have no uniqueness, so duplicates are possible. Lookups use `->first()`, so which row is used is non-deterministic. | `relax_product_id_unique_constraint_on_stocks_table.php` | High / Defect-enabling | +| D4 | No DB-level guarantee that `quantity_reserved ≤ quantity_available`, or that either is ≥ 0. `unsigned()` exists on MySQL only; tests run on SQLite. | `create_stocks_table.php` | Medium / Risk | +| D5 | `payment_intents.provider_reference` is neither unique nor indexed, yet it is the webhook lookup key. | `create_payment_intents_table.php` | Medium / Risk | +| D6 | `stock_movements` has no idempotency key (e.g. unique `(stock_id, type, reference_type, reference_id)`), so duplicated movements are possible. | `create_stock_movements_table.php` | Medium / Risk | +| D7 | `refunds` has no idempotency key and no unique `external_refund_id`. `payment_intent_id` is a free string (Stripe id), not an FK. | `create_refunds_table.php`, `make_payment_intent_id_nullable_on_refunds.php` | Medium / Risk | +| D8 | `idempotency_keys.key` is **globally unique** *and* `(key, action)` is unique. Two users choosing the same key collide. | `create_idempotency_keys_table.php` | Low / Defect | +| D9 | Product `slug` is unique per `(tenant_id, slug)`, but the storefront route `/{locale}/products/{product:slug}` is marketplace-wide. Two vendors with the same slug make the route ambiguous. | `add_tenant_id_to_tables.php:54-55`, `routes/web.php` | Medium / Defect (marketplace) | +| D10 | Categories are tenant-owned (`(tenant_id, slug)`). A marketplace needs a platform-owned taxonomy. | same | Medium / Model mismatch | +| D11 | `orders` has no `order_group_id`, `commission_*` or transfer fields. ADR-0004 is unimplemented. | `create_orders_table.php` + later alters | — / Gap | + +### 5.3 Implications of a fresh database (brief §3) + +**Benefits:** + +- Removes ~109 migrations of accreted patches. +- Allows vendor ownership (`vendor_id`), platform-owned taxonomy, `order_groups`, a ledger, `RESTRICT` FKs on financial tables and CHECK constraints from day one. + +**Costs:** + +- No continuity with `store.aljebal-albeedos.com`. Its users, vendors, orders and Stripe objects are not migrated. +- If that site stays live on the old code, it is a separate product. It must keep its own database, and the new app must never point at it (see R5). +- Any future data migration would be an explicit, separate project with its own ADR. + +--- + +## 6. Frontend assessment + +| Area | Finding | Evidence | Sev | +|---|---|---|---| +| Structure | Reasonable Inertia layout: `Pages/`, `Components/{UI,Layout,Products,…}`, `Composables/`, `Layouts/{Guest,Authenticated,Vendor}`. Every SFC uses `lang="ts"`. | `resources/js/**` | Info | +| Type safety | **`vue-tsc --noEmit` reports 22 errors** (TS2339 ×14, TS18046 ×4, TS2322 ×3, TS2353 ×1), mostly in `Pages/Products/Show.vue` (10), `Vendor/Dashboard.vue` (4) and `Orders/Show.vue` (3). CI never runs it. | Verified, run locally 2026-11-01 | Medium | +| Contracts | Types in `resources/js/types/{models,api}.ts` are hand-written. Controllers build ad-hoc arrays (e.g. `CheckoutController::summary`) with no generated or validated contract, and money is passed as raw ints with inconsistent naming (`price`, `subtotal`, `total_cents`). | `app/Http/Controllers/Web/CheckoutController.php` | Medium | +| Design system | Component kit is minimal (Alert, Badge, Modal, Toast, DataTable, EmptyState, Spinner…). No tokens doc. `resources/css/app.css` is 253 lines. The largest pages are monoliths: `Products/Show.vue` 1,370 lines, `Checkout/Summary.vue` 603. | | Medium | +| i18n | `vue-i18n` is created with **empty messages**. Translations come via the Inertia shared prop `translations` from `lang/*/ui.php` (117 lines/locale). 174 `t()` call sites, but many hard-coded English strings remain (e.g. `Pages/Refunds/Status.vue:84`). | `resources/js/i18n/index.ts`, `HandleInertiaRequests.php:64` | Medium | +| RTL | `` is set correctly for `ar`. But layout uses **201 physical direction utilities** (`ml-/mr-/pl-/pr-/left-/right-`) vs **1 logical** (`ms-/me-/ps-/pe-/start-/end-`), so Arabic layout will mirror incorrectly. | `resources/views/app.blade.php:2`; grep | High (for an Arabic-first product) | +| Payments UI | Uses Stripe Payment Element + client-side `confirmPayment` (`Composables/usePayments.ts:91`), which is correct. But it then calls the server `/api/v1/checkout/confirm-payment`, which re-confirms server-side (§7.1). | | Linked to C1 | +| Vendor UI | 19 vendor pages exist (dashboard, orders, products CRUD/import, inventory, returns, Q&A, reviews, promotions, storefront, settings, bundles, earnings, analytics, customers) | `Pages/Vendor/*` | Info: good feature inventory to re-spec | + +**Verdict.** Treat the frontend as a **feature and requirements reference**, not a base. A new design system (Phase 2) plus logical-property RTL plus a typed props contract will touch nearly every file anyway. + +--- + +## 7. Critical business-flow findings (payments, inventory, orders, refunds) + +### 7.1 C1 — Successful payments never transition orders to Paid · **Critical · Defect · Code-confirmed** + +**Trace:** + +1. The browser confirms the PaymentIntent with Stripe.js (`resources/js/Composables/usePayments.ts:91`). +2. Stripe sends `payment_intent.succeeded` → `StripeWebhookController` → `HandleStripeEventJob::handleSuccess()`. +3. `handleSuccess` calls `ConfirmPaymentIntentAction::execute()`. That calls `Infrastructure/Payment/Stripe/PaymentGatewayService::confirmIntent()`, which calls `$stripe->paymentIntents->confirm()` **on an intent that already succeeded**. Stripe rejects confirming a succeeded intent *(assumption, Stripe API behaviour; verify with a test-mode call)*. +4. The action catches the error, sets `status = Failed` and rethrows *inside* `DB::transaction`. The write rolls back and the job fails. +5. Even if step 3 succeeded, the action only sets `PaymentIntent.status = Succeeded`. **Nothing in `app/` dispatches `PaymentSucceeded`** (grep shows only the class definition, a listener type-hint and tests). So `Order/Listeners/MarkOrderPaid` never runs. + +**Further problems:** + +- `/api/v1/checkout/confirm-payment` (auth-only) has the same re-confirm problem. Guests have no equivalent. +- `payments:reconcile-orphaned` (`ReconcileOrphanedPaymentsCommand`) would repair orders, but it is **not scheduled** (`routes/console.php` schedules only loyalty expiry, abandoned carts and FX rates). It also only matches intents already marked `Succeeded`, which step 4 prevents. +- **Test gap:** no test exercises the webhook path (`grep HandleStripeEventJob|webhooks/stripe tests/` returns nothing). + +**Impact:** + +- Customers are charged while orders stay `pending`. +- Vendors filter by status and do not see the order as paid. +- Stock stays reserved. + +*Note:* recent commits on `main` ("return the actual client_secret…", "uncaught ModelNotFoundException on checkout…") suggest the live checkout was being debugged in this area. + +### 7.2 C2 — Domain-event outbox has no consumer · **Critical · Defect · Code-confirmed** + +- `App\Shared\Domain\DomainEventRecorder::record()` inserts into `domain_events`. +- `App\Jobs\DispatchDomainEventsJob` would replay unprocessed rows as Laravel events, but **nothing dispatches or schedules it**. +- Events recorded only this way (`ProcessRefundAction`, `RequestRefundAction`, `RetryRefundExecutionAction`, `HandleStripeEventJob::handleFailure`): + - `RefundSucceeded` → listeners `UpdateOrderRefundState` and `ReleaseStockOnRefund` **never run**. + - `RefundFailed` and `RefundRequested` likewise never reach listeners. + - `PaymentFailed` → `Payment/Listeners/PaymentFailed` **never runs**. +- Even if scheduled, the job is unsafe: + - No row locking or claim (`SELECT … FOR UPDATE SKIP LOCKED`), so concurrent workers double-dispatch. + - It marks a row processed after `event()`, so a listener exception leaves the row to be re-dispatched forever, with no attempts counter or dead-letter. + - `new $eventClass(...$record->payload)` depends on `get_object_vars` ordering and includes base-class properties. + +### 7.3 C3 — Inventory arithmetic is inconsistent · **Critical · Defect · Code-confirmed** + +| Operation | `quantity_available` | `quantity_reserved` | Implied meaning of `available` | +|---|---|---|---| +| `Stock::isAvailable()` (`Inventory/Models/Stock.php`) | uses `available − reserved` | | on-hand (gross) | +| `ReserveStock` / `ReserveStockAction` | **−qty** | +qty | net of reservations | +| `CompensateStockOnCancelAction` | +qty | −qty | net | +| `ReleaseStockAction` (used by `CancelOrder` and `ReleaseStockOnRefund`) | unchanged | −qty | gross | +| Payment success / fulfilment | *(nothing consumes reservations)* | | — | + +**Consequences:** + +- **Phantom stockouts.** With 10 units, reserving 3 leaves `available=7, reserved=3`, so sellable is 4. A second reservation of 3 leaves 4/6, so sellable is −2. Only 6 of 10 units can ever be sold. +- **Stock never returns.** `CancelOrder` decrements `reserved` but never restores `available`. +- **Variant blindness.** + - `ReleaseStockAction` and `CompensateStockOnCancelAction` look up `Stock` by `product_id` only. + - `CreateOrderFromCart::lockAndValidateStock()` and `ReserveStock` add the variant filter only when `variant_id !== null`. A product-level cart line can therefore lock or modify *any* variant's row. + - `ReserveStockAction` (used by `CheckoutAction`) never filters by variant at all. +- **No reservation expiry.** Unpaid or abandoned checkouts hold stock forever. No scheduled release exists. +- **No deadlock discipline.** Rows are locked in cart-item order, not sorted by id. Concurrent multi-item checkouts can deadlock. +- **The "torture" tests miss all of this.** `tests/Feature/Torture/ConcurrentStockReservationTest.php` runs a sequential `for` loop on SQLite, where `lockForUpdate()` is a no-op. Its assertions (`reserved ≤ initial`, `available ≥ 0`) pass even with double subtraction. + +### 7.4 C4 — Order state machine is bypassed · **High · Defect · Code-confirmed** + +- `OrderStatus::canTransitionTo()` and `Order/Support/OrderStateGuard` exist and are reasonable. +- `VendorOrderController::updateStatus()` does `$order->update(['status' => $validated['status']])` for **any** enum value: `paid`, `refunded`, back to `pending`, and so on. +- `CancelOrder` only refuses `cancelled`/`completed`. It cancels paid or shipped orders without a refund and without a guard. +- `HandleStripeEventJob::handleFailure()` sets `Order → Failed` on the first `payment_intent.payment_failed`. In Stripe that event is **not terminal**: the customer can retry with another card on the same intent. Because `PaymentStatus::Failed` is terminal locally, a later `succeeded` webhook is ignored. The result is a charged customer with a failed order, and **no stock release** on failure. +- `OrderStatus::PartiallyRefunded → PartiallyRefunded` is disallowed, so a second partial refund cannot be represented. +- No row lock or optimistic version on status writes, so concurrent transitions are last-write-wins. + +### 7.5 C5 — Refund races and unsafe provider calls · **High · Defect/Risk · Code-confirmed** + +- `RequestRefundAction` checks `getRemainingRefundableAmount()` **outside** the transaction and without locking the order. Concurrent requests can over-refund. +- `ProcessRefundAction`: + - Checks status, then updates, without `lockForUpdate`, so concurrent approvals can issue two Stripe refunds. + - Calls `gateway->refund()` **without a Stripe idempotency key** (`PaymentGatewayService::refund`). + - On any exception, including a timeout after Stripe succeeded, marks the refund `Failed`. A retry (`RetryStuckRefundsCommand`, `RetryRefundExecutionAction`) can then refund twice. +- There is no reconciliation against Stripe refund objects (`charge.refunded` / `refund.updated` webhooks are not handled). + +### 7.6 C6 — Checkout transaction design · **High · Risk · Code-confirmed** (`Application/UseCases/Order/CheckoutUseCase.php`, `Domain/Order/Actions/CreateOrderFromCart.php`) + +- **External call inside the DB transaction.** The Stripe `paymentIntents->create` call (`CreatePaymentIntentAction`) runs inside the outer `DB::transaction`, while holding stock row locks. + - If the commit fails afterwards, an orphan Stripe intent exists. + - Stripe latency extends lock hold time. + - The Stripe request carries **no idempotency key** (the local `idempotency_key` is not passed to Stripe). +- **Cart not locked.** Two concurrent submissions both pass `CartIsNotCompleted`. HTTP idempotency (`EnsureIdempotentAction`) only applies to **authenticated** users who send an `Idempotency-Key` header. It is check-then-act with no claim row: `lockForUpdate` on a missing row locks nothing, and it is not inside a transaction. +- **Prices taken from the cart snapshot** (`cart_items.price_cents_snapshot`) at add-to-cart time, without re-pricing or re-validation at checkout. +- **Currency conversion done per component** (subtotal, tax, shipping, each discount converted separately), so the converted parts may not sum to the converted total. The base currency comes from the ambient `Context::get('tenant')`. +- **Promotion usage recorded only when `$user !== null`.** Guests can reuse a limited code without bound. +- **Tax by region never applies.** `CreateOrderData::shippingAddress` is never populated by `CheckoutUseCase`. +- **Events dispatched before commit.** `OrderCreated` and `OrderStatusUpdated` (broadcast) fire inside the transaction with no `ShouldDispatchAfterCommit`, so listeners and broadcasts can observe rolled-back orders. + +--- + +## 8. Security assessment + +| # | Finding | Evidence | Sev / Kind / Evidence level | +|---|---|---|---| +| S1 *(revised in Addendum A.6: on production code this is **cross-vendor**)* | **Vendor dashboard has no role/permission check.** Routes use only `['auth','tenant.user']`. `ResolveTenantFromUser` just sets context from `users.tenant_id`. Storefront registration (`Web/AuthController::register` → `User::create`) gives customers the current store's `tenant_id` via `BelongsToTenant`'s `creating` hook. A **customer can open `/vendor/*`** and change order statuses, edit products and prices, adjust stock, export customer lists, answer reviews and toggle promotions. None of the 26 vendor controllers call `authorize`/Gate. Tests cover only "vendor" and "guest", never "customer". | `routes/web.php` vendor group; `app/Http/Controllers/Web/Vendor*.php`; `tests/Feature/Web/VendorDashboardTest.php` | **Critical / Defect / Code-confirmed** | +| S2 | **Tenant scope fails open.** `TenantScope::apply` adds no filter when `Context('tenant_id')` is null. Any route or job where context is not resolved is unscoped: users with `tenant_id NULL` on `/vendor/*`, guest API calls, queued jobs and console commands. Also, `/vendor/*` route-model binding (`{order}`, `{product}`, `{stock}`) may resolve **before** `tenant.user` sets context, depending on middleware priority. *Needs a runtime test to confirm cross-tenant IDOR.* | `app/Domain/Tenant/Scopes/TenantScope.php`; `bootstrap/app.php` | High / Risk / Code-confirmed (IDOR: assumption) | +| S3 | **Guest checkout of another user's cart.** `Api/V1/CheckoutController::checkout` checks ownership only when *both* the requester and the cart have a user. An anonymous request with any (sequential) `cart_id` checks out, completes and archives a logged-in user's cart, and reserves its stock. | `CheckoutController.php` (`if ($user !== null && $cart->user_id !== null …)`) | High / Defect / Code-confirmed | +| S4 | **Filament panel shared by platform and tenant staff**, with resources lacking policies. Filament grants access when no policy exists. No policy covers `SubscriptionPlan` (global, not tenant-scoped), `PaymentIntent`, `GiftCard`, `ShippingMethod`, `TaxZone`, `Bundle`, `ContentPage`, `Review`, `VariantAttributeType`, `PromotionExperiment`. `canAccessPanel` admits any tenant `admin`/`support`/`finance` user. | `app/Policies/` (10 policies), `User::canAccessPanel`, `app/Filament/Resources/*` | High / Risk / Code-confirmed (per-resource reachability: to verify) | +| S5 | **Exception messages exposed to clients.** The global Inertia error renderer passes `$e->getMessage()` for 500s. It also references an un-imported `HttpException` (resolves to global `\HttpException`, so `instanceof` is always false), and JSON clients receive an Inertia HTML page. | `bootstrap/app.php` `withExceptions` | Medium / Defect | +| S6 | **Webhook hardening.** An invalid signature throws an unhandled `SignatureVerificationException` and returns 500 (should be 400). There is no event-id de-duplication table. The whole Stripe event object is serialised into the queue payload. Amount and currency in the event are not checked against the local intent. | `Webhooks/StripeWebhookController.php`, `Jobs/HandleStripeEventJob.php` | Medium / Risk | +| S7 | Arbitrary status writes by vendors (S1 + §7.4) allow marking unpaid orders paid or delivered. That would trigger downloads (`GenerateDownloadsOnOrderPaid`) and loyalty or earnings if listeners key off status. | `VendorOrderController::updateStatus` | High / Defect | +| S8 | Rate limits exist on API groups (`throttle:20–60,1`). Login/register web routes have no explicit throttle *(assumption: no custom limiter found)*. | `routes/web.php`, `routes/api/v1.php` | Low / Risk | +| S9 | Staging shares the production DB and auto-deploys on CI success on `redesign`, with optional `migrate --force`. Staging code (including unreviewed WIP) can mutate production data. | `.github/workflows/deploy-staging.yml` header + "Finalize deploy" step | High / Risk / Verified (workflow text) | +| S10 | Demo credentials: all seeded users share `password`. Acceptable for demo only. The README documents it, and these seeders must never run against production. | `README.md`, `database/seeders/DatabaseSeeder.php` | Info | + +**Not found (positive):** mass-assignment uses `$fillable`; Sanctum stateful API; CSRF-exempt only `webhooks/stripe` and `_boost/*`; download links are token-gated; `dompurify` is used for rich content. + +--- + +## 9. Testing and quality assessment + +| Item | Finding | Evidence | +|---|---|---| +| Volume | ~1,314 test cases / 135 files. Broad surface coverage. | `grep -c` | +| Execution | See §9.1 for this assessment's run. | Verified | +| DB engine | **SQLite in-memory** (`phpunit.xml`). Production is MySQL/MariaDB. Row locks, `unsigned`, JSON functions, collation and FK behaviour differ, and no locking or concurrency claim is testable. | `phpunit.xml:26-27` | +| Concurrency | `tests/Feature/Torture/*` are sequential loops (§7.3). No multi-process or multi-connection tests. | | +| Payments | Gateway mocked with Mockery in API tests (fine). **No webhook tests**, and no tests for duplicate or late webhooks, decline-then-succeed, or provider timeouts. | `tests/Feature/Api/V1/CheckoutApiTest.php` | +| Authorization | No "customer cannot use vendor dashboard" or "vendor A cannot mutate vendor B by id" tests on web vendor routes (`MultiTenancy/TenantIsolationTest.php` covers model scoping). | | +| Architecture | No `arch()` tests, no deptrac, no PHPStan/Larastan. | | +| Static analysis in CI | Pint (`--test`), Rector `--dry-run`, `composer audit`, `npm audit`, CodeQL (JS/TS only). **Missing:** PHPStan, `vue-tsc`, ESLint, Mago. | `.github/workflows/ci.yml`, `codeql.yml` | +| Frontend tests | None (no Vitest, no Playwright beyond 3 Pest "Browser" files). | `tests/Feature/Browser` | +| Test isolation | No global `tests/Pest.php`. Each file calls `uses(TestCase::class, RefreshDatabase::class)` itself. Inconsistent and easy to forget. | | + +### 9.1 Test run performed for this assessment + +See **Addendum A.1** for complete results. Summary: 1,311 tests: **1,293 passed, 17 failed, 1 skipped** (4,403 assertions), 2,121 s locally. All 17 failures are caused by the uncommitted WIP change to `routes/web.php`. CI on the committed `redesign` head (`774d131`) reports 1,291 passed, 1 skipped. + +--- + +## 10. Performance risks + +| # | Risk | Evidence | Sev | +|---|---|---|---| +| P1 | Stripe HTTP call made while holding stock row locks inside the checkout transaction | §7.6 | High | +| P2 | Unsorted row locking makes concurrent multi-item checkouts deadlock-prone | `CreateOrderFromCart::lockAndValidateStock` | Medium | +| P3 | 48 controllers build queries inline. Repeated status-count queries (`VendorOrderController::index` runs a separate `GROUP BY` on every request). No caching of catalogue facets. | | Low–Medium | +| P4 | Translations cached "forever until cleared" per locale and shipped on **every** Inertia response as a shared prop | `HandleInertiaRequests.php:64-97` | Low | +| P5 | `payment_intents.provider_reference` unindexed (webhook lookup) | D5 | Low (pilot scale) | +| P6 | Large monolithic pages (`Products/Show.vue` 1,370 lines) and no route-level code-splitting review | | Low | +| P7 | Typesense index requires manual `scout:import-all` after deploys, and failure is silent (README admits this) | `README.md` | Medium (operational) | + +The pilot scale in `docs/product/goals.md` (<100 orders/day) means correctness, not throughput, is the binding constraint. + +--- + +## 11. Technical debt summary + +1. **Two product models in one codebase.** SaaS tenancy (subdomains, global scope, per-tenant categories, per-tenant currency) coexists with half-started marketplace semantics. On `redesign`, the session `shopping_tenant_id` (`ResolveTenantFromSubdomain::resolveShoppingTenant`, `Web/Concerns/RequiresShoppingTenant`) forces a cart to be single-store, contradicting ADR-0004. +2. **Two event mechanisms**, one of them dead (§7.2). +3. **Duplicate or dead code:** + - `ReserveStock` / `ReserveStockAction`. + - `CheckoutAction` (test-only). + - Empty `HandlePaymentWebhookAction.php`. + - `ConfirmPaymentIntentJob` (never dispatched in production; *to verify*). +4. **Feature sprawl.** About 10 "Experimental" features (subscriptions, gift cards, A/B promotion experiments, chat, Q&A, waitlist, abandoned cart, fraud dashboard, downloads, AI recs) each add schema, Filament resources and test load. +5. **Unpinned dependencies** (`stripe/stripe-php`, `laravel/reverb`, `nativephp/electron` set to `*`), and `faker` in production `require`. +6. **Stale documentation** that over-claims (README, INVARIANTS, PRODUCTION_READINESS_REVIEW). +7. **Uncommitted WIP** (44 files) on a branch that auto-deploys to a production-connected staging site. + +--- + +## 12. Reusable vs replaceable matrix + +| Component | Disposition | Rationale | +|---|---|---| +| Integer-cents money convention; `currency` + `exchange_rate` snapshot on orders | **Reuse (concept)** | Correct; formalise as a `Money` value object | +| `OrderStatus` / `PaymentStatus` / `RefundStatus` enums + `canTransitionTo` | **Reuse as reference** | Good starting state machines; fix gaps (§7.4) and enforce in one place | +| Specification pattern (`app/Shared/Specifications`) | **Reuse selectively** | Useful for composable eligibility rules (promotions, refundability). Avoid using it for every guard. | +| Typed IDs (`AbstractId`, `OrderId`…) | **Optional** | Adds ceremony. Decide in the ADR on conventions. | +| Order-line snapshotting (`price_cents_snapshot`, `tax_cents_snapshot`) | **Reuse (concept)** | Correct for financial history | +| Idempotency-key model | **Re-implement** | Concept right, implementation racy (§7.6) | +| `CheckoutUseCase` / `CreateOrderFromCart` | **Replace** | Must become OrderGroup + per-vendor orders, with an external call outside the transaction and sorted locks | +| Inventory actions | **Replace** | Semantics inconsistent (§7.3). Redesign as on-hand + reservations ledger. | +| Stripe gateway adapter & webhook job | **Replace** | Wrong confirm flow, no event de-dup, no idempotency keys | +| Refund actions | **Replace** | Races, unsafe retries (§7.5) | +| `DomainEventRecorder` / `DispatchDomainEventsJob` | **Replace** with a real transactional outbox (if ADR approves) | §7.2 | +| `BelongsToTenant` global scope | **Retire** | Per ADR-0003: explicit vendor scoping + policies + tests | +| Subdomain tenancy, onboarding `/start` trial, `tenant-switcher` | **Retire** | Contradicts the single-storefront model | +| Filament control plane | **Keep the technology; rebuild resources** | Filament 5 suits an operator back office. Rebuild with policies, separate it from vendor UI, and remove tenant-staff access. | +| Vendor dashboard (Vue pages) | **Reference only** | Good feature inventory. Rebuild on the new design system with authorization. | +| Storefront Vue pages | **Reference only** | Rebuild with logical-property RTL and typed contracts | +| `lang/*/ui.php` strings | **Reuse as seed** | Restructure per namespace | +| Metrics / alerting / correlation-id / audit log (`app/Shared`) | **Evaluate in Phase 8** | Possibly useful. Not on the critical path. | +| Experimental features (§11.4) | **Retire from v2 scope** unless the owner promotes them | Reduces surface area | +| CI workflows | **Rewrite** | Add PHPStan, vue-tsc, ESLint, MySQL service, arch tests. Remove the prod-DB staging. | +| Seeders/factories | **Rewrite** | New schema; curated demo dataset (brief §14) | + +--- + +## 13. Target architecture proposal (initial — for ADRs in Phase 1) + +### 13.1 Shape: a modular monolith, one Laravel app + +``` +app/ + Modules/ (or src/) one folder per bounded context, each with: + / + Domain/ entities/aggregates, value objects, domain events, policies (pure PHP where practical) + Application/ commands/queries (use cases), DTOs, ports (interfaces) + Infrastructure/Eloquent models, repositories/queries, gateways (Stripe), listeners + Http/ controllers, form requests, resources (Inertia props), routes + Contracts/ the ONLY namespace other modules may import (interfaces + DTOs + events) + Platform/ cross-cutting: Money, Clock, Ids, Outbox, Idempotency, Audit +``` + +Whether Eloquent models live in `Domain` (pragmatic) or behind repositories (purist) is itself an ADR. **Recommendation: pragmatic.** + +- Eloquent models live inside the module. +- Business rules go in aggregate methods and domain services. +- Other modules access them only through `Contracts`. +- Enforcement uses Pest `arch()` tests plus deptrac. + +### 13.2 Proposed bounded contexts (fewer than the 29 folders today) + +| Context | Owns | Notes | +|---|---|---| +| **Identity & Access** | users, roles/permissions, vendor memberships, sessions | Shopper is marketplace-wide. Vendor staff hold a membership row (`vendor_id`, role). Operators are platform roles. No `tenant_id` on users. | +| **Vendors** | vendor profile, application/approval lifecycle, payout account reference, commission overrides | Rename `tenant` → `vendor` (fresh DB makes this free) | +| **Catalogue** | platform taxonomy (categories, attributes), products, variants, media, moderation state, pricing (list/sale price per variant) | Pricing stays inside Catalogue until promotions need more | +| **Inventory** | stock per variant (on-hand), reservations (with TTL), movements ledger | Single-writer of stock; invariant `reserved ≤ on_hand` enforced in DB and code | +| **Ordering (Cart + Checkout + Orders)** | cart, checkout session, **OrderGroup**, **VendorOrder**, lines, fulfilment state machine | Checkout orchestrates Inventory + Payments + Promotions via contracts | +| **Payments & Finance** | payment attempts, provider events (de-duplicated), refunds, **ledger** (customer charge, commission, vendor payable, refund liability), payouts (later) | Its own context because money has its own lifecycle and audit needs | +| **Promotions** | campaigns, codes, eligibility, allocation | Platform-run first | +| **Engagement** | reviews, wishlist, (loyalty if promoted) | | +| **Search** | read model / Typesense projection | Fed by events; never a source of truth | +| **Notifications** | channels, templates, preferences | Event subscriber only | +| **Platform Ops** | audit log, configuration, feature flags, health | | + +**Dependency direction (proposed):** + +``` +Http → Application → Domain; Infrastructure → Domain +Ordering → {Catalogue, Inventory, Payments, Promotions}.Contracts +Payments → Ordering.Contracts (events only) +Search, Notifications → *.Contracts events only +Forbidden: any module importing another module's Domain/Infrastructure; + Domain importing Illuminate\Http, Filament, Inertia; + cross-module Eloquent relationships / joins outside read-model queries. +``` + +### 13.3 Key transactional design (to be decided by ADRs, not implemented yet) + +- **Checkout runs as a short saga:** + 1. Validate and re-price. + 2. One DB transaction creates the `OrderGroup`, `VendorOrders` and reservations (TTL), with locks taken in sorted order. It also writes outbox rows. + 3. After commit, create the provider payment (idempotency key = checkout id). + 4. Webhook drives `paid`, which consumes the reservations. + 5. A scheduled sweeper releases expired reservations and cancels unpaid groups. +- **Webhooks:** + - Verify the signature, return 400 on failure. + - Insert into `provider_events(provider, event_id UNIQUE)` and return 200. + - Process asynchronously with row locks. + - Re-fetch the provider object for authoritative state. +- **Money:** a double-entry ledger for commission, vendor payable and refund liability, even before real payouts exist. It makes reconciliation testable without Stripe Connect. +- **Outbox:** written in the same transaction and drained by a scheduled worker using `SKIP LOCKED`, with attempts, backoff and dead-letter. +- **Concurrency tests:** run on MySQL in CI using real parallel processes or connections (e.g. `pcntl_fork` or `amphp/parallel`, already a dependency). + +### 13.4 Stack recommendation (each item an ADR) + +| Decision | Recommendation | Why | +|---|---|---| +| Laravel version | **Laravel 13 / PHP 8.4+** for the fresh start | Fresh app, no upgrade cost. Local PHP is 8.5. Pick the newest stable LTS-equivalent the host supports. | +| Frontend | **Vue 3 + Inertia (v2 or v3) + TypeScript (strict)**, per the brief | The brief says Vue, but the separate `Nexo` repo chose React. Owner must confirm (Q2). | +| Admin | **Keep Filament** for the operator panel only. Vendor dashboard in Vue/Inertia. | Filament excels at operator back-office CRUD, and vendors deserve a product-grade UI | +| DB | **MySQL 8** (dev via Docker/Laragon; CI service container). Stop testing on SQLite for anything transactional. | Locking semantics | +| Queue/cache | Redis | Already present | +| Search | Start with **MySQL full-text + good indexes**. Add Typesense behind a `Search` port when demo data warrants it. | Reduces moving parts. Keeps search an ADR, not a default. | +| Realtime | Defer Reverb until a feature needs it (order status push) | Fewer services for reviewers to run | +| Payments | Stripe in **test mode** behind a `PaymentProvider` port. Connect/payout decision deferred to the finance ADR. | See Q1/Q3 | +| Static analysis | PHPStan/Larastan (level 8 target), Pint, Rector, `vue-tsc`, ESLint, Pest `arch()`, deptrac | Brief §13 | +| Contracts | Typed Inertia props generated from PHP DTOs (e.g. `spatie/laravel-data` + `typescript-transformer`) | Avoids duplicated rules and hand-written types. Needs an ADR. | + +--- + +## 14. ADR backlog (proposed, none written yet) + +| # | Title | Depends on | +|---|---|---| +| 0001 | ADR process, template (brief §18 fields), statuses | — | +| 0002 | Product context & market (portfolio showcase vs Sudan pilot), currencies, locales | Q1 | +| 0003 | Fresh repository vs fresh branch; Laravel/PHP versions; Vue vs React | Q2 | +| 0004 | Modular-monolith structure, module contracts and enforced dependency rules (arch tests + deptrac) | 0003 | +| 0005 | Identity model: shopper / vendor membership / operator; RBAC library choice | 0004 | +| 0006 | Vendor ownership & data isolation (explicit scoping, policies, isolation test suite as merge gate) — supersedes old ADR-0003 | 0005 | +| 0007 | Catalogue: platform taxonomy, attributes, variants, media, moderation | | +| 0008 | Inventory model: on-hand + reservations with TTL + movement ledger; DB constraints; lock ordering | | +| 0009 | Order aggregate: OrderGroup + VendorOrder; state machines; who may transition what | | +| 0010 | Checkout saga, transaction boundaries, idempotency (HTTP + provider) | 0008, 0009 | +| 0011 | Payment provider integration: intent lifecycle, webhook ingestion & de-dup, reconciliation | 0010 | +| 0012 | Financial model: ledger, commission freezing, vendor payables, refunds/chargebacks, payouts deferral | 0011 | +| 0013 | Domain events & transactional outbox | 0004 | +| 0014 | Backend↔frontend contract strategy (DTO → TS generation, money/date/enum serialisation, error shape) | | +| 0015 | Search strategy (DB first vs Typesense) | | +| 0016 | i18n & RTL strategy (logical properties, locale routing, currency/number/date formatting) | 0002 | +| 0017 | Testing strategy: MySQL in CI, concurrency harness, failure-simulation fakes | | +| 0018 | Environments & data isolation: demo env, seeding, **no shared prod DB** | | +| 0019 | Admin surface: Filament scope and separation from vendor UI | | +| 0020 | Feature retirement list (experimental features out of v2 scope) | Q5 | + +--- + +## 15. Risk register (prioritised) + +| ID | Risk | Likelihood | Impact | Severity | Mitigation / phase | +|---|---|---|---|---|---| +| R1 | Carrying the broken payment, inventory and refund cores into v2 by "reusing" them | High if reused | Critical | **Critical** | Re-implement behind ADR-0008..0012, test-first on MySQL (P3–P5) | +| R2 | Market/payment-provider mismatch (Sudan + Stripe) invalidates the money design | High | Critical | **Critical** | Owner decision Q1 before Phase 1 money ADRs | +| R3 | Vendor isolation regressions after removing the global scope | Medium | Critical | **Critical** | Policies + isolation suite as merge gate + arch test (ADR-0006) | +| R4 | Concurrency bugs undetected because tests run on SQLite | High | High | **High** | MySQL in CI + parallel-process harness (ADR-0017) | +| R5 | New work touching the production DB (shared-DB staging, copied `.env`) | Medium | Critical | **High** | Separate repo/env; never reuse prod credentials; disable staging-on-prod workflow for v2 (ADR-0018) | +| R6 | Scope creep from ~40 legacy features | High | Medium | **High** | Explicit v2 scope (Q5, ADR-0020) | +| R7 | Uncommitted `redesign` WIP lost or auto-deployed | Medium | Medium | **Medium** | Owner decides: commit to a branch, stash, or discard (Q8) | +| R8 | Financial model under-specified (commission, refunds, chargebacks) | Medium | High | **High** | ADR-0012 before Phase 5 | +| R9 | RTL treated as mirroring | Medium | Medium | **Medium** | Logical properties from the design system up (ADR-0016) | +| R10 | Portfolio credibility: README claims outrun behaviour | High (today) | High | **High** | Evidence-based docs; CI badges; reproducible demo (P10) | +| R11 | Unpinned dependencies / ignored audit advisory | Medium | Medium | **Medium** | Pin; review GHSA-vvj3-c3rp-c85p (Q9) | +| R12 | Solo-engineer bandwidth vs a 10-phase plan | High | Medium | **Medium** | Keep v2 scope tight; prefer depth (money/inventory) over breadth | + +--- + +## 16. Recommended phase sequence + +The brief's sequence is sound. Recommended adjustments: + +1. **Before Phase 1:** owner answers Q1–Q5 (they change the PRD and money ADRs). +2. **Phase 1:** write ADR-0001..0006 and 0008..0012 *before* any design work. Financial and inventory invariants drive schema, which drives UI states (pending, partially refunded, etc.). +3. **Phase 2 (design system)** can run in parallel with late Phase 1, since it does not depend on the money ADRs. +4. **Phase 3:** stand up a **new repository or orphan branch** (Q2) with MySQL-in-CI, arch tests and the test harness *first*, so every later phase lands against real gates. +5. **Phases 4–5:** implement Inventory and Checkout/Payments **test-first**, including the §10/§11 failure scenarios from the brief, before building any storefront UI on them. +6. **Phase 7 (frontend):** port features from the old Vue pages using the new design system and typed props. +7. Throughout: no environment of v2 ever receives production credentials (R5). + +--- + +## 17. Questions requiring owner decisions + +| # | Question | Why it matters | My recommendation | +|---|---|---|---| +| **Q1** | **What is v2's product context?** (a) Portfolio showcase of a generic marketplace (brief), (b) the real Sudan-market aljebal-albeedos pilot (ratified docs), or (c) both. | Determines payment provider, currency (SDG/MYR/USD), locales' priority, demo data, and whether Stripe Connect is viable. | (a) for v2, with a provider-agnostic payment port so (b) remains possible later | +| **Q2** | **Where does v2 live, and in what frontend?** (a) New repo, (b) orphan branch in `nexo-ecommerce`, or (c) continue `C:\laragon\www\Nexo` (Laravel 13 + **React**, 67 commits). And Vue (brief) vs React (Nexo repo)? | Repo history and recruiters' first impression; Vue/React choice is fundamental | New repo (or `Nexo` repo reset) on Laravel 13 + **Vue 3/Inertia + TS**, as your brief states. Keep `nexo-ecommerce` read-only as the reference. | +| **Q3** | Payments: test-mode Stripe with an **internal ledger** and simulated payouts (no Connect), or real Stripe Connect (separate charges & transfers)? | Connect adds onboarding/KYC flows and country constraints | Internal ledger + test-mode Stripe; Connect later via ADR | +| **Q4** | Is the old live site (`store.aljebal-albeedos.com`) staying up on the old code? If yes, who maintains it, and should the C1/C3/S1 defects be hot-fixed there? | Real customers may be charged without orders being marked paid; customers can access vendor tools | If it takes real payments, hot-fix or take payments offline; that is outside the v2 phases and needs your explicit go-ahead | +| **Q5** | v2 scope: which "Extended/Experimental" features survive? (loyalty, referrals, bundles, gift cards, subscriptions, chat, Q&A, waitlist, flash sales, digital downloads) | Scope and schema | Keep: promotions (platform-run), reviews, wishlist. Defer the rest. | +| Q6 | Single currency for v2 (and which), or multi-currency display with single settlement currency? | Money model | Single settlement currency (e.g. MYR or USD); display conversion later | +| Q7 | Primary locale and RTL priority: is Arabic primary, or English with Arabic/Malay as full peers? | Design system defaults | English primary for portfolio; AR/MS full peers with real RTL | +| Q8 | What to do with the 44 uncommitted files on `redesign` (and the auto-deploy-to-prod-DB staging workflow)? | Risk of loss or accidental prod mutation | Commit to a WIP branch you don't push, or stash; disable `deploy-staging.yml` for that branch | +| Q9 | Why is `GHSA-vvj3-c3rp-c85p` in `composer.json` audit ignore? | Supply-chain hygiene | Re-evaluate in the new repo | +| Q10 | Hosting target for the demo (VPS as today, or a PaaS)? | Phase 3 environment and Phase 10 deployment docs | Decide by Phase 3 | + +--- + +## 18. Phase 0 acceptance checklist (for the owner) + +- [ ] Major business workflows understood (browse → cart → checkout → pay → fulfil → refund) — §3, §7 +- [ ] Existing architecture documented — §3, §4 +- [ ] Critical invariants and their violations identified — §7 +- [ ] Weaknesses evidenced with file/class references — §5, §7, §8 +- [ ] Target architecture proposed with justification — §13 +- [ ] ADR backlog and risk register — §14, §15 +- [ ] Open questions listed — §17 + +**Method note:** all inspection was read-only. Commands executed: + +- `git` log, status and diff. +- `grep`/`find`. +- `vue-tsc --noEmit`. +- The Pest suite, with every external credential (Stripe, Google, Typesense, Mailtrap, FX API) overridden by dummy values in the process environment. + +The only file written to the repository is this report. + +--- + +# Addendum A — Owner review clarifications (2026-11-01, still Phase 0) + +This addendum answers the owner's eight review points. Everything was gathered read-only: + +- File reads, `git` read commands, `git ls-remote`, and `gh run list/view` (GitHub Actions metadata and logs). +- Local test runs, with every external credential replaced by a dummy value. + +No deployment, migration, stash, reset, clean, checkout or commit was executed in either repository. + +**Repository side effects, disclosed in full:** + +1. This report file (new, untracked). +2. My first test attempt set `DB_DATABASE=__discovery_never_used` in the process environment, intending to guard the real DB. Because environment variables override `phpunit.xml`'s `` values, that run used a SQLite **file** of that name in the repo root instead of `:memory:`. I stopped that run, deleted the file, and deleted a 0-byte remnant left by the stopped process. No MySQL database was touched. The `.env` points at MySQL, which was never reached. + +## A.1 Complete test-suite results (review point 1) + +### Old repository (`modular-ecommerce-system`, working tree = `redesign@774d131` + 44 uncommitted files) + +| Item | Value | +|---|---| +| Command | `php vendor/bin/pest --compact` (Pest 4 / PHPUnit 12, PHP 8.5.8 NTS, Windows 11) | +| Environment overrides | `DB_CONNECTION=sqlite DB_DATABASE=:memory:`. Dummy `STRIPE_SECRET/KEY/WEBHOOK_SECRET`, `GOOGLE_CLIENT_SECRET`, `TYPESENSE_*`, `MAILTRAP_API_TOKEN`, `REVERB_APP_SECRET`. `MAIL_MAILER=array`. `CURRENCY_API_URL=http://127.0.0.1:9` (unreachable on purpose). `phpunit.xml` additionally forces `QUEUE_CONNECTION=sync`, `CACHE_STORE=array`, `SCOUT_DRIVER=collection`, `BROADCAST_CONNECTION=null`. | +| **Total** | **1,311 tests, 4,403 assertions** | +| **Passed** | **1,293** | +| **Failed** | **17** | +| **Skipped** | **1**: `tests/Feature/Domains/Order/CreateOrderConcurrencyTest.php:85`, reason "Requires amphp/parallel package for concurrent testing". Notably, this is the only test that would have exercised real concurrency, and it never runs. | +| Risky / incomplete / todo | None reported | +| Warnings / deprecations | None printed by the runner (compact mode prints PHP deprecations if any are triggered; none appeared) | +| **Wall-clock duration** | **2,121.27 s (≈35 min)**. Each test re-runs 109 migrations on a fresh in-memory DB under Windows. CI on Linux takes 46.8 s for the same suite. | +| Raw log | `C:\Users\MCR\AppData\Local\Temp\claude\C--laragon-www\ef9ce4e1-abb5-4e83-856a-1347f2ba36a5\scratchpad\pest-clean.txt` | + +**The 17 failures:** + +| Test file | Failing tests | Error | +|---|---|---| +| `tests/Feature/Browser/AuthTest.php` | 15: Login (7), Registration (7), Logout (1) | `Expected response status code [>=200, <300] but received 404` (and the 3xx variants) | +| `tests/Feature/AdminImpersonationTest.php` | 2: "shares impersonation state as inactive…", "shares active impersonation state…" | `Not a valid Inertia response.` (the response was a 404, not an Inertia page) | + +**Root cause (verified):** + +- The uncommitted change to `routes/web.php` adds `tenant.subdomain` (`ResolveTenantFromSubdomain`) to the whole `/{locale}` storefront group (diff: `->middleware('locale')` → `->middleware(['locale', 'tenant.subdomain'])`). +- These two test files make requests without creating a tenant whose domain matches the test host, so the middleware throws `NotFoundHttpException('Tenant not found.')`. +- **Confirmation run:** re-running only these two files with `APP_URL=http://localhost` (CI's value) reproduced the same failures, which rules out my local `APP_URL=http://127.0.0.1:8000` as the cause. +- The tests that pass after the WIP change rely on the WIP's modified `tests/Traits/WithTenant.php`, which now sets the tenant's `domain` to the test host. These two files do not use that trait. + +**Baseline for comparison (read-only from GitHub Actions):** CI run `32982452177` on committed `redesign@774d131` (2026-08-26, Ubuntu, PHP 8.4): **1,291 passed, 1 skipped, 0 failed, 4,354 assertions, 46.77 s.** The 20-test difference comes from tests added or changed in the WIP. + +**What these numbers do and do not show:** + +- The suite passing on committed code does **not** contradict §7–§8. +- No test covers the webhook path, a customer reaching `/vendor/*`, guest cart takeover, or real concurrency. +- The torture tests' assertions tolerate the inventory double-subtraction. + +### Frontend type check (old repository) + +`npx vue-tsc --noEmit -p tsconfig.json` reports **22 errors**: TS2339 ×14, TS18046 ×4, TS2322 ×3, TS2353 ×1. By file: `Products/Show.vue` 10, `Vendor/Dashboard.vue` 4, `Orders/Show.vue` 3, `Vendor/Orders.vue` 2, `Refunds/Status.vue` 1, `Error.vue` 1, `UI/DataTable.vue` 1. The process exit status was reported as 0 through the shell pipeline, but the error count above is what the log contains. CI does not run this check. + +### Separate `Nexo` repository + +| Suite | Command | Result | Duration | +|---|---|---|---| +| Pest (backend) | `php vendor/bin/pest`, with `APP_CONFIG_CACHE= DB_CONNECTION=sqlite DB_DATABASE=:memory:` | **109 tests: 109 passed, 0 failed, 0 skipped**, 1,038 assertions (Laravel Pao JSON summary) | 52.1 s (56 s wall) | +| Vitest (frontend) | `npx vitest run` | **33 files / 78 tests: 78 passed, 0 failed** | 88.6 s | + +**Finding (environment hazard, Medium):** + +- `C:\laragon\www\Nexo\bootstrap\cache\config.php` (git-ignored, dated 2026-09-10) is a cached config. When it exists, Laravel ignores `phpunit.xml`'s `DB_CONNECTION=sqlite`, so the tests ran against the `.env` MySQL database **`nile`**. +- My first two attempts therefore tried `127.0.0.1:3306/nile`. MySQL was not running: each test waited ~16 s for a refused connection, and the run hit my 900 s timeout with no output (Pao prints only at the end). +- **Had MySQL been running, `RefreshDatabase` would have dropped and re-migrated that local `nile` database.** +- To avoid modifying the repository I did not run `config:clear`. I bypassed the cache with the `APP_CONFIG_CACHE` environment variable. +- Fix for v2: tests should refuse to run against a non-test database, for example a guard in `TestCase` that asserts the connection is the test connection. + +## A.2 Stripe payment failure: confirmed by source vs needs test-mode verification (review point 2) + +### A.2.1 Confirmed by source inspection + +| # | Fact | Evidence | +|---|---|---| +| F1 | No production code instantiates or dispatches `App\Domain\Payment\Events\PaymentSucceeded`. Only the class, the `MarkOrderPaid` handler type-hint and two tests reference it. | `grep -rn PaymentSucceeded app tests` | +| F2 | `MarkOrderPaid` is the only listener that sets `Paid` after payment. The other writer, `RetryOrderFinalizationAction`, is reachable only from the manual command `payments:reconcile-orphaned`, which `routes/console.php` does **not** schedule. | `Domain/Order/Listeners/MarkOrderPaid.php`, `Console/Commands/ReconcileOrphanedPaymentsCommand.php` | +| F3 | The browser confirms the intent client-side with `stripe.confirmPayment(...)`. | `resources/js/Composables/usePayments.ts:91` | +| F4 | On `payment_intent.succeeded`, `HandleStripeEventJob::handleSuccess()` → `ConfirmPaymentIntentAction::execute()` → `PaymentGatewayService::confirmIntent()` → `$stripe->paymentIntents->confirm($provider_reference)`. | `app/Jobs/HandleStripeEventJob.php`, `app/Infrastructure/Payment/Stripe/PaymentGatewayService.php` | +| F5 | That Stripe call runs inside `DB::transaction`. On any `Throwable` the action writes `status=Failed` and **rethrows**, so the write is rolled back and the job fails. | `Domain/Payment/Actions/ConfirmPaymentIntentAction.php` | +| F6 | On its success branch the action only updates `payment_intents.status`. It dispatches no event and never touches the order. | same | +| F7 | `handleFailure()` sets intent and order to `Failed`. `PaymentStatus::Failed` is terminal, so a later `payment_intent.succeeded` for the same intent hits the `isTerminal()` early-return and is ignored. | `HandleStripeEventJob.php`, `Domain/Payment/Enums/PaymentStatus.php` | +| F8 | `POST /api/v1/checkout/confirm-payment` (auth only) performs the same server-side confirm. Guests have no equivalent. | `Api/V1/CheckoutController::confirmPayment` | +| F9 | `SignatureVerificationException`/`UnexpectedValueException` are not caught, so an invalid signature returns HTTP 500 instead of 400. | `Webhooks/StripeWebhookController.php` | +| F10 | No test covers `HandleStripeEventJob` or `POST /webhooks/stripe`. | `grep` over `tests/` | + +**Conclusion that needs no Stripe call:** because of F1, F2 and F6, the webhook never moves an order to `Paid`, **whatever Stripe answers**. Stripe's behaviour only decides *how* it fails: an exception and failed job, or an intent silently marked `Succeeded` while the order stays `Pending`. + +### A.2.2 Requires Stripe test-mode verification (not executed; needs your authorisation) + +| # | Hypothesis | Why it matters | Test-mode procedure | +|---|---|---|---| +| V1 | Confirming an already-`succeeded` intent returns an error (Stripe documents `payment_intent_unexpected_state`) | Exception path (job fails, intent stays `Processing`) vs silent path (intent `Succeeded`, order `Pending`) | Fresh local DB, test keys, `stripe listen --forward-to http://localhost:8000/webhooks/stripe`, pay with `4242 4242 4242 4242`, inspect `payment_intents`, `orders`, `failed_jobs` | +| V2 | Decline then retry on the same intent ends with a charged customer and a `Failed` order (F7) | Real-money impact if live keys are in use | Card `4000 0000 0000 0002`, then `4242…` in the same Payment Element | +| V3 | Server `QUEUE_CONNECTION`: `sync` makes the webhook itself return 500 so Stripe retries for 3 days; `database`/`redis` records failures in `failed_jobs` | Where the evidence is, and whether Stripe has disabled the endpoint | Owner reads server config (read-only) | +| V4 | Intent creation without `automatic_payment_methods`/`payment_method_types` works under the API version pinned by `stripe/stripe-php v17.6.0` | Whether the Payment Element even renders | Same harness as V1 | +| V5 | The production webhook endpoint exists, subscribes to the right events, and the secret matches | If missing, orders stay `pending` for a different reason | Stripe Dashboard → Developers → Webhooks | + +Recommendation: run V1, V2 and V4 only in local test mode against a throw-away database. Do it now only if you need the answer for the live-site decision (A.6); otherwise it belongs in Phase 5's failure-simulation suite. + +## A.3 Preservation plan for uncommitted work (review point 3): proposal only, nothing executed + +### A.3.1 Inventory (verified) + +**`modular-ecommerce-system`, branch `redesign` @ `774d131`** (local and `origin/redesign` identical, 0/0 ahead/behind): + +- **33 modified tracked files** (+801/−73 lines): + - `README.md` + - `app/Domain/Tenant/Models/Tenant.php` + - 3 Filament `VariantAttributeTypes` files + - `Web/{Auth,Cart,Checkout,Home,Store}Controller.php` + - `Middleware/ResolveTenantFromSubdomain.php`, `config/tenancy.php` + - `database/factories/TenantFactory.php`, `database/seeders/DatabaseSeeder.php` + - `lang/{ar,en,ms}/ui.php` + - `Components/Layout/{BottomNav,MobileNav}.vue`, `Layouts/{Authenticated,Guest}Layout.vue`, `Pages/{Home,Stores/Show}.vue` + - `routes/web.php` + - 9 files under `tests/Feature/**` and `tests/Traits/WithTenant.php` +- **11 untracked paths:** + - `app/Domain/Tenant/Models/StoreFollow.php` + - `app/Http/Controllers/Web/Concerns/RequiresShoppingTenant.php` + - Migrations `2026_08_27_142036_add_marketplace_fields_to_tenants_table.php` and `2026_08_27_142044_create_store_follows_table.php` + - `database/seeders/VariantAttributeTypeSeeder.php` + - `docs/REDESIGN_BRIEF.md` + - `docs/architecture/` (ADR-0001..0004, `marketplace-migration.md`) + - `docs/product/` (5 docs) + - `resources/js/Components/Stores/StoreCard.vue`, `resources/js/Pages/Stores/Index.vue` + - `tests/Feature/Filament/VariantAttributeTypeResourceTest.php` + - Plus this report under `docs/discovery/`. +- **Git-ignored but not reproducible from Git:** `.env` (secrets), `CLAUDE.md`, `Check.md`, `boost.json`, `.mcp.json`, `.idea/`. +- **Stashes:** + - `stash@{0}` "WIP on main: d0ba78c bulk product import". + - `stash@{1}` "WIP on chore/ci-cd-wiki: 489e35e". +- **Agent worktree** `.claude/worktrees/agent-a8ba81d6`, branch `worktree-agent-a8ba81d6` @ `5975250` (2026-03-22). It has its own uncommitted changes: ≥10 modified files in the review-submission area. + +**`C:\laragon\www\Nexo`:** + +- **Branch `feat/vendor-frontend` holds 61 commits with no upstream.** `git ls-remote` lists only `refs/heads/main` on `Sentients-AI/Nexo`. These commits exist on this disk only. **This is the largest loss risk found.** +- 3 modified files (`app/Http/Middleware/SetLocale.php`, `app/Providers/AppServiceProvider.php`, `resources/css/app.css`) and `stash@{0}`. +- Untracked: `Form dependencies for artboards/` (3.1 MB design handoff `design_handoff_nile_lagoon`, cited as the design source of truth), `.github/hooks/`, `.github/workflows/hello.yaml`, and a vim swap file. + +### A.3.2 Plan (each step needs your approval; steps 1–3 do not modify either repository) + +1. **Out-of-repo snapshot** to e.g. `C:\laragon\backups\nexo-2026-11-01\`: + - `git diff --binary` → `redesign-tracked.patch`. + - Path-preserving copies of all untracked and ignored files above. `.env` is kept local only and **never committed or uploaded**. + - `git stash show -p --include-untracked stash@{N}` → one patch per stash. + - `git -C .claude/worktrees/agent-a8ba81d6 diff --binary` → `agent-worktree.patch`. + - `git bundle create … --all` for both repositories. This captures every branch, including the 61 unpushed commits and the worktree branch. + - The same diff and untracked copy for the `Nexo` repo. +2. **Verify** without touching the repositories: + - `git bundle verify`. + - Clone each bundle into a scratch folder. + - Check out `774d131` there and run `git apply --check` on the patch. + - Record SHA-256 checksums of every artefact. +3. **Off-machine copy:** you copy the folder somewhere you control. I will not upload it. +4. *(Optional, separate approval, mutating)* + - Commit the old-repo WIP to a **new local branch** `wip/marketplace-pivot-2026-08`. + - Push `feat/vendor-frontend` to `Sentients-AI/Nexo`. + - **Never push to `redesign`.** A green CI on `redesign` auto-deploys to the production-DB staging site (A.5). Any other branch name only runs CI. +5. *(Later)* Disposition: + - Keep `docs/product/*` and ADR-0002..0004 as v2 inputs. + - Treat the code WIP (session "shopping tenant", StoreFollow, `tenant.subdomain` on the storefront) as reference only. It contradicts ADR-0004 and breaks 17 tests. + +## A.4 The `C:\laragon\www\Nexo` repository (review point 4) + +| Dimension | `Nexo` (`Sentients-AI/Nexo`) | Old repo (`AdilAzhari/nexo-ecommerce`) | +|---|---|---| +| History | 67 commits, 2026-08-31 → 09-15. `main` (6 commits; `origin/main` 2 ahead with merged PR #4 "vendor domain documentation"). `chore/phase-0-foundation` (pushed). `feat/vendor-frontend`: 61 commits, **local only**. Plan-driven conventional commits (D1–D7, B1–B2). | 208 commits on `redesign`; SaaS history plus many deploy fixes | +| Author identity | 60 × `adelazhary1997@…`, 3 × `adelazhry1997@…`, 4 × `adilazhariosman@…`: **mixed** | — | +| Backend stack (verified, `composer.json`) | PHP `^8.5`, **Laravel `^13.17`**, `inertiajs/inertia-laravel ^3.3`, Ziggy. Dev: **Larastan 3 / PHPStan 2**, Pint, **Pest 5**, Pao, Pail | PHP ^8.2, Laravel 12.68, Inertia 2.0.19, Filament 5.7.6, Cashier 16.1, stripe-php 17.6, Reverb, Scout/Typesense, Pest 4, Rector, Mago | +| Frontend stack (verified, `package.json`) | **React 19.2, `@inertiajs/react` 3.7, TypeScript 6, Vite 8, Tailwind 4, Vitest 5 + Testing Library, ESLint 9, Prettier** | Vue 3.5, `@inertiajs/vue3` 2.3, TS 5.9, Vite 7, Tailwind 4; no frontend tests, no linter | +| Backend maturity | **Skeleton.** 42 PHP files: one model (`User`), 3 framework migrations (users/cache/jobs), 14 vendor + 11 auth controllers that render **static fixture arrays** (`app/Support/Fixtures/*`). No domain, persistence, auth, payments or inventory. | Extensive but defective (§7–§8) | +| Frontend maturity | **High for the vendor workspace:**
• 134 files, ~12.2k lines TS/TSX.
• UI kit of ~40 components (`components/ui`: DataTable, Sheet, MoneyInput, Stepper, Timeline, StatusBadge, TypedConfirm, charts, …) with co-located tests.
• `lib/` money, format, i18n, theme, status and countdown utilities, each with tests.
• "Lagoon" design tokens, Bidi/Money primitives, EN+AR locale files.
• Every vendor screen D1–D7 and auth B1–B2.
• Spec `docs/frontend/vendor-frontend-spec.md` (440 lines, "Shipped 2026-09-14"). | Functional but monolithic pages; 22 type errors; physical-direction CSS | +| Tests | 43 PHP test files: page renders, fixture shapes, locale routing, `arch()` presets (no debug helpers, php and security presets, `env()` only in config). ~20 Vitest files. Results below. | 1,311 Pest tests (A.1) | +| CI (`ci.yml`) | composer validate + audit, Pint, **PHPStan**, `php artisan test`. **Does not** run `npm ci`, `tsc`, ESLint or Vitest. | Pint, Rector dry-run, Pest/SQLite, audits, CodeQL-JS | +| Docs | Product docs (incl. `requirements/{shopper,vendor,platform}`), ADR-0001..0004 (same decisions as the old repo's untracked ADRs), engineering handbook, 4 implementation plans | SaaS-era docs, partly over-claiming | + +**Nexo test results** (A.1): + +- Pest 109/109 passed in 52 s. Vitest 78/78 passed in 89 s. +- Those tests cover page rendering, fixture shapes, locale routing, arch presets and UI components only. They say nothing about business logic, because there is none yet. + +**Suitability as the Nexo 2.0 foundation:** + +- **Backend:** equivalent to a fresh Laravel 13 app with better tooling and a good engineering handbook. There is no domain code to inherit, which is fine given a fresh-database rebuild. +- **Frontend and design:** the most valuable artefact in either repository. It provides a tested component kit, money and bidi utilities, real RTL handling and a written vendor UX spec. **It is React, while the brief's stack baseline is Vue + Inertia** (brief §5 requires an ADR to replace an existing technology). +- **The frontend choice belongs to you (decision D-1):** + - (a) **Vue, per the brief.** Port the React kit's *design*: tokens, component inventory, screen specs, copy. Roughly the kit has to be rebuilt. + - (b) **React, via an ADR that supersedes the brief's stack line.** Reuse ~12k lines of tested UI directly. +- **My engineering recommendation is (b).** It reuses the most recent, highest-quality work and matches the latest owner decisions recorded in that repo. But it overrides the brief's text, so I will not assume it. +- **Prerequisites either way:** + - Push or bundle the 61 local commits (A.3). + - Track the design-handoff folder. + - Decide author identity before the repo becomes the public portfolio (D-15). + - Add `npm` type-check, lint and Vitest to its CI. + +## A.5 Deployment workflows and the production database (review point 5) + +### A.5.1 Facts (verified from workflow files and read-only `gh` queries) + +**`deploy.yml` (production):** + +- **Triggers:** `workflow_run` when **CI** completes successfully on `main`, or `workflow_dispatch` (input `environment`: `production`/`staging`). +- **Server sequence:** `git fetch origin main` → auto-`git stash push -u` of any server drift → `git merge --ff-only origin/main` → `composer install --no-dev` → caches → **`php artisan migrate --force` by default** (`run_migrations` defaults to `true`; automatic runs use `|| 'true'`). +- **Last run:** `32973123836`, 2026-08-26 13:15 UTC, `headSha 2a43b9d` = current `origin/main`. + +**`deploy-staging.yml` (staging):** + +- Its header says staging "**shares the production database (same tenants, orders, users)**". +- **Triggers:** + 1. `workflow_run` after CI completes. For this event type GitHub runs the workflow file from the default branch, and `origin/main` contains it. The job runs only if `conclusion == success && head_branch == 'redesign'`. + 2. `workflow_dispatch`. +- **Checkout:** `ref: redesign`. Server: `git fetch origin redesign` → auto-stash drift → `git checkout redesign` → `merge --ff-only`. +- **Assets:** rsynced to `STAGING_DEPLOY_PATH/public/build`. The web root's `index.php`/`.htaccess` are regenerated to point at the staging app. +- **Migrations:** `RUN_MIGRATIONS = inputs.run_migrations || 'false'`, so **off for automatic runs** and opt-in on dispatch. +- **Typesense:** `RUN_SCOUT_IMPORT` (opt-in) runs `scout:flush` + `scout:import` for Product, Category and Order. + +**`ci.yml`:** runs on **every push to every branch**. + +**Staging run history (all 7 runs, 2026-08-26):** + +- `32982669102`, `32978174614`, `32972588491` (`workflow_run`) and `32974726689`, `32971048644` (dispatch) logged `RUN_MIGRATIONS: false`, with no executed migration output. +- `32970786677` and `32967584631` (dispatch) predate the migration step. +- **No staging run has executed migrations.** Staging code has been serving requests since 2026-08-26 against the production database *(per the workflow's own statement; the server `.env` was not inspected)*. + +### A.5.2 How `redesign` can interact with production data + +| Path | Trigger | Effect | Exposure | +|---|---|---|---| +| P1 Runtime writes | Any request to the staging site | Registrations, carts, checkouts, stock reservations, vendor-dashboard and Filament edits, queued jobs: all written to production tables by **unreviewed `redesign` code**, with every defect in §7–§8 | **Ongoing while staging is up** | +| P2 Schema change | Dispatch Deploy Staging with `run_migrations=true` | Runs `redesign` migrations on production. The two **uncommitted** migrations would run if they were committed and pushed. Production code (`main`) would then face an unknown schema. | Manual | +| P3 Auto-deploy | `git push origin redesign` → green CI | New code live against production data, **no human gate** (migrations off) | Any push to `redesign` | +| P4 Search wipe | Dispatch with `run_scout_import=true` | Flushes the Product/Category/Order indexes. If Typesense is shared *(unverified)*, production search is empty until re-import, which then comes from staging's code. | Manual | +| P5 Duplicate schedulers/queues | A cron/queue worker for the staging path *(unverified)* | Loyalty expiry, abandoned-cart e-mails and FX fetch run twice against the same data | Unknown | +| P6 Storage divergence | Uploads via staging | Production rows reference files that exist only in staging's storage | If used | +| P7 Merge to `main` | Merge `redesign` → `main` | Production deploy runs `migrate --force` **by default**, applying `redesign`'s migrations automatically | On merge | + +**Not verifiable without server access:** + +- The staging `.env` DB target. +- Whether staging cron or queue workers run. +- Whether Typesense, Stripe keys and webhooks are shared with production. + +**Containment options (your decision; not executed):** + +- Remove the `workflow_run` trigger from `deploy-staging.yml` (on `main`, since that copy governs `workflow_run`). +- Take staging offline, or give it its own database copy. + +For v2: no workflow may reach this server's database (ADR-0018). + +## A.6 Status of the old live storefront's risks (review point 6) + +### A.6.1 Confirmed from code (not from the live server) + +- The last production deploy shipped `origin/main @ 2a43b9d`. +- `git diff --stat origin/main redesign` is **empty** for every implicated path: + - `app/Jobs`, `app/Domain/{Payment,Inventory,Order}`, `app/Infrastructure`, `app/Shared`. + - `Web/Vendor*`, `Middleware/ResolveTenantFromUser.php`, `Api/V1/CheckoutController.php`. + - `routes/web.php`, `routes/console.php`. +- The uncommitted WIP does not touch those paths either, except `routes/web.php`. +- **So production and staging run the same C1 (payment finalisation), C2 (dead outbox), C3 (inventory) and S3 (guest cart) code.** + +### A.6.2 Correction to S1 for production + +Evidence: + +- On `origin/main`, the `tenant.subdomain` middleware is registered (`bootstrap/app.php`) but **applied to no route**. `git grep` finds no use in `routes/`. The storefront group uses only `locale`. +- Nothing else sets tenant context for web requests. `git grep "Context::add('tenant"` on `origin/main` finds only: + - `ResolveTenantFromSubdomain` (unused). + - `ResolveTenantFromUser` (vendor routes and API). + - `FilamentTenantMiddleware`. + - Two listeners/actions. +- So a shopper registering through `/{locale}/register` gets **`tenant_id = NULL`**: `BelongsToTenant`'s `creating` hook copies `Context::get('tenant_id')`, which is null. + +Consequence: on `/vendor/*` (`auth` + `tenant.user`), `ResolveTenantFromUser` treats a NULL-tenant user like a super-admin and adds no context. `TenantScope` then applies **no filter**. **Any self-registered shopper can therefore read and modify every vendor's data:** + +- Orders: any status, bypassing the state machine. +- Products and prices, and stock. +- Returns, reviews and Q&A. +- The customer list, which includes other users' names and e-mails. + +This is **worse than the single-store exposure §8 first described**. Code-confirmed; not runtime-verified against the live site. (With the WIP applied, a shopper registered on a store's domain gets that store's `tenant_id`, which narrows exposure to that one store.) + +### A.6.3 Not confirmed (needs your access; I used none) + +- That the server's checked-out code equals `2a43b9d`. Deploys auto-stash server drift, so manual hot-fixes may have been stashed away. +- Live vs test Stripe keys, and whether real payments occurred. +- Webhook registration and queue worker (A.2 V3/V5). +- Whether real (non-seed) shoppers or vendors exist. + +### A.6.4 Risk by scenario + +| Risk | Live keys + real users | Test keys / demo only | +|---|---|---| +| C1 payment finalisation | Customers charged; orders stay `pending`; vendors see no paid orders; stock stays reserved; decline-then-success leaves charged customer with `failed` order | Functional bug | +| S1 vendor dashboard | Anyone who registers can operate **all** vendors' dashboards and read customer PII | Same exposure on demo data; still a public privacy/defacement risk | +| S3 guest cart takeover | Anonymous request can complete another user's cart and reserve stock | Low impact | + +### A.6.5 Owner-side read-only checks (I will not run these without approval) + +1. Stripe Dashboard: mode, payments, webhook delivery failures. +2. Read-only SQL: + - `SELECT o.status, pi.status, COUNT(*) FROM orders o LEFT JOIN payment_intents pi ON pi.order_id = o.id GROUP BY o.status, pi.status;` + - `SELECT COUNT(*) FROM users WHERE tenant_id IS NULL AND role_id IS NULL;` (self-registered shoppers able to reach `/vendor/*`) +3. Web-server access logs: `/vendor/` requests by non-staff sessions. + +**Containment options (your decision; outside v2 scope):** + +- Maintenance mode, or disable registration and checkout. +- Take staging offline. +- A minimal reviewed hot-fix for S1 (role check on the vendor route group) and C1. + +## A.7 Target financial model, expanded (review point 7) + +**Scope note.** This is a proposal for ADR-0012, not a decision. It assumes: + +- One settlement currency, with amounts as integer minor units. +- The platform collects payment (Stripe test mode, "separate charges and transfers" semantics). +- Vendor payouts are **simulated** in the ledger until Stripe Connect is approved separately (brief §8: no real payouts before the model is defined). + +### A.7.1 Entities + +``` +OrderGroup (one customer checkout) 1──* VendorOrder 1──* VendorOrderLine + │ grand_total, currency │ commission_rate_bps (frozen at creation) + │ │ per-line: net price, allocated_discount, tax, shipping share + 1──* PaymentAttempt (≤1 active) 1──* Fulfilment / Return + │ provider_payment_id UNIQUE + ▼ +ProviderEvent (provider, event_id UNIQUE) ─► LedgerTransaction 1──* LedgerEntry(account, debit|credit, amount) +Refund (per VendorOrder, per line) ─► idempotency_key UNIQUE, provider_refund_id UNIQUE +Dispute (per charge) ─► DisputeAllocation (per VendorOrder) +Payout (simulated, per vendor) ─► from payable_available +``` + +### A.7.2 Chart of accounts (double-entry; every LedgerTransaction sums to zero) + +| Account | Type | Meaning | +|---|---|---| +| `platform:psp_clearing` | Asset | Funds held for the platform at the provider | +| `platform:commission_revenue` | Revenue | Commission earned | +| `platform:psp_fees` | Expense | Processing fees | +| `platform:promo_expense` | Expense | Platform-funded discounts | +| `platform:dispute_holding` | Asset | Funds withdrawn by the provider pending a dispute | +| `platform:dispute_losses` | Expense | Disputes lost and absorbed by the platform | +| `vendor:{id}:payable_pending` | Liability | Owed to vendor, not yet withdrawable | +| `vendor:{id}:payable_available` | Liability | Withdrawable | +| `vendor:{id}:payable_held` | Liability | Frozen (open dispute or investigation) | +| `vendor:{id}:receivable` | Asset | Vendor owes the platform (refund or chargeback after payout) | +| `platform:tax_payable` *(only if the marketplace collects tax)* | Liability | Tax collected on behalf | + +### A.7.3 Worked lifecycle (two vendors, 10 % commission) + +Vendor A goods 60.00, Vendor B goods 40.00, charge 100.00, provider fee 3.20. + +1. **Capture** (on `payment_intent.succeeded`, de-duplicated by `event_id`, amount and currency checked against the attempt): + - Dr `psp_clearing` 100.00 + - Cr `vendor:A:payable_pending` 54.00 · Cr `commission_revenue` 6.00 + - Cr `vendor:B:payable_pending` 36.00 · Cr `commission_revenue` 4.00 +2. **Fee** (from the provider's balance transaction): Dr `psp_fees` 3.20 / Cr `psp_clearing` 3.20. Who bears it is decision D-9. +3. **Maturity.** A's order is delivered and its return window (e.g. 7 days) has elapsed: Dr `A:payable_pending` 54.00 / Cr `A:payable_available` 54.00. The job is idempotent per VendorOrder. +4. **Simulated payout:** Dr `A:payable_available` 54.00 / Cr `psp_clearing` 54.00, plus a `Payout` row. Later, real transfers via Connect `transfers.create` with `source_transaction` and idempotency key = payout id. + +### A.7.4 Partial vendor-order refund + +Customer returns one Vendor A line worth 20.00. + +- **Eligibility under a lock:** + - Inside a transaction holding `SELECT … FOR UPDATE` on the VendorOrder: + `refundable_remaining = captured_allocated(A) − Σ refunds(A) in {requested, approved, submitted, succeeded}`. + - Per-line cap = line net after allocated discount, plus tax and shipping share per policy. + - This serialises concurrent requests (fixes §7.5). +- **Lifecycle:** `requested → approved → submitted → succeeded | failed | requires_action`. + - The provider call carries idempotency key = `refund.id`. + - **A timeout leaves `submitted`, never `failed`.** Resolution comes from `refund.updated`/`charge.refunded` or reconciliation, so a retry cannot double-refund. +- **Ledger, posted only when the provider confirms success:** + - Dr `A:payable_pending` (or `payable_available`) 18.00 + - Dr `commission_revenue` 2.00 (commission reversed pro-rata; decision D-10) + - Cr `psp_clearing` 20.00 + - The original fee is normally not returned by the provider and stays expensed. +- **Already paid out:** the debit drives A's payable negative. The shortfall moves to `vendor:A:receivable` and is recovered by netting future payables (or by a transfer reversal once Connect exists). +- **Siblings untouched.** Vendor B's entries and order state never change. OrderGroup status is *derived* (`partially_refunded` when any child has refunds). +- **Inventory:** + - Restock only on `ReturnReceived` (inspected). + - A refund for a never-shipped item releases or consumes the reservation instead. + - Shipping and tax refund portions follow the line's stored allocation. + +### A.7.5 Discounts and promotions + +- Group-level discounts are allocated to lines pro-rata to net amount, with largest-remainder rounding so they sum exactly. +- The allocation is stored on the line, so refunds reverse exactly what was allocated. +- **Funding source:** + - Platform-funded: the vendor payable is computed on the pre-discount amount, and the platform posts Dr `promo_expense`. + - Vendor-funded: the vendor payable is computed on the discounted amount. + +### A.7.6 Commission + +- `commission_rate_bps` is resolved at VendorOrder creation (vendor override → category → platform default) and **frozen**. +- It applies to line net after vendor-funded discounts, excluding tax. Shipping is included or excluded per D-10. +- Per-line rounding is half-even, with a documented residual-cent rule. +- Changing rates never alters existing orders; a test asserts this. + +### A.7.7 Vendor payable balances + +- Balances are **derived from ledger entries**, never stored as mutable columns. A cached projection is allowed, with a nightly consistency check. +- Vendor dashboard shows pending, available, held, receivable and paid, plus a statement export of ledger lines. +- Payout eligibility requires `available > 0` with no open dispute holds on the same orders. A minimum payout amount is a config value. + +### A.7.8 Chargebacks and disputes + +- **`charge.dispute.created`** (dispute is per charge, i.e. per OrderGroup): + - The provider withdraws amount plus fee: Dr `dispute_holding` / Cr `psp_clearing`. + - **Allocate** the disputed amount to VendorOrders: by the disputed items if identifiable, else pro-rata. + - Move each allocation from `payable_*` to `payable_held`. + - Notify the vendors and collect evidence through the vendor UI. +- **Won:** funds return (Dr `psp_clearing` / Cr `dispute_holding`), and holds are released to `available`. +- **Lost, per D-11:** + - Vendor bears the goods amount: Dr `payable_held` / Cr `dispute_holding`, with any shortfall moving to `receivable`. + - Or the platform bears it: Dr `dispute_losses`. + - The dispute fee is normally platform-borne. +- **Guards:** + - No refund while a dispute is open (prevents double recovery). + - No payout of held funds. + +### A.7.9 Reconciliation + +1. **Internal invariants** (tests and a nightly job): + - Every transaction balances. + - Per OrderGroup: `captured = Σ vendor payables + Σ commission − promo_expense adjustments`. + - `Σ refunds ≤ captured` per VendorOrder. + - `paid` VendorOrders ⇔ a succeeded PaymentAttempt. + - Reservations ⇔ unpaid open orders. +2. **Provider reconciliation** (daily): + - Pull provider balance transactions for the window and match by payment, refund, dispute and transfer id. + - Classify each as matched, missing-locally (replay from the provider object), missing-at-provider, or amount/currency mismatch. + - Exceptions go to a `reconciliation_exceptions` queue with Filament resolve actions (audit-logged). +3. **Operational sweeps:** + - Attempts stuck in `processing` beyond a threshold are re-fetched from the provider. + - Expired reservations are released and their unpaid groups cancelled. + - Refunds stuck in `submitted` are re-fetched. +4. **Reporting:** a daily settlement report (captures, refunds, fees, commission, payable movements, disputes) that ties out to the provider payout report. + +### A.7.10 Testing implications + +- **Property tests:** random baskets, discounts and partial refunds; the ledger always balances and allocations sum exactly. +- **MySQL concurrency tests:** + - Two refunds on one VendorOrder. + - Duplicate `event_id` delivery. + - Refund racing payout maturity. + - Dispute racing refund. +- **Failure tests with a fake provider adapter** (out-of-order, duplicated and delayed events; timeouts): + - No double refund. + - No payout of held funds. + - Reconciliation detects an injected mismatch. + +## A.8 What the brief already settles vs what needs your decision (review point 8) + +### A.8.1 Settled by the brief (treated as given) + +| Topic | Settled position | Brief § | +|---|---|---| +| Purpose & audience | Premium portfolio showcase for recruiters and engineers; must feel like a credible commercial product | header, §2 | +| Product model | Unified multi-vendor general-merchandise marketplace; three surfaces | §2, §6 | +| Process | One phase at a time; owner approval; ADRs for significant decisions; evidence-based reporting | §4, §19 | +| Strategy | Hybrid rebuild; **fresh database**; old app is a reference; never connect to the production DB | §3 | +| Architecture style | Modular monolith with explicit boundaries; no microservices | §5 | +| Order shape | Checkout/OrderGroup → per-vendor orders, designed explicitly | §8 | +| Payouts | No real vendor payouts until the financial model is defined | §8 | +| Admin | Evaluate and improve Filament rather than assume replacement | §6.3 | +| Technology changes | Any replacement of an existing technology needs an ADR | §5 | +| Visual identity | New from scratch; design system reviewed before frontend build | §7, Phase 2 | +| Quality | Concurrency, failure and authorization tests; CI gates must pass honestly | §10–§13 | +| Locales | Evaluate EN, AR (real RTL), MS | §16 | +| Docs layout & ADR template | As specified | §18 | + +**Correction to §1 and §17.** I called "product context" (Q1) the single most important open question. The brief already settles it: the purpose is a portfolio showcase. What actually remains for you is D-4 (formally supersede the Sudan-market docs) and D-5 (currency and locale priority). + +### A.8.2 Genuinely requires your decision + +| ID | Decision | Options | Recommendation | Needed by | +|---|---|---|---|---| +| D-1 | Frontend framework | Vue 3 (brief) · React 19 (Nexo repo, via ADR) | React on reuse grounds, *or* Vue to follow the brief literally (A.4) | Phase 1 ADR | +| D-2 | v2 repository | New repo · continue `Sentients-AI/Nexo` · orphan branch in `nexo-ecommerce` | Continue `Sentients-AI/Nexo` after preservation, or a new repo if clean authorship matters | Phase 1 ADR | +| D-3 | Laravel/PHP version | Laravel 13 / PHP 8.5 · Laravel 12 / 8.4 | Laravel 13; PHP per your demo host | Phase 1 ADR | +| D-4 | Sudan-market product docs | Supersede · keep as a future context | Supersede for v2; archive | Phase 1 PRD | +| D-5 | Settlement currency & primary locale | MYR · USD · other; EN- or AR-primary | Single currency (MYR suits Malay locale and the existing demo vendor); EN primary with AR/MS as full peers | Phase 1 | +| D-6 | Payment depth | Test-mode Stripe + internal ledger + simulated payouts · Stripe Connect | The former; Connect as a later ADR | ADR-0011/12 | +| D-7 | v2 feature scope | Loyalty, referrals, bundles, gift cards, subscriptions, chat, Q&A, waitlist, flash sales, downloads | Keep promotions, reviews, wishlist; defer others | Phase 1 PRD | +| D-8 | Search | Typesense from day one · DB search behind a port first | DB first; Typesense in Phase 8 if justified | ADR-0015 | +| D-9 | Provider fees bearer | Platform · pro-rata to vendors | Platform | ADR-0012 | +| D-10 | Commission on refunds / on shipping | Reverse pro-rata or keep · include or exclude shipping | Reverse pro-rata; exclude shipping | ADR-0012 | +| D-11 | Chargeback liability | Vendor bears goods · platform bears | Vendor bears goods, platform bears fee | ADR-0012 | +| D-12 | Old live site & staging containment | Leave · maintenance · hot-fix S1/C1 · staging offline | **Urgent:** at minimum take staging offline and close `/vendor/*` to non-staff (A.6) | Now | +| D-13 | Preservation of uncommitted work | Plan A.3 steps 1–3 · plus step 4 | Steps 1–3 now | Now | +| D-14 | Stripe test-mode verification (A.2.2) | Now · Phase 5 | Phase 5, unless needed for D-12 | — | +| D-15 | Commit author identity for the public repo | Keep history · rewrite to one identity before publishing | Decide together with D-2 | Before Phase 3 | + +--- + +**End of Phase 0 addendum A.** + +--- + +# Addendum B — Owner decisions and their execution (2026-11-01, still Phase 0) + +## B.1 Decisions recorded + +| ID | Owner decision | Status | +|---|---|---| +| D-12 | Close public access to `/vendor/*` and take staging offline until its production-database connection is removed | **Approved. Not executed:** needs server access (B.3) | +| D-13 | Preservation steps 1–3, strictly read-only against both repositories | **Steps 1–2 executed and verified (B.2). Step 3 is yours** | +| D-1 | **Vue 3 + Inertia.js + TypeScript.** The existing React implementation is a reference, not the foundation | Approved; becomes ADR in Phase 1 | +| D-2 | **The clean `Nexo` repository is the Nexo 2.0 foundation.** The old repository is retained as a historical reference | Approved; becomes ADR in Phase 1 | + +**Consequences of D-1 + D-2 together (for Phase 1 to plan, not acted on):** + +- `Nexo` keeps its backend shell and tooling: Laravel 13, Larastan/PHPStan, Pint, Pest 5 with `arch()` presets, the engineering handbook, product and requirements docs, ADR-0001..0004 and CI. +- Its frontend layer gets replaced by Vue: + - `@inertiajs/react` and React 19 give way to `@inertiajs/vue3`. + - The Testing Library flavour changes, and `tsconfig`/ESLint configuration follows. +- The React vendor workspace (`feat/vendor-frontend`, 61 commits) becomes reference material. Its reusable assets are framework-neutral: + - design tokens; + - `lib/` money, format, status and countdown logic, portable to TS modules almost unchanged; + - EN/AR locale files; + - the component inventory and `vendor-frontend-spec.md`; + - the design handoff folder. +- The old repo's Vue pages are reference only: they have 22 type errors and physical-direction CSS (§6). +- Phase 1 decides which `Nexo` branch v2 starts from. `main` holds the Phase 0 foundation, while `feat/vendor-frontend` adds the React UI that D-1 sets aside. +- D-15 (commit author identity) now applies to `Sentients-AI/Nexo`. It must be settled before that repository is presented publicly. +- This report lives, untracked, in the old repository. Moving it into `Nexo/docs/discovery/` is a Phase 1 housekeeping step. + +## B.2 D-13 execution record (steps 1–2) + +**Location:** `C:\laragon\backups\nexo-2026-11-01\` (9.8 MB, 144 files, outside both repositories). + +**Method:** every git command ran with `GIT_OPTIONAL_LOCKS=0`, so not even the index was refreshed. Only read commands were used: `diff`, `ls-files`, `stash show`, `bundle create` (writes only the output file), `rev-parse` and `status`. The script is kept beside the scratch verification output; nothing was written inside either repository. + +| Artefact | Contents | +|---|---| +| `old/redesign-tracked.patch` (69 KB) | `git diff --binary HEAD` of the 33 modified tracked files on `redesign@774d131` | +| `old/untracked/` (20 files) | All untracked files, paths preserved (includes this report) | +| `old/ignored/` | `.env`, `CLAUDE.md`, `Check.md`, `boost.json`, `.mcp.json`, `.claude/settings.local.json`, `.idea/`, `public/redesign/` | +| `old/stashes/stash-{0,1}.patch` | Both stashes, `--include-untracked`, binary | +| `old/agent-worktree/` | `agent-worktree.patch` (33 KB, 14 modified files) and `untracked/` (30 files) of `worktree-agent-a8ba81d6@5975250` | +| `old/nexo-ecommerce-all.bundle` | `git bundle --all`: every local and remote-tracking branch, including the worktree branch | +| `nexo/feat-vendor-frontend-tracked.patch` (7 KB) | The 3 modified tracked files on `feat/vendor-frontend@cc4e8bc` | +| `nexo/untracked/` (39 files) | Design handoff (`Form dependencies for artboards/…`), `.github/hooks/`, `hello.yaml`, swap file | +| `nexo/ignored/` | `.env`, `.idea/`, `.impeccable/` | +| `nexo/stashes/stash-0.patch` | The one stash | +| `nexo/nexo-all.bundle` | `git bundle --all`, including the **61 local-only commits** | +| `SHA256SUMS` | Checksums of every artefact | +| `SECRETS-WARNING.txt` | Both `.env` files are secrets: keep local, never upload or commit | + +**Excluded on purpose** because they are regenerable: `vendor/`, `node_modules/`, `storage/`, `bootstrap/cache/`, `public/hot`, and `Nexo/build/` (a 69 MB PHPStan cache). + +**Verification (step 2), all in a scratch folder:** + +- `git bundle verify` reports "okay" for both bundles. +- Clone of the old bundle: + - Check out `774d131`, then `git apply --check redesign-tracked.patch`: **applies**. + - Check out `5975250`, then `git apply --check agent-worktree.patch`: **applies**. +- Clone of the Nexo bundle: + - Check out `cc4e8bc`, then `git apply --check`: **applies**. + - `origin/main..origin/feat/vendor-frontend` = **61 commits**, so the local-only work is captured. +- Untracked file counts match the live inventory: 20, 30 and 39. +- **Both repositories confirmed unchanged afterwards:** + - old repo: `774d131 redesign`, 45 status entries, 2 stashes; + - Nexo: `cc4e8bc feat/vendor-frontend`, 7 status entries, 1 stash. + +**Step 3 (yours):** copy `C:\laragon\backups\nexo-2026-11-01\` to storage you control. Check it there with `sha256sum -c SHA256SUMS`. Prefer an encrypted location, since the folder holds the two `.env` files. Until this is done, the 61 commits still exist on one physical disk only. + +## B.3 D-12: why it is not executed yet, and the runbook + +**What I can and cannot reach:** + +- Staging and production run on one server, which is reached over SSH. The connection details are GitHub secrets (`DEPLOY_HOST`, `DEPLOY_KEY`, `STAGING_DEPLOY_PATH`, …), visible in `deploy-staging.yml` lines 73–185. I have no server access and will not ask for keys. +- From here I can only: + - disable GitHub workflows (B.3.3), which needs your go-ahead because it is an outward-facing change to the repo settings; + - afterwards, check the public URLs with read-only `curl -I` requests. + +**New finding while preparing the runbook (High, risk; depends on the server's `.env`):** + +- `.env.example` sets `QUEUE_CONNECTION=database` and `CACHE_STORE=database`. If staging inherits these against the production database: + - **Queue:** staging and production share one `jobs` table. A staging worker (running `redesign` code) can pick up and execute production jobs, including the Stripe webhook job, and the reverse is also true. + - **Cache:** they share the `cache` table, including rate-limit counters, locks and cached config-derived values. + - **Scheduler:** if staging has a `schedule:run` cron, scheduled tasks run twice against production data. +- This adds to A.5.2. It is one more reason the database connection, not just the website, must be cut. + +### B.3.1 Staging offline (on the server, by you) + +1. **Check the maintenance driver first:** `grep APP_MAINTENANCE_ "$STAGING_DEPLOY_PATH/.env"`. + - It must be `file` (the default). + - If it is `cache` with the `database` store, `artisan down` on staging **would also take production down**, because the flag would live in the shared cache table. In that case, skip step 2 and disable the staging subdomain in the hosting panel instead. +2. `cd "$STAGING_DEPLOY_PATH" && php artisan down`. In maintenance mode, queue workers stop pulling jobs and scheduled tasks are skipped. +3. Stop anything staging runs in the background: + - staging queue workers (supervisor or panel); + - any `schedule:run` cron line pointing at the staging path (`crontab -l`). +4. **Cut the database connection.** In the staging `.env`, blank `DB_HOST`, `DB_DATABASE`, `DB_USERNAME` and `DB_PASSWORD`, or point them at a separate, empty staging database. Then run `php artisan config:clear`, because the deploy caches config. +5. If staging uses **the same MySQL user** as production, removing it from the staging `.env` is not enough. Give production its own user (or rotate the password and update **only** the production `.env`), so the old credentials can no longer reach the data. +6. Staging stays down until it has its own database. Under D-2 it will not be redeployed from the old repo at all. + +### B.3.2 Close `/vendor/*` on production (on the server, by you, with no deploy) + +**Why not a code hot-fix?** It would mean committing to `main`. A push to `main` triggers `deploy.yml`, which runs `migrate --force` by default (`run_migrations: default true`) and re-runs the Scout imports. That is a production deployment with migrations, which this phase must avoid. + +**Use a web-server rule instead.** The site runs Apache (the deploy copies `public/.htaccess` into the web root), and nothing under `public/` is named `vendor`, so blocking the route prefix affects no assets. In the **web root** `.htaccess`, directly after `RewriteEngine On`, add: + +```apache +# NEXO D-12 containment 2026-11-01: vendor workspace closed pending v2 +# Optional staff allow-list: uncomment and set your IP +# RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.10$ +RewriteRule ^vendor(/.*)?$ - [F,L] +``` + +- This returns 403 for every `/vendor/*` request, including Inertia XHR requests. Filament admin (`/admin`), the storefront and checkout are untouched. +- The rule is undone by deleting those lines. +- **Every production deploy overwrites this `.htaccess`** (`cp public/.htaccess $DEPLOY_WEBROOT/.htaccess`). That is why B.3.3 matters. +- **Limitation:** this closes the hole for every vendor, legitimate ones included. Use the IP allow-list for yourself. Real vendor authorization belongs to v2. + +### B.3.3 Freeze the old repository's deploy pipelines (I can do this, with your go-ahead) + +```bash +gh workflow disable "Deploy Staging" -R AdilAzhari/nexo-ecommerce +gh workflow disable "Deploy" -R AdilAzhari/nexo-ecommerce +``` + +- This stops a green CI on `redesign` from redeploying staging (which would undo B.3.1 and run `config:cache` against whatever `.env` is there). +- It stops a push to `main` from deploying, migrating and overwriting the B.3.2 `.htaccess`. +- It changes no code and no server, and `gh workflow enable` reverses it. +- CI, Auto Format and PR Checks remain active. + +### B.3.4 Verification after B.3.1–B.3.3 + +I can run these read-only checks once you have done the server steps: + +- `curl -sI https://store.aljebal-albeedos.com/vendor/dashboard` → expect `403`. +- `curl -sI https://store.aljebal-albeedos.com/` → expect `200` (storefront unaffected). +- Staging URL → expect `503` (maintenance) or unreachable. I need the staging hostname from you; it is a secret. +- `gh workflow list -R AdilAzhari/nexo-ecommerce` → both deploy workflows `disabled_manually`. + +--- + +**End of Phase 0 addendum B. Phase 1 has not been started. Stopping for owner review.** diff --git a/docs/discovery/PHASE-1-REVIEW.md b/docs/discovery/PHASE-1-REVIEW.md new file mode 100644 index 0000000..3837d5d --- /dev/null +++ b/docs/discovery/PHASE-1-REVIEW.md @@ -0,0 +1,158 @@ +# Phase 1 Review: Product Specification and Architectural Foundation + +- **Date:** 2026-11-01 +- **Branch:** `docs/phase-1-specification` (worktree `C:\laragon\www\nexo-v2`, from `origin/main@1cc7b4e`). **Uncommitted.** Committing needs your authorisation. +- **Scope:** documentation only. No application code, migrations, dependencies or configuration were changed. + +--- + +## 1. Completed work + +| Brief Phase 1 deliverable | Where | +|---|---| +| Product Requirements Document | `docs/product/product-requirements.md` | +| Functional Requirements Specification | `docs/product/functional-requirements.md` (FR-* with acceptance criteria; legacy REQ-VENDOR-* mapped) | +| Non-functional Requirements | `docs/product/non-functional-requirements.md` | +| User roles and permissions | `docs/architecture/security-model.md` (permission matrices) + ADR-0011 | +| Customer and vendor workflows | `docs/product/user-journeys.md` (plus platform-staff journeys) | +| Bounded contexts / Domain Context Map | `docs/architecture/domain-context-map.md` (13 modules + shared kernel, glossary) | +| Dependency rules | `docs/architecture/dependency-rules.md` (layer rules, module matrix, events, read-model exceptions, frontend rules) | +| Database ownership + initial database model | `docs/architecture/database-design.md` | +| Business invariants | `docs/architecture/INVARIANTS.md` (INV-1…21, each with enforcement and proof) | +| Integration contracts / API and frontend contract strategy | `docs/architecture/integration-contracts.md` + ADR-0018 | +| Checkout and payment lifecycle | `docs/architecture/lifecycles/checkout-and-payment.md` (21-row scenario matrix) | +| Inventory lifecycle | `docs/architecture/lifecycles/inventory.md` (13-row scenario matrix) | +| Orders, fulfilment, returns, refunds | `docs/architecture/lifecycles/orders-fulfilment-and-returns.md` | +| Financial model | `docs/architecture/financial-model.md` (balanced worked examples, disputes, payouts, reconciliation) | +| System overview | `docs/architecture/system-overview.md` | +| ADR conventions and ADRs | ADR-0006 (process) + ADR-0007…0021; index in `docs/architecture/README.md` | +| Phase 0 report carried into this repository | `docs/discovery/PHASE-0-TECHNICAL-ASSESSMENT.md` (copy) | + +**Owner decisions applied this phase:** + +| Decision | Applied as | +|---|---| +| D-1 Vue 3 + Inertia + TS; D-2 this repository | ADR-0008 (Accepted) | +| D-4 supersede the Sudan/aljebal-albeedos product docs | ADR-0007 (Accepted); banners on old docs | +| D-5 MYR, EN-primary, AR/MS peers | ADR-0007 (Accepted) | +| D-6 ledger + simulated payouts | ADR-0016 (core approved) | +| D-7 promotions, reviews, wishlist and follows, loyalty and referrals in scope | PRD §6.1, FR-PRO/LOY/REV/ENG | + +## 2. Changed files + +**New (32):** + +- `docs/architecture/adr/0006…0021` (16 ADRs); +- the 4 product documents above; +- 7 architecture documents; +- `docs/architecture/lifecycles/` (3 files); +- `docs/discovery/PHASE-0-TECHNICAL-ASSESSMENT.md`; +- this file. + +**Modified (12):** + +| File | Change | +|---|---| +| `docs/README.md` | Start-here links, `discovery/` row | +| `docs/architecture/README.md` | Full index and ADR status table | +| `docs/architecture/INVARIANTS.md` | Rewritten as INV-1…21, with a change table versus the 2026-08-30 version | +| `docs/architecture/adr/ADR-TEMPLATE.md` | Brief §18 fields | +| `docs/product/README.md` | Current vs superseded index (also closes an unclosed code fence) | +| `docs/development/README.md` | Naming note → Vendor | +| `docs/product/{vision,scope,goals,personas,problem-statement}.md`, `docs/architecture/marketplace-migration.md` | One "Superseded" banner line each; content untouched | + +**Untouched on purpose:** + +- Existing ADR-0001…0005: status changes are yours. +- `docs/engineering/testing.md` and `database.md`: they still say SQLite and change only if ADR-0009 is accepted. +- `docs/vendor-domain-design.md`: a duplicate of `architecture/vendor-domain-design.md`. +- The empty `requirements/*` stubs. + +**Other repositories:** + +- Old repository `modular-ecommerce-system`: no change in this phase. +- `C:\laragon\www\Nexo` (`feat/vendor-frontend` checkout): untouched. The worktree adds only `.git/worktrees/nexo-v2` metadata and the new local branch. + +## 3. Architectural decisions (explained) + +These are the significant ones, each with its reason. The trade-offs and alternatives are in each ADR. + +1. **Modular monolith with `Contracts`-only cross-module imports, enforced by Pest arch tests (ADR-0010).** Boundaries become visible and CI-checked without purist repository layers. While writing the dependency matrix I found and removed **two cycles**: + - Ordering↔Finance: Finance now reads its own ledger instead of calling Ordering. + - Ordering↔Payments: the `OrderPayments` port is owned by Payments and implemented by Ordering. + + I also moved the reservation sweeper from Inventory to Ordering, so Inventory never depends on Payments. +2. **Fail-closed vendor context (ADR-0011).** This directly answers Phase 0 S1: no global scope, no null-means-all fallback, scoped bindings, and an isolation suite as a merge gate. +3. **MySQL for every database test (ADR-0009).** The old repository's concurrency tests passed on SQLite with real bugs present. Correctness claims must run on the real engine. +4. **Checkout saga (ADR-0015):** + - local transaction; + - provider call after commit; + - signed webhook as the only source of payment truth; + - idempotency keys at the HTTP, provider and event levels. + + Every brief §11 scenario has a defined outcome (P1–P21). +5. **Inventory as on-hand/reserved with conditional updates (ADR-0014).** Overselling is impossible by construction, and duplicate movements are blocked by a unique key. +6. **Refund cap owned by Ordering via guarded counters (ADR-0015 §9).** I first wrote a design where Payments locked Ordering's table. That breaks the module rules, so it was corrected. +7. **Internal double-entry ledger (ADR-0016).** The worked examples in `financial-model.md` were checked by hand to balance: 113.00 / 20.00 / 45.00 / 11.11. They become executable tests in Phase 5. +8. **Outbox + inbox (ADR-0017).** This replaces the old write-only event table (Phase 0 C2). +9. **Generated TypeScript types from PHP data objects (ADR-0018).** This addresses the 22 type errors and the prop drift found in the old repository. + +## 4. Tests and verification + +- **No application tests were run or needed.** This phase changed only Markdown, and the worktree has no `vendor/` installed. +- **Verification performed:** + - A script resolved every relative Markdown link under `docs/`. **1 broken:** `marketplace-migration.md` → `../REDESIGN_BRIEF.md`. It pre-exists inside a superseded document and I left it. + - Line endings were normalised to LF. + - The financial worked examples balance (checked manually). + - FR → INV cross-references were checked by hand. + - The dependency matrix was checked by hand for cycles. +- **Not verified, so these are assumptions:** + - Filament, `spatie/laravel-data` and `spatie/laravel-typescript-transformer` compatibility with Laravel 13 / PHP 8.5 (ADR-0018, ADR-0020, checked in Phase 3). + - MYR card + 3-D Secure support in Stripe test mode (D-14, Phase 5). + - The fee figure in the worked example (RM 4.09) is illustrative. + +## 5. Known limitations + +- These are specifications. Nothing is implemented, and none of the guarantees exist yet. +- Several product parameters are **proposals** (P-1…P-8 in the PRD) and need your confirmation. +- Tax is out of scope, and prices are tax-inclusive (ADR-0007). A reviewer may notice this, so the README must state it. +- One vendor per user; staff cannot shop; guest checkout is excluded (P-1). +- The admin panel is English-only in v2 (ADR-0020). +- Loyalty points are not a ledger liability (financial-model §1). This is a deliberate simplification. +- MySQL FULLTEXT search with limited Arabic stemming (ADR-0019). + +## 6. Unresolved questions (need your decision) + +| ID | Question | My recommendation | +|---|---|---| +| ADR status | Accept ADR-0009…0021? | Accept all, or name the ones to revise | +| ADR-0005 | Accept the Vendor naming, mark ADR-0002 superseded, rename the file to `0005-vendor-naming.md` | Yes | +| D-3 | Laravel 13 / PHP 8.5 / MySQL 8.4 (ADR-0009) | Yes | +| D-8 | Search: MySQL first (ADR-0019) | Yes | +| D-9 | Platform bears Stripe fees | Yes | +| D-10 | Commission reversed exactly on refunds; shipping excluded from commission | Yes | +| D-11 | Vendor bears lost-dispute goods value; platform bears the fee | Yes | +| P-1…P-8 | Product parameters in PRD §6.3 | As proposed | +| D-12 follow-up | **Your GitHub account `AdilAzhariOmsan` has read-only access to `AdilAzhari/nexo-ecommerce`.** I could not disable "Deploy Staging" or "Deploy". Run the two `gh workflow disable` commands as the repository owner, or disable them in GitHub → Actions. The server steps in Phase 0 Addendum B.3.1–B.3.2 are still pending on your side. | Do both before Phase 3 | +| D-13 step 3 | Copy `C:\laragon\backups\nexo-2026-11-01\` off-machine | Pending on your side | +| D-15 | Commit author identity. **The repository's local git config is `adelazhary1997@gmail.com`.** Commits made for you will use `adilazhariosman@gmail.com` explicitly. Do you want the history rewritten before going public? | Decide before Phase 3 | +| Commit | May I commit this phase to `docs/phase-1-specification` and open a PR to `main`? | Yes, as one commit per document group | +| Tooling | The repository's `CLAUDE.md` asks for Laravel Boost (`composer require laravel/boost --dev`). It is a dependency, so it was not installed in this docs-only phase. | Install at the start of Phase 3 if you want it | +| 61 React commits | Push `feat/vendor-frontend` to a reference branch on GitHub (they are only bundled locally) | Yes | + +## 7. Review checklist + +- [ ] PRD objectives, audience and scope (in and out) match your intent; P-1…P-8 confirmed or changed. +- [ ] Functional requirements: priorities (M/S/C) acceptable; nothing missing from your expectations for the three surfaces. +- [ ] NFR targets are acceptable (performance numbers are assumptions until Phase 9 measures them). +- [ ] Roles and permission matrices (`security-model.md`) reflect how you want vendors and staff to work. +- [ ] 13 modules and their ownership (`domain-context-map.md`) make sense; the dependency matrix is acceptable. +- [ ] Database model: naming and constraints acceptable; no table you expected is missing. +- [ ] Invariants INV-1…21 are the right guarantees. +- [ ] Lifecycle scenario matrices cover the failure cases you care about. +- [ ] Financial model: posting rules and worked examples are acceptable; D-9/10/11 decided. +- [ ] ADR-0005 and 0009…0021: accept, revise or reject each. +- [ ] Containment actions (D-12) and the off-machine backup (D-13 step 3) done on your side. +- [ ] Authorise (or not) committing this branch; decide D-15. + +**Phase 1 stops here. Phase 2 (Brand Identity and Design System) has not been started.** From c88dab7fdde047603a3b258029a09e43e8a2702b Mon Sep 17 00:00:00 2001 From: AdilAzhariOmsan Date: Tue, 3 Nov 2026 20:53:14 +0800 Subject: [PATCH 2/5] docs(product): specify Nexo 2.0 requirements and supersede legacy product docs --- docs/product/README.md | 17 ++ docs/product/functional-requirements.md | 220 ++++++++++++++++++++ docs/product/goals.md | 2 + docs/product/non-functional-requirements.md | 91 ++++++++ docs/product/personas.md | 2 + docs/product/problem-statement.md | 2 + docs/product/product-requirements.md | 175 ++++++++++++++++ docs/product/scope.md | 2 + docs/product/user-journeys.md | 153 ++++++++++++++ docs/product/vision.md | 2 + 10 files changed, 666 insertions(+) create mode 100644 docs/product/functional-requirements.md create mode 100644 docs/product/non-functional-requirements.md create mode 100644 docs/product/product-requirements.md create mode 100644 docs/product/user-journeys.md diff --git a/docs/product/README.md b/docs/product/README.md index 47c6555..74ead24 100644 --- a/docs/product/README.md +++ b/docs/product/README.md @@ -21,3 +21,20 @@ Stories Tasks ↓ Implementation +``` + +## Nexo 2.0 product documents (current) + +| Document | Purpose | +|---|---| +| [`product-requirements.md`](product-requirements.md) | PRD: objectives, audience, roles, surfaces, scope, release gate | +| [`functional-requirements.md`](functional-requirements.md) | FR-* requirements with acceptance criteria | +| [`non-functional-requirements.md`](non-functional-requirements.md) | NFR-*: correctness, security, performance, a11y, i18n, quality gates | +| [`user-journeys.md`](user-journeys.md) | Customer, vendor and platform-staff workflows with failure branches | + +## Superseded (history only) + +`vision.md`, `scope.md`, `goals.md` and `personas.md` (aljebal-albeedos framing) were superseded on +2026-11-01 by ADR-0007. `problem-statement.md` and the empty stubs under `requirements/` are +also superseded: `requirements/vendor/onboarding.md` and +`requirements/platform/vendor-management.md` are carried into FR-VEN. diff --git a/docs/product/functional-requirements.md b/docs/product/functional-requirements.md new file mode 100644 index 0000000..7479161 --- /dev/null +++ b/docs/product/functional-requirements.md @@ -0,0 +1,220 @@ +# Functional Requirements Specification + +- **Status:** Draft for owner review (Phase 1) +- **Date:** 2026-11-01 +- **Source:** [product-requirements.md](product-requirements.md) + +**Priority** uses MoSCoW: M = must, S = should, C = could. Every M requirement is in the v2 release gate. + +**Acceptance criteria** are written so that each can become one or more Pest tests. **[INV-n]** refers to [`../architecture/INVARIANTS.md`](../architecture/INVARIANTS.md). + +**Legacy IDs.** `REQ-VENDOR-*` and `REQ-VENDOR-MGMT-*` from `requirements/vendor/onboarding.md` and `requirements/platform/vendor-management.md` are carried into FR-VEN. Their mapping is listed in each row. + +--- + +## FR-ID: Identity and accounts + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-ID-01 | M | Visitors can register with name, email and password, and must verify their email before checkout | Unverified users are redirected to verification when they reach checkout; the verification link is signed and expires in 60 minutes | +| FR-ID-02 | M | Login, logout, password reset, with rate limiting | Login is throttled to 5 attempts per minute per email+IP; reset tokens are single-use and expire | +| FR-ID-03 | M | Customers manage profile and address book (Malaysian address format, zone derived from state) | Address state determines the shipping zone; at most one default address | +| FR-ID-04 | M | Platform staff accounts are created by an Admin, require 2FA, and cannot register publicly | A staff login without 2FA confirmation cannot reach `/admin` | +| FR-ID-05 | M | Staff accounts cannot place orders or hold vendor membership [INV-2] | Attempting either fails with a domain error; covered by tests | +| FR-ID-06 | S | Customers can delete their account; personal data is anonymised and order records are retained | After deletion the name and email are anonymised; orders and ledger remain intact | +| FR-ID-07 | M | The UI language is selectable (en/ar/ms) and persisted per user; Arabic renders RTL | `` reflects the choice; the preference survives logout and login | + +## FR-VEN: Vendor onboarding and management + +| ID | P | Requirement | Acceptance criteria | Legacy | +|---|---|---|---|---| +| FR-VEN-01 | M | A signed-in customer can submit a vendor application (store name, description, contact, category focus) | Submission does not create a Vendor; one open application per user | REQ-VENDOR-001 | +| FR-VEN-02 | M | Platform Operations approves or rejects applications with a reason | Approve creates a Vendor in `pending`, plus an Owner membership for the applicant, in one transaction. Reject creates no Vendor and notifies the applicant. | REQ-VENDOR-001, 001A | +| FR-VEN-03 | M | A Vendor has a stable id and a unique slug that never changes | The slug has a unique constraint; renaming the vendor keeps the slug | REQ-VENDOR-002 | +| FR-VEN-04 | M | Vendor lifecycle is `pending → approved ⇄ suspended`; other transitions are rejected and leave the status unchanged | Every invalid pair is tested; transitions are audit-logged | REQ-VENDOR-003…007, MGMT-002…005, 009 | +| FR-VEN-05 | M | Operating eligibility: only `approved` vendors' products are sellable. A `suspended` vendor can view history and fulfil existing orders, but cannot edit or create products or receive new orders. | Suspension hides products from the storefront (search documents updated); existing VendorOrders stay fulfillable | REQ-VENDOR-008, MGMT-010 | +| FR-VEN-06 | M | Vendor identity and history are preserved through suspension; vendors are never hard-deleted | No delete action exists; foreign keys use RESTRICT | REQ-VENDOR-009, MGMT-006 | +| FR-VEN-07 | M | The Vendor Owner invites team members by email with a role (manager, fulfilment, finance, support); invitations expire in 7 days | The invitee must not already belong to a vendor or be staff; exactly one owner per vendor | REQ-VENDOR-010 | +| FR-VEN-08 | M | The Owner changes roles and deactivates members; the Owner role is transferred explicitly | A deactivated member gets 403 on `/vendor` immediately (next request) | | +| FR-VEN-09 | M | Vendors manage their store profile: logo, banner, description, return-policy text | Shown on the public store page | | +| FR-VEN-10 | M | Vendor data is isolated: no vendor context can read or modify another vendor's records [INV-1] | Isolation test suite (merge gate) | MGMT-007 | + +## FR-CAT: Catalogue + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-CAT-01 | M | Platform Operations manages a category tree (max depth 3) with names in en/ar/ms and an optional commission override | A category with products cannot be deleted, only deactivated | +| FR-CAT-02 | M | Platform defines option attributes (for example Colour, Size) with values, usable for variants | Variant option combinations are unique per product | +| FR-CAT-03 | M | Vendors create products: title, description, brand, category (leaf), images (≤ 8, ordered), status `draft/active/inactive` | Only an `approved` vendor can set `active`; image MIME type and size are validated; images are stored privately until processed | +| FR-CAT-04 | M | Products have ≥ 1 variant; each variant has a SKU (unique per vendor), price, optional compare-at price (> price), weight and option values | Price > 0; compare-at > price or null | +| FR-CAT-05 | M | A product is **sellable** iff: product `active` ∧ not blocked ∧ vendor `approved` ∧ ≥ 1 variant active | The storefront and checkout use the same sellability rule (single implementation) | +| FR-CAT-06 | M | Platform Operations can block or unblock a product with a reason (reactive moderation, P-2) | A blocked product disappears from the storefront; the vendor sees the reason | +| FR-CAT-07 | M | Price changes never affect existing orders [INV-6] | Snapshot test | +| FR-CAT-08 | S | Vendors import products by CSV with a validation report (dry-run first) | Invalid rows are reported with line numbers; no partial import unless confirmed | + +## FR-INV: Inventory + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-INV-01 | M | Each variant has on-hand and reserved quantities; available = on-hand − reserved, never negative [INV-7] | DB CHECK constraint present; tests attempt violations | +| FR-INV-02 | M | Checkout reserves stock atomically; insufficient stock fails the whole checkout and names the short lines | Parallel test: N buyers for the last unit, exactly one succeeds | +| FR-INV-03 | M | Reservations expire after 15 minutes (P-4), with one extension while payment is processing | Sweeper test with a frozen clock | +| FR-INV-04 | M | Payment success consumes reservations; failure, expiry or cancellation releases them; each happens at most once | Duplicate events give a single movement | +| FR-INV-05 | M | Vendors adjust stock by delta (receive, damage, correction with reason) or by absolute count with conflict detection | A stale count returns 409 with the current value; on-hand below reserved is rejected | +| FR-INV-06 | M | Every change records a stock movement with type, deltas, reference and actor; the vendor can view the history per variant | Movement sums equal on-hand (reconciliation test) | +| FR-INV-07 | S | Low-stock threshold per variant, shown in the vendor dashboard | | +| FR-INV-08 | M | A returned item is restocked only when the vendor marks it received and resellable | | + +## FR-SHP: Shipping + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-SHP-01 | M | Each vendor sets a flat rate per zone (Peninsular, East Malaysia) and an optional free-shipping threshold | Checkout quotes per VendorOrder, using the vendor's goods subtotal before discounts | +| FR-SHP-02 | M | A vendor that has no rate for the customer's zone cannot be checked out to that address | Checkout shows which vendor cannot ship there | +| FR-SHP-03 | M | Vendors enter carrier and tracking number when marking shipped | Required fields; the customer sees them on the order timeline | + +## FR-CART: Cart + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-CART-01 | M | Visitors and customers add variants from any number of vendors to one cart, grouped by vendor in the UI | | +| FR-CART-02 | M | A guest cart merges into the customer cart at login (quantities summed, capped at available) | Merge is idempotent | +| FR-CART-03 | M | The cart revalidates on view: price changes, unavailable items and quantity above available are flagged, and checkout is blocked until acknowledged | | +| FR-CART-04 | M | Quantity per line is limited to 1–20 (configurable) and to available stock | | +| FR-CART-05 | M | A cart is owned by exactly one session or customer; no endpoint accepts a cart id from the client | Test: another user's cart cannot be read or checked out | + +## FR-CHK: Checkout + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-CHK-01 | M | Checkout requires a verified customer account (P-1) | | +| FR-CHK-02 | M | The customer selects a shipping address; the system quotes shipping per vendor and shows a breakdown per vendor and in total | | +| FR-CHK-03 | M | The customer may apply one coupon and redeem points; the system shows the allocation as a total discount | Invalid coupons give a reason code | +| FR-CHK-04 | M | Placing the order re-prices server-side; if the total differs from the confirmed total, nothing is charged and the customer reviews again | 409 test | +| FR-CHK-05 | M | Placing is idempotent per checkout key; double-submit creates one OrderGroup [INV-9] | Parallel test | +| FR-CHK-06 | M | One payment for the group total by card (Stripe test mode, 3-D Secure supported) | | +| FR-CHK-07 | M | The payment outcome is determined only by verified provider events; the customer sees "confirming" until then | Closing the browser does not change the outcome (test) | +| FR-CHK-08 | M | After a declined payment the customer can retry with another card until the reservation expires | One active attempt at a time | +| FR-CHK-09 | M | If the provider is unavailable the customer sees a retryable error; no order is paid and stock stays reserved until TTL | Fake-provider outage test | +| FR-CHK-10 | M | Successful payment creates one VendorOrder per vendor in `awaiting_fulfilment`, notifies each vendor and emails the customer one confirmation | | + +## FR-ORD: Orders and fulfilment + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-ORD-01 | M | Customers see their orders as groups expanding into vendor shipments, each with its own status timeline | | +| FR-ORD-02 | M | Vendors see only their VendorOrders, filterable by status and date, with the shipping address and items | No sibling or group totals are exposed [INV-1] | +| FR-ORD-03 | M | Vendor transitions follow the state machine in ADR-0012; invalid transitions are rejected | Every pair is tested; concurrent transition test | +| FR-ORD-04 | M | A vendor cancels with a reason before shipping, which triggers a full refund of that VendorOrder and releases stock | Siblings unaffected [INV-4] | +| FR-ORD-05 | M | A customer cancels a VendorOrder while `awaiting_fulfilment` (P-8) | | +| FR-ORD-06 | M | Delivered is confirmed by the vendor or the customer, or automatically after 10 days shipped (P-3) | | +| FR-ORD-07 | M | Completed is set automatically when the return window has ended with no open return or dispute; this triggers payable maturity and points award | | +| FR-ORD-08 | M | Every transition is recorded in the status history with actor and reason | | +| FR-ORD-09 | S | Vendors export orders as CSV for a date range | Only their own orders | + +## FR-RET: Returns + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-RET-01 | M | A customer requests a return for delivered lines within the return window (quantity per line, reason, optional photos) | No more than the purchased minus already-returned quantity | +| FR-RET-02 | M | The vendor approves or rejects (with reason) within 3 days; after 3 days with no response the request escalates to Platform Operations | | +| FR-RET-03 | M | On approval and receipt, the vendor marks items received (resellable or not), which triggers the refund of those lines | | +| FR-RET-04 | M | Platform Operations can override a vendor rejection | Audit-logged | +| FR-RET-05 | S | "Not received" claim on a shipped order past its expected window, handled as a return without item receipt | | + +## FR-REF: Refunds + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-REF-01 | M | Refunds are per VendorOrder and per line (goods + proportional shipping when all lines are refunded); the amount never exceeds the refundable remainder [INV-5] | Concurrent refund test | +| FR-REF-02 | M | Initiators: system (cancellation, return receipt, stock unavailable after payment), Vendor Owner/Finance (goodwill, capped), Platform Finance (override) | | +| FR-REF-03 | M | Refund lifecycle `requested → approved → submitted → succeeded / failed`; a provider timeout leaves it `submitted` until confirmed | | +| FR-REF-04 | M | Refunds reverse the refunded lines' commission, discount allocation and points exactly | Ledger tests | +| FR-REF-05 | M | No refund while a dispute is open on the payment | | +| FR-REF-06 | M | Customer, vendor and finance see refund status per VendorOrder | | + +## FR-PAY / FR-FIN: Payments and finance + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-PAY-01 | M | Webhooks are signature-verified; invalid signatures get 400 and nothing is stored | | +| FR-PAY-02 | M | Each provider event is processed at most once in effect [INV-9] | Concurrent duplicate-delivery test | +| FR-PAY-03 | M | An amount or currency mismatch between provider and attempt never marks paid; it raises a reconciliation exception | | +| FR-FIN-01 | M | Every money movement posts a balanced ledger transaction exactly once [INV-10] | | +| FR-FIN-02 | M | The commission rate (vendor → category → default) is frozen per VendorOrder [INV-6] | Changing rates affects new orders only | +| FR-FIN-03 | M | Vendors see balances (pending, available, held, receivable, paid out) and a statement of ledger lines, exportable as CSV | Balance = Σ statement | +| FR-FIN-04 | M | A weekly simulated payout run pays each vendor's available balance ≥ the minimum, net of receivables; it is idempotent | Running it twice concurrently produces one payout per vendor | +| FR-FIN-05 | M | Disputes hold the allocated vendor funds and resolve per ADR-0016 | | +| FR-FIN-06 | M | Reconciliation: nightly invariant checks and provider matching; exceptions are listed for Finance to resolve | Injected mismatch is detected | +| FR-FIN-07 | M | Platform Finance edits the default and override commission rates; changes are audit-logged | | +| FR-FIN-08 | S | Platform finance dashboard: GMV, commission, refunds, payables, payouts by period | | + +## FR-PRO: Promotions + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-PRO-01 | M | Platform Operations creates promotions: coupon code or automatic; percentage or fixed; minimum spend; target all, categories or vendors; total and per-customer usage limits; date window | | +| FR-PRO-02 | M | One coupon per checkout; the best automatic promotion is applied when no coupon is used (no stacking in v2) | | +| FR-PRO-03 | M | Discounts are allocated to eligible lines by largest remainder and stored per line [INV-8] | Property test: sum exact | +| FR-PRO-04 | M | Usage limits hold under concurrency (redemption reserved at checkout, released on expiry) | Parallel test on the last allowed use | +| FR-PRO-05 | M | Promotions are platform-funded; vendor payable is unaffected | Ledger test | + +## FR-LOY: Loyalty and referrals + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-LOY-01 | M | Points are earned on completed VendorOrders at the configured rate (P-5), recorded in a points ledger | Award is idempotent per VendorOrder | +| FR-LOY-02 | M | Points are redeemed at checkout up to the cap; held at placement, confirmed on payment, released on expiry | | +| FR-LOY-03 | M | A refund of redeemed-point lines returns the points; a refund after award claws back the earned points (balance may not go below zero; the shortfall is recorded) | | +| FR-LOY-04 | M | Each customer has a referral code; the referee benefit and referrer award follow P-6; self-referral and repeat referral are blocked | | +| FR-LOY-05 | S | Points balance and history page | | + +## FR-REV: Reviews + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-REV-01 | M | Customers review a purchased product after delivery: one review per purchased VendorOrder line, rating 1–5, text, optional photos | Unique (line) | +| FR-REV-02 | M | Vendors reply once per review (editable) | | +| FR-REV-03 | M | Platform Operations hides or unhides reviews with a reason; product rating aggregates only visible reviews | | +| FR-REV-04 | S | Customers report reviews | | + +## FR-ENG: Wishlist and follows + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-ENG-01 | M | Customers add and remove products to a wishlist; unavailable items are shown as such | | +| FR-ENG-02 | M | Customers follow or unfollow vendor stores; the account page shows new products from followed stores | | + +## FR-SRCH: Search and discovery + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-SRCH-01 | M | Keyword search across sellable products with facets: category, price range, vendor, rating ≥ n, in stock; sorting by relevance, price, newest, rating | Hidden products never returned | +| FR-SRCH-02 | M | Category pages list with the same facets | | +| FR-SRCH-03 | M | Search documents update within about 1 minute of product, stock, vendor or review changes | Outbox-driven | +| FR-SRCH-04 | S | Arabic queries normalise letter forms and diacritics | | + +## FR-NOT: Notifications + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-NOT-01 | M | In-app notifications and email for: order placed or paid, vendor order received, shipped, delivered, cancelled, return updates, refund succeeded, payout made, application decision, team invite | Sent once per event (inbox de-duplication) | +| FR-NOT-02 | M | Emails are localised in the recipient's language | | +| FR-NOT-03 | S | Users manage email preferences for non-transactional notifications | | + +## FR-ADM: Platform administration + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-ADM-01 | M | Staff roles and permissions per `security-model.md`; every action is policy-checked | Denied-access tests per resource | +| FR-ADM-02 | M | Audit log for: vendor lifecycle, moderation, refunds, commission changes, payouts, reconciliation resolutions, impersonation, staff changes | Each action writes an audit row (actor, before/after, IP) | +| FR-ADM-03 | M | Admin impersonates vendor members or customers for support; impersonation is banner-marked and audited, and money-moving actions are blocked | | +| FR-ADM-04 | M | Operational health: outbox backlog and dead letters, failed jobs, last webhook received, reconciliation exceptions, sweeper last run | | +| FR-ADM-05 | M | Order lookup across vendors by reference, customer email or provider payment id | | + +## FR-DEMO: Demo environment + +| ID | P | Requirement | Acceptance criteria | +|---|---|---|---| +| FR-DEMO-01 | M | One command builds the demo from an empty database (ADR-0021), deterministically | CI runs it; invariants pass | +| FR-DEMO-02 | M | Documented demo logins per role; demo-only banner; Stripe test cards documented | | diff --git a/docs/product/goals.md b/docs/product/goals.md index 3d7f215..381f172 100644 --- a/docs/product/goals.md +++ b/docs/product/goals.md @@ -1,5 +1,7 @@ # Goals & Non-Goals +> **Superseded 2026-11-01** by [ADR-0007](../architecture/adr/0007-product-positioning-currency-and-locales.md) (owner decision D-4) and the Nexo 2.0 product documents: [product-requirements.md](../product/product-requirements.md), [functional-requirements.md](../product/functional-requirements.md). Kept for history only; do not implement from this file. + **Status:** Ratified 2026-08-30 --- diff --git a/docs/product/non-functional-requirements.md b/docs/product/non-functional-requirements.md new file mode 100644 index 0000000..0ee48de --- /dev/null +++ b/docs/product/non-functional-requirements.md @@ -0,0 +1,91 @@ +# Non-functional Requirements + +- **Status:** Draft for owner review (Phase 1) +- **Date:** 2026-11-01 + +These requirements are measurable. Where a target is an assumption rather than a measurement, it says so. Targets apply to the **demo environment** at demo scale. Demo scale is about 12 vendors, ≤ 1,000 products and ≤ 5,000 orders (PRD §8). + +## NFR-COR: Correctness (highest priority) + +| ID | Requirement | Verification | +|---|---|---| +| NFR-COR-01 | No overselling under concurrent checkout | Parallel-process MySQL tests (ADR-0014) | +| NFR-COR-02 | Every ledger transaction balances; every business event posts at most once | Property tests + nightly invariant job | +| NFR-COR-03 | Every brief §10 and §11 scenario has a defined outcome and an automated test | Scenario matrix in `lifecycles/*.md`, each row linked to a test name in Phase 4–5 | +| NFR-COR-04 | Retries (user, queue, webhook, scheduler) are idempotent | Duplicate-execution tests per use case | + +## NFR-SEC: Security + +| ID | Requirement | Verification | +|---|---|---| +| NFR-SEC-01 | OWASP ASVS 4.0 Level 1 controls for authentication, session, access control, validation, and error handling and logging | Phase 9 checklist | +| NFR-SEC-02 | Default-deny authorisation; vendor isolation suite is a merge gate | ADR-0011 | +| NFR-SEC-03 | Staff 2FA mandatory; staff sessions expire after 2 hours idle | Tests | +| NFR-SEC-04 | Webhook signature verification; CSRF on all state-changing web routes; rate limits on auth, checkout, coupon validation and review creation | Tests | +| NFR-SEC-05 | Uploads: images only (JPEG, PNG, WebP), ≤ 5 MB, re-encoded server-side, randomised names, EXIF stripped | Tests | +| NFR-SEC-06 | No secrets in the repository; live payment keys refused at boot (ADR-0021); `composer audit` and `npm audit --omit=dev` gate CI | CI | +| NFR-SEC-07 | Production error pages never expose exception messages or stack traces (Phase 0 S-finding) | Test with `APP_DEBUG=false` | +| NFR-SEC-08 | Security headers: CSP (nonce-based for Vite), HSTS on demo, `X-Content-Type-Options`, `Referrer-Policy`, `frame-ancestors 'none'` | Header tests | + +## NFR-PERF: Performance (assumption-based targets; measured in Phase 9) + +| ID | Requirement | +|---|---| +| NFR-PERF-01 | Server time p95 ≤ 300 ms for storefront pages (home, category, product, search) at demo scale | +| NFR-PERF-02 | `POST /checkout` server time p95 ≤ 800 ms, excluding the provider call | +| NFR-PERF-03 | No N+1 queries on list pages; enforced with `Model::preventLazyLoading()` outside production, plus query-count assertions on key pages | +| NFR-PERF-04 | Largest Contentful Paint ≤ 2.5 s on a mid-range mobile profile (Lighthouse) for home and product pages; responsive images in WebP with explicit dimensions | +| NFR-PERF-05 | Initial JS for the storefront entry ≤ 200 KB gzip; vendor and admin code split from storefront | + +## NFR-REL: Reliability and operability + +| ID | Requirement | +|---|---| +| NFR-REL-01 | Outbox, sweeper and reconciliation jobs are idempotent and safe to run concurrently | +| NFR-REL-02 | Health endpoint reports DB, queue, outbox age, scheduler heartbeat and last webhook time | +| NFR-REL-03 | Structured JSON logs with a correlation id propagated from request → jobs → outbox consumers | +| NFR-REL-04 | Failed jobs and dead-lettered messages are visible and retryable from the admin panel | +| NFR-REL-05 | Demo database backed up daily and restorable via the documented procedure (or rebuilt from the seed) | + +## NFR-A11Y: Accessibility + +| ID | Requirement | +|---|---| +| NFR-A11Y-01 | WCAG 2.2 AA for storefront and vendor dashboard key journeys (browse, cart, checkout, order tracking, vendor fulfilment) | +| NFR-A11Y-02 | Full keyboard operation, visible focus, semantic landmarks, labelled form controls, error messages linked with `aria-describedby` | +| NFR-A11Y-03 | Colour contrast ≥ 4.5:1 for text (verified against Phase 2 tokens) | +| NFR-A11Y-04 | Automated axe checks in browser tests; manual screen-reader pass (NVDA) on checkout in Phase 9 | + +## NFR-I18N: Internationalisation + +| ID | Requirement | +|---|---| +| NFR-I18N-01 | en, ar and ms have 100 % key coverage for the UI; CI fails on missing keys | +| NFR-I18N-02 | RTL uses CSS logical properties and direction-aware components (icons, carousels, steppers, progress); no physical `left/right` utilities in components (lint rule) | +| NFR-I18N-03 | Currency, number and date formatting is locale-aware through `Intl`; dates display in MYT | +| NFR-I18N-04 | Locale in the storefront URL (`/ms/...`, `/ar/...`; `en` unprefixed) for shareable links; `hreflang` alternates | + +## NFR-MNT: Maintainability and quality gates + +| ID | Requirement | +|---|---| +| NFR-MNT-01 | CI gates (all blocking): Pint, PHPStan/Larastan level 8 (raised from the current 6 during Phase 3), Pest (unit + feature + arch on MySQL), concurrency suite, `vue-tsc`, ESLint, Vitest, Vite build, generated-types diff, translation-key check, `composer audit`, `npm audit` | +| NFR-MNT-02 | Module dependency rules enforced by arch tests (ADR-0010) | +| NFR-MNT-03 | Every boundary decision has an ADR; PRs reference the ADRs they implement | +| NFR-MNT-04 | No test is skipped in CI without a linked issue; skipped tests are reported in CI output | + +## NFR-COMP: Compatibility + +| ID | Requirement | +|---|---| +| NFR-COMP-01 | Latest two versions of Chrome, Edge, Firefox and Safari; iOS Safari 17+; Android Chrome | +| NFR-COMP-02 | Responsive from 360 px wide; storefront is mobile-first; vendor dashboard is fully usable at ≥ 768 px and functional on mobile for order fulfilment | + +## NFR-PRIV: Privacy and data + +| ID | Requirement | +|---|---| +| NFR-PRIV-01 | All demo personal data is fictional; no real customer data is ever imported | +| NFR-PRIV-02 | Vendors see only the customer data needed to fulfil (name, shipping address, phone); never email or other orders | +| NFR-PRIV-03 | Card data never touches Nexo servers (Stripe Elements / Payment Element) | +| NFR-PRIV-04 | Logs never contain passwords, tokens, full addresses or provider secrets | diff --git a/docs/product/personas.md b/docs/product/personas.md index e4594fa..980904b 100644 --- a/docs/product/personas.md +++ b/docs/product/personas.md @@ -1,5 +1,7 @@ # Personas +> **Superseded 2026-11-01** by [ADR-0007](../architecture/adr/0007-product-positioning-currency-and-locales.md) (owner decision D-4) and the Nexo 2.0 product documents: [product-requirements.md](../product/product-requirements.md), [functional-requirements.md](../product/functional-requirements.md). Kept for history only; do not implement from this file. + **Status:** Ratified 2026-08-30 Four primary personas. The vendor-team sub-roles already exist in code diff --git a/docs/product/problem-statement.md b/docs/product/problem-statement.md index a48999b..9d87bcb 100644 --- a/docs/product/problem-statement.md +++ b/docs/product/problem-statement.md @@ -1,5 +1,7 @@ # Problem Statement +> **Superseded 2026-11-01** by [ADR-0007](../architecture/adr/0007-product-positioning-currency-and-locales.md) (owner decision D-4) and [product-requirements.md](product-requirements.md). Kept for history only. + **Status:** Ratified 2026-08-30 --- diff --git a/docs/product/product-requirements.md b/docs/product/product-requirements.md new file mode 100644 index 0000000..4e1e2d8 --- /dev/null +++ b/docs/product/product-requirements.md @@ -0,0 +1,175 @@ +# Nexo — Product Requirements Document + +- **Status:** Draft for owner review (Phase 1) +- **Date:** 2026-11-01 +- **Supersedes:** `vision.md`, `scope.md`, `goals.md`, `personas.md` (aljebal-albeedos framing), per ADR-0007 +- **Related:** [functional-requirements.md](functional-requirements.md) · [non-functional-requirements.md](non-functional-requirements.md) · [user-journeys.md](user-journeys.md) + +--- + +## 1. Product summary + +Nexo is a multi-vendor general-merchandise marketplace: + +- Independent vendors sell through one shared storefront. +- Customers discover products across vendors, buy from several vendors in one checkout and pay once. +- Each vendor fulfils its own part of the order. +- The platform collects payment, keeps a commission, and accounts for what it owes each vendor. + +Nexo is built as a **portfolio showcase**. It must look, behave and hold up under scrutiny like a credible commercial product. It is not operated as a real business: + +- all vendors, customers and orders are fictional; +- payments run in Stripe test mode; +- vendor payouts are simulated. + +## 2. Objectives + +| # | Objective | Evidence of success | +|---|---|---| +| O1 | Demonstrate correct marketplace commerce | Multi-vendor checkout, split vendor orders, independent fulfilment and refunds, all passing the failure scenarios in `docs/architecture/lifecycles/` | +| O2 | Demonstrate trustworthy money handling | Ledger balances under property and concurrency tests; vendor statements reconcile to orders and refunds | +| O3 | Demonstrate engineering quality | All CI gates green (tests on MySQL, static analysis, types, lint, arch rules); ADRs for every boundary decision | +| O4 | Feel premium | New visual identity (Phase 2); polished storefront, vendor dashboard and admin; real RTL Arabic | +| O5 | Be reproducible | Demo rebuilt from a clean database with one command; documented credentials for each role | + +## 3. Audience + +| Audience | What they evaluate | +|---|---| +| Recruiters and hiring managers | First impression, polish, a clear README, a live demo with role logins | +| Engineers and reviewers | Architecture, ADRs, tests, concurrency and payment correctness, code quality | +| Project owner | A maintainable codebase that can grow | + +## 4. Users and roles + +The full permission matrix is in `docs/architecture/security-model.md`. + +| Role | Description | +|---|---| +| Visitor | Not signed in. Browses, searches, keeps a guest cart. | +| Customer | Signed-in shopper. Checks out, tracks orders, returns, reviews, wishlist, follows stores, loyalty and referrals. | +| Vendor Owner | Created when the platform approves a vendor application. Full control of their own vendor, including team management. | +| Vendor Manager | Catalogue, inventory, orders, returns, shipping settings, review replies. | +| Vendor Fulfilment | Order fulfilment and stock adjustments. | +| Vendor Finance | Earnings, statements, payouts, refund approval. | +| Vendor Support | Order lookup, return handling, review replies. Read-only on catalogue. | +| Platform Admin | Everything, including staff management and impersonation. | +| Platform Operations | Vendor applications and lifecycle, catalogue moderation, order and return arbitration, promotions. | +| Platform Finance | Commission settings, refunds override, disputes, reconciliation, payout runs. | +| Platform Support | Read access to orders, customers and vendors for support. No money-moving actions. | + +## 5. Product surfaces + +1. **Customer storefront** (`/`, Vue). Covers: + - home and merchandising; + - category browsing; + - search with facets; + - product pages with variants; + - vendor store pages; + - multi-vendor cart and checkout; + - account pages: orders, returns, addresses, wishlist, followed stores, points, referrals, reviews, notifications. +2. **Vendor dashboard** (`/vendor`, Vue). Covers: + - overview; + - products and variants; + - inventory; + - orders and fulfilment; + - returns; + - earnings, statement and payouts; + - reviews; + - store profile; + - shipping rates; + - team; + - notifications. +3. **Platform administration** (`/admin`, Filament). Covers: + - vendor applications and vendors; + - catalogue taxonomy and moderation; + - orders and returns arbitration; + - refunds and disputes; + - finance (ledger, payouts, reconciliation); + - promotions; + - loyalty settings; + - customers; + - staff; + - audit log; + - system health (outbox, queues, webhooks). + +## 6. Scope + +### 6.1 In scope for v2 + +| Area | Notes | +|---|---| +| Identity | Registration, login, email verification, password reset, profile, address book; staff 2FA | +| Vendor onboarding | Application → platform review → vendor created (pending) → approved; suspension and reactivation | +| Vendor team | Invite members with roles; one vendor per user | +| Catalogue | Platform-owned category tree and attribute definitions; vendor products with variants (option combinations), images, descriptions; product status (draft, active, inactive); platform moderation (block or unblock) | +| Pricing | Price per variant, optional compare-at price; tax-inclusive (no tax engine) | +| Inventory | On-hand and reserved per variant; reservations with TTL; adjustments; movement history; low-stock indicator | +| Shipping | Per-vendor flat rates by zone (Peninsular Malaysia, East Malaysia); per-vendor free-shipping threshold; manual carrier and tracking entry | +| Cart | Guest and customer cart, multi-vendor, grouped by vendor; merged on login; stock and price revalidation | +| Checkout | Account required; address; per-vendor shipping; coupon; points redemption; single card payment (Stripe test mode, 3-D Secure) | +| Orders | OrderGroup + VendorOrders; customer timeline per vendor; vendor fulfilment states; cancellation rules | +| Returns and refunds | Customer return requests per VendorOrder line; vendor decision; platform arbitration; partial and full refunds; refunds tracked per VendorOrder | +| Disputes | Ingest provider disputes (test mode), hold vendor funds, record evidence, record the outcome | +| Finance | Double-entry ledger, frozen commission, vendor balances (pending, available, held, receivable), statement, weekly simulated payouts, reconciliation | +| Promotions | Platform-run coupon codes and automatic discounts (percentage or fixed, minimum spend, category or vendor targeting, usage limits, date window) | +| Loyalty and referrals | Marketplace-wide points earned on completed vendor orders and redeemed at checkout; referral codes (referee discount, referrer points) | +| Reviews | Verified-purchase product reviews (rating + text), one per purchased line, vendor reply, platform moderation | +| Wishlist and store follow | Customer wishlist; follow vendor stores; followed-store feed on the account page | +| Notifications | Database (in-app) and email for order, return, refund, payout and application events | +| Search | Full-text search with facets: category, price, vendor, rating, in stock. MySQL-backed (ADR-0019) | +| i18n | `en` (primary), `ar` (RTL), `ms` for all UI. Platform taxonomy translated. Vendor-entered content is single-language. | +| Demo | Reproducible seeded environment with role logins | + +### 6.2 Out of scope for v2 (with reason) + +| Excluded | Reason | +|---|---| +| Guest checkout | Account-required checkout removes a class of ownership bugs; the cart still works for guests (decision P-1, below) | +| Real vendor payouts / Stripe Connect | D-6; simulated payouts only | +| Tax calculation and invoices | ADR-0007; prices are tax-inclusive | +| Multi-currency | ADR-0007 | +| Carrier integrations and shipping labels | Manual tracking entry | +| Vendor-funded promotions | Platform-funded only, which keeps the finance model tight | +| Subscriptions, gift cards, digital downloads, bundles, flash-sale engine, A/B experiments, chat, Q&A, waitlist, abandoned-cart email, AI recommendations | Legacy experimental features; not needed for the showcase objectives | +| Public REST API and mobile apps | Inertia is the only client (ADR-0018) | +| Realtime push (WebSockets) | Polling where needed. Reverb is deferred. | +| Multiple vendors per user | ADR-0011 | +| Translation of vendor-entered content | Single-language listing content | + +### 6.3 Product decisions proposed in this PRD (need owner confirmation) + +| ID | Proposal | Rationale | +|---|---|---| +| P-1 | Checkout requires an account (inline sign-up on the checkout page); guest browsing and cart allowed | Removes guest-ownership bugs; simpler order access | +| P-2 | Vendor products publish immediately once the vendor is approved; platform moderation is reactive (block or unblock) | Pre-moderation slows demos; reactive moderation still demonstrates control | +| P-3 | Return window of 7 days after delivery; auto-deliver 10 days after shipping when not confirmed | No carrier integration exists | +| P-4 | Reservation TTL of 15 minutes, with one extension while payment is processing | Brief §11 | +| P-5 | Points: earn 1 point per RM 1 of goods on **completed** vendor orders; 100 points = RM 1; redeem up to 20 % of goods subtotal | Award after the return window avoids most clawbacks | +| P-6 | Referral: referee gets RM 10 off a first order over RM 50; referrer gets 500 points when that order completes | Simple and demonstrable | +| P-7 | Default commission 10 %, with category and vendor overrides; minimum payout RM 50; weekly payout run (Monday 09:00 MYT) | Plausible marketplace parameters | +| P-8 | Customer may cancel a VendorOrder only while `awaiting_fulfilment`; afterwards they use a return | Standard practice | + +## 7. Success criteria for v2 (release gate) + +1. A customer buys products from **three vendors** in one checkout. They see one order with three vendor shipments, tracked independently. +2. Each vendor fulfils independently. One vendor cancels; the customer is refunded for that vendor only, and the other two are unaffected. +3. A customer returns one line from one vendor. The partial refund reverses exactly that line's goods, commission and points, and the vendor statement shows it. +4. A disputed payment holds the affected vendors' funds and resolves according to the dispute rules. +5. A weekly payout run produces simulated payouts that equal each vendor's available balance, and the ledger reconciles. +6. Every scenario in `lifecycles/` passes on MySQL in CI, including the parallel concurrency tests. +7. Every page works in `en`, `ar` (RTL) and `ms`; WCAG 2.2 AA checks pass on the key journeys. +8. The demo rebuilds from an empty database with one command, and the seeded data passes the ledger invariants. + +## 8. Assumptions and constraints + +- Single developer; phase-gated delivery per the brief. +- Demo scale: about 12 vendors, up to 1,000 products, up to 5,000 seeded orders. Architecture is not designed beyond 10× that without a new ADR. +- Stripe test mode is available for MYR card payments, including 3-D Secure test cards. This is an assumption to verify in Phase 5 (D-14). +- All content (names, addresses, images) is fictional or licensed. + +## 9. Open questions + +- P-1 to P-8 above. +- D-9, D-10, D-11 in ADR-0016. +- D-15: commit author identity before the repository is public. diff --git a/docs/product/scope.md b/docs/product/scope.md index fa58eff..b2f4f70 100644 --- a/docs/product/scope.md +++ b/docs/product/scope.md @@ -1,5 +1,7 @@ # Scope +> **Superseded 2026-11-01** by [ADR-0007](../architecture/adr/0007-product-positioning-currency-and-locales.md) (owner decision D-4) and the Nexo 2.0 product documents: [product-requirements.md](../product/product-requirements.md), [functional-requirements.md](../product/functional-requirements.md). Kept for history only; do not implement from this file. + **Status:** Ratified 2026-08-30 Every feature area is tagged so a new developer knows what is load-bearing. diff --git a/docs/product/user-journeys.md b/docs/product/user-journeys.md new file mode 100644 index 0000000..86cc2f9 --- /dev/null +++ b/docs/product/user-journeys.md @@ -0,0 +1,153 @@ +# User Journeys and Workflows + +- **Status:** Draft for owner review (Phase 1) +- **Date:** 2026-11-01 + +Each journey lists its steps, the requirements it exercises, and the failure branches the design must handle. The journeys drive the Phase 2 screen inventory and the Phase 9 end-to-end tests. + +--- + +## Customer journeys + +### CJ-1 Discover and buy from several vendors (happy path) + +1. Visitor lands on home (featured categories, curated collections, stores to follow) → category page → applies facets. +2. Opens a product page and selects variant options: + - price and stock update per variant; + - the vendor card links to the store page. +3. Adds three items from three vendors. The cart groups lines by vendor and shows per-vendor subtotals and shipping estimates. +4. Proceeds to checkout and is asked to sign in or create an account. The guest cart is merged (FR-CART-02). +5. Selects an address. Shipping is quoted per vendor, then the customer applies a coupon and redeems points. The summary shows goods, shipping per vendor, discount, points and total. +6. Places the order: + - the stock is reserved; + - the payment form shows a timer for the reservation; + - the customer pays with a test card and completes 3-D Secure. +7. Sees "Confirming your payment…", then the confirmation page: group reference and three vendor shipments. +8. Receives one confirmation email. Each vendor gets a new-order notification. + +**Requirements:** FR-SRCH-01, FR-CAT-05, FR-CART-01…04, FR-CHK-01…10. + +**Failure branches:** + +| Branch | Expected behaviour | +|---|---| +| Price changed between cart and placement | 409; review the new total | +| Line out of stock at placement | Order not placed; short lines named | +| Card declined | Retry with another card within TTL | +| Provider unavailable | Retryable error; nothing charged | +| Browser closed after paying | Order still marked paid by webhook; email sent | +| Reservation expired before paying | Group expired; the customer can return to the cart, which is kept | +| Double-click on "Place order" | One order | + +### CJ-2 Track and receive + +1. Account → Orders. Each group expands into vendor shipments, each with a timeline: paid → processing → shipped (carrier + tracking) → delivered. +2. The customer confirms delivery, or it auto-confirms after 10 days. + +### CJ-3 Cancel before shipping + +1. On a vendor shipment in `awaiting_fulfilment`, the customer chooses Cancel and gives a reason. +2. That VendorOrder is cancelled: + - a full refund for that vendor's portion (goods + shipping, minus nothing); + - points and discount allocation for those lines are returned. +3. The other shipments are unchanged. + +**Failure branch:** the vendor moved it to `processing` at the same moment. The customer sees "This shipment is already being prepared; request a return after delivery." (409). + +### CJ-4 Return and partial refund + +1. Within 7 days of delivery, the customer requests a return of 1 of 2 units on one line, with a reason and photos. +2. The vendor approves and the customer ships the item back (out of band). +3. The vendor marks it received and resellable. The refund is created for exactly that unit's goods value, net of its allocated discount. +4. The customer sees the refund go `submitted` → `succeeded`. Points for that unit are adjusted. + +**Branches:** + +| Branch | Expected behaviour | +|---|---| +| Vendor rejects | Customer can escalate; Platform Operations arbitrates | +| Vendor silent for 3 days | Auto-escalation | +| Provider refund timeout | Status stays "Processing refund", resolved by webhook or reconciliation | + +### CJ-5 Review + +After delivery, the customer reviews a purchased line (rating, text, photos). The vendor replies. Platform Operations may hide the review. + +### CJ-6 Engage + +- Wishlist add and remove. +- Follow a store; see new products from followed stores. +- Share a referral code; the friend gets a discount; the customer earns points when the friend's order completes. + +--- + +## Vendor journeys + +### VJ-1 Apply and get approved + +1. A signed-in user selects "Sell on Nexo" and submits an application. +2. Platform Operations reviews it and approves: + - a Vendor is created as `pending` with the applicant as Owner; + - an operator then approves the vendor (`approved`). +3. The Owner is notified, opens `/vendor` and completes the store profile and shipping rates. A checklist shows readiness: profile, shipping rates for at least one zone, at least one active product. + +**Branch:** rejected application, with reason; the applicant may apply again after 30 days. + +### VJ-2 Build the catalogue + +1. Create a product: category, details and images. +2. Define options (Colour × Size), which generates the variant matrix. Set SKU, price and stock per variant. +3. Activate the product. It appears in search within about 1 minute. +4. Optionally run a CSV import: dry-run report, then confirm. + +### VJ-3 Fulfil an order + +1. The vendor receives a notification. The orders list shows filters by status. +2. Opens the VendorOrder: items, shipping address, customer name and phone. +3. Moves it to `processing` (packing), then `shipped` with carrier and tracking. +4. Later the order becomes `delivered`, then `completed`. The earnings change from pending to available. + +**Branches:** + +| Branch | Expected behaviour | +|---|---| +| Item cannot be supplied | Vendor cancels with reason; automatic full refund for this VendorOrder; stock returned | +| Concurrent edit by two team members | One transition wins; the other sees "Status changed by " (409) | + +### VJ-4 Manage stock + +- Receive stock (delta) or do a stock count (absolute). +- A conflicting count gets 409 with the current figure. +- Reserved quantity is visible, with the explanation "held in unpaid checkouts". + +### VJ-5 Handle a return + +Approve or reject a return, then mark the items received (resellable or damaged). The refund is triggered automatically. The vendor statement shows the reversal line, with commission reversed. + +### VJ-6 Earnings and payouts + +1. The Finance role opens Earnings and sees balances: pending, available, held, receivable, paid out. +2. The statement lists every ledger line, linked to orders, refunds and disputes. It can be exported as CSV. +3. The Monday payout run creates a simulated payout, labelled "Simulated (demo)". + +### VJ-7 Manage the team + +The Owner invites a fulfilment member. The member accepts and sees only the permitted menus. The Owner deactivates the member, whose access ends on their next request. + +--- + +## Platform staff journeys (Filament) + +| ID | Journey | Steps | +|---|---|---| +| PJ-1 | Vendor applications | Queue → review → approve or reject → approve vendor → (later) suspend with reason → reactivate | +| PJ-2 | Catalogue moderation | Report or queue → block product with reason → vendor notified → unblock | +| PJ-3 | Return arbitration | Escalated returns queue → view evidence → uphold or override → refund triggered | +| PJ-4 | Dispute | Dispute appears (provider event) → funds held for affected vendors → upload evidence (simulated) → record outcome (won or lost) → ledger postings | +| PJ-5 | Refund failure | Failed refund queue → inspect provider error → retry (same idempotency key) or mark resolved with note | +| PJ-6 | Reconciliation | Nightly report → exceptions list → investigate (links to attempt, event, ledger) → resolve with note (audit) | +| PJ-7 | Payout run | View the upcoming run → trigger manually (idempotency key) or wait for the schedule → payouts list | +| PJ-8 | Commission | Change default, category or vendor rate → effective for new orders only → audit entry | +| PJ-9 | Promotions | Create coupon → limits and targeting → monitor redemptions | +| PJ-10 | Support | Look up an order by reference, email or payment id → impersonate the customer or vendor member (audited, no money actions) | +| PJ-11 | Health | Outbox backlog, dead letters (retry), failed jobs, last webhook, sweeper heartbeat | diff --git a/docs/product/vision.md b/docs/product/vision.md index 64a66e9..150fb29 100644 --- a/docs/product/vision.md +++ b/docs/product/vision.md @@ -1,5 +1,7 @@ # Product Vision +> **Superseded 2026-11-01** by [ADR-0007](../architecture/adr/0007-product-positioning-currency-and-locales.md) (owner decision D-4) and the Nexo 2.0 product documents: [product-requirements.md](../product/product-requirements.md), [functional-requirements.md](../product/functional-requirements.md). Kept for history only; do not implement from this file. + **Status:** Ratified 2026-08-30 · Owner: Platform Operator **Supersedes:** the "generic multi-tenant e-commerce SaaS" framing used in `README.md` and `docs/COMPREHENSIVE_CODEBASE_GUIDE.md` prior to 2026-08-30. From 3fc43e855e30471f3922328716ab3852c55566b4 Mon Sep 17 00:00:00 2001 From: AdilAzhariOmsan Date: Tue, 3 Nov 2026 20:53:15 +0800 Subject: [PATCH 3/5] docs(adr): record ADR-0006 to ADR-0021 and update the ADR template --- docs/architecture/adr/0006-adr-process-v2.md | 77 +++++++++ ...roduct-positioning-currency-and-locales.md | 103 ++++++++++++ .../adr/0008-repository-and-frontend-stack.md | 98 +++++++++++ .../0009-runtime-and-persistence-baseline.md | 92 ++++++++++ ...0-modular-monolith-and-dependency-rules.md | 116 +++++++++++++ ...011-identity-roles-and-vendor-isolation.md | 112 ++++++++++++ .../adr/0012-order-group-and-vendor-orders.md | 126 ++++++++++++++ ...013-money-representation-and-allocation.md | 102 +++++++++++ .../adr/0014-inventory-reservations.md | 130 ++++++++++++++ ...0015-checkout-and-payment-orchestration.md | 159 ++++++++++++++++++ ...-financial-ledger-and-simulated-payouts.md | 141 ++++++++++++++++ ...-domain-events-and-transactional-outbox.md | 93 ++++++++++ .../adr/0018-backend-frontend-contracts.md | 105 ++++++++++++ .../adr/0019-search-behind-a-port.md | 85 ++++++++++ ...020-platform-administration-in-filament.md | 72 ++++++++ ...ents-demo-data-and-production-isolation.md | 88 ++++++++++ docs/architecture/adr/ADR-TEMPLATE.md | 59 ++++--- 17 files changed, 1731 insertions(+), 27 deletions(-) create mode 100644 docs/architecture/adr/0006-adr-process-v2.md create mode 100644 docs/architecture/adr/0007-product-positioning-currency-and-locales.md create mode 100644 docs/architecture/adr/0008-repository-and-frontend-stack.md create mode 100644 docs/architecture/adr/0009-runtime-and-persistence-baseline.md create mode 100644 docs/architecture/adr/0010-modular-monolith-and-dependency-rules.md create mode 100644 docs/architecture/adr/0011-identity-roles-and-vendor-isolation.md create mode 100644 docs/architecture/adr/0012-order-group-and-vendor-orders.md create mode 100644 docs/architecture/adr/0013-money-representation-and-allocation.md create mode 100644 docs/architecture/adr/0014-inventory-reservations.md create mode 100644 docs/architecture/adr/0015-checkout-and-payment-orchestration.md create mode 100644 docs/architecture/adr/0016-financial-ledger-and-simulated-payouts.md create mode 100644 docs/architecture/adr/0017-domain-events-and-transactional-outbox.md create mode 100644 docs/architecture/adr/0018-backend-frontend-contracts.md create mode 100644 docs/architecture/adr/0019-search-behind-a-port.md create mode 100644 docs/architecture/adr/0020-platform-administration-in-filament.md create mode 100644 docs/architecture/adr/0021-environments-demo-data-and-production-isolation.md diff --git a/docs/architecture/adr/0006-adr-process-v2.md b/docs/architecture/adr/0006-adr-process-v2.md new file mode 100644 index 0000000..0eb6abe --- /dev/null +++ b/docs/architecture/adr/0006-adr-process-v2.md @@ -0,0 +1,77 @@ +# ADR-0006: Adopt the Nexo 2.0 ADR template and status model + +- **Number:** 0006 +- **Status:** Accepted (settled by the Nexo 2.0 brief §4.3 and §18) +- **Date:** 2026-11-01 +- **Decision owner:** Project owner +- **Supersedes:** the template section of ADR-0001 (its "when to write one" rules stay in force) + +--- + +## Context + +ADR-0001 introduced ADRs with a four-section template: Context, Decision, Alternatives, Consequences. The Nexo 2.0 brief requires more fields: + +- problem; +- decision drivers; +- considered alternatives; +- positive and negative consequences; +- risks; +- testing implications; +- migration implications. + +It also requires the statuses Proposed, Accepted, Rejected, Deprecated and Superseded. + +Two template files currently exist: the one inside ADR-0001 and `ADR-TEMPLATE.md`. Filenames are inconsistent; `ADR-0005-vendor-naming.md` breaks the `NNNN-kebab-title.md` rule. + +## Problem + +Which template, statuses and location do Nexo 2.0 ADRs use? + +## Decision drivers + +- Compliance with the brief. +- One template, not two. +- Keep the existing repository convention, so no churn for its own sake. + +## Considered alternatives + +### A. Move ADRs to `docs/decisions/ADR-NNNN-*.md` (the brief's example layout) + +Rejected. The brief says to adapt to existing repository conventions. `docs/architecture/adr/` already holds ADR-0001 to ADR-0005, and moving them breaks links for no gain. + +### B. Keep the location; adopt the brief's fields in `ADR-TEMPLATE.md` (chosen) + +## Decision + +1. ADRs stay in `docs/architecture/adr/`, named `NNNN-short-kebab-title.md` and numbered sequentially. +2. `ADR-TEMPLATE.md` is the single template. It contains every field the brief requires. +3. Statuses are: Proposed, Accepted, Rejected, Deprecated, and Superseded by ADR-XXXX. +4. **Proposed ADRs are not implemented.** An ADR moves to Accepted only on owner approval, and the approval date is recorded in the status line. +5. After acceptance, only the status line changes. A changed decision needs a new ADR. +6. The ADR index in `docs/architecture/README.md` is updated in the same change as any ADR. + +## Positive consequences + +- Every significant decision records its testing and migration implications, which feed the Phase 3–5 test plans directly. + +## Negative consequences + +- Longer ADRs. This is mitigated by keeping one decision per ADR. + +## Risks + +- ADRs drift from the code. Mitigation: the definition of done requires ADR references in pull requests that touch a decided boundary. + +## Testing implications + +None directly. + +## Migration implications + +- ADR-0001 to ADR-0005 keep their original format; they are not rewritten. +- `ADR-0005-vendor-naming.md` should be renamed to `0005-vendor-naming.md` when ADR-0005 is accepted (owner action listed in the Phase 1 review). + +## Related + +- ADR-0001; the brief §4.3 and §18. diff --git a/docs/architecture/adr/0007-product-positioning-currency-and-locales.md b/docs/architecture/adr/0007-product-positioning-currency-and-locales.md new file mode 100644 index 0000000..1dafc12 --- /dev/null +++ b/docs/architecture/adr/0007-product-positioning-currency-and-locales.md @@ -0,0 +1,103 @@ +# ADR-0007: Position Nexo as a portfolio-grade general marketplace in MYR, English-primary + +- **Number:** 0007 +- **Status:** Accepted, by owner decisions D-4 and D-5 (2026-11-01) +- **Date:** 2026-11-01 +- **Decision owner:** Project owner +- **Supersedes:** the product framing in `docs/product/vision.md`, `scope.md`, `goals.md` and `personas.md` (ratified 2026-08-30), and `docs/architecture/marketplace-migration.md` + +--- + +## Context + +The existing product documents describe **aljebal-albeedos**: + +- a Sudan-market, Arabic-first marketplace; +- settlement in SDG; +- Stripe Connect transfers paying each vendor; +- a migration plan for an existing live SaaS database. + +The Nexo 2.0 brief describes something different: + +- **Nexo**, a premium multi-vendor general-merchandise marketplace; +- built as a portfolio showcase that must feel like a credible commercial product; +- on a fresh database, with no connection to the old production data. + +Phase 0 found (R2) that the Sudan + Stripe pairing was not viable as a real payment design. Stripe does not operate in Sudan, which is an assumption based on Stripe's published country list as of mid-2026. + +## Problem + +What product, market, currency and locale set do the Phase 1 specifications assume? + +## Decision drivers + +- Brief §2: the purpose is portfolio credibility for recruiters and engineers. +- A payment provider that genuinely supports the currency in test mode. +- A real RTL locale, to demonstrate internationalisation (brief §16). +- One currency, to keep the financial model provable. + +## Considered alternatives + +### A. Keep aljebal-albeedos, SDG, Arabic-primary + +Rejected (D-4). This contradicts the brief's purpose, and SDG has no Stripe support. + +### B. USD, English-primary + +This was a viable option. Owner chose MYR instead. + +### C. MYR, English-primary, with Arabic (RTL) and Malay as full peers (chosen) + +- Stripe supports MYR, including card payments in test mode. +- Malay is a natural second locale. +- Arabic demonstrates genuine RTL. + +### D. Multi-currency display + +Rejected. It needs an FX ADR and complicates the ledger and refunds, with no showcase benefit proportional to the cost. + +## Decision + +1. **Product:** Nexo, a single unified multi-vendor general-merchandise marketplace. It has three surfaces: customer storefront, vendor dashboard and platform administration. +2. **Settlement and display currency: MYR only.** + - Amounts are stored as integer sen (see ADR-0013). + - Every money column carries an explicit `currency` value, so a second currency later is an additive change. +3. **Locales:** + - `en` is primary and the fallback. + - `ar` (full RTL) and `ms` are full peers. Every UI string ships in all three before a feature is done. +4. **Prices are tax-inclusive.** Nexo v2 does not calculate, itemise or remit tax. + - This is a stated product limitation, not an oversight. + - A tax engine needs its own ADR. +5. **Market framing for demo content: Malaysia.** + - Fictional vendors and customers. + - Malaysian addresses and shipping zones: Peninsular, Sabah/Sarawak/Labuan. +6. The previous product documents stay in the repository marked **Superseded** for history. + +## Positive consequences + +- The payment design can be exercised end to end in Stripe test mode. +- One currency keeps allocation, rounding and reconciliation exact and testable. + +## Negative consequences + +- Arabic users see prices in MYR. This is acceptable for a showcase. +- No tax handling. This is visible to a knowledgeable reviewer, so it is stated in the README limitations. + +## Risks + +- Malaysian regulations (PDPA, SST registration) are out of scope. All demo data is fictional. The README must say Nexo is not a production service. + +## Testing implications + +- Locale coverage tests: every translation key exists in `en`, `ar` and `ms`. +- RTL visual checks in Phase 9. +- Money formatting tests per locale. + +## Migration implications + +- Product documents are superseded. New `product-requirements.md` and `functional-requirements.md` replace them. +- The REQ-VENDOR-* identifiers are carried into the new functional specification. + +## Related + +- ADR-0013 (money); `docs/product/product-requirements.md`; Phase 0 report §A.8. diff --git a/docs/architecture/adr/0008-repository-and-frontend-stack.md b/docs/architecture/adr/0008-repository-and-frontend-stack.md new file mode 100644 index 0000000..0f0aa0c --- /dev/null +++ b/docs/architecture/adr/0008-repository-and-frontend-stack.md @@ -0,0 +1,98 @@ +# ADR-0008: Build Nexo 2.0 in this repository with Vue 3, Inertia and TypeScript + +- **Number:** 0008 +- **Status:** Accepted, by owner decisions D-1 and D-2 (2026-11-01) +- **Date:** 2026-11-01 +- **Decision owner:** Project owner + +--- + +## Context + +Phase 0 found two candidate foundations. + +**The old repository `AdilAzhari/nexo-ecommerce`:** + +- Laravel 12, Vue 3, Inertia 2, Filament. +- Extensive, but with critical defects in payments, inventory and vendor authorisation. +- Its CI/CD is connected to a production database. + +**This repository `Sentients-AI/Nexo`:** + +- A clean Laravel 13 / PHP 8.5 shell: Pest 5, Larastan, Pint, arch tests and an engineering handbook. +- Branch `feat/vendor-frontend` (61 commits) holds a tested React 19 vendor workspace. +- `main` contains no frontend framework. + +The brief's technology baseline names Vue and Inertia. + +## Problem + +Which repository is the foundation, and which frontend framework does v2 use? + +## Decision drivers + +- A clean history and no inherited production coupling. +- Alignment with the brief's baseline. +- Reuse of the highest-quality existing work, where it does not compromise the above. + +## Considered alternatives + +### A. Continue in the old repository on an orphan branch + +Rejected (D-2). It inherits deploy pipelines wired to production and a confusing history. + +### B. Use this repository with React, reusing `feat/vendor-frontend` + +Rejected (D-1). The owner prefers the brief's Vue baseline. + +### C. Use this repository with Vue 3 + Inertia + TypeScript, keeping the React work as reference (chosen) + +## Decision + +1. `Sentients-AI/Nexo` is the Nexo 2.0 repository. The old repository is a read-only historical reference. +2. v2 starts from `main`, the Laravel 13 shell. `feat/vendor-frontend` is not merged. +3. The frontend is **Vue 3 (Composition API, `