Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ built, operated, and evolved.
|-----------------|---------------------------------------------------------------------|
| `architecture/` | Architecture, boundaries, invariants, and ADRs |
| `development/` | Local development and implementation guidance |
| `discovery/` | Phase reports: Phase 0 assessment, Phase 1 review |
| `engineering/` | Engineering process and standards — testing, quality, workflow |
| `operations/` | Deployment, infrastructure, monitoring, and incident procedures |
| `product/` | Product requirements, scope, personas, goals, and product decisions |
Expand All @@ -18,10 +19,14 @@ built, operated, and evolved.
- New contributor? The repository [`../README.md`](../README.md) covers setup.
- Building a change? [`engineering/definition-of-done.md`](engineering/definition-of-done.md)
and [`engineering/git-workflow.md`](engineering/git-workflow.md).
- Understanding the system? [`product/vision.md`](product/vision.md),
- Understanding the product? [`product/product-requirements.md`](product/product-requirements.md)
and [`product/user-journeys.md`](product/user-journeys.md).
- Understanding the system? [`architecture/system-overview.md`](architecture/system-overview.md),
[`architecture/domain-context-map.md`](architecture/domain-context-map.md),
[`architecture/INVARIANTS.md`](architecture/INVARIANTS.md), and the ADRs in
[`architecture/adr/`](architecture/adr/).
- Where the marketplace is headed? [`architecture/marketplace-migration.md`](architecture/marketplace-migration.md).
- How Nexo 2.0 got here? [`discovery/`](discovery/): the Phase 0 assessment and the
Phase 1 review.

## Documentation Principle

Expand Down
173 changes: 130 additions & 43 deletions docs/architecture/INVARIANTS.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,172 @@
# System Invariants

These are properties that must remain true regardless of implementation details.
- **Status:** Revised 2026-11-01 (Phase 1 draft for owner review). This replaces the 2026-08-30 list. Every earlier invariant is kept, reworded or strengthened; the changes are listed at the end.

They represent architectural constraints rather than feature requirements.
These properties must hold regardless of implementation. Each one names its **enforcement**, meaning the mechanism that makes a violation impossible or detectable, and its **proof**, meaning the test or check that demonstrates it. An invariant without both is a wish, not an invariant.

Requirement documents cite these as **[INV-n]**.

---

## Vendor Isolation
## Access and identity

Vendor-context queries must only expose records belonging to the
authenticated vendor.
**INV-1 · Vendor isolation.** A vendor context can read or change only records owned by its vendor. Storefront catalogue queries are intentionally not vendor-scoped.

Storefront catalog queries are intentionally not vendor-scoped.
- **Enforcement:** the `vendor.context` middleware fails closed; there is no global scope; route bindings are scoped; vendor queries require an explicit `vendorId`.
- **Proof:** the isolation suite (merge gate), plus an arch test that every vendor controller is covered (ADR-0011).

---
**INV-2 · Identity.**

## Marketplace Identity
- A customer identity is marketplace-wide.
- A user is a member of at most one vendor.
- Platform staff belong to no vendor and place no orders.
- **Enforcement:** unique `vendor_members.user_id`, and application guards.
- **Proof:** domain tests.

A shopper has one marketplace-wide identity.
## Orders

A vendor-team user belongs to exactly one vendor.
**INV-3 · Order structure.**

A platform operator does not belong to a vendor.
- An OrderGroup is one customer checkout.
- A VendorOrder is exactly one vendor's portion, and all of its lines belong to that vendor.
- The group total equals the sum of its VendorOrder totals at creation.
- **Enforcement:** the checkout use case, plus a DB foreign key from line → variant → vendor that is checked at creation.
- **Proof:** property test over random carts.

---
**INV-4 · Refund isolation.** A refund, cancellation or dispute outcome on one VendorOrder never changes the state or ledger entries of a sibling VendorOrder.

## Order Structure
- **Proof:** sibling-unchanged assertions in every refund and cancellation test.

An `OrderGroup` represents one shopper checkout.
**INV-5 · Refund cap.** For each VendorOrder, the sum of refunds in `requested | approved | submitted | succeeded` never exceeds the captured amount allocated to it. Per line, refunds never exceed the line's net amount.

An `Order` represents exactly one vendor's portion of that checkout.
- **Enforcement:** guarded counter updates on `vendor_orders.refund_committed_minor` and `vendor_order_lines.refund_committed_qty` (ADR-0015 §9).
- **Proof:** concurrent refund test on MySQL.

An order must never contain products belonging to multiple vendors.
**INV-6 · Snapshots and frozen rates.** Line prices, names and options, allocated discounts and points, shipping amounts, and the commission rate are fixed when a VendorOrder is created. Later catalogue, price or commission-configuration changes never alter existing orders.

---
- **Proof:** mutation-after-order tests.

## Payment Ownership
**INV-14 · Guarded transitions.** OrderGroup, VendorOrder, ReturnRequest, Refund, PaymentAttempt, Reservation and Vendor statuses change only through their state machine. Each change uses a conditional update on the current status, so concurrent transitions cannot both succeed.

An `OrderGroup` has exactly one PaymentIntent for its total.
- **Proof:** an arch test (no status assignment outside state machines), plus concurrent transition tests.

---
## Inventory

## Refund Isolation
**INV-7 · Stock.** For every variant:

A refund against one vendor's `Order` must never alter the financial state
of another vendor's `Order`.
- `0 ≤ reserved ≤ on_hand`;
- `reserved = Σ qty of active reservations`;
- `on_hand = Σ delta_on_hand of movements`.

---
Stock is never oversold.

## Commission
- **Enforcement:** DB CHECK constraint, conditional updates, and the unique movement key.
- **Proof:** parallel-process tests and the nightly reconciliation check (ADR-0014).

The commission rate applied to an order is frozen at order creation.
## Money

Changing the platform commission configuration must not alter historical orders.
**INV-8 · Exact allocation.** Every split (discounts, points value, commission, dispute amounts) sums exactly to its source amount, in integer minor units.

---
- **Proof:** property tests (ADR-0013).

## Inventory
**INV-9 · Idempotency.**

Inventory must never be oversold.
- One OrderGroup per (customer, checkout key).
- One effect per provider event id.
- At most one active payment attempt per group.
- At most one succeeded attempt per group.
- At most one ledger transaction per (type, reference).
- At most one consumption per inbox (consumer, message).
- **Enforcement:** unique keys.
- **Proof:** concurrent duplicate tests.

Stock reservation and release operations must be concurrency-safe.
**INV-10 · Ledger.**

---
- Every ledger transaction balances (Σ debits = Σ credits, single currency).
- Entries are append-only.
- Balances are derived from entries.
- **Proof:** the posting-service assertion, the nightly check, and property tests (ADR-0016).

## Vendor Visibility
**INV-11 · Paid means captured.** An OrderGroup is `paid` if and only if exactly one PaymentAttempt has `succeeded`, with amount and currency equal to the group total. A provider event with a mismatched amount never marks a group paid.

A vendor's products are sellable only when the vendor satisfies the required
marketplace approval and payment-onboarding conditions.
- **Proof:** webhook processing tests plus nightly reconciliation.

---
**INV-16 · Payout safety.**

## Auditability
- A payout never exceeds the vendor's available balance net of receivable.
- Held funds are never paid out.
- Each payout run pays each vendor at most once.
- **Proof:** concurrent payout-run test.

Money-moving operations and material vendor-state changes must produce an
auditable record.
**INV-17 · Points.**

---
- A customer's spendable points balance equals the sum of their points-ledger entries and is never negative.
- Redemptions are held, then confirmed or released, exactly once.

**INV-18 · Promotion limits.** Confirmed plus pending redemptions never exceed a promotion's total or per-customer limit.

- **Enforcement:** a conditional update on the usage counter.
- **Proof:** parallel test on the last allowed use.

## Catalogue and vendors

## Documentation
**INV-12 · Sellability.** A product is visible in the storefront and purchasable only if all of these hold:

Boundary-shaping architectural decisions must have an ADR.
- product `active`;
- not blocked;
- vendor `approved`;
- at least one active variant.

One implementation is used by the storefront, search indexing and checkout.

- **Proof:** a sellability matrix test across all three paths.

**INV-13 · Vendor lifecycle.**

- Vendor status is always one of `pending | approved | suspended`, and changes only through defined transitions.
- Vendors are never physically deleted.
- **Enforcement:** DB CHECK constraint, the state machine, and `RESTRICT` foreign keys.

## Platform

**INV-15 · Events if and only if commit.** An integration event is published if and only if the transaction that produced it commits. Consumers tolerate duplicates and reordering.

- **Enforcement:** the outbox and inbox (ADR-0017).
- **Proof:** rollback and duplicate tests.

**INV-19 · Auditability.** These actions write an audit record (actor, impersonator if any, action, subject, before/after, IP, timestamp) in the same transaction as the change:

- money-moving actions;
- vendor lifecycle changes;
- moderation;
- commission changes;
- staff changes;
- reconciliation resolutions;
- impersonation.

**INV-20 · Environment isolation.**

- No Nexo 2.0 environment holds legacy production credentials.
- No environment accepts live payment keys.
- Tests run only against databases whose names end in `_testing`.
- **Enforcement:** boot checks and the test bootstrap guard (ADR-0009, ADR-0021).

**INV-21 · Documentation.** Boundary-shaping decisions have an ADR. Scope changes update the product documents in the same change.

---

Changes to product scope must update the corresponding product documentation.
## Changes from the 2026-08-30 version

| Earlier invariant | Now |
|---|---|
| Vendor Isolation | INV-1, with enforcement made explicit |
| Marketplace Identity | INV-2, adding "staff place no orders" |
| Order Structure | INV-3 |
| Payment Ownership ("exactly one PaymentIntent for its total") | INV-9 / INV-11: retries can create several attempts over time; at most one active and at most one succeeded |
| Refund Isolation | INV-4, plus the new refund cap INV-5 |
| Commission (frozen at creation) | INV-6, extended to every snapshot |
| Inventory | INV-7, made quantitative |
| Vendor Visibility ("approval and payment-onboarding conditions") | INV-12; payment onboarding removed (no Stripe Connect, D-6) |
| Auditability | INV-19, with an explicit list |
| Documentation | INV-21 |
| — | New: INV-8, 10, 13–18, 20 |
60 changes: 40 additions & 20 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
# Architecture Documentation

This directory records the architectural boundaries, invariants, and decisions
that shape Nexo.
This directory records Nexo's architectural boundaries, invariants and decisions.

## Contents

| Path | Purpose |
|-----------------------------|--------------------------------------------------------------------|
| `INVARIANTS.md` | Properties that must hold regardless of implementation |
| `marketplace-migration.md` | The plan for pivoting from multi-tenant SaaS to a single marketplace |
| `adr/` | Architecture Decision Records |
| Path | Purpose |
|---|---|
| [`system-overview.md`](system-overview.md) | Runtime components, code organisation, key flows |
| [`domain-context-map.md`](domain-context-map.md) | Modules (bounded contexts), owned data, relationships, glossary |
| [`dependency-rules.md`](dependency-rules.md) | Allowed and forbidden dependencies, enforced by arch tests |
| [`database-design.md`](database-design.md) | Initial database model, constraints and table ownership |
| [`INVARIANTS.md`](INVARIANTS.md) | Business invariants [INV-n], each with enforcement and proof |
| [`security-model.md`](security-model.md) | Roles, permission matrix, isolation and enforcement points |
| [`integration-contracts.md`](integration-contracts.md) | Browser ↔ Laravel, module ↔ module, and Nexo ↔ Stripe contracts |
| [`financial-model.md`](financial-model.md) | Ledger, commission, refunds, disputes, payouts, reconciliation, worked examples |
| [`lifecycles/`](lifecycles/) | Checkout and payment, inventory, orders/fulfilment/returns: state machines and failure-scenario matrices |
| [`vendor-domain-design.md`](vendor-domain-design.md) | Vendor entity design (2026-09), still current |
| [`marketplace-migration.md`](marketplace-migration.md) | *Superseded:* SaaS-to-marketplace migration plan |
| [`adr/`](adr/) | Architecture Decision Records |

## Architecture Decision Records

An ADR captures **one** boundary-shaping decision: why it was made, what was
rejected, and what must now be lived with. See
[`adr/0001-adr-template-and-process.md`](adr/0001-adr-template-and-process.md)
for when to write one and the template to use.
The template is [`adr/ADR-TEMPLATE.md`](adr/ADR-TEMPLATE.md) (ADR-0006). Proposed ADRs are not implemented until the owner accepts them.

| ADR | Decision |
|-----|----------|
| [0001](adr/0001-adr-template-and-process.md) | ADR template and process |
| [0002](adr/0002-vendor-is-a-tenant-record.md) | A "vendor" is the existing tenant record, reframed |
| [0003](adr/0003-context-dependent-vendor-scoping.md) | Context-dependent vendor scoping (retire the blanket global scope) |
| [0004](adr/0004-order-group-and-per-vendor-orders.md) | One basket, one payment, one Order per vendor |
| ADR | Decision | Status |
|---|---|---|
| [0001](adr/0001-adr-template-and-process.md) | ADR template and process | Accepted; template superseded by 0006 |
| [0002](adr/0002-vendor-is-a-tenant-record.md) | A vendor is the existing tenant record | Accepted. **Proposed to be marked Superseded by 0005** (fresh database) |
| [0003](adr/0003-context-dependent-vendor-scoping.md) | Context-dependent vendor scoping | Accepted; refined by 0011 |
| [0004](adr/0004-order-group-and-per-vendor-orders.md) | One basket, one payment, one order per vendor | Accepted. **Superseded by 0012 on acceptance** |
| [0005](adr/ADR-0005-vendor-naming.md) | Use Vendor as the seller entity | Proposed. **Recommended: accept** (file to be renamed `0005-vendor-naming.md`) |
| [0006](adr/0006-adr-process-v2.md) | Nexo 2.0 ADR template and statuses | Accepted (brief) |
| [0007](adr/0007-product-positioning-currency-and-locales.md) | Portfolio marketplace; MYR; EN primary with AR/MS | Accepted (D-4, D-5) |
| [0008](adr/0008-repository-and-frontend-stack.md) | This repository; Vue 3 + Inertia + TS | Accepted (D-1, D-2) |
| [0009](adr/0009-runtime-and-persistence-baseline.md) | Laravel 13 / PHP 8.5; MySQL 8.4 everywhere, including tests | Proposed (D-3) |
| [0010](adr/0010-modular-monolith-and-dependency-rules.md) | Modular monolith with enforced contracts | Proposed |
| [0011](adr/0011-identity-roles-and-vendor-isolation.md) | Identity, roles and fail-closed vendor isolation | Proposed |
| [0012](adr/0012-order-group-and-vendor-orders.md) | OrderGroup + VendorOrder state machines | Proposed |
| [0013](adr/0013-money-representation-and-allocation.md) | Money: integer minor units, deterministic allocation | Proposed |
| [0014](adr/0014-inventory-reservations.md) | Inventory: on-hand/reserved, TTL reservations, movement journal | Proposed |
| [0015](adr/0015-checkout-and-payment-orchestration.md) | Checkout saga, idempotency, webhook-driven payment | Proposed |
| [0016](adr/0016-financial-ledger-and-simulated-payouts.md) | Double-entry ledger; simulated payouts | Proposed (core approved as D-6; D-9/10/11 pending) |
| [0017](adr/0017-domain-events-and-transactional-outbox.md) | Transactional outbox and idempotent consumers | Proposed |
| [0018](adr/0018-backend-frontend-contracts.md) | PHP data objects → generated TS types | Proposed |
| [0019](adr/0019-search-behind-a-port.md) | Search port, MySQL first | Proposed (D-8) |
| [0020](adr/0020-platform-administration-in-filament.md) | Filament for staff; Vue for vendors and customers | Proposed |
| [0021](adr/0021-environments-demo-data-and-production-isolation.md) | Isolated environments; reproducible demo | Proposed |

Engineering standards that do **not** shape a domain, data-isolation, money,
runtime-dependency, or deployment boundary live in
[`../engineering/`](../engineering/), not in an ADR.
Engineering standards that do **not** shape a domain, data-isolation, money, runtime-dependency or deployment boundary live in [`../engineering/`](../engineering/), not in an ADR.
Loading
Loading