Wip stage M4
This commit is contained in:
+60
-20
@@ -102,7 +102,9 @@ a framework, one of those consumers breaks.
|
||||
- Domain types and API contracts (`@sport/types`)
|
||||
- Input shape/format rules (`@sport/validation`)
|
||||
- API access (`@sport/api-client`)
|
||||
- Design-system primitives: Button, Input, Badge, Skeleton (`@sport/ui`)
|
||||
- UI infrastructure: Button, Input, Dialog, Sheet, DropdownMenu, Tabs, Badge,
|
||||
Skeleton (`@sport/ui`, shadcn/ui owned as source — see
|
||||
[ADR-0017](./adr/0017-shadcn-for-infrastructure-hand-built-for-brand.md))
|
||||
- Build configuration (`@sport/config`, `@sport/eslint-config`)
|
||||
|
||||
**Do not share** — things that only look shareable:
|
||||
@@ -110,7 +112,10 @@ a framework, one of those consumers breaks.
|
||||
- **Domain components.** `<ProductCard>` knows about sale badges, price ranges and colour
|
||||
swatches. It belongs to the storefront. The admin's product row needs status, stock and
|
||||
margin. Merging them produces a component with fourteen props and two consumers who both
|
||||
fight it.
|
||||
fight it. These live in `apps/storefront/src/components/commerce/` — hero, mega menu, site
|
||||
header, product card, product gallery, product detail, filter sheet — and are built by hand
|
||||
because they _are_ the brand. The dividing line: infrastructure comes from the registry,
|
||||
identity is written here.
|
||||
- **Business logic.** It lives in the API. A discount calculated in a shared package is a
|
||||
discount that can disagree with the invoice.
|
||||
- **App state.** Cart, auth session and filter state are app-specific. Shared stores create
|
||||
@@ -418,23 +423,27 @@ Places where the instinct to generalise should be resisted until a second real c
|
||||
|
||||
## 14. Architectural risks to prevent from day one
|
||||
|
||||
| Risk | Why it is fatal later | Prevention in place |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| Size/colour as product columns | Cannot express per-combination stock or price; requires re-modelling after orders exist | Variant model ([ADR-0003](./adr/0003-product-and-productvariant-as-separate-entities.md)) |
|
||||
| Float money | Silent discrepancies, unfixable retroactively | Integer minor units ([ADR-0011](./adr/0011-money-as-integer-minor-units.md)) |
|
||||
| Role checks scattered in code | Authorization becomes unauditable; new roles need deploys | RBAC + global guard ([ADR-0007](./adr/0007-rbac-permissions-instead-of-role-checks.md)) |
|
||||
| Admin querying the DB directly | A second write path where authorization is forgotten | No DB driver in admin ([ADR-0004](./adr/0004-the-admin-dashboard-has-no-database-access.md)) |
|
||||
| Module boundary erosion | The monolith becomes unsplittable and untestable | ESLint boundary rules + `public/` barrels |
|
||||
| Order lines joined to live catalog | Historical invoices change when prices do | Snapshot fields on order lines (milestone 5) |
|
||||
| Overselling under concurrency | Real money, real customers, real refunds | `reserved` column + transactional reservation |
|
||||
| Unversioned API | Cannot ship a breaking change once a mobile app exists | URI versioning from request one ([ADR-0005](./adr/0005-uri-based-api-versioning.md)) |
|
||||
| Binaries in PostgreSQL | Backups and replication degrade permanently | Object storage ([ADR-0009](./adr/0009-media-in-s3-compatible-storage-metadata-in-postgresql.md)) |
|
||||
| Single-warehouse inventory | Adding a location later means migrating live stock history | `(variant, location)` keys from the start |
|
||||
| Secrets in the repository | One leak compromises production | Validated env, generated dev secrets, `.env` gitignored |
|
||||
| No request correlation | Production incidents become guesswork | `x-request-id` end to end |
|
||||
| Browser-only failures passing every check | Server rendering and curl both succeed while every click fails | Client paths must be exercised in a real browser before a milestone closes |
|
||||
| `onUnauthorized` retrying the refresh call | Unbounded refresh loop hammering the API from the browser | `skipAuthRetry` on all auth endpoints plus a single-flight refresh |
|
||||
| Concurrent 401s each rotating the token | The second rotation reads as token reuse and revokes the family | One shared in-flight refresh promise |
|
||||
| Risk | Why it is fatal later | Prevention in place |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Size/colour as product columns | Cannot express per-combination stock or price; requires re-modelling after orders exist | Variant model ([ADR-0003](./adr/0003-product-and-productvariant-as-separate-entities.md)) |
|
||||
| Float money | Silent discrepancies, unfixable retroactively | Integer minor units ([ADR-0011](./adr/0011-money-as-integer-minor-units.md)) |
|
||||
| Role checks scattered in code | Authorization becomes unauditable; new roles need deploys | RBAC + global guard ([ADR-0007](./adr/0007-rbac-permissions-instead-of-role-checks.md)) |
|
||||
| Admin querying the DB directly | A second write path where authorization is forgotten | No DB driver in admin ([ADR-0004](./adr/0004-the-admin-dashboard-has-no-database-access.md)) |
|
||||
| Module boundary erosion | The monolith becomes unsplittable and untestable | ESLint boundary rules + `public/` barrels |
|
||||
| Order lines joined to live catalog | Historical invoices change when prices do | Snapshot fields on order lines (milestone 5) |
|
||||
| Overselling under concurrency | Real money, real customers, real refunds | `reserved` column + transactional reservation |
|
||||
| Unversioned API | Cannot ship a breaking change once a mobile app exists | URI versioning from request one ([ADR-0005](./adr/0005-uri-based-api-versioning.md)) |
|
||||
| Binaries in PostgreSQL | Backups and replication degrade permanently | Object storage ([ADR-0009](./adr/0009-media-in-s3-compatible-storage-metadata-in-postgresql.md)) |
|
||||
| Single-warehouse inventory | Adding a location later means migrating live stock history | `(variant, location)` keys from the start |
|
||||
| Secrets in the repository | One leak compromises production | Validated env, generated dev secrets, `.env` gitignored |
|
||||
| No request correlation | Production incidents become guesswork | `x-request-id` end to end |
|
||||
| Browser-only failures passing every check | Server rendering and curl both succeed while every click fails | Client paths must be exercised in a real browser before a milestone closes |
|
||||
| `onUnauthorized` retrying the refresh call | Unbounded refresh loop hammering the API from the browser | `skipAuthRetry` on all auth endpoints plus a single-flight refresh |
|
||||
| Concurrent 401s each rotating the token | The second rotation reads as token reuse and revokes the family | One shared in-flight refresh promise |
|
||||
| Editing options wiping variant pricing | A merchandiser's price and stock work vanishes on a cosmetic edit | Order-independent combination signatures; `planVariantMatrix` is pure and tested ([ADR-0016](./adr/0016-option-values-are-retained-when-variants-reference-them.md)) |
|
||||
| Deleting an option value referenced by history | Breaks or cascades into past orders | `Restrict` FK plus retain-if-referenced ([ADR-0016](./adr/0016-option-values-are-retained-when-variants-reference-them.md)) |
|
||||
| A catalog write not invalidating the cache | Edits appear not to work, so operators repeat them | Every write path ends in `afterWrite` → `deleteByPrefix('catalog:')` |
|
||||
| Stock edited outside the ledger | "Why is this number wrong?" becomes unanswerable | Stock is not settable on the variant endpoint; it moves only through inventory |
|
||||
|
||||
---
|
||||
|
||||
@@ -456,19 +465,50 @@ interactions have been clicked in a real browser**, with the console and network
|
||||
Regression tests now cover the first two (`packages/api-client/src/http-client.spec.ts`); the
|
||||
third belongs in an end-to-end test when one exists.
|
||||
|
||||
Authoring the M3 editor added a second rule. Two defects survived a green pipeline and a
|
||||
successful-looking click-through, and both needed the _second_ step of a flow to appear:
|
||||
|
||||
- The variant grid saved correctly, showed "saved", and left every row marked dirty. It compared
|
||||
its draft against a `product` prop the editor shell never refreshed, so a save could not be seen
|
||||
by the thing measuring whether one was needed. One save looked fine; it was saving _twice_ that
|
||||
exposed it. The shell now owns the product and every tab hands its result back.
|
||||
- The inventory screen listed `StockLevel`, which is a projection of the ledger. A variant that has
|
||||
never moved has no row — so freshly created variants were invisible there, and since that screen
|
||||
is the only route to an adjustment, they could never be given stock at all. Every seeded product
|
||||
had stock already, so the whole catalog looked healthy. Only a product created from scratch
|
||||
showed it.
|
||||
|
||||
M4's listing controls added a third variant of the same lesson. Sorting a listing _after_
|
||||
loading more products showed the same product twice — once from the freshly sorted first page
|
||||
and once left over from the products appended under the previous order. `LoadMore` keeps its
|
||||
position in the React tree across a soft navigation, so its `useState` survived a query change
|
||||
the server had already acted on. Neither action alone revealed anything; only the pair did. It
|
||||
is keyed on the listing identity now.
|
||||
|
||||
The same pass caught a link built with a raw `<a href>` instead of the locale-aware `Link`, which
|
||||
sent an English shopper following "load more" to the Vietnamese listing — invisible in the
|
||||
default locale, which is exactly why it survived.
|
||||
|
||||
So: **exercise a flow on data you just created, not only on seeded data, perform each action
|
||||
twice, and do it in a non-default locale.** Seed data has been through every code path already; new data has been through none. The
|
||||
inventory case is pinned in `apps/api/src/modules/inventory/inventory-list.spec.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Deliberate limitations
|
||||
|
||||
Stated plainly so they are choices rather than oversights.
|
||||
|
||||
**Shipped (M0–M2)**
|
||||
**Shipped (M0–M3)**
|
||||
|
||||
- Catalog reads: products, variants, options, categories, collections, brands, navigation —
|
||||
localised, cached, filtered and faceted.
|
||||
- Storefront browsing and PDP in Vietnamese and English, with per-locale slugs and `hreflang`.
|
||||
- Auth: login, refresh rotation with reuse detection, audience separation, login throttling.
|
||||
- RBAC enforcement end to end, plus user administration and a role viewer in the admin.
|
||||
- Admin catalog authoring: product create/edit with per-locale content tabs, an option builder that
|
||||
regenerates the variant matrix, per-variant SKU and pricing, imagery assigned per colourway,
|
||||
media uploaded straight to storage, and stock received through the append-only ledger.
|
||||
|
||||
**Not built yet, and why**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user