diff --git a/docs/architecture/foundation/README.md b/docs/architecture/foundation/README.md index febaff9..f501ad3 100644 --- a/docs/architecture/foundation/README.md +++ b/docs/architecture/foundation/README.md @@ -55,6 +55,7 @@ It is a platform runtime that must support unlimited tenants from one Angular ap - [Seller-Management-Domain-Models.md](Seller-Management-Domain-Models.md) — typed models, optional sellerId fields - [Seller-Management-UX-Review.md](Seller-Management-UX-Review.md) — UX/accessibility review of the Phase 1 UI - [Seller-Management-Backoffice-Readiness-Audit.md](Seller-Management-Backoffice-Readiness-Audit.md) — per-module scoping/permissions readiness audit +- [Seller-Management-Storefront-Audit.md](Seller-Management-Storefront-Audit.md) — `market.com`/`seller.market.com` storefront readiness audit ### Engineering Rule Documents diff --git a/docs/architecture/foundation/Seller-Management-Storefront-Audit.md b/docs/architecture/foundation/Seller-Management-Storefront-Audit.md new file mode 100644 index 0000000..9f2357e --- /dev/null +++ b/docs/architecture/foundation/Seller-Management-Storefront-Audit.md @@ -0,0 +1,197 @@ +# Seller Management — Storefront Audit (`market.com` / `seller.market.com`) + +Audit only, no code changed. Every fact below was gathered by reading +current source on `feature/seller-management-foundation`, not assumed. +Companion to [Seller-Management.md](Seller-Management.md) §6 (Seller +Storefronts, Seller Branding — both marked Future there) and +[Seller-Management-Backoffice-Readiness-Audit.md](Seller-Management-Backoffice-Readiness-Audit.md) +(the equivalent admin-side audit). + +## The core question: does the architecture already support a seller subdomain? + +**Yes, at the resolution layer — no frontend change needed there.** Tenant +resolution is entirely backend-side by request Host (ADR-001): the frontend +calls `GET /bootstrap` and renders whatever comes back, with no client-side +knowledge of what hostname it's running on beyond what it reads from +`window.location`. If a backend ever resolves `seller.market.com` to a +seller-scoped bootstrap response, the frontend's fetch-and-render pipeline +doesn't need to know that happened — it already just consumes +`BootstrapConfig`. **This is the single most important finding of this +audit**: the hard problem is not "can the frontend handle a second +hostname" (it already architecturally can, by design), it's "does the +*data* the frontend renders (branding, breadcrumbs, SEO, contact info) +carry a seller-aware value to render instead of the marketplace-wide one." +That's a bootstrap-response and per-surface question, audited area by area +below — not a routing or hosting question. + +## Global structure + +Every storefront route sits under one `:lang` prefix (`app.routes.ts`) — +`/${lang}/catalog/:id`, `/${lang}/product/:id`, `/${lang}/wishlist`, +`/${lang}/cart`, `/${lang}/search`. No seller/tenant segment exists in the +route tree today, and none needs to for the subdomain approach — the +hostname carries the seller scope, not a path segment. `/search` is not a +distinct page — it lazy-loads the same `CatalogContainerComponent` as +`/catalog`. **Checkout is not a separate route or component** — it's a +payment-popup flow inline inside `cart.component.ts` +(`features/website/checkout/` and `features/website/cart/` are empty +placeholder folders, `.gitkeep` only). + +## Area by area + +### Homepage — Ready structurally +`pages/home/home.component.ts` is a thin wrapper: gets a page-render model +from `WebsiteRuntimeFacade.getPageRenderModelForUrl()` and renders it. No +branding/URL logic of its own, nothing hardcoded. **Future:** a seller-scoped +home would need `WebsiteRuntimeFacade`'s page-model resolution to become +seller-aware — out of scope of this file, belongs to whichever facade +resolves the page model once a seller bootstrap concept exists. + +### Categories / Search — single injection point identified +`features/website/catalog/containers/catalog-container.component.ts` +(shared by both `/catalog` and `/search`) builds its own breadcrumb via +`CategoryFacade.getBreadcrumb()` → `CategoryTreeUtils.getBreadcrumb()` — pure +category-parent-chain traversal, no marketplace-root assumption baked into +the algorithm itself, but no seller dimension either. Reads +`catalogConfig`/`userExperienceConfig` straight from the bootstrap snapshot +(marketplace-wide today). **Future — Seller breadcrumbs:** since **no +dedicated breadcrumb component or service exists anywhere in the +storefront** (confirmed repo-wide — this is the only breadcrumb logic that +exists at all), a future "Seller X > Category > Product" trail has exactly +one call site to touch: this component's `breadcrumb` signal build. + +### Products — reviews live here too, one dead SEO hook found +`features/website/product/containers/product-details-container.component.ts`. +Product URLs and share links (`shareProduct()`) are built from +`window.location.origin` dynamically, never hardcoded. Reviews and Q&A +render inside this same container (`ReviewListComponent`, +`QuestionListComponent`) — **there is no separate Reviews page/route to +audit independently.** **Real finding, not seller-specific but directly +relevant:** `SeoService.setItemMeta(item)` — the method that would set +per-product OG/canonical tags — is defined but **never called anywhere in +the codebase**. Product pages today get only the site-wide default meta +tags, not per-product ones. **Future — Seller SEO on product pages** +depends on first wiring this already-existing but currently dead hook, not +on inventing a new one. + +### Favorites / Wishlist — Ready, no change needed today +`features/website/user-experience/wishlist/containers/wishlist-page.component.ts`. +Thin list view, no branding, no URL construction, no SEO. **Future:** if +sellers want their own "favorited from my store" filtering, that's a filter +on the already-prepared `Item.sellerId?` field (`Seller-Management-Domain-Models.md`) +applied at read time — no structural change to this page. + +### Cart / Checkout — one literal string worth flagging +`pages/cart/cart.component.ts`. Checkout logic (`openPaymentPopup`, +`createPayment`, status polling) lives entirely in this one file — there is +no separate checkout page. `getPaymentDescription()` falls back, in order: +`bootstrap.branding.brandName` → `TenantResolverService.getHostname()` +(non-localhost) → the literal string `'Покупка на Маркетплейсе'` +("Purchase on Marketplace") for the payment-provider's description field. +This is a generic last-resort fallback, not a hardcoded brand name, but it +is single-tenant-framed. `recordOrder()` posts to `apiService.createOrder` +with no explicit marketplace/seller field — backend infers via Host +(ADR-001), same pattern as everywhere else. **Future — this is where +Checkout Modes and Unified/Split Orders (both marked Future, undecided, in +`Seller-Management.md` §6) would actually land**: today one cart always +produces one order via one popup flow; a cart containing items from +multiple sellers has no defined behavior here at all yet. + +### Header — clean, single injection point for branding +`components/header/header.component.ts`. `brandName` → +`UiRuntimeFacade.marketplaceDisplayName()`; `logo` → +`UiRuntimeFacade.logoUrl()`. Both fully dynamic, sourced from +`bootstrap.branding` via `UiRuntimeFacade.reloadFromBootstrap()` +(`facades/runtime/ui-runtime.facade.ts`). No hardcoded name/logo anywhere. +`homeUrl` is `/${lang}` (root-relative — correct as-is for a seller +subdomain, since the subdomain itself carries the scope, not a path +segment). **Future — Seller logo / Seller banner / Seller branding**: this +facade's `reloadFromBootstrap()` method is the **single highest-leverage +place to add a seller-branding override** — if `bootstrap.seller?.branding` +(the already-typed but unused `SellerBranding`, `Seller-Management-Domain-Models.md`) +is ever populated, this one method could prefer it over +`bootstrap.branding` before setting facade state, and Header/Footer would +pick it up automatically with zero changes of their own, since they already +read exclusively through this facade. + +### Footer — same source as Header, plus contact info +`components/footer/footer.component.ts`. `brandName` → +`uiRuntime.marketplaceName()`, `contactEmail` → `uiRuntime.contactEmail()` — +identical facade source as Header. Footer link groups/payment icons come +from `FooterResolverService.resolveFooterModelFromBootstrap()`, entirely +bootstrap-driven, nothing hardcoded. **Future — Seller contact page**: no +such page or concept exists today; the footer currently only ever surfaces +one marketplace-wide contact email/address. A seller contact page would be +net-new routed content, not an extension of the footer's existing contact +surfacing — the footer would, at most, link to it once it exists. + +### SEO — canonical URLs already correct; structured data and sitemap are 100% net-new +`services/seo.service.ts` is the sole SEO surface in the app — confirmed no +sitemap generator, no robots.txt handler, and no JSON-LD/structured-data +code exists anywhere in the repository. + +- **Canonical URLs — Ready today, no change needed.** `siteUrl` is derived + from `this.doc?.location?.origin` (actual browser location), not + hardcoded — a page served from `seller.market.com` already gets a + correct `seller.market.com` canonical URL with zero code changes. This is + the one area in the whole audit that needs nothing further. +- **Seller branding in meta tags** — `siteName` getter reads + `this.uiRuntime.marketplaceDisplayName() || 'Marketplace'`; the + `resetToDefaults()` effect reads `bootstrap.seo.default` + + `bootstrap.branding` (including `og:image` from + `branding.socialImageUrl`/`logoUrl`). **Future:** the same single + injection point as Header/Footer above (`UiRuntimeFacade`) — once that + facade can prefer seller branding, `SeoService` inherits it automatically + without its own changes, since it already reads through the same facade. +- **Seller structured data (JSON-LD)** — **does not exist for anything + today**, marketplace or seller. This is entirely Future/net-new work, not + an extension of an existing pattern. +- **Seller sitemap** — no client-side sitemap code exists at all; dynamic + sitemap generation is already flagged in `BACKEND.md` as a + server-side-only remaining-work item with no frontend action. A + per-seller sitemap is the same story: entirely a backend concern. +- **Pre-existing, unrelated to seller-scoping, worth flagging anyway**: + `og:locale` is hardcoded to `'ru_RU'` in both `setItemMeta()` and + `resetToDefaults()` — a real gap for multi-locale SEO generally, not + something to fix as part of Seller Management, but adjacent enough to + note here since it lives in the same service this audit reviewed closely. + +### Breadcrumbs — no shared component, one call site +Already covered under Categories/Search above — repeating for completeness +since the mission listed it separately: there is no dedicated breadcrumb +component or service anywhere in the storefront. The only breadcrumb logic +in the entire codebase is `catalog-container.component.ts`'s local signal, +built from `CategoryFacade.getBreadcrumb()`. + +## Summary — future changes only (nothing here is implemented) + +| Area | Current state | Future seller-scoped change | +|---|---|---| +| Homepage | Ready — thin page-model wrapper | Depends on `WebsiteRuntimeFacade` becoming seller-aware | +| Categories / Search | One breadcrumb call site, no seller dimension | Extend `CategoryFacade.getBreadcrumb()` call in `catalog-container.component.ts` | +| Products | URLs already dynamic; `setItemMeta()` exists but is dead code | Wire the existing (currently unused) per-page SEO hook before adding seller data to it | +| Reviews | Lives inside Product detail, no separate page | No independent seller-scoping work — follows Products | +| Favorites | Ready — no branding/URL logic | Optional future filter on already-prepared `Item.sellerId?` | +| Cart / Checkout | One inline flow, one popup, no seller/multi-vendor concept | Where Checkout Modes + Unified/Split Orders (both Future in `Seller-Management.md`) would land | +| Header | Fully dynamic via `UiRuntimeFacade` | **Highest-leverage single injection point**: prefer `bootstrap.seller?.branding` in `reloadFromBootstrap()` | +| Footer | Same facade source as Header | Inherits the Header fix automatically; Seller contact page is net-new routed content | +| SEO — canonical URLs | **Already correct**, derived from `location.origin` | None needed | +| SEO — branding in meta tags | Reads through `UiRuntimeFacade` | Inherits the Header/Footer fix automatically | +| SEO — structured data (JSON-LD) | Does not exist for anything today | 100% net-new, not an extension | +| SEO — sitemap | No client-side code at all; backend-only concern (`BACKEND.md`) | No frontend action, ever | +| Breadcrumbs | One ad hoc signal, no shared component | Same single call site as Categories/Search | + +## Conclusion + +The storefront's existing discipline — everything reads through +`UiRuntimeFacade`/`ConfigService`/bootstrap, nothing hardcodes marketplace +identity, canonical URLs derive from actual browser location — means a +`seller.market.com` subdomain is **structurally closer to already working +than any other part of this audit found**. The entire future-work surface +collapses to two real gaps: (1) `UiRuntimeFacade.reloadFromBootstrap()` +needs to prefer seller branding when present (one method, cascades to +Header/Footer/SEO for free), and (2) Cart/Checkout's single-seller-per-order +assumption needs the Unified-vs-Split-Orders decision from +`Seller-Management.md` before multi-vendor carts can be handled at all. +Structured data and sitemap are not seller-specific gaps — they're simply +unbuilt for anyone today.