From 687891cbaf828d76d8d6836a9da7aef469af9fa4 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sat, 15 Aug 2026 20:13:10 +0400 Subject: [PATCH 01/29] docs: platform super-admin Phase 1 design spec Co-Authored-By: Claude Sonnet 5 --- .../2026-08-15-platform-super-admin-design.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-15-platform-super-admin-design.md diff --git a/docs/superpowers/specs/2026-08-15-platform-super-admin-design.md b/docs/superpowers/specs/2026-08-15-platform-super-admin-design.md new file mode 100644 index 0000000..4913dbc --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-platform-super-admin-design.md @@ -0,0 +1,137 @@ +# Platform Super-Admin — Phase 1 Design + +**Status:** Approved +**Date:** 2026-08-15 +**Audience:** Internal admin & risk team ("super puper user") + +## Purpose + +A cross-tenant view for internal admin/risk staff: see every project (store/tenant) on the +platform, drill into one, and review its access list, audit log, admin edit history, and +purchase history. Read-only in this phase. + +Editing project data / impersonating a store's admin ("edit all", with a per-change "notify +this store's admin" toggle) is explicitly **out of scope** for this phase — see +[Phase 2](#phase-2-out-of-scope-here) below. Phase 1 exists first because Phase 2's edit and +notify plumbing depends on the tenant-context switch this phase builds. + +## Non-goals (Phase 1) + +- No editing of any tenant's data. +- No impersonation of a store's admin. +- No "notify store admin" mechanism (that's a Phase 2 concern, tied to edit actions that + don't exist yet). +- No real backend — this repo is frontend-only; the backend contract is specified here for + whoever owns that service, not implemented here. + +## Architecture + +- New top-level feature module: `src/app/features/platform-admin/`. +- New route tree `/platform-admin/**`, own shell/layout. **Not** nested under any tenant's + `/admin/**` — a project is not "logged into" the way a store admin is. +- New `platformAdminAuthGuard` (parallel to, but sharing no state with, `adminAuthGuard` in + `core/admin-auth/admin-auth.guard.ts`). +- `PlatformAuthService` — session/login state for the super-admin, backed by a + `PlatformAuthGateway` interface: `login(credentials)`, `logout()`, `session()`. + - `PlatformAuthLocalGateway` — dev-only implementation. Reads the expected credential from + a **git-ignored** local file (`platform-auth.local-secret.ts`, added to `.gitignore`), + never committed, never present in a production build path. + - `PlatformAuthApiGateway` — later swap-in once the backend endpoint exists; same + interface, no caller changes needed. + +## Data model + +```ts +interface PlatformProjectSummary { + id: UUID; + name: string; + slug: string; + host: string; + status: 'active' | 'suspended'; + createdAt: number; + adminCount: number; + lastActivityAt: number | null; +} + +interface PlatformProjectAccessEntry { + userId: UUID; + displayName: string; + telegramUsername: string; + roleId: string; // maps to existing AdminRole / ROLE_PERMISSIONS +} + +type PlatformProjectHistoryEntry = + | { kind: 'access'; tenantId: UUID; actorLabel: string; timestamp: number; summary: string } + | { kind: 'edit'; tenantId: UUID; actorLabel: string; timestamp: number; summary: string } + | { kind: 'purchase'; tenantId: UUID; actorLabel: string; timestamp: number; summary: string }; +``` + +- `PlatformProjectSummary[]` is produced by `PlatformProjectsGateway.list()`, which aggregates + the existing `TenantConfig` fixture list plus derived stats. Mock gateway now; real + aggregation is a backend concern later. +- `PlatformProjectAccessEntry` reuses the existing `AdminRole` / `ROLE_PERMISSIONS` shape from + `core/auth/models/permission.model.ts` — no new role system. +- `PlatformProjectHistoryEntry` is a discriminated union covering all three history types the + user asked for (access/audit, admin edit history, purchase history). Mock gateway simulates + aggregation from existing per-tenant sources (e.g. the pattern in + `AdminDashboardHistoryService`, `admin-transactions`); real aggregation is a backend concern. +- Every super-admin **view** into a project also writes its own `kind: 'access'` entry + (`platform.viewedProject`) — the risk team needs to know who looked at what, not just what + changed. + +## Components / pages + +- `PlatformProjectsListPageComponent` — table of all projects: name, status, admin count, + last activity. Search/filter by status. +- `PlatformProjectDetailPageComponent` — project overview stats, then tabs: + - **Access** — `PlatformProjectAccessEntry[]` for that tenant. + - **Audit Log** — `history` filtered to `kind: 'access'`. + - **Edit History** — `history` filtered to `kind: 'edit'`. + - **Purchase History** — `history` filtered to `kind: 'purchase'`. + - All read-only in this phase. + +## Security + +- `platformAdminAuthGuard` denies unless the session carries `platform.superadmin`. Like the + existing `AdminPermissionsService`, the frontend check is defense-in-depth only — real + enforcement must happen server-side once the backend endpoint exists. This is called out + explicitly so it's never mistaken for the source of truth. +- No credential is ever hardcoded in committed source. Dev-only credential lives in a + git-ignored local file; production auth goes through the real backend endpoint below. +- Session timeout for platform-admin: 15 minutes idle (shorter than regular tenant-admin + sessions — higher-privilege session, smaller blast radius if a session is left open). +- Every super-admin action (including read-only views) is itself audit-logged. +- After implementation, run `/security-audit` on this feature specifically before it ships. + +### Backend contract (for whoever owns that service — not implemented in this repo) + +Add to `BACKEND-API-REFERENCE.md`: + +- `POST /platform-admin/auth` — verifies a hashed credential server-side, returns a session + token scoped to `platform.superadmin`. Never a plaintext credential check in a client-shipped + artifact. +- `GET /platform-admin/projects` — returns `PlatformProjectSummary[]`. +- `GET /platform-admin/projects/:id/history` — returns `PlatformProjectHistoryEntry[]` for + that tenant, paginated. + +## Testing + +- Unit tests: `platformAdminAuthGuard`, `PlatformProjectsGateway` (mock), history-aggregation + mapping logic. +- No E2E in this phase — no real backend to exercise end-to-end yet. + +## Phase 2 (out of scope here) + +A separate spec/plan cycle, once Phase 1 ships: + +- Full edit / impersonation: super-admin acts as a tenant's admin across every existing admin + module (products, orders, categories, settings, etc.), reusing those modules under a + tenant-context switch. +- Per-edit-action **"notify this store's admin about this change"** checkbox, **default + unchecked**. Uses the existing in-app notification pattern (the one behind + `admin-order-watcher.service.ts`'s unread-badge flow) so the affected tenant's admin sees it + in their notification feed. Unchecked-by-default matters: some super-admin edits are + discreet technical fixes where alerting the store admin would be noise or a reputational + concern, not every edit should ping them. +- This phase needs the tenant-context switch and audit-logging plumbing this Phase 1 spec + establishes, which is why it's sequenced after. From 3c72c37e312c92ab3910474f047493a24cf09efa Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:24:57 +0400 Subject: [PATCH 02/29] docs: v3.1 gap analysis + delivery plan; add DI seams to 9 admin gateways Adds InjectionToken + factory for Orders, Products, Users, Transactions, Monitoring, Moderation (mirrors existing Categories/Dashboard pattern) and repoints their facades plus the derived Analytics/Customers facades and admin-order-watcher off the mock LocalGateway class directly. No behavior change today - still resolves to the mock - but a real backend can now be bound per domain with zero facade edits. Docs: full gap analysis of Product Plan v3.1 against current repo state, and a phased delivery plan (10 phases, 34 sprints, 5 tracks) breaking every identified gap into scoped, sequenced work. Co-Authored-By: Claude Sonnet 5 --- docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md | 461 ++++++++++++++++++ docs/PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md | 277 +++++++++++ .../facade/admin-analytics.facade.ts | 12 +- .../facade/admin-customers.facade.ts | 4 +- .../facade/admin-moderation.facade.ts | 4 +- .../admin-moderation-gateway.token.ts | 9 + .../facade/admin-monitoring.facade.ts | 4 +- .../admin-monitoring-gateway.token.ts | 9 + .../orders/facade/admin-orders.facade.ts | 4 +- .../services/admin-orders-gateway.token.ts | 9 + .../products/facade/admin-products.facade.ts | 4 +- .../services/admin-products-gateway.token.ts | 9 + .../services/admin-order-watcher.service.ts | 4 +- .../facade/admin-transactions.facade.ts | 4 +- .../admin-transactions-gateway.token.ts | 9 + .../admin/users/facade/admin-users.facade.ts | 4 +- .../services/admin-users-gateway.token.ts | 9 + 17 files changed, 814 insertions(+), 22 deletions(-) create mode 100644 docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md create mode 100644 docs/PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md create mode 100644 src/app/features/admin/moderation/services/admin-moderation-gateway.token.ts create mode 100644 src/app/features/admin/monitoring/services/admin-monitoring-gateway.token.ts create mode 100644 src/app/features/admin/orders/services/admin-orders-gateway.token.ts create mode 100644 src/app/features/admin/products/services/admin-products-gateway.token.ts create mode 100644 src/app/features/admin/transactions/services/admin-transactions-gateway.token.ts create mode 100644 src/app/features/admin/users/services/admin-users-gateway.token.ts diff --git a/docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md b/docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md new file mode 100644 index 0000000..8ea89c2 --- /dev/null +++ b/docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md @@ -0,0 +1,461 @@ +# Product Plan v3.1 — Delivery Plan (Phases → Sprints → Todos) + +Companion to [PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md](PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md). Every gap identified there is assigned here exactly once. + +**No calendar dates.** The plan itself (§12) refuses invented dates and fixes *sequence + exit criteria* instead. This document does the same. Sprints are ordered units of work, not two-week promises. Sizes are relative: **S** / **M** / **L** / **XL**. + +**Ownership tags:** `[FE]` this repo · `[BE]` backend/platform service · `[BOTH]` coordinated contract change · `[DEC]` decision, no code. + +**Deviation from the plan's own order, and why:** the plan sequences P0-C (external ingestion) before P0-D (catalog integrity). We swap them. External order ingestion maps `externalSKU → internal offer` (§5.1), and `Offer` does not exist yet — ingestion has nothing to map onto until the Product/Offer split ships. Everything else follows the plan's ordering. + +--- + +## Phase map + +| Phase | Name | Plan ref | Gate | +|---|---|---|---| +| **0** | Unblock & seams | — | Decisions answered; every admin domain swappable | +| **1** | Money & payment truth | P0-A, §2.3 §3.3 §3.8 §7 | An order total is explainable from data | +| **2** | Orders canonical + notifications | P0-B, §2.8 §2.10 §3.5 | Paid order appears and notifies without refresh | +| **3** | Catalog integrity + fulfillment | P0-D, §2.1 §2.4 §3.6 | Any published offer is genuinely buyable and fulfillable | +| **4** | External order ingestion | P0-C, §5 §3.7 | External purchase lands in Orders, no duplicates | +| **🚦** | **PRODUCTION LAUNCH GATE** | §3 LAUNCH BLOCKERS, §13.2 | All P0 closed and evidenced | +| **5** | Seller Portal | P1-A, §2.2 | Seller runs own offers and orders in scoped UI | +| **6** | Server cart + checkout session | P1-B, §2.5 §2.6 | Client price never trusted; repeat-safe | +| **7** | Payments hardening + reconciliation | P1-C, §2.7 §7.3 | Internal vs provider matched, mismatches visible | +| **8** | Identity & messaging | §2.9 §3.4 §14 | VK/MAX/Telegram linked; bot collects delivery | +| **9** | Tenant registry, domains, releases | P2-A, §4.3 §8 | New marketplace launched with no hardcode | +| **10** | Tenant content modules (Gorbushka) | P2-B, §11 | Content tenant on same runtime/backoffice | + +**Parallel tracks** (start early, run across phases): **A** Analytics pipeline · **S** Security/RBAC/audit · **Q** QA & E2E · **N** API namespace migration · **Z** Pre-existing repo debt. + +--- + +## Phase 0 — Unblock & seams + +Nothing downstream can be honestly estimated until this closes. Two sprints: one is other people answering questions, one is work we can do today with no answers. + +### Sprint 0.1 — Decisions `[DEC]` + +Blocking. Escalate as a single list, not one at a time. + +- [ ] **Backend ownership.** Platform API, Workers, Integration Hub, Domain Automation — our team or a service team? Everything `[BE]` below is unassigned until answered. +- [ ] **Unfreeze the payment chain.** `BACKEND-API-REFERENCE.md §7` marks it do-not-modify. Phases 1, 6 and 7 are unbuildable otherwise. +- [ ] **Name the external marketplaces** for §5 connectors (Ozon / Wildberries / Yandex Market / Avito / other). Each is a separate connector; Phase 4 cannot be sized without the list. +- [ ] **FX rate source** — which provider, and does the backend return converted prices or serve rates? Plan §2.3 implies backend-converted. +- [ ] **§14 vs. approved email/phone OTP spec** — VK ID first, or finish OTP first? +- [ ] **Multi-seller orders: unified or split?** Open in three of our own documents. Blocks Phase 3 and Phase 5 data model. +- [ ] **Reproduce or retract the "fixed 5-second payment" claim (§3.2).** Not present in this codebase. +- [ ] **API namespace migration (§9.3)** — migrate before new endpoints, or accept two conventions? Cost rises every phase this is deferred. +- [ ] **Document version** — file says v3.1, version block says 3.0. Which is canonical? + +**Exit:** all nine answered in writing. + +### Sprint 0.2 — Seams and type reconciliation `[FE]` — runs regardless of answers + +- [ ] Add DI tokens to the 9 admin domains that have none: Orders, Products, Users, Transactions, Monitoring, Moderation (+ derived Customers, Analytics). **M** — hard prerequisite for every `[BE]` swap in Phases 1–7. +- [ ] Reconcile `AdminRole` — defined twice with unrelated shapes (auth string-union vs. Users-page display interface). **S** +- [ ] Reconcile the two `Category` types, both fed by the same `/category` response, both in use. **S** +- [ ] Resolve `SellerConfig` (bootstrap) vs. `Seller`/`SellerBranding` (domain) — pick one or document the mapping. Blocks Phase 5. **S** +- [ ] Build the feature-flag / capability-guard service an existing ADR already promises; migrate the hand-rolled `sellerManagement.enabled` check onto it. **S** +- [ ] Build the centralized error-handling layer (`core/error-handling/`, `core/interceptors/` are `.gitkeep`-only today): error-envelope interceptor + 429 handling. **M** `[BOTH]` — envelope shape needs backend agreement. +- [ ] Fix `toAuthErrorShape()` to read a body-level code, not HTTP status alone — the built "session expired" / "invalid signature" screens are currently dead UI. **S** +- [ ] Bind mock implementations to `PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY`, or delete the dead mock branch. Today both silently ignore `useMockData`. **S** + +**Exit:** any admin domain can be pointed at a real backend by swapping one provider. + +--- + +## Phase 1 — Money & payment truth (P0-A) + +Closes §3.3 and §3.8, and half of the §13.1 acceptance table. The single highest-value phase: it is what makes totals explainable to a bank. + +### Sprint 1.1 — Money model `[BOTH]` + +- [ ] `Money = { amountMinor: int, currency }` end to end. Kill float arithmetic in `CurrencyRatesService.convert()`. **L** +- [ ] Currency minor-units + rounding rules table (RUB/USD/EUR/AMD at minimum). **M** +- [ ] Delete browser-owned rates: remove `currencyRates.v1` from `localStorage` and the hardcoded `DEFAULT_RATES` fallbacks (`USD: 0.011`, `AMD: 4.3`). **S** +- [ ] Remove the admin-typed rate editor from Admin Settings once a real source exists. **S** + +### Sprint 1.2 — FX quote + rate source `[BE]` + `[FE]` + +- [ ] `FxQuote { base, quote, rate, source, observedAt, expiresAt, quoteId }` entity + endpoint. **M** +- [ ] Rate-source adapter behind an interface; concrete provider pluggable (§7.1). **M** +- [ ] Stale/outlier quote rules; checkout **blocks** or uses an explicitly configured fallback. **M** +- [ ] `PriceBook`: offer base currency + allowed display/checkout currencies per tenant. **M** + +### Sprint 1.3 — Price snapshot + server-authoritative amount `[BOTH]` — needs the freeze lifted + +- [ ] `PriceSnapshot { offerId, amount, currency, fxQuoteId, capturedAt }`, immutable. **L** +- [ ] Server computes and validates the charged amount. Stop trusting `CartPaymentRequest.amount` and the per-item `price[]` array from the browser. **L** — the plan's §2.5 headline requirement. +- [ ] Old orders never recalculated when a rate updates. **S** +- [ ] Backoffice "total formula" panel: lines × qty − discounts + delivery + fees, plus the FX quote used (§7.2). **M** +- [ ] `PriceHistory` on offer price and stock, with author/source (§2.1). **M** + +### Sprint 1.4 — Payment timeline `[BE]` + `[FE]` + +- [ ] Explicit state machines: `PaymentIntent` (created→pending→authorized/paid→failed/cancelled), `Payment` (received→confirmed→captured/settled→refunded), `Order` (pending_payment→paid→processing→fulfilled). **L** +- [ ] Persist `provider event id`, `provider timestamp`, `receivedAt`, `processedAt` per transition. **M** +- [ ] Webhook entrypoint with signature verification + idempotency (§2.7). **L** +- [ ] Idempotency keys on checkout, payment and order creation. Zero `idempot*` exists today. **M** +- [ ] Replace client-polled status signals with server truth; keep polling only as a UI fallback. **M** +- [ ] Keep the current honest behaviour: no artificial delay. Already compliant — protect it with a test. **S** + +**Exit criteria (plan's own):** currency converts correctly; payment timeline reconstructable from provider events; every total explainable from `SKU/qty/delivery/discount/FX`. + +--- + +## Phase 2 — Orders canonical + notifications (P0-B) + +### Sprint 2.1 — Canonical order model `[BOTH]` + +- [ ] `Order` header: `marketplaceId, source, customer, currency, subtotal, discounts, delivery, total, paymentStatus, orderStatus`. **L** +- [ ] `OrderLine` with `offerId, sellerId, skuSnapshot, titleSnapshot, qty, unitPriceMinor, lineTotalMinor, priceSnapshotId`. **M** +- [ ] `OrderEvent` timeline: created, paid, seller notified, accepted, fulfilled, cancelled, refunded (§2.8). Closes our own "Real order audit trail" TODO. **M** +- [ ] Real `AdminOrdersApiGateway` replacing the 24-row static seed with no create path. **L** `[BE]` +- [ ] Admin order actions: assign, resend notification, replay sync, cancel/refund by permission, comment, export. **M** +- [ ] `OrderContactSnapshot` — name/contacts frozen at order time, immune to later profile edits (§2.9). **S** + +### Sprint 2.2 — Event bus + Notification Center `[BE]` + `[FE]` + +- [ ] Platform event bus emitting `order.created`, `order.paid`, `payment.failed`, `webhook.error`, `stock.low`, `oversell`, `refund.requested/completed`, `external_order.imported`. **L** +- [ ] `Notification` entity: `unread/read`, `severity`, `marketplaceId`, entity type/id, **deep link**. **M** +- [ ] `DeliveryAttempt` log per external channel — a Telegram/email failure must never lose the internal notification (§2.10). **M** +- [ ] Backoffice Notifications section: unread queue, incidents, filter by marketplace and event type. Missing entirely from our nav today. **M** +- [ ] Repoint `AdminOrderWatcherService` from polling to the event stream. Feature is already built and inert — this is what switches it on. **S** + +**Exit:** a paid order appears in backoffice without manual refresh, with deep link and seller/source. + +--- + +## Phase 3 — Catalog integrity + fulfillment (P0-D) + +Biggest structural change in the whole programme. Everything about multi-seller commerce hangs off it. + +### Sprint 3.1 — Product / Offer split `[BOTH]` + +- [ ] Introduce `Offer/Listing { id, marketplaceId, sellerId, variantId, sellerSku, priceMinor, currency, stockPolicy, status, publishedAt }`. **XL** — does not exist in any form today. +- [ ] Move price, stock, currency and status off `Product` onto `Offer`. **L** +- [ ] Formalise `Product` / `Variant` / `SKU` / `Category` (with `attributesSchema`, SEO) as content-only. **L** +- [ ] Unify the admin mock product domain with the live storefront `Item` domain — two unrelated shapes today. **L** +- [ ] Offer lookup in backoffice by internal SKU, seller SKU, product ID or external mapping (§2.1 "готово, когда"). **M** + +### Sprint 3.2 — Lifecycle, import, inventory `[BOTH]` + +- [ ] `draft → moderation → published → paused/archived` for both product and offer; wire the existing mock Moderation module to it. **M** +- [ ] Bulk import CSV/API: required-field validation, **error preview before apply**. Nothing exists (current "bulk" is Admin Categories edit actions only). **L** +- [ ] `InventoryRecord`: `available` / `reserved` / `sold` counted separately. **L** +- [ ] Reservations at checkout or pre-payment per strategy, with TTL. **M** +- [ ] Idempotent upsert for seller feed stock updates; repeat webhook must not double-decrement. **M** +- [ ] Oversell → dedicated incident queue, never silently hidden (§2.4). **M** + +### Sprint 3.3 — Fulfillment + executability `[BOTH]` + +- [ ] `Fulfillment` entity: manual / warehouse / pickup / digital; `status, assignedTo, issuedAt/shippedAt`, evidence where applicable. One `fulfil*` reference exists in the entire codebase today. **L** +- [ ] Publish-time executability validation — an offer that cannot actually be fulfilled cannot be published (§3.6). **M** +- [ ] Explicit test proving there is **no** inspector-detection branch anywhere: same production flow for every buyer (§3.6, §10.2, §13.2 last item). **S** +- [ ] Multi-seller cart grouping by seller and fulfillment rules — currently undefined behaviour (§2.5). **M** — blocked on the unified-vs-split decision in Sprint 0.1. + +**Exit:** any published, available offer really passes order → fulfillment. + +--- + +## Phase 4 — External order ingestion (P0-C) + +Zero percent built today. Sized per connector; Sprint 4.2 repeats for each marketplace named in Sprint 0.1. + +### Sprint 4.1 — Connector framework `[BE]` + +- [ ] `Connector` + `ConnectorCredentialRef` in secret storage, scoped per marketplace/seller. **M** +- [ ] Inbound: webhook where the provider supports it, polling fallback with cursor/since. **L** +- [ ] `RawExternalEvent` — persist the raw payload before parsing, for traceability. **S** +- [ ] Normalizer: external payload → canonical `ExternalOrderEvent` → internal `Order`. **L** +- [ ] `ExternalOrderMapping`: `externalSellerId / externalProductId / externalSKU → internal seller/offer`. **L** +- [ ] Idempotency on `source + externalOrderId/eventId`; a repeat must not create a duplicate order. **M** +- [ ] Exponential retry, `DeadLetter`, manual replay from backoffice. **M** +- [ ] **Unmatched queue** for events with no SKU mapping. **M** +- [ ] Status/fulfillment push back to the external marketplace where its API allows (§5.2 step 8). **M** + +### Sprint 4.2 — Per-connector implementation `[BE]` — ×N + +- [ ] One sprint per named marketplace: auth, endpoint mapping, rate limits, sandbox verification. **L each** + +### Sprint 4.3 — Connector observability `[FE]` + `[BE]` + +- [ ] Backoffice **Integrations** section (missing from our nav): connectors, payment providers, FX sources, messaging. **M** +- [ ] Per-connector health: last success, lag, errors, rate limit, backlog, unmatched mapping. **M** +- [ ] Trace id on every connector error, visible in backoffice (§5.2 SLA). **S** +- [ ] SLA instrumentation: webhook 99% under 60s; polling ≤ interval + 60s; **0** duplicate orders. **M** + +**Exit:** an external purchase creates/updates an order automatically, never duplicates, and notifies the responsible manager. + +--- + +## 🚦 PRODUCTION LAUNCH GATE + +Per §3 "LAUNCH BLOCKERS" and the §13.2 checklist. Do not schedule a launch before every line is green **and evidenced by a test, not an assertion**. + +- [ ] All P0 closed and confirmed by tests +- [ ] Production analytics collecting real events (Track A) +- [ ] Catalog contains only genuinely available/publishable offers +- [ ] Seller permissions verified (Phase 5 or enforced-empty) +- [ ] cart → checkout → payment → order end-to-end smoke passed +- [ ] Webhook signatures, idempotency, retry verified +- [ ] External connector reconciliation passed +- [ ] FX source live, stale-quote policy verified +- [ ] Notification delivery + fallback verified +- [ ] Refund flow + reconciliation smoke passed +- [ ] Domains/SSL/health checks green (Phase 9) +- [ ] Backup/rollback exists +- [ ] Audit enabled (Track S) +- [ ] **No branch anywhere alters commerce flow based on who the buyer appears to be** + +--- + +## Phase 5 — Seller Portal (P1-A) + +A placeholder page with a `false` flag and zero backend bytes today. Note: the enabled code path has **never been exercised even once** — every prior verification ran with the flag at its real value. + +### Sprint 5.1 — Seller foundation `[BOTH]` +- [ ] `SellerOrganization`, `SellerUser`, `SellerMarketplaceMembership`, `SellerIntegration`. **L** +- [ ] Onboarding: organisation, credentials/profile, contacts, marketplace applications, moderation status. **L** +- [ ] Backoffice **Sellers** section (missing from nav): organisations, applications, roles, status, listings, integration health. **L** + +### Sprint 5.2 — Seller working surfaces `[FE]` + `[BE]` +- [ ] Catalog: create/edit products & offers, media, attributes, submit for moderation, bulk import. **L** +- [ ] Prices & Stock: mass edit, API/feed sync, change history, sync errors. **L** +- [ ] Orders: new, confirm, pick/issue/ship, cancel, return, SLA, comments. **L** +- [ ] Finance: accruals, commissions, refunds, settlement/payout register, report export. **L** +- [ ] Team: `SELLER_OWNER`, `SELLER_CATALOG_MANAGER`, `SELLER_ORDER_MANAGER`, `SELLER_FINANCE_VIEWER`, `SELLER_VIEWER`. **M** +- [ ] Integrations: API credentials, webhook/feed status, external SKU mapping, sync logs. **M** + +### Sprint 5.3 — Seller isolation `[BE]` + `[Q]` +- [ ] A seller cannot see another seller's products, orders, customers, finance or API keys — enforced backend-side, tested. **M** +- [ ] Bank/payment detail changes: step-up auth + audit event + approval when maker/checker is on. **M** +- [ ] Seller staff permissions verified backend-side regardless of UI visibility. **M** +- [ ] First-ever fixture test of the seller-management enabled state. **S** + +--- + +## Phase 6 — Server cart + checkout session (P1-B) + +Partly pulled forward into Sprint 1.3 (server-authoritative amount). This phase completes the move. + +### Sprint 6.1 — Server cart `[BOTH]` +- [ ] `Cart` / `CartLine` server-side, keyed on `offerId`. Replaces `localStorage` + Telegram CloudStorage. **L** +- [ ] Idempotent add/update/remove; quantity validated against stock and seller rules. **M** +- [ ] Price-refresh: cart surfaces price changes before checkout and requires explicit confirmation when the total moved. **M** +- [ ] Guest cart via session token; authenticated cart bound to customer account. **M** +- [ ] Expiration: inactive carts cleared, reservations released on TTL. **S** + +### Sprint 6.2 — Checkout session `[BOTH]` +- [ ] `CheckoutSession` entity. `features/website/checkout/` is an empty directory today; checkout lives in a 751-line cart popup. **XL** +- [ ] Server re-validates offers and stock at checkout start. **M** +- [ ] Contact requirements enforced by tenant policy: email and/or phone verifiable (§2.6 step 4). **M** +- [ ] Clear total breakdown shown to the customer. **M** +- [ ] `PaymentIntent` via provider adapter; repeat click must not create a second intent. **M** +- [ ] Guest-checkout on/off per tenant policy (§6.2). **S** +- [ ] `DeliveryOption` entity. **M** + +--- + +## Phase 7 — Payments hardening + reconciliation (P1-C) + +### Sprint 7.1 — Refunds `[BOTH]` +- [ ] `Refund` as a first-class operation with reason, actor and order-line linkage. `requestRefund(id)` is a mock method today. **L** +- [ ] Partial refunds; `refunded / partially_refunded` states. **M** + +### Sprint 7.2 — Reconciliation `[BE]` + `[FE]` +- [ ] `ReconciliationRecord`; match on `providerPaymentId` / merchant reference / amount+currency fallback (§7.3). **L** — zero `reconcil*` in the codebase today. +- [ ] Classify: unmatched, duplicate, amount mismatch, status mismatch. **M** +- [ ] Backoffice **Payments & Finance** section (missing from nav): payments, refunds, reconciliation queue, unmatched events, settlements. **L** +- [ ] Controlled resolution with full audit trail. **M** +- [ ] Settlements / payout register. **L** — zero `settlement*` today. + +### Sprint 7.3 — Provider breadth `[DEC]` + `[BOTH]` +- [ ] Decide additional providers beyond the current QR/card flow (wallets, BNPL) — open business question. **DEC** +- [ ] Provider adapter interface so a new provider is a plug-in, not a rewrite. **M** + +--- + +## Phase 8 — Identity & messaging (§2.9, §3.4, §14) + +Order inside this phase depends on the Sprint 0.1 decision (VK ID first vs. OTP first). + +### Sprint 8.1 — Customer identity core `[BOTH]` +- [ ] `Customer`, `ExternalIdentity`, `ContactMethod`, `Verification`, `Consent`. **L** +- [ ] Telegram demoted from sole identity to one provider among several. **M** +- [ ] `emailVerifiedAt` / `phoneVerifiedAt` / `telegramLinkedAt`. **S** +- [ ] Backoffice **Customers** on real data: profiles, verified contacts, orders, consent. **M** +- [ ] Sensitive profile changes logged. **S** + +### Sprint 8.2 — Email/phone OTP `[BOTH]` +- [ ] Implement the approved [email/phone login spec](superpowers/specs/2026-08-15-email-phone-login-design.md). **L** +- [ ] Position it as recovery/fallback per v3.1 §14, not as the primary path. **S** + +### Sprint 8.3 — VK ID `[BOTH]` — new in v3.1 +- [ ] OAuth 2.1/PKCE completed **backend-side**; link external identity to `Customer`. **L** +- [ ] VK ID as the primary storefront social login. **M** +- [ ] Repeat login must never create a duplicate customer. **M** +- [ ] Identity-conflict handling → controlled resolution, never overwrite an existing binding (§14.3). **M** + +### Sprint 8.4 — MAX + Telegram bot channels `[BOTH]` — new in v3.1 +- [ ] `ContactChannel`, `BotConversationBinding`, `MessagingConsent`. **L** +- [ ] MAX bot-assisted linking: one-time code, TTL, single-use, bound to marketplace + browser session. **L** +- [ ] Provider secrets never reach the frontend; all bot updates handled idempotently. **M** +- [ ] Bot adapters (VK / MAX / Telegram) normalised into one `MessagingEvent` keyed to `orderId`. **L** + +### Sprint 8.5 — Notification Orchestrator + delivery conversation `[BE]` — new in v3.1 +- [ ] Orchestrator routes `order.paid` to the customer's chosen channel; the backoffice notification always fires regardless. **L** +- [ ] Channel choice in checkout ("where should we send confirmation?"), recorded in `OrderContactSnapshot`; linking flow must not lose the cart or checkout session. **M** +- [ ] Delivery Conversation State Machine: `not_started → awaiting_customer → details_received → manager_assigned/auto_confirmed → shipment_planned → completed`. **L** +- [ ] Bot collects city/address/recipient/phone/time window/comment; backend validates and snapshots into the order. **L** +- [ ] **The bot must never change financial statuses** — delivery fields only, via Delivery Service. **M** +- [ ] Follow-up rules per tenant; after N attempts hand off to a manager, no infinite spam. **M** +- [ ] Manager handoff view: message history, current conversation state, accept handoff. **M** +- [ ] Messenger unavailability creates a `DeliveryAttempt` error and triggers fallback — never blocks the order. **M** + +--- + +## Phase 9 — Tenant registry, domains, releases (P2-A) + +### Sprint 9.1 — Marketplace Registry `[BOTH]` +- [ ] `Marketplace`, `MarketplaceDomain`, `MarketplaceFeatureSet`, `MarketplaceRevision`. **L** +- [ ] Backoffice **Marketplaces** section (missing from nav): registry, type, status, domains, currencies, feature set, responsible manager. **L** +- [ ] Onboarding wizard, all 8 steps of §4.3 (card → feature set → domains → design → roles → integrations → staging + smoke → production launch). **XL** +- [ ] Lifecycle state machine `draft → configured → content_ready → domains_planned → staging_live → qa_passed → production_ready → live → paused/archived`, **showing which blocker prevents the next transition**. **L** +- [ ] Marketplace dashboard (§4.2): GMV, paid orders, conversion, payment failure rate, orders needing action, seller moderation queue, low stock, unmatched events, integration health, domain/SSL/release status. **L** +- [ ] Re-scope the [super-admin Phase 1 design](superpowers/specs/superuser.md) against this — it overlaps registry and audit. **M** +- [ ] Consolidate `MarketplaceRef` vs. `TenantConfig` if a third marketplace-shaped type appears. **S** + +### Sprint 9.2 — Domain automation `[BE]` +- [ ] Hostinger DNS integration, all 7 endpoints from §8.2. Zero references exist today. **L** +- [ ] Read current zone → snapshot/rollback payload → build and validate plan → apply only after production approval. **L** +- [ ] **Never touch MX/SPF/DKIM/DMARC/CAA** without a separate task. **S** +- [ ] Propagation, SSL and health verification; mark domain active only after checks pass. **M** +- [ ] Backoffice **Domains & Releases** section (missing from nav). **M** + +### Sprint 9.3 — Publish model `[BOTH]` +- [ ] `draft → validation → preview → publish` with immutable published revisions; rollback creates a new revision (§8.3). **L** +- [ ] Real builder persistence — today `apiEndpoints.builder` is an empty placeholder and "publish" only promotes a `localStorage` signal. **L** +- [ ] CMS/static pages get a real backend write path (currently in-memory bootstrap only). **L** +- [ ] Enforce that orders/payments/inventory ledger are **not** part of a content revision and never roll back with the storefront. **S** +- [ ] Tenant resolution hardening: verified Host server-side, unknown Host → 404 with **no fallback tenant** (§6.1). **M** + +--- + +## Phase 10 — Tenant content modules (P2-B, Gorbushka) + +Only after Commerce Core is real. The plan is explicit that Gorbushka does not define the architecture. + +### Sprint 10.1 — Directory content entities `[BOTH]` +- [ ] `Shop`, `ShopCategory`, `Service`, `Floor`, `SchemePin`, `RentListing`, `News/Promo`, `StaticPage`, `Lead`, `MallSettings`. Only static pages exist today. **XL** +- [ ] Every entity carries `marketplaceId`, audit, and publish/preview flow. **M** +- [ ] Mall scheme / floors / pins UI. **L** +- [ ] Rent listings + lead capture. **M** + +### Sprint 10.2 — Gorbushka tenant config `[FE]` +- [ ] Feature set per §11.1: CMS, shops, services, scheme, rent, news, SEO/media/domains **on**; catalog / seller portal / commerce **platform-ready but off**. **M** +- [ ] Prove commerce can be switched on later without touching backend or storefront code. **M** + +--- + +## Parallel tracks + +### Track A — Analytics pipeline (P1-D, §3.1 §6.3) + +**Start at Phase 1, not last.** Longest lead time in the programme, and it is a P0 in the plan's own §3. There is no tracking infrastructure at all today — this is not a missing endpoint. + +- [ ] **A1** Server-side event logging spine. **XL** `[BE]` +- [ ] **A2** Traffic events: `session_started`, `page_view`, source/utm/referrer, unique users/sessions. **M** +- [ ] **A3** Catalog events: `search`, `category_view`, `product_view`, `seller_view`. **M** +- [ ] **A4** Commerce events: `add_to_cart`, `cart_view`, `checkout_started`, `payment_started`, `payment_success/failed`, `order_created`. **M** +- [ ] **A5** Operations metrics: `order_paid_to_notification` latency, fulfillment time, connector lag, payment webhook lag. **M** +- [ ] **A6** Quality metrics: frontend/backend errors, checkout validation failures, FX stale-rate blocks. **M** +- [ ] **A7** Real funnel dashboard in backoffice, replacing the mock-composed Analytics facade. **L** +- [ ] **A8** **Synthetic traffic technically separated** from production analytics — staging/test only, never presented as real visits (§3.1, §6.3). **M** +- [ ] **A9** Real product view counts — the shipped "Views" column always renders `0`. Either bridge to the live storefront `Item.visits` or serve it from the real Products backend. **S** +- [ ] **A10** Post-launch monitoring set (§13.3): checkout conversion, payment success/failure, webhook lag, order-notification lag, connector lag, FX quote age, unmatched reconciliation, stuck fulfillment. **L** +- [ ] **A11** Trending search terms endpoint — `loadTrending()` is a stub returning `of(null)`. **S** + +### Track S — Security, RBAC, audit (§4.4, §10) + +**Gate on Phase 5 and on the launch gate.** Today the role model is decorative: types exist, nothing gates any button, page or action. Anyone who authenticates has full access. + +- [ ] **S1** Enforce RBAC backend-side with tenant scope on every request. **L** +- [ ] **S2** Implement the 17 roles across 3 scopes (5 platform / 7 marketplace / 5 seller). **L** +- [ ] **S3** Frontend permission guards on routes and actions — currently zero. **M** +- [ ] **S4** Audit log covering permissions, seller changes, catalog moderation, price, payment/refund, manual order actions, integrations, production launch. `audit` appears only as mock display fields today. **L** +- [ ] **S5** Backoffice **Audit & Security** section (missing from nav): role changes, sensitive actions, login/security events, exports. **M** +- [ ] **S6** Step-up authentication for sensitive financial actions. **M** +- [ ] **S7** Rate limits and abuse controls on storefront/auth/provider endpoints; client-side 429 handling (zero today). **M** +- [ ] **S8** Secret storage for provider/connector credentials, scoped per marketplace/seller. **M** +- [ ] **S9** PII minimisation: store only necessary customer data, restrict access and export. **M** +- [ ] **S10** Ed25519 admin auth backend — wired client-side, 404s today. Decide: build it, or drop it for the plan's conventional RBAC. **DEC** + **L** +- [ ] **S11** HttpOnly session cookie (existing frontend-blocked TODO). **M** + +### Track Q — QA & E2E (§13) + +The plan's entire Definition of Done is end-to-end. We have **zero** E2E tests and ~32% statement / ~19% branch coverage across 11 spec files. + +- [ ] **Q1** Stand up an E2E harness (Playwright or equivalent) — none exists. **L** +- [ ] **Q2** Solve automated admin login; several past "verified live" claims were code-inspection only because `/edit` and `/backoffice` need Telegram login. **M** +- [ ] **Q3** E2E: full §13.1 acceptance path — seller → catalog → storefront → cart → checkout → payment → order → notification → fulfillment. **XL** +- [ ] **Q4** E2E: currency switch recalculates by FX quote — explicitly, `160 RUB` must not become `160 USD/AMD`. **M** +- [ ] **Q5** E2E: repeat webhook and double-click create exactly one order. **M** +- [ ] **Q6** E2E: external marketplace purchase imports and notifies. **M** +- [ ] **Q7** Facade tests for cart/checkout, moderation, Orders, Products, Users, Transactions, Monitoring — the domains about to get real backends carry the most regression risk with the least coverage. **L** +- [ ] **Q8** Regression pattern for reactive flag/config reads that must track `bootstrapRevision()` — this bug class already bit us once and was invisible until specifically hunted. **S** +- [ ] **Q9** Set a justified coverage floor and a CI gate. Deliberately unset today. **M** +- [ ] **Q10** One real screen-reader pass (NVDA/VoiceOver). Never performed on this codebase — every accessibility claim to date is automated tree inspection only. **M** + +### Track N — API namespace migration (§9.3) + +Cheapest now, more expensive every phase. Decision in Sprint 0.1. + +- [ ] **N1** Adopt `/api/v2/storefront/*`, `/api/admin/v2/*`, `/api/seller/v1/*`, `/api/identity/v1/*`, `/api/providers/v1/*`, `/api/integrations/v1/*`. **L** `[BOTH]` +- [ ] **N2** Migrate today's flat unversioned endpoints (`/cart`, `/orders`, `/items`, `/category`, `/searchitems`) plus the separate `qrApiUrl` host. **L** +- [ ] **N3** Agree the structured error envelope; today no interceptor reads error bodies at all. **M** (implementation lands in Sprint 0.2) + +### Track Z — Pre-existing repo debt + +Not in the plan, but real. Fold into whichever phase touches the same surface. + +- [ ] **Z1** Dark-mode selector does nothing — nothing reads `data-theme-mode`. **S** +- [ ] **Z2** "Site Layout" selector has no effect — `layout.type` is edited but never read. **S** +- [ ] **Z3** Footer "Contacts" link has no content behind it. **S** +- [ ] **Z4** `SeoService.setItemMeta()` exists but is **never called** — product pages ship only site-wide meta. **S** +- [ ] **Z5** `og:locale` hardcoded to `ru_RU` regardless of active locale. **S** +- [ ] **Z6** No JSON-LD structured data, no sitemap generation. **M** +- [ ] **Z7** Hardcoded Russian payment-description fallback (`'Покупка на Маркетплейсе'`) in a multi-tenant product. **S** +- [ ] **Z8** Brand colours fail WCAG AA — `--border-color` at 1.24–1.42:1 against a 3:1 requirement; status colours fail 4.5:1 as text. **Needs theme-owner sign-off, not just a code fix.** **M** +- [ ] **Z9** Literal hex `#cdd6d5` in `stars.component.scss:10` with no token behind it. **S** +- [ ] **Z10** Two large lazy chunks unaddressed: `project-editor` (~1.0 MB), `catalog-container` (~330–375 kB). Profile under real backend latency, not instant mock responses. **M** +- [ ] **Z11** `navigation.header` is editable in the builder with zero runtime consumer — needs a product decision, not a wiring fix. **DEC** +- [ ] **Z12** `catalog.navigationMode` renders a deliberate placeholder; the mega-menu / carousel / left-nav variants it implies do not exist. **DEC** +- [ ] **Z13** `sellerId` typed as bare `string` instead of the `UUID` alias used elsewhere. **S** +- [ ] **Z14** No shared breadcrumb component; the only breadcrumb logic is a local signal in the catalog container. **S** +- [ ] **Z15** Duplicate search models under two module paths. **S** +- [ ] **Z16** Consolidate the eight cross-linked Seller Management documents — at least three independently restate the same undecided unified-vs-split orders question. Do this **before** Phase 5 starts. **M** +- [ ] **Z17** Angular 22 upgrade — researched, not started; needs a dependency fix and a Node bump. **Its own dedicated session, never bundled with feature work.** **M** + +--- + +## Critical path + +``` +Sprint 0.1 (decisions) + └─> Sprint 0.2 (seams) + └─> Phase 1 (money truth) ──────────────┐ + └─> Phase 2 (orders + notif) │ + └─> Phase 3 (offer split) │ + └─> Phase 4 (external ingestion) + └─> 🚦 LAUNCH GATE +Track A (analytics) ── starts at Phase 1, gates the launch ──┘ +Track S (RBAC/audit) ── starts at Phase 2, gates the launch ──┘ +Track Q (E2E) ── starts at Phase 1, evidences the gate ┘ +``` + +Phases 5–10 all sit behind the launch gate and can be resequenced by business priority. Phases 1–4 cannot. + +**Single hardest dependency:** Phase 1 Sprint 1.3 needs the payment chain unfrozen. If that answer is "no", the programme stops at Sprint 0.2 and the plan's P0s cannot be delivered — that outcome should go back to them in writing, not be worked around. diff --git a/docs/PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md b/docs/PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md new file mode 100644 index 0000000..56a79fd --- /dev/null +++ b/docs/PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md @@ -0,0 +1,277 @@ +# Product Plan v3.1 — What They Want vs. What We Have + +**Source:** `Marketplaces-Platform-Product-Plan-v3.1.pdf` (27 pages, RU). Version block inside still reads `3.0 / 17 августа 2026` — the filename says v3.1. Section 14 is the v3.1 addition (appended after the document's own conclusion). + +**Our side, as verified in this repo:** Angular frontend only (426 `.ts` files). Sources for "what we have": [BACKEND-API-REFERENCE.md](../BACKEND-API-REFERENCE.md), [GAPS-AND-IMPROVEMENTS.md](../GAPS-AND-IMPROVEMENTS.md), and direct source inspection. + +--- + +## 1. What they are actually asking for + +One sentence: **stop building storefronts, build a platform** — a single multi-tenant commerce core where launching a new marketplace is a configuration act, not an engineering project. + +Their own acceptance bar (§"ГЛАВНЫЙ КРИТЕРИЙ" and §13): + +> A real product walks the whole path: seller → catalog → storefront → cart → checkout → payment → order → notification → fulfillment → reconciliation. + +Three things the document is really about, under the product language: + +1. **They do not trust our numbers.** Traffic counters, payment timings, order totals and currency amounts are all called out as unexplainable. §10.2 says it outright: don't fix appearance, fix the data. +2. **They suspect demo behaviour in production.** "No fixed 5-second payment", "no synthetic traffic in production analytics", "no special branch for banks/inspectors" (§3.2, §3.1, §3.6, §10.2, and again in the launch checklist). This is an audit/compliance posture, not a feature request — a bank or NSPK is checking this platform. +3. **Commerce Core is no longer optional.** In v3.0 language, Catalog/Seller Portal/Cart/Checkout/Payments/Orders stopped being "a possible extension" and became mandatory platform modules. Gorbushka is demoted to "one tenant scenario" (§11) — it does not define the architecture. + +**Launch blockers they define (§3, "LAUNCH BLOCKERS"):** all P0s — money/FX, payment timeline, notifications, external order ingestion, price traceability, guaranteed fulfillability of published offers. + +--- + +## 2. What is new in v3.1 vs v3.0 + +Everything in **§14 "Customer Identity и коммуникация после покупки"** (pages 26–27). Nothing else in the document is marked as changed. + +| New in v3.1 | Detail | Our state | +|---|---|---| +| **VK ID as primary social login** | Backend completes OAuth 2.1/PKCE, links external identity to `Customer` | Zero. No `vk` reference anywhere in source; one `oauth` reference total. | +| **MAX messenger bot** | Bot-assisted account linking via one-time code; official MAX Bot API | Zero. | +| **Telegram demoted** | Kept, but as *one* identity provider among several | Today Telegram is the **only** login for both customers and admins. | +| **Notification Orchestrator** | Routes `order.paid` to the customer's chosen channel; backoffice notification always fires even if the messenger is down | Zero. | +| **Delivery Conversation State Machine** | `not_started → awaiting_customer → details_received → manager_assigned/auto_confirmed → shipment_planned → completed`, bot collects delivery details, manager handoff | Zero. | +| **Channel choice in checkout** | "Where should we send confirmation?" — VK / MAX / Telegram / email-SMS fallback, recorded in `OrderContactSnapshot` | Zero. | +| **`ExternalIdentity` / `ContactChannel` / `BotConversationBinding` / `MessagingConsent`** | Four new entities | Zero. | + +**Manager note:** §14 partially collides with our approved [email/phone OTP login spec](superpowers/specs/2026-08-15-email-phone-login-design.md). v3.1 keeps email/phone but reduces them to *recovery/fallback* when a messenger is unavailable. Our in-flight work is still valid, but its priority drops below VK ID. Needs a call before that spec is implemented. + +--- + +## 3. The differences — detailed + +Legend: ✅ have · 🟡 partial / mock only · ❌ missing · ⚠️ conflicts with something we already decided. + +### 3.1 Platform components (§1.1) — 8 named components, we have 2 + +| Plan component | Our state | +|---|---| +| Storefront Runtime | ✅ Bootstrap-driven, tenant-configured, no per-project fork. This is our strongest match to the plan. | +| Platform Backoffice | 🟡 14 admin modules exist, but only **Categories** has a real HTTP backend. 9 of 11 admin domains inject their mock gateway directly — no DI seam to swap at all. | +| Platform API | 🟡 Storefront catalog/search/cart-payment are live; everything admin-side is mock. | +| Seller Portal | ❌ A static placeholder page, feature flag `false` by default, zero backend bytes, zero `HttpClient` reference. | +| Workers / Event Processing | ❌ Nothing. No event bus, no retry, no dead-letter. | +| Integration Hub | ❌ Nothing. Zero `reconcil*`, zero `idempot*` in the whole codebase. | +| Domain Automation | ❌ Nothing. Zero `hostinger` references — the plan's §8.2 lists seven Hostinger DNS endpoints we have never touched. | +| Marketplace Registry / Launch Center | ❌ Nothing shipped. Closest thing is our unshipped [super-admin Phase 1 design](superpowers/specs/superuser.md), which covers cross-tenant *viewing* but not registry/feature-set/launch. | + +### 3.2 Catalog model (§2.1) — the biggest structural gap + +The plan's core catalog idea is a **two-layer split**: `Product` (content card) vs. `Offer/Listing` (the seller's commercial proposition, which owns price, stock, currency, status). Order lines then snapshot the offer. + +| Plan entity | Our state | +|---|---| +| `Product` / `Variant` / `SKU` | 🟡 Exists as admin mock + a separate live storefront `Item` domain. Two unrelated `Category` types, both fed by the same response, both in use. | +| `Offer / Listing` | ❌ Does not exist. Price and stock hang off the product. Multi-seller pricing on one product card is not expressible. | +| `PriceSnapshot` | ❌ Does not exist. | +| `InventoryRecord` (available/reserved/sold) | ❌ Does not exist. No reservations, no TTL, no oversell queue. | +| `PriceHistory` | ❌ Does not exist. | +| Draft → moderation → published → paused/archived | 🟡 An admin Moderation module exists, on mock data. | +| Bulk import CSV/API with pre-apply error preview | ❌ Only bulk *edit* actions inside Admin Categories. No import pipeline. | +| "Storefront search/filters run on published data, not local mock arrays" | ⚠️ Directly aimed at us. `PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY` silently always resolve to the real API — but Search, wishlist/compare, cart contents and CMS are entirely `localStorage`. | + +### 3.3 Money, FX and price traceability (§2.3, §3.3, §3.8, §7) + +This is where the plan is most explicit, and where we most clearly do the forbidden thing. + +| Plan requirement | Our state | +|---|---| +| `Money = amountMinor + currency`, **no float for money math** | ⚠️ We use plain `number` prices and float division/multiplication in `CurrencyRatesService.convert()`. | +| Rates come from a configurable **external source** with `source`, `rate`, `timestamp`, `TTL` | ⚠️ Rates are **hand-typed by an admin** into Admin Settings and stored in **browser `localStorage`** (`currencyRates.v1`), with hardcoded fallbacks (`USD: 0.011`, `AMD: 4.3`). They never update and drift from market. | +| `FxQuote { base, quote, rate, source, observedAt, expiresAt, quoteId }` | ❌ Does not exist. | +| Stale-quote control blocks checkout | ❌ Does not exist. | +| Checkout writes an immutable price snapshot; old orders never recalculated | ❌ Does not exist. | +| `PriceBook` (base currency + allowed display/checkout currencies) | ❌ Does not exist. | +| Backoffice shows the total formula: lines × qty − discounts + delivery + fees, plus the FX quote used | ❌ Does not exist. | +| Reconciliation of internal orders vs. provider transactions | ❌ Does not exist (`reconcil*` = 0 hits repo-wide). | + +**Nuance worth telling them:** their §3.3 complaint is *"switching RUB/USD/AMD keeps the same number"*. Our storefront **does** convert the displayed number. Their real, unstated problem is the one our own [§12.7](../BACKEND-API-REFERENCE.md) already flagged: the **charged** amount is computed client-side in RUB and posted to `/cart` as `amount`, so bank settlement totals don't reconcile against order counts. We agree with the plan here — we raised it first. + +### 3.4 Cart and Checkout (§2.5, §2.6) — ⚠️ head-on conflict with a frozen system + +| Plan requirement | Our state | +|---|---| +| Cart is **server-side**, keyed on `offerId` | ⚠️ Cart is `localStorage` + Telegram CloudStorage. There is no backend cart at all. | +| "Client never sends a trusted price to the server" | ⚠️ `CartPaymentRequest` sends `amount`, `currency`, and a per-item `price` array from the browser. This is exactly the pattern the plan forbids. | +| Checkout is a **server session** producing a price snapshot + contact snapshot | ❌ Checkout is an inline popup in `pages/cart/cart.component.ts` (751 lines). `features/website/checkout/` is an empty directory. | +| Idempotent order creation keyed on the payment | ❌ `/orders` is called fire-and-forget after payment success. Zero `idempot*` in the codebase. | +| Backend re-validates offers/stock at checkout | ❌ No stock concept exists to validate. | +| Multi-seller cart grouped by seller and fulfillment rules | ❌ Undefined behaviour — already flagged in our own gaps doc. | +| No duplicate payment intents on double-click | 🟡 Popup state guards the UI; nothing server-side. | + +**Blocker:** [BACKEND-API-REFERENCE.md §7](../BACKEND-API-REFERENCE.md) states *"Payments are frozen — this call chain is explicitly out of scope for changes."* The plan's P0-A and P0-C cannot be delivered without unfreezing it. **This needs an explicit decision from whoever froze it.** + +### 3.5 Payments (§2.7, §3.2) + +| Plan requirement | Our state | +|---|---| +| Explicit state machines: `PaymentIntent` / `Payment` / `Order` | ❌ None. Payment status is a client-side signal with values `creating/waiting/success/timeout/error`. | +| Webhook signature verification + idempotency | ❌ None. `webhook` appears only as a display field in the admin **monitoring mock**. | +| Store `provider event id`, `provider timestamp`, `receivedAt`, `processedAt` | ❌ None. | +| "No artificial fixed delays" | ✅ **We already comply.** We poll real provider status (`/qr/dynamic/{partnerId}/{qrId}`, `/card/{partnerId}/{orderId}`) on an interval bounded by the QR TTL. There is no 5-second timer in this codebase. | +| Refunds as a first-class operation with reason/actor/order-line link | ❌ `requestRefund(id)` exists only as a mock gateway method. | +| Reconciliation queue | ❌ None. | + +**Ask them:** §3.2 describes a fixed 5-second payment. We cannot reproduce it here. Either they observed a different build/environment, or they inferred it from the *admin* mock data. Worth pinning down before we spend P0 budget on a problem that may not be ours. + +### 3.6 Orders and Fulfillment (§2.8, §3.6) + +| Plan requirement | Our state | +|---|---| +| Canonical `Order` regardless of source (storefront / external marketplace / backoffice / API partner) | ❌ Admin Orders is a **static 24-row in-memory seed with no create path**, and no DI token to swap it. | +| `OrderLine` with SKU/title/price snapshots | ❌ | +| `Source mapping` (`externalMarketplace`, `externalOrderId`, `connectorId`) | ❌ | +| `Fulfillment` (manual / warehouse / pickup / digital) with evidence | ❌ One `fulfil*` hit in the entire codebase. | +| `Timeline` of all order events | ❌ Already logged as our own frontend-blocked TODO ("Real order audit trail"). | +| Admin actions: assign, resend notification, replay sync, cancel/refund by permission | ❌ | +| **No special branch for inspectors — any published, available product must be genuinely buyable and fulfillable** | ❌ We have no publish-time executability validation and no fulfillment flow, so we cannot currently *prove* compliance either way. | + +### 3.7 Customer identity (§2.9, §3.4, §14) + +| Plan requirement | Our state | +|---|---| +| `Customer` + multiple `ExternalIdentity` + verified `ContactMethod` | ❌ Telegram user is effectively the customer identity. | +| `emailVerifiedAt` / `phoneVerifiedAt` / `telegramLinkedAt` | ❌ | +| Order contact snapshot, immutable after order creation | ❌ | +| Email/phone OTP | 🟡 **Designed, not built** — spec approved 2026-08-15. | +| VK ID / MAX | ❌ New in v3.1, nothing exists. | +| Guest checkout toggled by tenant policy | ❌ | + +### 3.8 Notifications (§2.10, §3.5) + +| Plan requirement | Our state | +|---|---| +| Platform event bus emitting `order.created` / `order.paid` / `payment.failed` / `webhook.error` / `stock.low` / `oversell` / `refund.*` / `external_order.imported` | ❌ | +| Notification with `unread/read`, `severity`, `marketplaceId`, entity type/id, **deep link** | 🟡 `AdminOrderWatcherService` polls for new orders and toasts/badges the admin — the right shape, wrong data source. | +| Unread counter + filter by marketplace / event type in backoffice | 🟡 Partial (counter yes, marketplace filter no). | +| External channel delivery status logged; a Telegram/email failure must not lose the internal notification | ❌ | + +**Status:** the notification feature is built and **functionally inert** — it polls the mock Orders gateway, which has no create path, so no new order can ever appear. It starts working the day Orders gets a real backend, with no further frontend change. + +### 3.9 Analytics (§3.1, §6.3) + +| Plan requirement | Our state | +|---|---| +| Server-side event logging: `session_started`, `page_view`, `product_view`, `add_to_cart`, `checkout_started`, `payment_started/success/failed`, `order_created` | ❌ **No tracking pipeline exists at all.** Not a missing endpoint — missing infrastructure. Our own docs rate it the single largest remaining backend effort. | +| Operational metrics: notification latency, fulfillment time, connector lag, webhook lag | ❌ | +| Quality metrics: frontend/backend errors, checkout validation failures, FX stale blocks | ❌ | +| Real funnel in backoffice | ❌ Admin Analytics composes five mock gateways and has no data source. | +| Synthetic traffic technically separated from production analytics | ⚠️ Cannot comply — there is no production analytics to separate it from. | +| Product view counts | 🟡 A "Views" column was shipped in Admin Products; it always renders `0` because no tracking source exists. Storefront `Item.visits` is live-wired but displayed nowhere. | + +### 3.10 Backoffice navigation (§4.1) — 12 required sections, 5 missing outright + +Have (mock unless noted): Overview/Dashboard, Catalog (Categories real, Products mock), Orders, Payments partial (Transactions), Customers, Notifications partial, Content & Design (builder/CMS, `localStorage` only), Monitoring, Reports, Users, Settings. + +Missing entirely: + +- **Marketplaces** — registry, type, status, domains, currencies, feature set, responsible manager. Nothing. +- **Sellers** — organizations, applications, roles, listings, integration health. Placeholder page only. +- **Payments & Finance** — refunds, reconciliation, unmatched events, settlements. `settlement*` = 0 hits. +- **Integrations** — external connectors, payment providers, FX sources, messaging. Nothing. +- **Domains & Releases** — DNS/SSL, staging, production, health checks, rollback. Nothing. +- **Audit & Security** — role changes, sensitive actions, login/security events, exports. `audit` appears only as display fields on mock models. + +### 3.11 Roles and RBAC (§4.4, §10.1) — ⚠️ our most serious security gap + +The plan specifies three scopes and 17 named roles (5 platform, 7 marketplace, 5 seller). + +Our state: **the admin role model is decorative.** `AdminRole` and permissions exist as types, but nothing gates any button, page or action anywhere in the app. Anyone who passes admin authentication has full access. `AdminRole` is additionally defined twice with unrelated shapes. + +Also missing from §10.1: idempotency keys, rate-limit handling (429 has zero client-side handling), step-up authentication for financial actions, audit log, PII minimisation policy. + +### 3.12 External marketplace integrations (§5) — 0% built + +Nothing in this section exists in any form: connector contract, webhook-preferred/polling-fallback ingestion, raw event storage, normalizer, SKU mapping, unmatched queue, exponential retry, dead-letter, manual replay, reconciliation, connector observability, and the proposed SLA (99% of webhook events processed under 60s, zero duplicate orders). + +**Blocking unknown:** the plan never names which external marketplaces. Ozon? Wildberries? Yandex Market? Avito? Each is a separate connector with its own auth and rate limits. We cannot size this without the list. + +### 3.13 Domains, publishing and tenant launch (§8) + +| Plan requirement | Our state | +|---|---| +| Marketplace lifecycle `draft → configured → content_ready → domains_planned → staging_live → qa_passed → production_ready → live → paused/archived`, with the blocking item shown per transition | ❌ | +| DNS automation via Hostinger API (7 endpoints listed), snapshot + rollback, never touching MX/SPF/DKIM/DMARC/CAA, approval gate in production, propagation + SSL + health checks | ❌ Zero references. | +| Publish model: `draft → validation → preview → publish`, immutable published revision, rollback creates a new revision | 🟡 The builder edits an in-memory config and persists drafts to `localStorage`. "Publish" only promotes a local signal. No revisions, no server-side publish endpoint (`apiEndpoints.builder` is an empty placeholder). | +| Commerce data explicitly **not** part of content revisions | ✅ Structurally true today — orders/payments simply aren't in the revision at all. | + +### 3.14 API boundaries (§9.3) — ⚠️ a naming migration we have not planned + +Plan namespaces: `/api/v2/storefront/*`, `/api/admin/v2/*`, `/api/seller/v1/*`, `/api/identity/v1/*`, `/api/providers/v1/*`, `/api/integrations/v1/*`. + +Ours: unversioned, flat — `/cart`, `/orders`, `/items`, `/category`, `/searchitems`, plus a separate `qrApiUrl` host. Our own reference says **"No API versioning scheme has been decided"**. + +Adopting the plan's namespaces is a coordinated frontend+backend rename, not a config change. It should be sequenced *before* the new commerce endpoints are built, not after. + +Also in §9: the plan's error model assumes a structured envelope. Ours is a proposal only — no interceptor inspects error bodies today; every error reaction happens at raw HTTP-status level. + +### 3.15 Gorbushka as a tenant (§11) + +The plan lists mall-directory content entities: `Shop`, `ShopCategory`, `Service`, `Floor`, `SchemePin`, `RentListing`, `News/Promo`, `StaticPage`, `Lead`, `MallSettings` — each with `marketplaceId`, audit, and publish/preview. + +We have: static pages inside the bootstrap document. None of the other nine entity types exist, and CMS content has no backend write path at all. + +Positive read: the plan explicitly says Gorbushka must **not** dictate platform architecture, and that the existing frontend is UX reference only. That matches our ADR-0001 constraint ("frontend must not contain marketplace-specific code"). No conflict here — just unbuilt scope. + +### 3.16 Definition of Done (§13) — where we stand today + +Of the 13 launch-checklist items, we can currently claim **zero** as green. Additionally, our own QA position makes their DoD hard to evidence: + +- ~32% statement coverage, ~19% branch coverage, 11 spec files repo-wide. +- **Zero E2E tests** — no Playwright/Cypress config anywhere. The plan's acceptance criteria are all end-to-end by construction. +- Several past "verified live" claims were code-inspection only, because `/edit` and `/backoffice` require Telegram admin login that automated environments cannot complete. + +--- + +## 4. What we have that the plan does not account for + +Not gaps — assets and risks they should know about before sequencing: + +1. **Project editor / builder** (~1.0 MB lazy chunk) — a full visual site builder. The plan's §8.3 publish model would replace its persistence layer entirely. +2. **Ed25519 challenge/response admin auth** — fully wired client-side, backend returns 404 today. The plan never mentions it; it assumes conventional RBAC. +3. **Widget manifest / dynamic renderer** — the mechanism that makes one storefront runtime serve many tenants. This is the part of the plan we have *already* solved and should defend. +4. **Super-admin Phase 1 design** (`docs/superpowers/specs/superuser.md`) — cross-tenant read-only view. Overlaps §4.3 Marketplace Registry and §10 audit. Worth re-scoping against the plan rather than building as specified. +5. **Three in-flight items already answer v3.0 P0s:** admin purchase notifications (§3.5), admin product views column (§3.1), email/phone OTP login (§3.4). Two of the three are inert until a real backend exists. + +--- + +## 5. Manager's read — the honest framing + +**Split of ownership.** Roughly 80% of this document is backend and platform-service work: Platform API, Workers/Event Processing, Integration Hub, Domain Automation, payment state machines, reconciliation, analytics pipeline. This repository is a frontend. Of the plan's ~14 sections, only Storefront Runtime (§6.1) is substantially delivered, and it is delivered *well*. + +**The real message is trust, not features.** Every P0 in §3 is a variant of "we cannot explain your numbers." Sequencing should follow that: traceability first (money model, price snapshot, payment timeline, audit), feature breadth second. That happens to also be the plan's own P0-A ordering. + +**The largest single risk is not scope — it is the frozen payment chain.** Cart is client-owned, price is client-supplied, orders are fire-and-forget, and the whole chain is marked "do not modify." Three P0s sit behind that freeze. Nothing else in this list can be honestly estimated until that decision is reversed or explained. + +**Second risk: RBAC.** The plan assumes 17 enforced roles across three scopes. We enforce none. Any real admin backend going live before this is fixed hands full platform access to every authenticated operator. + +--- + +## 6. Decisions we need from them before estimating + +1. **Who owns the backend?** The plan assumes Platform API, Workers, Integration Hub and Domain Automation exist or will be built. None do. Same team, or a service team we integrate with? +2. **Is the payment chain unfrozen?** P0-A/P0-C are unbuildable otherwise. +3. **Which external marketplaces**, by name, for §5 connectors? +4. **Which FX rate source?** The plan says "configurable external source"; our earlier ask named Rapira as a candidate. Also: does the backend return converted prices, or serve rates for the client to apply? The plan implies the former. +5. **§14 vs. our approved email/phone OTP spec** — build VK ID first, or finish OTP first? +6. **Multi-seller orders: unified or split?** Undecided in three of our own documents; the plan requires cart-level seller grouping (§2.5) but never resolves the order-splitting question. +7. **Where does the "fixed 5-second payment" (§3.2) come from?** We cannot reproduce it in this codebase. +8. **API namespace migration (§9.3)** — do it now, before new endpoints, or accept two conventions? +9. **Document version** — the file is v3.1 but the version block says 3.0. Which is canonical for change tracking? + +--- + +## 7. Suggested first slice (if they want a proposal back) + +Following their own dependency order, restricted to what is buildable and provable: + +1. **Money model + FX quote + price snapshot** (P0-A) — needs the payment freeze lifted. Removes client-supplied `amount`, kills the float math, gives every total an explainable formula. This one item closes §3.3, §3.8 and half of §13.1. +2. **Order canonical model + timeline + notification wiring** (P0-B) — the notification feature already exists and switches on for free. +3. **RBAC enforcement** — not on their P0 list, but it is the gate on everything else in the backoffice going live safely. +4. **Analytics event pipeline** (P0/§3.1) — long lead time, so start it in parallel rather than last. + +Explicitly *not* in a first slice: Seller Portal, external connectors, domain automation, VK/MAX bots. All of them depend on the commerce core being real first, which is what the plan itself says in §12.1. diff --git a/src/app/features/admin/analytics/facade/admin-analytics.facade.ts b/src/app/features/admin/analytics/facade/admin-analytics.facade.ts index 466e83d..c1d41bf 100644 --- a/src/app/features/admin/analytics/facade/admin-analytics.facade.ts +++ b/src/app/features/admin/analytics/facade/admin-analytics.facade.ts @@ -13,10 +13,10 @@ import { AdminRecentActivityEntry, AdminRecommendationCard, } from '../models/admin-analytics.model'; -import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway'; -import { AdminProductsLocalGateway } from '../../products/services/admin-products-local.gateway'; +import { ADMIN_ORDERS_GATEWAY } from '../../orders/services/admin-orders-gateway.token'; +import { ADMIN_PRODUCTS_GATEWAY } from '../../products/services/admin-products-gateway.token'; import { ADMIN_CATEGORIES_GATEWAY } from '../../categories/services/admin-categories-gateway.token'; -import { AdminModerationLocalGateway } from '../../moderation/services/admin-moderation-local.gateway'; +import { ADMIN_MODERATION_GATEWAY } from '../../moderation/services/admin-moderation-gateway.token'; import { AdminDashboardFacade } from '../../dashboard/facade/admin-dashboard.facade'; import { AdminOrder } from '../../orders/models/admin-order.model'; import { AdminProduct } from '../../products/models/admin-product.model'; @@ -31,10 +31,10 @@ import { AdminReview } from '../../moderation/models/admin-review.model'; */ @Injectable({ providedIn: 'root' }) export class AdminAnalyticsFacade { - private readonly ordersGateway = inject(AdminOrdersLocalGateway); - private readonly productsGateway = inject(AdminProductsLocalGateway); + private readonly ordersGateway = inject(ADMIN_ORDERS_GATEWAY); + private readonly productsGateway = inject(ADMIN_PRODUCTS_GATEWAY); private readonly categoriesGateway = inject(ADMIN_CATEGORIES_GATEWAY); - private readonly moderationGateway = inject(AdminModerationLocalGateway); + private readonly moderationGateway = inject(ADMIN_MODERATION_GATEWAY); private readonly dashboardFacade = inject(AdminDashboardFacade); readonly dateRange = signal(30); diff --git a/src/app/features/admin/customers/facade/admin-customers.facade.ts b/src/app/features/admin/customers/facade/admin-customers.facade.ts index b58dbfc..ee8d52f 100644 --- a/src/app/features/admin/customers/facade/admin-customers.facade.ts +++ b/src/app/features/admin/customers/facade/admin-customers.facade.ts @@ -1,12 +1,12 @@ import { Injectable, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; -import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway'; +import { ADMIN_ORDERS_GATEWAY } from '../../orders/services/admin-orders-gateway.token'; import { AdminOrder } from '../../orders/models/admin-order.model'; import { AdminCustomer } from '../models/admin-customer.model'; @Injectable({ providedIn: 'root' }) export class AdminCustomersFacade { - private readonly ordersGateway = inject(AdminOrdersLocalGateway); + private readonly ordersGateway = inject(ADMIN_ORDERS_GATEWAY); readonly customers = signal([]); readonly loading = signal(false); diff --git a/src/app/features/admin/moderation/facade/admin-moderation.facade.ts b/src/app/features/admin/moderation/facade/admin-moderation.facade.ts index 07d151c..b4964bb 100644 --- a/src/app/features/admin/moderation/facade/admin-moderation.facade.ts +++ b/src/app/features/admin/moderation/facade/admin-moderation.facade.ts @@ -2,7 +2,7 @@ import { Injectable, computed, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { AdminReview, AdminReviewListFilters, AdminReviewStatus } from '../models/admin-review.model'; import { AdminReport, AdminReportStatus } from '../models/admin-report.model'; -import { AdminModerationLocalGateway } from '../services/admin-moderation-local.gateway'; +import { ADMIN_MODERATION_GATEWAY } from '../services/admin-moderation-gateway.token'; import { LocalStorageService } from '../../../../core/storage/local-storage.service'; export type AdminModerationViewMode = 'table' | 'cards'; @@ -38,7 +38,7 @@ const COLUMNS_KEY = 'admin-moderation:visible-columns'; @Injectable({ providedIn: 'root' }) export class AdminModerationFacade { - private readonly gateway = inject(AdminModerationLocalGateway); + private readonly gateway = inject(ADMIN_MODERATION_GATEWAY); private readonly localStorage = inject(LocalStorageService); readonly filters = signal({ search: '', status: 'all', rating: 'all', page: 1, pageSize: 10 }); diff --git a/src/app/features/admin/moderation/services/admin-moderation-gateway.token.ts b/src/app/features/admin/moderation/services/admin-moderation-gateway.token.ts new file mode 100644 index 0000000..bf313ce --- /dev/null +++ b/src/app/features/admin/moderation/services/admin-moderation-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { AdminModerationGateway } from './admin-moderation-gateway.interface'; +import { AdminModerationLocalGateway } from './admin-moderation-local.gateway'; + +/** Swap point for a real Moderation backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */ +export const ADMIN_MODERATION_GATEWAY = new InjectionToken('ADMIN_MODERATION_GATEWAY', { + providedIn: 'root', + factory: () => inject(AdminModerationLocalGateway), +}); diff --git a/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts b/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts index a58b2b6..c581086 100644 --- a/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts +++ b/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts @@ -1,11 +1,11 @@ import { Injectable, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { AdminMonitoringEvent, AdminMonitoringEventFilters, AdminQueue, AdminWebhookDelivery } from '../models/admin-monitoring.model'; -import { AdminMonitoringLocalGateway } from '../services/admin-monitoring-local.gateway'; +import { ADMIN_MONITORING_GATEWAY } from '../services/admin-monitoring-gateway.token'; @Injectable({ providedIn: 'root' }) export class AdminMonitoringFacade { - private readonly gateway = inject(AdminMonitoringLocalGateway); + private readonly gateway = inject(ADMIN_MONITORING_GATEWAY); readonly filters = signal({ category: 'all', search: '' }); readonly events = signal([]); diff --git a/src/app/features/admin/monitoring/services/admin-monitoring-gateway.token.ts b/src/app/features/admin/monitoring/services/admin-monitoring-gateway.token.ts new file mode 100644 index 0000000..6689199 --- /dev/null +++ b/src/app/features/admin/monitoring/services/admin-monitoring-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { AdminMonitoringGateway } from './admin-monitoring-gateway.interface'; +import { AdminMonitoringLocalGateway } from './admin-monitoring-local.gateway'; + +/** Swap point for a real Monitoring backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */ +export const ADMIN_MONITORING_GATEWAY = new InjectionToken('ADMIN_MONITORING_GATEWAY', { + providedIn: 'root', + factory: () => inject(AdminMonitoringLocalGateway), +}); diff --git a/src/app/features/admin/orders/facade/admin-orders.facade.ts b/src/app/features/admin/orders/facade/admin-orders.facade.ts index 14a1d39..3761f6b 100644 --- a/src/app/features/admin/orders/facade/admin-orders.facade.ts +++ b/src/app/features/admin/orders/facade/admin-orders.facade.ts @@ -1,7 +1,7 @@ import { Injectable, computed, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { AdminOrder, AdminOrderListFilters, AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model'; -import { AdminOrdersLocalGateway } from '../services/admin-orders-local.gateway'; +import { ADMIN_ORDERS_GATEWAY } from '../services/admin-orders-gateway.token'; import { LocalStorageService } from '../../../../core/storage/local-storage.service'; export type AdminOrdersViewMode = 'table' | 'cards'; @@ -30,7 +30,7 @@ const COLUMNS_KEY = 'admin-orders:visible-columns'; @Injectable({ providedIn: 'root' }) export class AdminOrdersFacade { - private readonly gateway = inject(AdminOrdersLocalGateway); + private readonly gateway = inject(ADMIN_ORDERS_GATEWAY); private readonly localStorage = inject(LocalStorageService); readonly filters = signal({ search: '', status: 'all', page: 1, pageSize: 10 }); diff --git a/src/app/features/admin/orders/services/admin-orders-gateway.token.ts b/src/app/features/admin/orders/services/admin-orders-gateway.token.ts new file mode 100644 index 0000000..f916580 --- /dev/null +++ b/src/app/features/admin/orders/services/admin-orders-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { AdminOrdersGateway } from './admin-orders-gateway.interface'; +import { AdminOrdersLocalGateway } from './admin-orders-local.gateway'; + +/** Swap point for a real Orders backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */ +export const ADMIN_ORDERS_GATEWAY = new InjectionToken('ADMIN_ORDERS_GATEWAY', { + providedIn: 'root', + factory: () => inject(AdminOrdersLocalGateway), +}); diff --git a/src/app/features/admin/products/facade/admin-products.facade.ts b/src/app/features/admin/products/facade/admin-products.facade.ts index 2eaa9eb..72b1c94 100644 --- a/src/app/features/admin/products/facade/admin-products.facade.ts +++ b/src/app/features/admin/products/facade/admin-products.facade.ts @@ -2,7 +2,7 @@ import { Injectable, computed, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { AdminProduct, AdminProductCategoryOption, AdminProductEditorMode, AdminProductListFilters } from '../models/admin-product.model'; import { AdminProductsFormFactory } from '../services/admin-products-form.factory'; -import { AdminProductsLocalGateway } from '../services/admin-products-local.gateway'; +import { ADMIN_PRODUCTS_GATEWAY } from '../services/admin-products-gateway.token'; import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade'; import { LocalStorageService } from '../../../../core/storage/local-storage.service'; @@ -41,7 +41,7 @@ export type AdminProductColumn = typeof ALL_PRODUCT_COLUMNS[number]; @Injectable({ providedIn: 'root' }) export class AdminProductsFacade { - private readonly gateway = inject(AdminProductsLocalGateway); + private readonly gateway = inject(ADMIN_PRODUCTS_GATEWAY); private readonly formFactory = inject(AdminProductsFormFactory); private readonly projectEditor = inject(ProjectEditorFacade); private readonly localStorage = inject(LocalStorageService); diff --git a/src/app/features/admin/products/services/admin-products-gateway.token.ts b/src/app/features/admin/products/services/admin-products-gateway.token.ts new file mode 100644 index 0000000..604d05c --- /dev/null +++ b/src/app/features/admin/products/services/admin-products-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { AdminProductsGateway } from './admin-products-gateway.interface'; +import { AdminProductsLocalGateway } from './admin-products-local.gateway'; + +/** Swap point for a real Products backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */ +export const ADMIN_PRODUCTS_GATEWAY = new InjectionToken('ADMIN_PRODUCTS_GATEWAY', { + providedIn: 'root', + factory: () => inject(AdminProductsLocalGateway), +}); diff --git a/src/app/features/admin/shell/services/admin-order-watcher.service.ts b/src/app/features/admin/shell/services/admin-order-watcher.service.ts index f9a8472..b7509a2 100644 --- a/src/app/features/admin/shell/services/admin-order-watcher.service.ts +++ b/src/app/features/admin/shell/services/admin-order-watcher.service.ts @@ -1,6 +1,6 @@ import { Injectable, Signal, computed, effect, inject, signal } from '@angular/core'; import { AdminOrder } from '../../orders/models/admin-order.model'; -import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway'; +import { ADMIN_ORDERS_GATEWAY } from '../../orders/services/admin-orders-gateway.token'; import { LocalStorageService } from '../../../../core/storage/local-storage.service'; import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service'; import { LanguageService } from '../../../../services/language.service'; @@ -19,7 +19,7 @@ const TOAST_DURATION_MS = 4000; @Injectable({ providedIn: 'root' }) export class AdminOrderWatcherService { - private readonly gateway = inject(AdminOrdersLocalGateway); + private readonly gateway = inject(ADMIN_ORDERS_GATEWAY); private readonly storage = inject(LocalStorageService); private readonly notifications = inject(UserNotificationService); private readonly languageService = inject(LanguageService); diff --git a/src/app/features/admin/transactions/facade/admin-transactions.facade.ts b/src/app/features/admin/transactions/facade/admin-transactions.facade.ts index da8de06..2b467f6 100644 --- a/src/app/features/admin/transactions/facade/admin-transactions.facade.ts +++ b/src/app/features/admin/transactions/facade/admin-transactions.facade.ts @@ -1,11 +1,11 @@ import { Injectable, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { AdminTransaction, AdminTransactionListFilters } from '../models/admin-transaction.model'; -import { AdminTransactionsLocalGateway } from '../services/admin-transactions-local.gateway'; +import { ADMIN_TRANSACTIONS_GATEWAY } from '../services/admin-transactions-gateway.token'; @Injectable({ providedIn: 'root' }) export class AdminTransactionsFacade { - private readonly gateway = inject(AdminTransactionsLocalGateway); + private readonly gateway = inject(ADMIN_TRANSACTIONS_GATEWAY); readonly filters = signal({ search: '', status: 'all', type: 'all', page: 1, pageSize: 10 }); readonly transactions = signal([]); diff --git a/src/app/features/admin/transactions/services/admin-transactions-gateway.token.ts b/src/app/features/admin/transactions/services/admin-transactions-gateway.token.ts new file mode 100644 index 0000000..314bed1 --- /dev/null +++ b/src/app/features/admin/transactions/services/admin-transactions-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { AdminTransactionsGateway } from './admin-transactions-gateway.interface'; +import { AdminTransactionsLocalGateway } from './admin-transactions-local.gateway'; + +/** Swap point for a real Transactions backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */ +export const ADMIN_TRANSACTIONS_GATEWAY = new InjectionToken('ADMIN_TRANSACTIONS_GATEWAY', { + providedIn: 'root', + factory: () => inject(AdminTransactionsLocalGateway), +}); diff --git a/src/app/features/admin/users/facade/admin-users.facade.ts b/src/app/features/admin/users/facade/admin-users.facade.ts index c324be7..d3acfcd 100644 --- a/src/app/features/admin/users/facade/admin-users.facade.ts +++ b/src/app/features/admin/users/facade/admin-users.facade.ts @@ -1,11 +1,11 @@ import { Injectable, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { AdminInvitation, AdminUserRoleRecord, AdminSession, AdminUser, AdminUserAuditEntry, AdminUserScope, AdminUserStatus } from '../models/admin-user.model'; -import { AdminUsersLocalGateway } from '../services/admin-users-local.gateway'; +import { ADMIN_USERS_GATEWAY } from '../services/admin-users-gateway.token'; @Injectable({ providedIn: 'root' }) export class AdminUsersFacade { - private readonly gateway = inject(AdminUsersLocalGateway); + private readonly gateway = inject(ADMIN_USERS_GATEWAY); readonly users = signal([]); readonly roles = signal([]); diff --git a/src/app/features/admin/users/services/admin-users-gateway.token.ts b/src/app/features/admin/users/services/admin-users-gateway.token.ts new file mode 100644 index 0000000..1ee7ae9 --- /dev/null +++ b/src/app/features/admin/users/services/admin-users-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { AdminUsersGateway } from './admin-users-gateway.interface'; +import { AdminUsersLocalGateway } from './admin-users-local.gateway'; + +/** Swap point for a real Users backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */ +export const ADMIN_USERS_GATEWAY = new InjectionToken('ADMIN_USERS_GATEWAY', { + providedIn: 'root', + factory: () => inject(AdminUsersLocalGateway), +}); From ffaa6d2a1c9714f9a908f664d8a6ad750cbe27fe Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:31:11 +0400 Subject: [PATCH 03/29] refactor: rename storefront CategoryApiModel; correct stale auth-error doc; add Phase 1 backend contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - models/category.model.ts: Category -> CategoryApiModel, disambiguated from core/categories/models/category-domain.model.ts's Category (admin domain shape). Removes a dead unused import in item.utils.ts along the way. Only live consumer was services/api.service.ts, updated in place. - BACKEND-API-REFERENCE.md §5: corrected two rows documenting the TOKEN_EXPIRED/INVALID_SIGNATURE auth-error bug as still open - the fix (reading error.error.code before falling back to HTTP status) is already in auth.service.ts. Doc was stale, not the code. - Sprint 0.2 audit: AdminRole duplication and the PRODUCT_DATA_PROVIDER/CATEGORY_REPOSITORY dead mock branches were already resolved in a prior pass - verified, no code change needed. - docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md: new wire contract for Money/FxQuote/PriceSnapshot/payment state machine, so backend can start Phase 1 the moment the frozen payment chain is unblocked. Co-Authored-By: Claude Sonnet 5 --- BACKEND-API-REFERENCE.md | 6 +- .../PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md | 228 ++++++++++++++++++ src/app/models/category.model.ts | 7 +- src/app/services/api.service.ts | 12 +- src/app/utils/item.utils.ts | 1 - 5 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md diff --git a/BACKEND-API-REFERENCE.md b/BACKEND-API-REFERENCE.md index 4d6e074..bd71f72 100644 --- a/BACKEND-API-REFERENCE.md +++ b/BACKEND-API-REFERENCE.md @@ -243,7 +243,7 @@ No cursor/keyset pagination exists anywhere. No server-side page-size cap is enf ## 5. Error model -**The frontend does not currently parse any backend error envelope for any real endpoint** — no interceptor inspects error responses; every caller reacts at the raw `HttpErrorResponse.status`/`.message` level. The one partial exception (Ed25519 admin auth) derives its error code from **HTTP status only**, ignoring any body field, which is itself a known bug (see below). Everything in this section is therefore a **recommended envelope to adopt going forward**, not something already wired end-to-end — apply it to new endpoints and treat the frontend gaps below as follow-up work, not something this doc can silently paper over. +**The frontend does not currently parse any backend error envelope for any real endpoint** — no interceptor inspects error responses; every caller reacts at the raw `HttpErrorResponse.status`/`.message` level. The one partial exception (Ed25519 admin auth) now reads `error.error.code` from the body when present (`authErrorCodeFromBackendCode()`), falling back to HTTP status only when no body code is sent. Everything in this section is therefore a **recommended envelope to adopt going forward**, not something already wired end-to-end — apply it to new endpoints and treat the frontend gaps below as follow-up work, not something this doc can silently paper over. ### The envelope @@ -281,8 +281,8 @@ No cursor/keyset pagination exists anywhere. No server-side page-size cap is enf | 503 (infra down) | `SERVICE_UNAVAILABLE` | Same "backend unavailable, retry" screen as 500, on the Ed25519 flow only. | | 503 (maintenance) | `MAINTENANCE_MODE` (+`maintenanceUntil`) | **No maintenance-mode concept exists in the frontend at all today.** Same HTTP status as infra-down 503 — `error.code` is the only way to distinguish them. | | 403 (tenant disabled) | `TENANT_DISABLED` | **No handling exists.** No code path today distinguishes "tenant exists but is disabled" from any other 403. | -| 401 (token expired) | `TOKEN_EXPIRED` | **Known bug, not just a gap:** the client has a dedicated "Session expired" screen wired and ready, but `toAuthErrorShape()` only reaches it via a no-refresh-token-present client-side branch — a *real* backend 401 on `/refresh` always renders the generic "Unauthorized" screen instead, because the mapping function ignores any body code and derives purely from HTTP status. Fix requires the backend to send `error.code: "TOKEN_EXPIRED"` **and** a small frontend change to prefer it. | -| 401 (bad signature) | `INVALID_SIGNATURE` | Same bug class as above — dedicated screen exists, unreachable from a real HTTP response for the identical reason. | +| 401 (token expired) | `TOKEN_EXPIRED` | **Fixed** — `toAuthErrorShape()` (`core/auth/services/auth.service.ts`) now reads `error.error.code` via `authErrorCodeFromBackendCode()` before falling back to HTTP status. A backend 401 on `/refresh` sending `error.code: "TOKEN_EXPIRED"` reaches the dedicated "Session expired" screen. | +| 401 (bad signature) | `INVALID_SIGNATURE` | **Fixed**, same mechanism — reaches the dedicated screen when the backend sends `error.code: "INVALID_SIGNATURE"`. | **Every admin backoffice list page** (Users/Orders/Monitoring/Moderation/Transactions/Products/Categories/Analytics/Customers/Dashboard) shares one generic pattern: a boolean `error` signal → "Something went wrong" + retry button. None of them branch on status or `code` today — every status above collapses into the same generic UI until facades are individually updated. diff --git a/docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md b/docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md new file mode 100644 index 0000000..85e384b --- /dev/null +++ b/docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md @@ -0,0 +1,228 @@ +# Phase 1 Backend Contract — Money, FX, Price Snapshot, Payment State Machine + +Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 1 (Sprints 1.1–1.4) and [PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md](../PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md) §3.3/§3.5/§3.6. + +**Status: blocked.** [BACKEND-API-REFERENCE.md §7](../../BACKEND-API-REFERENCE.md) marks the cart/payment call chain "frozen — explicitly out of scope for changes." This document specifies the target contract so backend work can start the moment that freeze lifts or is scoped around; it does not imply the freeze has been lifted. See open decision in the delivery plan's Sprint 0.1. + +This doc is the frontend's ask, in the same style as `BACKEND-API-REFERENCE.md`. It does not prescribe backend implementation (DB schema, service boundaries) — only the wire contract and the invariants the frontend needs to hold. + +--- + +## 1. Why this exists + +Current behaviour (`services/currency-rates.service.ts`, `pages/cart/cart.component.ts`): + +- Currency conversion rates are typed by an admin into Admin Settings and persisted to browser `localStorage`. They never update and drift from market. +- The amount charged is computed **client-side** and sent as `CartPaymentRequest.amount` to `POST /cart`. The backend currently trusts this number. +- No record exists anywhere of which FX rate produced a given displayed price, or when it was captured. + +Result: bank/NSPK settlement totals don't reconcile against order counts, because nothing on the backend can reconstruct *why* a given amount was charged. This document's contract exists to close that gap — it is the same complaint as Product Plan v3.1 §3.3/§3.8, and our own [§12.7](../../BACKEND-API-REFERENCE.md) raised it first. + +--- + +## 2. Money representation + +All money fields in every new endpoint below use minor units, never float. + +```ts +interface Money { + amountMinor: number; // integer, no float. 4990 = 49.90 for a 2-decimal currency. + currency: string; // ISO 4217, e.g. "RUB" | "USD" | "EUR" | "AMD" +} +``` + +| Currency | Minor unit | Decimals | +|---|---|---| +| RUB | kopeck | 2 | +| USD | cent | 2 | +| EUR | cent | 2 | +| AMD | luma | 2 | + +Rounding rule for any conversion: round half up to the currency's minor-unit precision, applied once, at the point of conversion — never re-rounded on redisplay. + +--- + +## 3. FX Quote + +### 3.1 Endpoint + +``` +GET /api/v2/pricing/fx-quote?base=RUB"e=USD +``` + +```json +{ + "quoteId": "fxq_8a3f1c2a", + "base": "RUB", + "quote": "USD", + "rate": 0.0108, + "source": "rapira", + "observedAt": "2026-08-20T09:14:00Z", + "expiresAt": "2026-08-20T09:19:00Z" +} +``` + +| Field | Notes | +|---|---| +| `quoteId` | Opaque, referenced by every `PriceSnapshot` that used this quote. | +| `rate` | `1 base = rate * quote`. Float is acceptable here — it's a market rate, not a money amount. | +| `source` | Adapter name. Frontend never hardcodes a provider; treat as an opaque label for display in the backoffice reconciliation panel. | +| `expiresAt` | TTL, provider-configurable. Frontend must not use an expired quote to display or charge. | + +### 3.2 Stale-quote policy + +- If the frontend holds a quote past `expiresAt`, it must re-fetch before checkout can proceed. +- If the rate source is unavailable, the backend decides: **block** (`503 SERVICE_UNAVAILABLE` with `error.code: "FX_SOURCE_UNAVAILABLE"`) or serve a configured fallback quote explicitly marked `"source": "fallback"`. Which policy applies is a tenant setting, not a frontend choice — see delivery-plan Sprint 0.1 decision on FX source. +- Outlier detection (e.g. a quote >X% off the previous one) is a backend concern; the frontend has no opinion on the threshold, only on obeying `expiresAt`. + +--- + +## 4. PriceSnapshot + +Created once, at checkout, immutable afterward. This is what makes a total explainable months later. + +```ts +interface PriceSnapshot { + id: string; + offerId: string; + amount: Money; // price in the offer's base currency + displayAmount: Money; // price in the currency the customer checked out in + fxQuoteId: string | null; // null when displayAmount.currency === amount.currency + capturedAt: string; // ISO 8601 +} +``` + +Rule: once a `PriceSnapshot` exists on an order line, it is never recalculated — not on rate update, not on currency-setting change, not on replay. An old order shows the price it was actually charged at. + +--- + +## 5. Server-authoritative checkout amount + +This is the contract change with the highest priority in Phase 1 — it removes the client-trusted `amount` field entirely. + +### 5.1 Current (to be replaced) + +```http +POST /cart +{ "amount": 4990, "currency": "RUB", "items": [{ "itemID": 101, "price": 4990, ... }], ... } +``` + +The backend trusts `amount` and each line's `price` as sent by the browser. + +### 5.2 Target + +```http +POST /api/v2/storefront/checkout +{ + "offers": [{ "offerId": "off_9a1", "qty": 2 }], + "currency": "USD", + "deliveryOptionId": "del_standard" +} +``` + +```json +{ + "checkoutSessionId": "chk_7f2e", + "lines": [ + { + "offerId": "off_9a1", + "qty": 2, + "unitPrice": { "amountMinor": 5390, "currency": "USD" }, + "lineTotal": { "amountMinor": 10780, "currency": "USD" }, + "priceSnapshotId": "snap_3b1c" + } + ], + "subtotal": { "amountMinor": 10780, "currency": "USD" }, + "discount": { "amountMinor": 0, "currency": "USD" }, + "delivery": { "amountMinor": 500, "currency": "USD" }, + "total": { "amountMinor": 11280, "currency": "USD" }, + "fxQuoteId": "fxq_8a3f1c2a", + "expiresAt": "2026-08-20T09:19:00Z" +} +``` + +**The frontend sends offer IDs and quantities. The backend computes every price, using the offer's live price and the current FX quote. No `amount` or `price` field is ever accepted from the client for anything that affects the charge.** + +`POST /api/v2/storefront/payments/intents` then references `checkoutSessionId` only — the amount charged is read server-side from the checkout session, never re-sent by the client. + +### 5.3 Total formula (must be reconstructable, per line) + +``` +order.total = sum(line.unitPrice * line.qty) + - discounts + + delivery + + taxes/fees (if applicable) +``` + +Backoffice must be able to render this formula, with the FX quote used, for any order — this is what Product Plan §7.2 asks for and what a bank reconciliation needs. + +--- + +## 6. Payment state machine + +### 6.1 States + +``` +PaymentIntent: created -> pending -> authorized/paid -> failed/cancelled +Payment: received -> confirmed -> captured/settled -> refunded/partially_refunded +Order: pending_payment -> paid -> processing -> fulfilled/completed +``` + +### 6.2 Required fields per transition + +```ts +interface PaymentEvent { + id: string; + paymentIntentId: string; + fromState: string; + toState: string; + providerEventId: string; // idempotency key from the provider + providerTimestamp: string; // when the provider says it happened + receivedAt: string; // when our webhook received it + processedAt: string; // when our system finished processing it +} +``` + +No fixed delays anywhere in this chain. The frontend already complies with this (polls real provider status via `/qr/dynamic/{partnerId}/{qrId}` and `/card/{partnerId}/{orderId}` on an interval bounded by QR TTL) — this section documents the backend side of the same principle. + +### 6.3 Webhook contract + +``` +POST /api/providers/v1/payments/{provider}/webhook +``` + +- Signature verification is mandatory; reject unsigned/invalid-signature payloads with `401`, do not silently accept. +- Idempotency key = `provider + providerEventId`. A repeated delivery of the same event must be a no-op — same `PaymentEvent` row, no second order, no second notification. +- On success, emit `payment.confirmed` / `payment.failed` onto the platform event bus (Phase 2) so Order creation is driven by the event, not by the webhook handler doing double duty. + +### 6.4 Idempotent order creation + +``` +POST /api/admin/v2/orders (internal, from the payment-confirmation handler) +Idempotency-Key: +``` + +A retried call with the same `checkoutSessionId` must return the existing order, not create a second one. This is the mechanism that makes "double-click doesn't create two orders" true regardless of frontend debouncing. + +--- + +## 7. What the frontend will stop doing once this ships + +- Delete `CurrencyRatesService`'s `localStorage`-persisted admin-typed rates and hardcoded `DEFAULT_RATES` fallback (`USD: 0.011`, `AMD: 4.3`). +- Delete the Admin Settings currency-rate editor UI. +- Stop sending `amount` / `price` in any checkout-related request. +- Replace client-side float conversion (`CurrencyRatesService.convert()`) with server-supplied `Money` values everywhere a price is displayed. + +## 8. What the frontend will start doing + +- Fetch `GET /api/v2/pricing/fx-quote` on currency switch; block checkout if the held quote has expired. +- Render the backoffice "total formula" panel (lines × qty − discounts + delivery + fees, FX quote used) once §5.2 and the admin Orders API exist (Phase 2). +- Surface `FX_SOURCE_UNAVAILABLE` and `error.code`-driven stale-quote UI per the error envelope in `BACKEND-API-REFERENCE.md §5`. + +--- + +## 9. Open questions (mirrors delivery-plan Sprint 0.1) + +1. Payment chain freeze — must be lifted or explicitly scoped around before §5 can ship. +2. FX rate source/provider — not named yet; `source` field above is provider-agnostic pending that answer. +3. Does the backend return already-converted prices, or does the frontend request a specific display currency at checkout time (as modeled in §5.2)? This doc assumes the latter; confirm before implementation. diff --git a/src/app/models/category.model.ts b/src/app/models/category.model.ts index 43ede8e..a531a05 100644 --- a/src/app/models/category.model.ts +++ b/src/app/models/category.model.ts @@ -1,6 +1,11 @@ import { ItemName } from './item.model'; -export interface Category { +/** + * Storefront-side raw category shape (item.service.ts/api.service.ts responses). + * Unrelated to core/categories/models/category-domain.model.ts's Category, which is + * the normalized admin-backoffice domain shape produced by CategoryMapper. + */ +export interface CategoryApiModel { categoryID: number; name: string; parentID: number; diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 48da298..4b5b77c 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable, timer } from 'rxjs'; import { map, retry } from 'rxjs/operators'; -import { Category, DeliveryOption, Item, Subcategory } from '../models'; +import { CategoryApiModel, DeliveryOption, Item, Subcategory } from '../models'; import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils'; import { environment } from '../../environments/environment'; import { ApiConfigService } from '../core/config/api-config.service'; @@ -199,7 +199,7 @@ export class ApiService { || (subcategory.subcategories?.length ?? 0) > 0; } - private isDisplayableCategory(category: Category): boolean { + private isDisplayableCategory(category: CategoryApiModel): boolean { return category.visible !== false; } @@ -462,8 +462,8 @@ export class ApiService { * Normalize a category from the API response — supports both * the flat legacy format and nested backOffice format. */ - private normalizeCategory(raw: any): Category { - const cat: Category = { ...raw }; + private normalizeCategory(raw: any): CategoryApiModel { + const cat: CategoryApiModel = { ...raw }; if (raw.id != null && raw.categoryID == null) { cat.id = String(raw.id); @@ -522,7 +522,7 @@ export class ApiService { return cat; } - private normalizeCategories(cats: any[] | null | undefined): Category[] { + private normalizeCategories(cats: any[] | null | undefined): CategoryApiModel[] { if (!cats || !Array.isArray(cats)) return []; return cats .map(c => this.normalizeCategory(c)) @@ -535,7 +535,7 @@ export class ApiService { return this.http.get<{ message: string }>(`${this.baseUrl}/ping`); } - getCategories(): Observable { + getCategories(): Observable { return this.http.get(`${this.baseUrl}/category`) .pipe(retry(this.retryConfig), map(cats => this.normalizeCategories(cats))); } diff --git a/src/app/utils/item.utils.ts b/src/app/utils/item.utils.ts index 256f91c..7715bfe 100644 --- a/src/app/utils/item.utils.ts +++ b/src/app/utils/item.utils.ts @@ -1,5 +1,4 @@ import { Item } from '../models'; -import { Category } from '../models/category.model'; export function getDiscountedPrice(item: Item): number { return item.price * (1 - (item.discount || 0) / 100); From 288c7ac33fba2c2946416736370858ea0208fe87 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:35:13 +0400 Subject: [PATCH 04/29] fix: wire dark-mode theme tokens for all three tenant themes (Z1) theme-engine.service.ts already sets [data-theme-mode] on the root, but no CSS anywhere consumed it - picking Dark/System never changed anything visually. Adds a structural dark override block per theme (dexar/lavero/ novo): bg/text/border/shadow tokens only. Brand colors (primary/secondary/ accent/gradients) are left untouched - a distinct dark-mode brand palette is a design decision for the theme owner, not made here. Also verified during this pass, no change needed (GAPS-AND-IMPROVEMENTS.md was stale on these): - og:locale already reads languageService.currentLanguage() dynamically - SeoService.setItemMeta() is already called from product-details-container - stars.component.scss already uses var(--border-color), no literal hex - sellerId is already typed UUID, no bare-string field remains Co-Authored-By: Claude Sonnet 5 --- src/styles/themes/dexar.theme.scss | 21 +++++++++++++++++++++ src/styles/themes/lavero.theme.scss | 21 +++++++++++++++++++++ src/styles/themes/novo.theme.scss | 21 +++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/src/styles/themes/dexar.theme.scss b/src/styles/themes/dexar.theme.scss index 893ef68..2ce97ca 100644 --- a/src/styles/themes/dexar.theme.scss +++ b/src/styles/themes/dexar.theme.scss @@ -39,6 +39,27 @@ --radius-full: 999px; } +// Structural dark-mode overrides only (surface/text/border tokens). Brand +// colors (primary/secondary/accent/gradients) are unchanged - a distinct +// dark-mode brand palette is a design decision, not made here. See +// GAPS-AND-IMPROVEMENTS.md item "Dark mode selector does nothing". +:root[data-theme-mode="dark"] { + --text-primary: #eef4f3; + --text-secondary: #b7c4c2; + --text-light: #8a9c9a; + + --bg-primary: #10201e; + --bg-secondary: #16302c; + --bg-tertiary: #1c3a35; + --bg-header: rgba(0, 0, 0, 0.35); + + --border-color: #2c4a45; + --border-dark: #3f6660; + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55); +} + body { font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } diff --git a/src/styles/themes/lavero.theme.scss b/src/styles/themes/lavero.theme.scss index 83a970e..b4f6015 100644 --- a/src/styles/themes/lavero.theme.scss +++ b/src/styles/themes/lavero.theme.scss @@ -38,3 +38,24 @@ --radius-xl: 20px; --radius-full: 999px; } + +// Structural dark-mode overrides only (surface/text/border tokens). Brand +// colors (primary/secondary/accent/gradients) are unchanged - a distinct +// dark-mode brand palette is a design decision, not made here. See +// GAPS-AND-IMPROVEMENTS.md item "Dark mode selector does nothing". +:root[data-theme-mode="dark"] { + --text-primary: #f3f4f6; + --text-secondary: #cbd2da; + --text-light: #9aa4b1; + + --bg-primary: #16181d; + --bg-secondary: #10231a; + --bg-tertiary: #123a29; + --bg-header: rgba(0, 0, 0, 0.35); + + --border-color: #33383f; + --border-dark: #4b5563; + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.5); +} diff --git a/src/styles/themes/novo.theme.scss b/src/styles/themes/novo.theme.scss index 56bcd44..86cd25e 100644 --- a/src/styles/themes/novo.theme.scss +++ b/src/styles/themes/novo.theme.scss @@ -38,3 +38,24 @@ --radius-xl: 20px; --radius-full: 999px; } + +// Structural dark-mode overrides only (surface/text/border tokens). Brand +// colors (primary/secondary/accent/gradients) are unchanged - a distinct +// dark-mode brand palette is a design decision, not made here. See +// GAPS-AND-IMPROVEMENTS.md item "Dark mode selector does nothing". +:root[data-theme-mode="dark"] { + --text-primary: #f3f4f6; + --text-secondary: #cbd2da; + --text-light: #9aa4b1; + + --bg-primary: #14181a; + --bg-secondary: #10231a; + --bg-tertiary: #123a29; + --bg-header: rgba(0, 0, 0, 0.35); + + --border-color: #313a37; + --border-dark: #4b5563; + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.5); +} From d24ba384796fb349a78506b1f861dbef91e4a47e Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:37:04 +0400 Subject: [PATCH 05/29] docs: mark 7 GAPS-AND-IMPROVEMENTS.md items fixed, verified against current source Doc was 4 days stale relative to source - each item below was independently verified against the current file/line during this session's Track Z sweep, not just marked off the todo list: - Ed25519 auth-error body-code bug - Dark mode selector (now actually fixed this session) - Site Layout selector fallback - setItemMeta() wiring - og:locale dynamic locale - stars.component.scss token usage - AdminRole duplication - PRODUCT_DATA_PROVIDER/CATEGORY_REPOSITORY dead mock branch - sellerId UUID typing Co-Authored-By: Claude Sonnet 5 --- GAPS-AND-IMPROVEMENTS.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/GAPS-AND-IMPROVEMENTS.md b/GAPS-AND-IMPROVEMENTS.md index 259b89e..733ed6e 100644 --- a/GAPS-AND-IMPROVEMENTS.md +++ b/GAPS-AND-IMPROVEMENTS.md @@ -6,16 +6,16 @@ Findings only — nothing in this document has been fixed as part of writing it. ## As a Customer / End User -1. **Ed25519 admin-auth "session expired" and "invalid signature" recovery screens are dead UI.** Both are fully built and wired, but `toAuthErrorShape()` (`core/auth/services/auth.service.ts:110-118`) derives the error code from HTTP status only, never a body-level code — so a real backend 401 always shows the generic "Unauthorized" screen instead. Also an [Engineering](#as-backend--api-engineer) and [Backend](#as-backend--api-engineer) item. -2. **Dark mode selector does nothing.** The light/dark/system dropdown saves correctly, but no CSS anywhere reads the `data-theme-mode` attribute it sets — picking anything but Light changes nothing visually. -3. **"Site Layout" selector (Theme section) has no effect.** `layout.type` is edited but page rendering only ever reads each page's own `layout`, never the top-level selector. +1. **FIXED (verified 2026-08-17).** ~~Ed25519 admin-auth "session expired"/"invalid signature" screens were dead UI~~ — `toAuthErrorShape()` now reads `error.error.code` via `authErrorCodeFromBackendCode()` before falling back to HTTP status. See `BACKEND-API-REFERENCE.md` §5. +2. **FIXED (2026-08-17).** ~~Dark mode selector did nothing~~ — structural dark overrides (bg/text/border/shadow) now wired for all three tenant themes under `[data-theme-mode="dark"]`. Brand colors intentionally unchanged pending a theme-owner-approved dark palette. +3. **FIXED (verified 2026-08-17).** ~~"Site Layout" selector had no effect~~ — `SectionEngineService.resolveLayoutType()` now falls back to `bootstrap.layout.type` when a page has no layout of its own. 4. **Footer "Contacts" link has nothing behind it.** No static-page content exists for it at all in the bootstrap data (unlike other footer legal pages, which are populated). -5. **Product pages get no per-product SEO.** `SeoService.setItemMeta(item)` — the method that would set per-product Open Graph/canonical tags — exists but is **never called anywhere in the codebase**. Every product page ships only the site-wide default meta tags. -6. **`og:locale` is hardcoded to `'ru_RU'`** in both SEO meta-tag code paths, regardless of the active locale — a real gap for EN/HY visitors' social-share previews. +5. **FIXED (verified 2026-08-17).** ~~Product pages got no per-product SEO~~ — `SeoService.setItemMeta(item)` is called from `product-details-container.component.ts`. +6. **FIXED (verified 2026-08-17).** ~~`og:locale` was hardcoded to `ru_RU`~~ — reads `languageService.currentLanguage()` via `OG_LOCALE_MAP` at both call sites. 7. **No structured data (JSON-LD) and no sitemap generation exist anywhere** — confirmed absent, not partially built. Sitemap is backend-only work; JSON-LD would need net-new frontend code. 8. **Checkout's payment-description fallback is a hardcoded Russian string** (`'Покупка на Маркетплейсе'`) used as a last resort when no brand name or hostname is available — single-tenant-framed wording in a multi-tenant product. 9. **Brand color contrast fails WCAG AA.** `--border-color` measures 1.24–1.42:1 against a 3:1 UI-component requirement in every theme; `--success`/`--warning`/`--error`/`--info-color` fail 4.5:1 when used as plain text. Real palette colors, not a token bug — see [Accessibility](#as-accessibility-reviewer). -10. **`stars.component.scss:10` uses a literal hex color** (`#cdd6d5`) with no design token behind it — any future palette change will silently miss this one glyph. +10. **FIXED (verified 2026-08-17).** ~~`stars.component.scss:10` used a literal hex color~~ — now uses `var(--border-color)`. 11. **No multi-vendor cart handling exists.** Checkout is one inline flow producing exactly one order from one payment popup; a cart with items from multiple sellers has no defined behavior (relevant the moment Seller Management ships beyond its current disabled-by-default placeholder). --- @@ -50,7 +50,7 @@ Findings only — nothing in this document has been fixed as part of writing it. See [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md) for the full contract. Structural gaps worth flagging here specifically: 1. **Only 2 of 11 admin gateway domains (Categories, Dashboard-metrics) have a DI-token seam.** The other 9 — Orders, Products, Users, Transactions, Monitoring, Moderation, plus derived Customers/Analytics — inject their mock gateway class directly. A token has to be added to each before any real backend can be bound, independent of how easy that domain's actual endpoint is to build. -2. **`AdminRole` is defined twice with unrelated shapes** (auth string-union vs. a Users-page display interface) — needs a naming reconciliation before the real role table is built. +2. **FIXED (verified 2026-08-17).** ~~`AdminRole` was defined twice with unrelated shapes~~ — only one `AdminRole` export exists (`core/auth/models/permission.model.ts`); the Users-page shape is `AdminUserRoleRecord` with a disambiguating comment. 3. **Two unrelated `Category` types exist**, both fed by the same `/category` response, both still in active use. 4. **Duplicate search models** exist under two different module paths. 5. **The error envelope is entirely a proposal** — no interceptor in the app inspects error response bodies today; every error reaction happens at the raw HTTP-status level. Adopting an envelope is a net-new build for both sides, not a preservation of existing behavior. @@ -58,7 +58,7 @@ See [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md) for the full contract. 7. **No API versioning scheme has been decided** — no version segment, no version header, anywhere in the client. 8. **Centralized error-handling scaffolding exists but was never built.** `src/app/core/error-handling/`, `src/app/core/guards/`, and `src/app/core/interceptors/` each contain only a `.gitkeep` file — someone planned a shared error-handling layer, and every caller still handles failures ad hoc at the call site instead. Worth building once real backends start returning the error envelope in [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md), rather than adding another one-off handler per facade. 9. **Admin Reports and Seller Management pages have zero data wiring of any kind** — not even a mock gateway call. Reports reuses `AdminAnalyticsFacade` (itself mock-derived) for its numbers; Seller Management is a static placeholder page with no `HttpClient` reference anywhere. Neither is currently a "swap the gateway" job — Reports inherits whatever Analytics becomes, Seller Management has no data layer to swap yet. -10. **Two per-domain provider tokens have a dead mock branch, silently.** `PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY` always resolve to the real API implementation regardless of `useMockData` — there is no mock class bound to either token. Anyone toggling mock mode expecting storefront products/categories to mock out will be surprised; only Bootstrap, Backoffice-widget-data, and Admin-Categories actually respect the mock/api switch. +10. **FIXED (verified 2026-08-17).** ~~`PRODUCT_DATA_PROVIDER`/`CATEGORY_REPOSITORY` had a dead mock branch~~ — both tokens' factories now resolve directly to the real API implementation with the dead switch removed, documented inline as intentional. --- @@ -76,7 +76,7 @@ See [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md) for the full contract. 2. **`catalog.navigationMode`** renders an intentional placeholder — confirmed not a bug, but the alternate nav UIs it implies (mega-menu, top-carousel, left-nav) don't exist yet if ever wanted. 3. **Angular 22 upgrade is researched but not started** (~2–3.5 days estimated, needs a dependency fix and Node version bump first). Explicitly recommended as its own dedicated session, never bundled with feature work. 4. **`MarketplaceRef` and `TenantConfig` both represent "a marketplace" from two different vantage points** — a deliberate, documented distinction today, but worth consolidating if a third marketplace-shaped type is ever proposed. -5. **`sellerId` fields are typed as bare `string` instead of the `UUID` alias** used everywhere else in the newer sellers domain — zero functional impact, pure convention drift, cheap to fix opportunistically. +5. **FIXED (verified 2026-08-17).** ~~`sellerId` fields were typed as bare `string`~~ — `core/sellers/models/seller-scope.model.ts` and all other sellers-domain usages type it `UUID`. 6. **No shared breadcrumb component exists anywhere** — the only breadcrumb logic in the entire storefront is one local signal inside the catalog container, duplicated conceptually wherever a future breadcrumb might be needed. 7. **Bootstrap `apiEndpoints.{website,builder,backoffice}` are empty objects in the mock today** — meaning no builder or backoffice CRUD path exists as a literal anywhere in the client. Any concrete path documented for those domains is a proposal until this is populated. From bf367fc5feb05a8c23e965f75a02ac958f7c5a23 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:38:53 +0400 Subject: [PATCH 06/29] docs: correct search-model duplication finding - three shapes, not two Re-verified GAPS-AND-IMPROVEMENTS.md's "duplicate search models" item: core/search/models/search.model.ts was already a re-export shim (fine), but core/search/models/search-state.model.ts is a real second copy, and features/search/facade/search.facade.ts has a third, private LegacySearchState interface with the same fields again. Documented as its own scoped task rather than fixed here - reconciling three shapes on the catalog rendering path needs full consumer tracing first. Co-Authored-By: Claude Sonnet 5 --- GAPS-AND-IMPROVEMENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GAPS-AND-IMPROVEMENTS.md b/GAPS-AND-IMPROVEMENTS.md index 733ed6e..e167634 100644 --- a/GAPS-AND-IMPROVEMENTS.md +++ b/GAPS-AND-IMPROVEMENTS.md @@ -52,7 +52,7 @@ See [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md) for the full contract. 1. **Only 2 of 11 admin gateway domains (Categories, Dashboard-metrics) have a DI-token seam.** The other 9 — Orders, Products, Users, Transactions, Monitoring, Moderation, plus derived Customers/Analytics — inject their mock gateway class directly. A token has to be added to each before any real backend can be bound, independent of how easy that domain's actual endpoint is to build. 2. **FIXED (verified 2026-08-17).** ~~`AdminRole` was defined twice with unrelated shapes~~ — only one `AdminRole` export exists (`core/auth/models/permission.model.ts`); the Users-page shape is `AdminUserRoleRecord` with a disambiguating comment. 3. **Two unrelated `Category` types exist**, both fed by the same `/category` response, both still in active use. -4. **Duplicate search models** exist under two different module paths. +4. **Worse than previously stated (re-verified 2026-08-17): three overlapping `SearchState`-shaped types, not two.** `core/search/models/search.model.ts` is already a clean re-export shim (fixed), but `core/search/models/search-state.model.ts` is a genuine second copy consumed by `catalog-container.component.ts`, and `features/search/facade/search.facade.ts` additionally defines its own private `LegacySearchState` interface with the same fields again. Reconciling all three touches the highest-traffic storefront surface (catalog rendering) — needs its own careful pass with full consumer tracing, not a quick rename. 5. **The error envelope is entirely a proposal** — no interceptor in the app inspects error response bodies today; every error reaction happens at the raw HTTP-status level. Adopting an envelope is a net-new build for both sides, not a preservation of existing behavior. 6. **429 (rate limiting) has zero client-side handling anywhere** — no interceptor, facade, or component references it. If the backend rate-limits, today's frontend has no graceful path for that response. 7. **No API versioning scheme has been decided** — no version segment, no version header, anywhere in the client. From 6ca672987ec585c2d4dee00c492fcf94277fea14 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:49:54 +0400 Subject: [PATCH 07/29] refactor: extract shared app-breadcrumb component (Z14) Only breadcrumb logic anywhere in the storefront was a local signal + inline markup inside catalog-container. Extracted a generic shared/ui/breadcrumb component (rootLabel/items/ariaLabel inputs, rootClick/itemClick outputs) and repointed catalog-container onto it, removing the now-dead inline SCSS block. Future breadcrumb usages (product detail, admin) have something to reuse instead of duplicating. Co-Authored-By: Claude Sonnet 5 --- .../catalog-container.component.html | 13 +++++----- .../catalog-container.component.scss | 21 --------------- .../containers/catalog-container.component.ts | 15 ++++++++++- .../ui/breadcrumb/breadcrumb.component.html | 7 +++++ .../ui/breadcrumb/breadcrumb.component.scss | 19 ++++++++++++++ .../ui/breadcrumb/breadcrumb.component.ts | 26 +++++++++++++++++++ 6 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 src/app/shared/ui/breadcrumb/breadcrumb.component.html create mode 100644 src/app/shared/ui/breadcrumb/breadcrumb.component.scss create mode 100644 src/app/shared/ui/breadcrumb/breadcrumb.component.ts diff --git a/src/app/features/website/catalog/containers/catalog-container.component.html b/src/app/features/website/catalog/containers/catalog-container.component.html index 7be50fb..672c9a7 100644 --- a/src/app/features/website/catalog/containers/catalog-container.component.html +++ b/src/app/features/website/catalog/containers/catalog-container.component.html @@ -3,13 +3,12 @@ @if (catalogConfig().showBreadcrumbs && breadcrumb().length > 0) { - + } (() => + this.breadcrumb().map(category => ({ id: category.id, label: category.title })) + ); + + onBreadcrumbItemClick(item: BreadcrumbItem): void { + const category = this.breadcrumb().find(c => c.id === item.id); + if (category) { + this.selectCategory(category); + } + } + selectCategory(category: Category): void { this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]); } diff --git a/src/app/shared/ui/breadcrumb/breadcrumb.component.html b/src/app/shared/ui/breadcrumb/breadcrumb.component.html new file mode 100644 index 0000000..218376c --- /dev/null +++ b/src/app/shared/ui/breadcrumb/breadcrumb.component.html @@ -0,0 +1,7 @@ + diff --git a/src/app/shared/ui/breadcrumb/breadcrumb.component.scss b/src/app/shared/ui/breadcrumb/breadcrumb.component.scss new file mode 100644 index 0000000..47f1ce8 --- /dev/null +++ b/src/app/shared/ui/breadcrumb/breadcrumb.component.scss @@ -0,0 +1,19 @@ +.app-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + color: var(--text-secondary); + font-size: var(--font-size-md, 0.9375rem); + + button { + border: 0; + padding: 0; + background: transparent; + color: var(--primary-color); + font: inherit; + font-weight: var(--font-weight-bold, 700); + text-decoration: none; + cursor: pointer; + } +} diff --git a/src/app/shared/ui/breadcrumb/breadcrumb.component.ts b/src/app/shared/ui/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000..d660d2e --- /dev/null +++ b/src/app/shared/ui/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; + +export interface BreadcrumbItem { + id: string | number; + label: string; +} + +/** + * Generic breadcrumb trail. Consumer owns navigation - this component only + * renders the trail and emits which item was clicked (root or a trail item). + */ +@Component({ + selector: 'app-breadcrumb', + standalone: true, + templateUrl: './breadcrumb.component.html', + styleUrl: './breadcrumb.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BreadcrumbComponent { + readonly rootLabel = input.required(); + readonly items = input([]); + readonly ariaLabel = input(''); + + readonly rootClick = output(); + readonly itemClick = output(); +} From 634e3faf3deb2e7f5567c335392f7940a74f4c05 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 21:50:59 +0400 Subject: [PATCH 08/29] docs: correct Z6 finding - Product/Organization JSON-LD already shipped SeoService.setJsonLd() already injects real application/ld+json for Product (per-item) and Organization (site default) - the gap doc's "confirmed absent" claim was stale. Noted the one real remaining gap (BreadcrumbList/ItemList schema) as future net-new scope rather than implementing it now. Sitemap generation remains backend-only, unchanged. Co-Authored-By: Claude Sonnet 5 --- GAPS-AND-IMPROVEMENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GAPS-AND-IMPROVEMENTS.md b/GAPS-AND-IMPROVEMENTS.md index e167634..c5f7891 100644 --- a/GAPS-AND-IMPROVEMENTS.md +++ b/GAPS-AND-IMPROVEMENTS.md @@ -12,7 +12,7 @@ Findings only — nothing in this document has been fixed as part of writing it. 4. **Footer "Contacts" link has nothing behind it.** No static-page content exists for it at all in the bootstrap data (unlike other footer legal pages, which are populated). 5. **FIXED (verified 2026-08-17).** ~~Product pages got no per-product SEO~~ — `SeoService.setItemMeta(item)` is called from `product-details-container.component.ts`. 6. **FIXED (verified 2026-08-17).** ~~`og:locale` was hardcoded to `ru_RU`~~ — reads `languageService.currentLanguage()` via `OG_LOCALE_MAP` at both call sites. -7. **No structured data (JSON-LD) and no sitemap generation exist anywhere** — confirmed absent, not partially built. Sitemap is backend-only work; JSON-LD would need net-new frontend code. +7. **FIXED (verified 2026-08-17) on JSON-LD; sitemap remains backend-only work.** ~~No structured data (JSON-LD) exists anywhere~~ — `SeoService.setJsonLd()` injects a real `