From d853ecb1daae54f6224579d4222779ee3cfafa22 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sun, 19 Jul 2026 15:28:35 +0400 Subject: [PATCH] changes --- .impeccable/design.json | 124 ++ .impeccable/live/config.json | 6 + DESIGN.md | 234 +++ PRODUCT.md | 49 + docs/ADMIN.md | 8 +- docs/ARCHITECTURE.md | 4 +- docs/BACKEND-DIFF-VS-MAIN.md | 50 - docs/BACKEND-INTEGRATION-PROMPT.md | 54 - docs/BACKEND-INTEGRATION.md | 120 -- docs/BACKEND.md | 197 -- docs/BOOTSTRAP.md | 153 -- docs/EDITOR.md | 8 +- docs/FRONTEND.md | 2 +- docs/PROJECT-STRUCTURE.md | 4 +- docs/PROJECT.md | 11 +- docs/StaticPages.md | 2 +- .../backend/Backend-Platform-API-Spec.md | 451 ----- ...tion-bootstrap-and-provider-abstraction.md | 35 +- ...tibility-for-auth-payment-authorization.md | 34 +- docs/backend/BACKEND-INTEGRATION.md | 1744 +++++++++++++++++ public/robots.txt | 2 +- public/sitemap.xml | 2 +- src/app/core/admin-auth/admin-auth.service.ts | 2 +- 23 files changed, 2189 insertions(+), 1107 deletions(-) create mode 100644 .impeccable/design.json create mode 100644 .impeccable/live/config.json create mode 100644 DESIGN.md create mode 100644 PRODUCT.md delete mode 100644 docs/BACKEND-DIFF-VS-MAIN.md delete mode 100644 docs/BACKEND-INTEGRATION-PROMPT.md delete mode 100644 docs/BACKEND-INTEGRATION.md delete mode 100644 docs/BACKEND.md delete mode 100644 docs/BOOTSTRAP.md delete mode 100644 docs/architecture/backend/Backend-Platform-API-Spec.md create mode 100644 docs/backend/BACKEND-INTEGRATION.md diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..52a1d86 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,124 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-07-17T00:00:00Z", + "title": "Design System: Marketplaces Platform", + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Muted Pine", "canonical": "#497671", "tonalRamp": ["#182927", "#243d3a", "#2f4f4b", "#3d635f", "#497671", "#6b918d", "#93b3af", "#c3d6d3"] }, + "secondary": { "role": "secondary", "displayName": "Sage Grey", "canonical": "#a1b4b5", "tonalRamp": ["#2c3838", "#3f5150", "#556c6b", "#6c8583", "#8da3a4", "#a1b4b5", "#c0cfcf", "#e2eaea"] }, + "accent": { "role": "tertiary", "displayName": "Pale Mint", "canonical": "#a7ceca", "tonalRamp": ["#243936", "#33514d", "#456a65", "#5a857f", "#7fa9a3", "#a7ceca", "#c6e0dd", "#e6f2f0"] }, + "text-primary": { "role": "neutral", "displayName": "Deep Pine Ink", "canonical": "#1e3c38", "tonalRamp": ["#0f1e1c", "#1e3c38", "#2c5651", "#3d716b", "#5a8d87", "#84aca7", "#b1cbc8", "#dfeae9"] }, + "bg-secondary": { "role": "neutral", "displayName": "Soft Grey", "canonical": "#f5f5f5", "tonalRamp": ["#2b2b2b", "#4a4a4a", "#6e6e6e", "#949494", "#b8b8b8", "#d7d7d7", "#eaeaea", "#f5f5f5"] }, + "border": { "role": "neutral", "displayName": "Divider Grey", "canonical": "#d3dad9", "tonalRamp": ["#333938", "#4a5251", "#636d6c", "#7f8a89", "#9da8a7", "#bcc5c4", "#d3dad9", "#eef1f1"] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Page-level and storefront hero titles; ceiling ~2.75rem." }, + "headline": { "displayName": "Headline", "purpose": "Section headings and admin page titles." }, + "title": { "displayName": "Title", "purpose": "Card titles, editor section labels." }, + "body": { "displayName": "Body", "purpose": "Default reading text; cap prose at 65-75ch." }, + "label": { "displayName": "Label", "purpose": "Badges and tags only; tracked uppercase." } + }, + "shadows": [ + { "name": "shadow-sm", "value": "0 2px 8px rgba(0,0,0,0.1)", "purpose": "Resting cards, inputs, low panels. Default ambient layer." }, + { "name": "shadow-md", "value": "0 4px 12px rgba(0,0,0,0.15)", "purpose": "Hover state for cards and buttons; raised toolbars." }, + { "name": "shadow-lg", "value": "0 12px 32px rgba(73,118,113,0.2)", "purpose": "Structural float: modals, dropdowns, save bar. Brand-tinted." } + ], + "motion": [ + { "name": "transition-fast", "value": "120ms ease", "purpose": "Button and small-control state changes." }, + { "name": "transition-normal", "value": "180ms ease", "purpose": "Card hover lift, transforms." }, + { "name": "transition-slow", "value": "300ms ease", "purpose": "Default for links/inputs/textareas." } + ], + "breakpoints": [ + { "name": "sm", "value": "640px" }, + { "name": "md", "value": "900px" }, + { "name": "lg", "value": "1200px" }, + { "name": "container", "value": "1280px" } + ] + }, + "components": [ + { + "name": "Primary Button", + "kind": "button", + "refersTo": "button-primary", + "description": "The default confident action. Muted Pine fill, lifts on hover.", + "html": "", + "css": ".ds-btn-primary { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; background: #497671; color: #fff; border: 1px solid #497671; border-radius: 12px; padding: 0.625rem 1rem; font-weight: 600; line-height: 1.2; cursor: pointer; transition: background-color 180ms ease, transform 180ms ease, box-shadow 180ms ease; } .ds-btn-primary:hover { background: #3d635f; border-color: #3d635f; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .ds-btn-primary:active { transform: translateY(0); } .ds-btn-primary:focus-visible { outline: 2px solid #497671; outline-offset: 2px; }" + }, + { + "name": "Ghost Button", + "kind": "button", + "refersTo": "button-ghost", + "description": "Low-emphasis action. Transparent with a divider border until hover.", + "html": "", + "css": ".ds-btn-ghost { display: inline-flex; align-items: center; justify-content: center; background: transparent; color: #1e3c38; border: 1px solid #d3dad9; border-radius: 12px; padding: 0.625rem 1rem; font-weight: 600; cursor: pointer; transition: background-color 180ms ease, border-color 180ms ease; } .ds-btn-ghost:hover { background: rgba(73,118,113,0.08); border-color: #497671; } .ds-btn-ghost:focus-visible { outline: 2px solid #497671; outline-offset: 2px; }" + }, + { + "name": "Card", + "kind": "card", + "refersTo": "card", + "description": "Resting surface with a soft ambient shadow that lifts on hover.", + "html": "

Product title

Supporting copy sits in Muted Pine Grey at a comfortable line height.

", + "css": ".ds-card { background: #ffffff; border: 1px solid #d3dad9; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 16px; transition: transform 180ms ease, box-shadow 180ms ease; } .ds-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .ds-card-title { margin: 0 0 6px; font-size: 1.125rem; font-weight: 600; color: #1e3c38; line-height: 1.3; } .ds-card-body { margin: 0; font-size: 1rem; font-weight: 400; color: #667a77; line-height: 1.6; }" + }, + { + "name": "Text Input", + "kind": "input", + "refersTo": "input", + "description": "Editor/admin field with a divider stroke and brand focus outline.", + "html": "", + "css": ".ds-field { display: grid; gap: 6px; color: #1e3c38; font-weight: 600; } .ds-field-label { font-size: 1rem; } .ds-field-desc { font-weight: 400; font-size: 12px; line-height: 1.4; color: #667a77; } .ds-input { width: 100%; padding: 10px 12px; border: 1px solid #d3dad9; border-radius: 10px; background: #fff; color: #1e3c38; font: inherit; } .ds-input:focus-visible { outline: 2px solid #497671; outline-offset: 2px; } .ds-input::placeholder { color: #828e8d; }" + }, + { + "name": "Badge", + "kind": "chip", + "refersTo": "badge", + "description": "Uppercase status marker overlaid on product media.", + "html": "Sale", + "css": ".ds-badge { display: inline-block; padding: 2px 8px; border-radius: 8px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: #fff; line-height: 1.4; } .ds-badge-sale { background: #f44336; }" + }, + { + "name": "Tag", + "kind": "chip", + "refersTo": "badge", + "description": "Low-emphasis metadata pill in brand tint.", + "html": "Digital", + "css": ".ds-tag { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.72rem; color: #497671; background: rgba(73,118,113,0.08); border: 1px solid rgba(73,118,113,0.15); }" + } + ], + "narrative": { + "northStar": "The Operator's Workbench", + "overview": "This is a tool before it is a brand. The platform chrome is a dependable workbench an operator returns to session after session to build and run a marketplace: state is always legible, controls map to what they change, and nothing competes with the work. The palette is a calm Muted Pine teal-green, warm enough to feel like commerce, quiet enough to disappear behind a tenant's own theme. The system is configuration-first: every storefront is themed per tenant from a runtime bootstrap, so the platform's identity stays neutral and the tenant's leads. Components are tactile and confident; depth is real but restrained, with structural elevation reserved for things that genuinely float.", + "keyCharacteristics": [ + "Quiet, neutral chrome so per-tenant themes lead the storefront.", + "Muted Pine teal-green primary; retail-warm but low-drama.", + "Tactile, confident components with decisive states.", + "Legible state above decoration in every tool surface.", + "WCAG 2.2 AA; contrast holds across tenant themes, not just the default." + ], + "rules": [ + { "name": "The Quiet Chrome Rule", "body": "The platform's own surfaces stay neutral so tenant themes carry storefront identity. Never introduce a platform-branded color that would fight a tenant's palette.", "section": "colors" }, + { "name": "The Variable-Only Rule", "body": "Components and widgets consume CSS custom properties only. A hardcoded hex in a component is a bug (ADR-008) that breaks per-tenant theming.", "section": "colors" }, + { "name": "The One Family Rule", "body": "DM Sans in multiple weights carries the entire system. Do not pair a second sans; do not add a display serif. Contrast is weight and size.", "section": "typography" }, + { "name": "The Uppercase-Is-Earned Rule", "body": "Tracked uppercase lives on badges/tags exclusively. It is forbidden as a section eyebrow.", "section": "typography" }, + { "name": "The Lift-on-Intent Rule", "body": "Resting surfaces carry at most shadow-sm. shadow-md is a response to hover/focus; shadow-lg means the element floats above the page.", "section": "elevation" } + ], + "dos": [ + "Do consume theme CSS custom properties, never hardcode hex in a component (ADR-008).", + "Do keep platform chrome neutral so tenant themes lead the storefront.", + "Do carry hierarchy with DM Sans weight and size; one family only.", + "Do keep resting surfaces on shadow-sm; reserve shadow-lg for genuinely floating elements.", + "Do make state unambiguous in every tool surface.", + "Do give every hover/transform a prefers-reduced-motion fallback.", + "Do hold 4.5:1 body-text contrast across every tenant theme, not just Dexar." + ], + "donts": [ + "Don't ship dated enterprise admin: cluttered gray dashboards, tiny dense tables, 2010-era Bootstrap backoffice.", + "Don't ship generic AI-SaaS template: cream/violet gradient landings, hero-metric card rows, tracked-uppercase eyebrows, identical card grids.", + "Don't ship consumer-toy UI: bubbly rounded-everything, mascots, candy colors, gamified surfaces.", + "Don't use tracked uppercase anywhere except badges/tags.", + "Don't exceed ~2.75rem on display headings.", + "Don't add a second type family or a display serif.", + "Don't let platform-branded color fight a tenant's palette." + ] + } +} diff --git a/.impeccable/live/config.json b/.impeccable/live/config.json new file mode 100644 index 0000000..65e5faf --- /dev/null +++ b/.impeccable/live/config.json @@ -0,0 +1,6 @@ +{ + "files": ["src/index.html"], + "insertBefore": "", + "commentSyntax": "html", + "cspChecked": true +} diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..2169239 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,234 @@ +--- +name: Marketplaces Platform +description: Config-driven multi-tenant marketplace platform — quiet chrome, tenant-led storefronts. +colors: + primary: "#497671" + primary-hover: "#3d635f" + secondary: "#a1b4b5" + secondary-hover: "#8da3a4" + accent: "#a7ceca" + accent-hover: "#91b9b5" + text-primary: "#1e3c38" + text-secondary: "#667a77" + text-light: "#828e8d" + bg-primary: "#ffffff" + bg-secondary: "#f5f5f5" + bg-tertiary: "#f0f0f0" + border: "#d3dad9" + border-dark: "#677b78" + success: "#10b981" + warning: "#f59e0b" + error: "#ef4444" + info: "#3b82f6" +typography: + display: + fontFamily: "DM Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" + fontSize: "clamp(2rem, 4vw, 2.75rem)" + fontWeight: 700 + lineHeight: 1.25 + letterSpacing: "normal" + headline: + fontFamily: "DM Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" + fontSize: "clamp(1.5rem, 3vw, 2rem)" + fontWeight: 700 + lineHeight: 1.25 + letterSpacing: "normal" + title: + fontFamily: "DM Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" + fontSize: "1.125rem" + fontWeight: 600 + lineHeight: 1.3 + letterSpacing: "normal" + body: + fontFamily: "DM Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" + fontSize: "1rem" + fontWeight: 400 + lineHeight: 1.6 + letterSpacing: "normal" + label: + fontFamily: "DM Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" + fontSize: "0.7rem" + fontWeight: 600 + lineHeight: 1.4 + letterSpacing: "0.4px" +rounded: + sm: "8px" + md: "12px" + lg: "13px" + xl: "22px" + field: "10px" +spacing: + xs: "4px" + sm: "8px" + md: "16px" + lg: "24px" + xl: "32px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "#ffffff" + rounded: "{rounded.md}" + padding: "0.625rem 1rem" + button-primary-hover: + backgroundColor: "{colors.primary-hover}" + textColor: "#ffffff" + rounded: "{rounded.md}" + button-secondary: + backgroundColor: "{colors.secondary}" + textColor: "#ffffff" + rounded: "{rounded.md}" + padding: "0.625rem 1rem" + button-ghost: + backgroundColor: "transparent" + textColor: "{colors.text-primary}" + rounded: "{rounded.md}" + padding: "0.625rem 1rem" + card: + backgroundColor: "{colors.bg-primary}" + rounded: "{rounded.md}" + padding: "16px" + input: + backgroundColor: "{colors.bg-primary}" + textColor: "{colors.text-primary}" + rounded: "{rounded.field}" + padding: "10px 12px" + badge: + textColor: "#ffffff" + rounded: "{rounded.sm}" + padding: "2px 8px" +--- + +# Design System: Marketplaces Platform + +## 1. Overview + +**Creative North Star: "The Operator's Workbench"** + +This is a tool before it is a brand. The platform chrome — the Project Editor, the Admin backoffice, the shared UI primitives — is a dependable workbench an operator returns to session after session to build and run a marketplace. It rewards precision and speed: state is always legible (draft vs published, saved vs unsaved, safe vs destructive), controls map visibly to what they change, and nothing on screen competes with the work. The palette is a calm Muted Pine teal-green, warm enough to feel like commerce, quiet enough to disappear behind a tenant's own theme. + +The system is deliberately configuration-first. Every storefront is themed per tenant from a runtime `bootstrap.json`, so the platform's own identity stays neutral by design — the tenant's colors, type, and layout carry the storefront's character, and the workbench chrome recedes. Where components do appear, they are tactile and confident: solid fills, decisive hover lift, honest disabled and error states. Depth is real but restrained — surfaces sit on soft tonal shadows at rest, and structural elevation is reserved for things that genuinely float (modals, dropdowns, the save bar). + +This system explicitly rejects three looks. It is **not dated enterprise admin** — no cluttered gray dashboards, no tiny dense tables, no 2010-era Bootstrap backoffice. It is **not a generic AI-SaaS template** — no cream/violet gradient landings, no hero-metric card rows, no tracked-uppercase eyebrows on every section, no identical icon-heading-text grids. It is **not a consumer toy** — no bubbly rounded-everything, no mascots, no candy colors, no gamified UI. + +**Key Characteristics:** +- Quiet, neutral chrome so per-tenant themes lead the storefront. +- Muted Pine teal-green primary; retail-warm but low-drama. +- Tactile, confident components with decisive states. +- Legible state above decoration in every tool surface. +- WCAG 2.2 AA; contrast holds across tenant themes, not just the default. + +## 2. Colors + +A grounded teal-green core over cool near-white neutrals; retail warmth without shouting. The tokens below are the canonical Dexar theme — the platform default. Tenant themes (Lavero, Novo, and future tenants) override these same CSS custom properties, so components must consume the variables, never hardcode hex (ADR-008). + +### Primary +- **Muted Pine** (#497671): The core brand teal-green. Primary buttons, active nav, focus outlines, links, key accents. On hover it deepens to **Pine Deep** (#3d635f). Grounded and natural — the color of the workbench itself. + +### Secondary +- **Sage Grey** (#a1b4b5): Muted blue-grey-green for secondary actions and supporting surfaces; hover **Sage Grey Deep** (#8da3a4). Quieter than primary, never competes. + +### Tertiary +- **Pale Mint** (#a7ceca): Soft light accent (#91b9b5 on hover) for gentle highlights, hero gradient stops, and low-emphasis fills. + +### Neutral +- **Deep Pine Ink** (#1e3c38): Primary text. Tinted toward the brand hue, not pure black — carries 4.5:1+ on white. +- **Muted Pine Grey** (#667a77): Secondary text, captions, field descriptions. +- **Faint Pine Grey** (#828e8d): Light/tertiary text, placeholders — reserve for large or non-essential text. +- **White** (#ffffff): Primary surface (cards, inputs, panels). +- **Soft Grey** (#f5f5f5): App background, secondary surface. +- **Faint Grey** (#f0f0f0): Tertiary surface, subtle fills. +- **Divider Grey** (#d3dad9): Borders, dividers, input strokes. +- **Border Deep** (#677b78): Stronger borders where a divider needs weight. + +### Status +- **Success** (#10b981), **Warning** (#f59e0b), **Error** (#ef4444), **Info** (#3b82f6): Standard semantic set, consistent across all themes. Error text darkens to #991b1b on light backgrounds for AA. + +### Named Rules +**The Quiet Chrome Rule.** The platform's own surfaces stay neutral so tenant themes carry storefront identity. Never introduce a platform-branded color that would fight a tenant's palette. + +**The Variable-Only Rule.** Components and widgets consume CSS custom properties (`--primary-color`, `--text-primary`, `--border-color`) only. A hardcoded hex in a component is a bug (ADR-008) — it breaks per-tenant theming. + +## 3. Typography + +**Display / Body Font:** DM Sans (with `-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif` fallback) +**Label Font:** DM Sans (same family, tracked and uppercased for badges) + +**Character:** One family, four weights (400/500/600/700). DM Sans is a low-contrast geometric-humanist sans — clean, legible at dense sizes, neutral enough to sit behind tenant content. Hierarchy comes from weight and size, never a second display face. + +### Hierarchy +- **Display** (700, clamp(2rem, 4vw, 2.75rem), 1.25): Page-level headings, storefront hero titles. Never exceeds ~2.75rem — the workbench does not shout. +- **Headline** (700, clamp(1.5rem, 3vw, 2rem), 1.25): Section headings, admin page titles. +- **Title** (600, 1.125rem, 1.3): Card titles, editor section labels, form group headings. +- **Body** (400, 1rem, 1.6): Default reading text. Cap prose at 65–75ch. +- **Label** (600, 0.7rem, 1.4, letter-spacing 0.4px, uppercase): Badges and tags only — the one place tracked uppercase is legitimate. + +### Named Rules +**The One Family Rule.** DM Sans in multiple weights carries the entire system. Do not pair a second sans; do not add a display serif. Contrast is weight and size. + +**The Uppercase-Is-Earned Rule.** Tracked uppercase lives on badges/tags exclusively. It is forbidden as a section eyebrow — that is a named anti-reference. + +## 4. Elevation + +A hybrid: soft tonal shadows give resting surfaces gentle separation from the background, while structural elevation is reserved for elements that genuinely float — modals, dropdowns, the sticky save bar. On top of that, interactive surfaces lift on hover (a 1–2px translate plus a stronger shadow). Depth is present and purposeful, never heavy. + +### Shadow Vocabulary +- **shadow-sm** (`0 2px 8px rgba(0,0,0,0.1)`): Resting cards, inputs, low panels. The default ambient layer. +- **shadow-md** (`0 4px 12px rgba(0,0,0,0.15)`): Hover state for cards and buttons; raised toolbars. +- **shadow-lg** (`0 12px 32px rgba(73,118,113,0.2)`): Structural float — modals, dropdowns, popovers, the save bar. Tinted with the brand hue. + +### Named Rules +**The Lift-on-Intent Rule.** Resting surfaces carry at most `shadow-sm`. `shadow-md` is a response to hover/focus; `shadow-lg` means the element floats above the page. Never use `shadow-lg` as decoration on a static card. + +## 5. Components + +### Buttons +- **Shape:** Gently curved (12px radius, `{rounded.md}`); editor action buttons use 10px (`{rounded.field}`). +- **Primary:** Muted Pine fill (#497671), white text, padding `0.625rem 1rem`, weight 600–700. Tactile and confident. +- **Hover / Focus:** Background deepens to #3d635f, `translateY(-1px)` lift with `shadow-sm`; focus-visible shows a 2px Muted Pine outline offset 2px. `:active` returns to `translateY(0)`. +- **Secondary:** Sage Grey (#a1b4b5) fill, white text; hover #8da3a4. +- **Ghost:** Transparent, Deep Pine Ink text, Divider Grey border; hover fills `rgba(73,118,113,0.08)` and border shifts to Muted Pine. +- **Disabled:** `opacity: 0.6`, no lift, no shadow, `cursor: not-allowed`. + +### Cards / Containers +- **Corner Style:** 12px (`{rounded.md}`). +- **Background:** White (#ffffff) on Soft Grey (#f5f5f5) page. +- **Border:** 1px Divider Grey (#d3dad9). +- **Shadow Strategy:** `shadow-sm` at rest → `shadow-md` on hover with `translateY(-2px)` (product cards add a subtle `scale(1.01)`). See Elevation. +- **Internal Padding:** 16px (`{spacing.md}`). +- **Nested cards:** Editor sub-cards use `#fbfcfc` fill with the same 12px radius and 1px border. + +### Inputs / Fields +- **Style:** White fill, 1px Divider Grey border, 10px radius (`{rounded.field}`), padding `10px 12px`, inherits body font. +- **Focus:** 2px Muted Pine focus-visible outline, offset 2px (global rule). +- **Field description:** 12px, Muted Pine Grey (#667a77), sits under the label at weight 400. +- **Error:** Error text #991b1b; color input controls get a 44px min-height touch target. + +### Navigation +- Neutral chrome, DM Sans, weight 600 for active items. Default text is Deep Pine Ink; active/hover carries Muted Pine. Header uses a low-tint `--bg-header` wash (brand hue at ~10% alpha). Mobile collapses to a menu; `body.platform-menu-open` locks scroll. + +### Badges & Tags (signature) +- **Badge:** Uppercase Label type (0.7rem, 600, 0.4px tracking), white text, 8px radius, `2px 8px` padding, solid semantic fills (new #4caf50, sale #f44336, hot #ff5722, limited #ff9800, bestseller #2196f3, featured #607d8b). Absolutely-positioned overlay top-left on product media. +- **Tag:** Pill (12px radius), Muted Pine text on `rgba(73,118,113,0.08)` fill with a faint brand border. Low-emphasis metadata. + +### Save Bar (signature) +- Sticky, structurally elevated (`shadow-lg`), always states current state (unsaved changes / saving / published). The clearest expression of the Operator's Workbench: the operator always knows where the work stands. + +## 6. Do's and Don'ts + +### Do: +- **Do** consume theme CSS custom properties (`--primary-color`, `--text-primary`, `--border-color`) — never hardcode hex in a component (ADR-008). +- **Do** keep platform chrome neutral so tenant themes lead the storefront (The Quiet Chrome Rule). +- **Do** carry hierarchy with DM Sans weight and size; one family only. +- **Do** keep resting surfaces on `shadow-sm`; reserve `shadow-lg` for genuinely floating elements. +- **Do** make state unambiguous — draft vs published, saved vs unsaved, safe vs destructive — in every tool surface. +- **Do** give every hover/transform a `prefers-reduced-motion: reduce` fallback (handled globally in `styles.scss`). +- **Do** hold 4.5:1 body-text contrast across every tenant theme, not just Dexar. + +### Don't: +- **Don't** ship dated enterprise admin: no cluttered gray dashboards, tiny dense tables, or 2010-era Bootstrap backoffice. +- **Don't** ship generic AI-SaaS template: no cream/violet gradient landings, hero-metric card rows, tracked-uppercase eyebrows on every section, or identical icon-heading-text card grids. +- **Don't** ship consumer-toy UI: no bubbly rounded-everything, mascots, candy colors, or gamified surfaces. +- **Don't** use tracked uppercase anywhere except badges/tags (The Uppercase-Is-Earned Rule). +- **Don't** exceed ~2.75rem on display headings — the workbench does not shout. +- **Don't** add a second type family or a display serif. +- **Don't** let platform-branded color fight a tenant's palette. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..f6b73bc --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,49 @@ +# Product + +## Register + +product + +## Platform + +web + +## Users + +Primary users are tenant operators — merchants and admins who build and run their own marketplace through the Project Editor (builder) and the Admin/backoffice. They are task-focused power users: configuring theme, layout, navigation, pages, products, and static content, then publishing. Their context is repeated, deliberate work sessions where speed, clarity, and confidence that a change did what they expected matter more than delight. + +Secondary users are end shoppers browsing a tenant storefront — catalog, product pages, cart, static pages. They arrive casually, judge fast, and are conversion-driven. Every storefront is themed per tenant, so shoppers should experience the tenant's identity, not the platform's. + +Operators come first; shoppers second. The tool must be genuinely good to work in, and the storefront it produces must convert. + +## Product Purpose + +A configuration-driven, multi-tenant marketplace platform. One Angular frontend serves unlimited tenants: identity, theme, navigation, page/section/widget composition, and static content all resolve at runtime from a per-tenant `bootstrap.json`, with the tenant chosen by request host. No tenant-specific code paths exist. A new marketplace is onboarded by domain plus config plus backend data — never by forking the frontend. Success is an operator standing up and running a complete, on-brand storefront end to end without writing code, and a shopper on that storefront never sensing the platform underneath. + +## Positioning + +Launch and run a full marketplace with no code: from one runtime config a tenant gets a brandable storefront, an admin backoffice, and a visual editor, onboarded by domain alone. The frontend renders entirely from bootstrap JSON, so tenant identity is fully configurable and the data backend can change without touching the app. The single claim every surface reinforces: everything you see is config, not custom code. + +## Brand Personality + +Precise, calm, trustworthy. The platform chrome behaves like commerce infrastructure: confident, low-drama, and out of the way. It states what happened plainly, makes destructive and publishing actions unambiguous, and never competes for attention with the tenant's own branding. Voice is direct and operator-literate, not salesy. + +## Anti-references + +Not dated enterprise admin: no cluttered gray dashboards, tiny dense tables, or 2010-era Bootstrap backoffice. Not generic AI-SaaS template: no cream/violet gradient landings, hero-metric card rows, tracked-uppercase eyebrows on every section, or identical icon-heading-text card grids. Not consumer toy: no bubbly rounded-everything, mascots, candy colors, or gamified UI. + +## Design Principles + +Config, not custom — the UI's job is to make an entirely configuration-driven system feel direct and predictable; every editor control maps visibly to what it changes. + +Quiet chrome, tenant identity leads — the platform's own shell stays neutral so per-tenant themes carry the storefront's character; the platform never imposes an identity over the tenant's. + +Operator-first clarity — density, task speed, and unambiguous state (draft vs published, saved vs unsaved, destructive vs safe) win over decoration in the tooling surfaces. + +Trust through precision — plain confirmation of what happened, honest empty/error states, and no surprises around publish, reset, or delete. + +Practice what you preach — the editor and admin should feel as considered as the storefronts they produce; the tool is itself a demonstration of the platform's quality. + +## Accessibility & Inclusion + +WCAG 2.2 AA. Body text meets 4.5:1 contrast, all interactive flows are keyboard-navigable with visible focus states, and every animation has a `prefers-reduced-motion` alternative. Because storefront palettes are tenant-configurable, contrast must hold across themes, not just the default one. diff --git a/docs/ADMIN.md b/docs/ADMIN.md index fab5fd1..a63bf6d 100644 --- a/docs/ADMIN.md +++ b/docs/ADMIN.md @@ -297,7 +297,7 @@ implementation) + `features/backoffice/media/` + the shared container/facade/service split as the rest of `admin/*`: - **No real data source exists for orders anywhere in this repo** (already - called out in Sprint 19's dashboard gap and `docs/BACKEND.md` item 7) - + called out in Sprint 19's dashboard gap and `docs/backend/BACKEND-INTEGRATION.md#611-backoffice--orders-planned`) - `AdminOrdersLocalGateway` seeds 24 deterministic synthetic orders in memory (cycling through all statuses/customers) rather than reading from `BackofficeDataService`, since there is nothing there to read. This is @@ -355,14 +355,14 @@ routes at all, this is a net-new admin section. creates the local record. - **Passwordless login**: already existed before this sprint - `AdminAuthService`'s Telegram QR flow (`docs/ADMIN.md`'s existing admin - login section, `docs/BACKEND.md` item 1). This sprint's Users page links + login section, `docs/backend/BACKEND-INTEGRATION.md#25-the-admin-authorization-gap-critical--security-relevant-unresolved`). This sprint's Users page links to it via a hint, doesn't reimplement it. - **Session manager / device manager**: per-user session list (device, IP, last active, current-session badge) with per-session revoke, mocked (`AdminUsersLocalGateway.loadSessions()` fabricates 2 sessions per user on first view) - the real `AdminAuthService`/session-cookie flow only ever tracks the *current* browser's session, so multi-device session - listing has no real backend counterpart yet (see `docs/BACKEND.md` item 1). + listing has no real backend counterpart yet (see `docs/backend/BACKEND-INTEGRATION.md#25-the-admin-authorization-gap-critical--security-relevant-unresolved`). - **Audit**: per-user audit log (role/status changes), same dialog pattern as Sprint 24's per-transaction audit - not the system-wide security/audit log planned for Sprint 26. @@ -492,7 +492,7 @@ final result, not just the later commit. multi-tenant platform - locales/categories/products/static pages are only known at runtime per tenant, not enumerable client-side at build time. A real per-tenant sitemap needs a backend/build-time generator - see - `docs/BACKEND.md` item 17. + `docs/backend/BACKEND-INTEGRATION.md#619-sitemap-future--static-baseline-only-today`. - **Responsive**: spot-checked the admin backoffice and customer-facing marketplace at mobile/tablet/desktop widths. `shared/ui/table` already wraps every admin table in `overflow-x: auto` (no changes needed); the diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index aeea612..f8cb990 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -41,7 +41,7 @@ Component (container) --> Facade --> Domain Service --> Repository/Provide ## Bootstrap / configuration engine -- `ConfigService` loads `BootstrapConfig` (see `docs/BOOTSTRAP.md`) once at startup; `PlatformRuntimeService` applies it (theme, branding, runtime state) and can `reloadFromBootstrap()` for in-memory preview without a full page reload. +- `ConfigService` loads `BootstrapConfig` (see `docs/backend/BACKEND-INTEGRATION.md#4-bootstrap`) once at startup; `PlatformRuntimeService` applies it (theme, branding, runtime state) and can `reloadFromBootstrap()` for in-memory preview without a full page reload. - The bootstrap is the single source of truth for pages, sections, widgets, theme, navigation, footer, static pages, and feature flags (ADR-004). - The Project Editor mutates an in-memory draft of the same `BootstrapConfig` — there is no parallel editor-only model. @@ -65,7 +65,7 @@ Render pipeline: `page config -> section engine -> section renderer -> widget ho ## Feature flags / capability guards (ADR-009) - `bootstrap.featureFlags` (typed) plus the broader `bootstrap.features` (`MarketplaceFeaturesConfig`) surface for UI-facing toggles (wishlist, compare, reviews, recommendations, search history, etc.). -- Feature resolution falls back across older config surfaces to preserve behavior as the flag model evolved across sprints — see `docs/BOOTSTRAP.md` for the full field list. +- Feature resolution falls back across older config surfaces to preserve behavior as the flag model evolved across sprints — see `docs/backend/BACKEND-INTEGRATION.md#4-bootstrap` for the full field list. ## Diagnostics (dev-only) diff --git a/docs/BACKEND-DIFF-VS-MAIN.md b/docs/BACKEND-DIFF-VS-MAIN.md deleted file mode 100644 index d0d733e..0000000 --- a/docs/BACKEND-DIFF-VS-MAIN.md +++ /dev/null @@ -1,50 +0,0 @@ -# Backend Handoff — B2B branch vs `main` - -Audience: backend developer. This is the "what changed and what you need to build" summary for the `B2B` branch compared to `main`. It frames the detailed punch list in [`BACKEND.md`](BACKEND.md) — read that file for exact endpoint shapes and the frontend files that change once each endpoint exists. - -## TL;DR - -`B2B` is **~138 commits ahead of `main`**. Almost all of it is **frontend that has been built ahead of the backend**: a full admin backoffice, a project editor, a media manager, and a design-system component library. Every data-writing feature runs today against an **in-memory / `localStorage` mock gateway** that is already designed to be swapped for a real API via an injection token — the UI, facades, and pages do **not** change when you wire a real backend; you implement one gateway/provider class per domain and rebind its token. - -**You are not changing any existing contract.** Existing auth, payments, `GET /bootstrap`, and `GET /category` are frozen. Everything below is *new* backend surface the frontend is waiting on. - -## What `B2B` adds over `main` (feature-level) - -| Area | What's new on the frontend | Backend today | Priority | -|---|---|---|---| -| Design system | Reusable primitives (Button, Input, Card, Badge, Dialog, Table, Pagination, Skeleton, EmptyState, FormField) + editor primitives (Toggle, Select, ColorPicker, SectionCard, LocaleTabs, KeyValueEditor) | n/a (pure UI) | — | -| Project Editor | Full tenant `BootstrapConfig` editor (11 sections, draft/publish, validation, per-locale content, HTML editor for static pages) | **mock**: draft/publish is `localStorage` only, no persistence | **High** | -| Media manager | Upload / grid / delete + reusable media picker | **mock**: IndexedDB adapter, nothing server-side | High | -| Categories admin | List + create/edit, hierarchy, drag reorder, soft delete/restore, draft/publish, SEO/translations | **mock**: in-memory, no write path | High | -| Products admin | List + editor (variants, barcode, archive, related products, bulk actions) | **mock**: in-memory gateway | High | -| Orders admin | List/detail, status changes, refund, cancel, notes, CSV, invoice | **mock**: 24 synthetic orders | High | -| Transactions admin | List, retry, fraud flag, audit log, CSV | **mock**: derived from mock orders | Medium | -| Users / roles admin | Users, roles, invitations, sessions, audit log | **mock**: synthetic; ties to auth gap below | High | -| Dashboard metrics | Counts, status, health, activity feed | **partial**: counts composed client-side; activity is `localStorage` | Medium | -| Monitoring | Health (real) + event/queue/webhook feeds | **mock** except Health | Medium | -| Analytics | Revenue/orders/top-products (from mock orders) + visitors/funnels/heatmaps | **no data source** for traffic/funnels | Low | -| SEO | Tenant-driven meta tags, `sitemap.xml` / `robots.txt` baseline | **static baseline** only, not per-tenant | Low | - -## The two things to do first - -1. **Admin authorization (security).** Admin and customer login currently share **one** Telegram QR session backend, so the server has no concept of "this is an admin session." Any Telegram user who completes the QR flow on the admin login screen gets an `adminSessionID`. **Server-side authorization keyed off the session id is required** — nothing on the frontend can substitute. See [`BACKEND.md`](BACKEND.md) §1. - -2. **Bootstrap draft / publish persistence.** The Project Editor edits the same `BootstrapConfig` the storefront consumes, but Save is `localStorage`-only and Publish is in-memory — no backend call. Needs `GET/PUT /builder/bootstrap/draft`, `POST /builder/bootstrap/publish`, optional `POST /builder/bootstrap/validate`, and **server-side re-validation** (the client validator is not a trust boundary). See [`BACKEND.md`](BACKEND.md) §2. - -## The pattern for every mocked domain - -For categories, products, orders, transactions, users, monitoring, dashboard metrics: - -1. A `*LocalGateway` (or `*Provider`) implements a gateway interface and is bound via an Angular injection token. -2. You implement a `*ApiGateway` against the **same interface**, hitting real endpoints matching the domain model in `features/admin//models/*.model.ts`. -3. You rebind the token in DI config. **Facades and page components do not change.** - -Model shapes to build against are named per area in [`BACKEND.md`](BACKEND.md) §5–§16. - -## Known production issue not fixable here - -Intermittent `502`/`504` on refresh / back-navigation in production originates from the **backend API's own reverse proxy** (`api.dexarmarket.ru:445`, `users.vitanova.network:456`) — the frontend calls those absolute URLs directly, bypassing this repo's nginx. Needs DevOps/backend investigation of upstream health and timeouts around session-check + bootstrap endpoints. See [`BACKEND.md`](BACKEND.md) "Known reliability issues". - -## Verifying the frontend locally without a backend - -`environment.ts` ships `useMockBootstrapOnLocal: true` and mock gateways are the default bindings, so `npm run dexar` runs the whole thing offline. Admin routes are reachable in dev via `?devBypassAdmin=true`. Flipping a domain to a real API is the gateway-swap above — no mock removal needed to start. diff --git a/docs/BACKEND-INTEGRATION-PROMPT.md b/docs/BACKEND-INTEGRATION-PROMPT.md deleted file mode 100644 index a4acd6c..0000000 --- a/docs/BACKEND-INTEGRATION-PROMPT.md +++ /dev/null @@ -1,54 +0,0 @@ -# Backend Implementation Prompt — B2B branch - -Copy-paste this to a backend developer or an AI agent. It is self-contained; pair it with [`BACKEND-INTEGRATION.md`](BACKEND-INTEGRATION.md) (contract detail) and [`BACKEND.md`](BACKEND.md) (per-domain models). - ---- - -## Prompt - -You are implementing the backend for the **`B2B` branch** of an Angular marketplace/storefront + admin builder. The frontend is already built and runs offline against mocks. Your job is to expose real HTTP endpoints the frontend already calls. **Do not touch authentication (Telegram QR session) or payments — those contracts are frozen and out of scope.** - -### Context you must honor -- **Single build, multi-tenant.** Base URL is resolved per-tenant at runtime. Serve each tenant at `https://.api.dexarmarket.ru:445` (template) or get it registered in `environment.tenantApiBaseUrls`. Default tenant → `https://api.dexarmarket.ru:445`. -- Every storefront request carries headers: `X-Region` (`Moscow`/`ST. Petersburg`/`Yerevan`), `X-Language` (`RU`/`EN`/`AM`), `Currency` (`RUB`/`USD`/`AMD`), `WebSessionID` (32-char hex). Localize and session-scope responses off these. -- The frontend normalizes both legacy and "backOffice" field variants, but you should match the documented models to avoid surprises. - -### Part A — Keep existing storefront endpoints working (already in `main`, do not change shapes) -- `GET /ping` -- `GET /category` -- `GET /category/:id?count=&skip=` -- `GET /items/:id` -- `GET /searchitems?search=&count=&skip=&categoryIDs=&minPrice=&maxPrice=&tag=&sort=` → `{ items, total }` -- `GET /items/randomitems?count=&category=` -- `POST /websession/:sessionId` (cart lines) -- `POST /items/:id/callback` (review), `POST /items/:id/questiion` (question — keep the typo), `POST /purchase-email` - -### Part B — New: `GET /bootstrap` (blocking dependency for prod) -Return one JSON document of type `BootstrapConfig` per tenant: `tenant, branding, theme, company, featureFlags, apiEndpoints, localization, seo, permissions, navigation, footer, pages, staticPages, widgetRegistry`. -- Schema: `src/app/shared/models/config/bootstrap-config.model.ts` -- Reference payload to match field-for-field: `src/assets/mock/bootstrap/bootstrap.json` -- Must be cacheable; frontend loads it once at startup. - -### Part C — New: Builder (Project Editor persistence) — highest priority -The editor edits the same `BootstrapConfig` the storefront reads. Implement: -- `GET /builder/bootstrap/draft` → current draft -- `PUT /builder/bootstrap/draft` ← save draft (whole document) -- `POST /builder/bootstrap/publish` → promote draft to the live `GET /bootstrap` -- `POST /builder/bootstrap/validate` (optional endpoint) — **but server-side validation on publish is mandatory**; treat the client validator as untrusted. - -### Part D — New: Backoffice (admin) CRUD, per domain -For each domain — categories, products, orders, transactions, users/roles, dashboard metrics, monitoring, media — expose REST endpoints under `/backoffice/` matching the interfaces the frontend mock gateways implement. Model shapes and required operations per domain: `BACKEND.md` §5–§16 and `features/admin//models/*.model.ts` / `*-gateway.interface.ts`. The frontend swaps mock→api by rebinding one DI token per domain; your endpoints must satisfy the same interface (list/get/create/update/soft-delete/restore/draft-publish where the interface declares them). - -### Constraints -- No changes to auth or payment endpoints. -- Preserve existing storefront request/response shapes exactly. -- Populate `apiEndpoints.website / builder / backoffice` in the bootstrap document as you add endpoints, so the frontend can discover them. -- Add server-side authorization: admin/builder sessions must be distinguishable from customer sessions server-side (the frontend cannot enforce this). - -### Deliverables -1. `GET /bootstrap` per tenant. -2. Builder draft/publish/validate with server-side re-validation. -3. Backoffice CRUD per domain against the documented interfaces. -4. OpenAPI/spec for B, C, D. Storefront (A) is already specified by the client normalizers — do not deviate. - -Ask before assuming any field you cannot find in the referenced models or `bootstrap.json`. diff --git a/docs/BACKEND-INTEGRATION.md b/docs/BACKEND-INTEGRATION.md deleted file mode 100644 index 9a2a131..0000000 --- a/docs/BACKEND-INTEGRATION.md +++ /dev/null @@ -1,120 +0,0 @@ -# Backend Integration — B2B branch - -How the `B2B` frontend **sends** and **gets** data, and how that differs from `main`. -Scope excludes **login/auth** and **payments** — those contracts are frozen and untouched here. - -Source of truth in code: -- `src/app/services/api.service.ts` — storefront reads/writes -- `src/app/core/config/api-config.service.ts` — base-URL resolution -- `src/app/interceptors/api-base-url.interceptor.ts` + `api-headers.interceptor.ts` — request rewriting + headers -- `src/app/core/config/config.service.ts` + `core/bootstrap/providers/*` — bootstrap load -- `src/app/shared/models/config/*` — bootstrap/endpoint models - ---- - -## 1. What changed vs `main` (one screen) - -| Concern | `main` | `B2B` | -|---|---|---| -| API base URL | Hardcoded `environment.apiUrl` (`/api`) | Resolved per-tenant at runtime by `ApiConfigService` | -| Multi-tenant | One build per brand (`environment.lavero.ts`, `index.novo.html`, …) | **Single build**, tenant resolved from host; per-brand env/html files deleted | -| Config source | Compiled into `environment.ts` (brand, theme, logo, phones) | Fetched at runtime via **`GET /bootstrap`** → `BootstrapConfig` | -| Data providers | `ApiService` called directly everywhere | Same endpoints, now behind **repository/provider interfaces + DI tokens** (swap mock↔api without touching UI) | -| Mock strategy | `mockDataInterceptor` intercepts HTTP | Provider/gateway pattern picks mock vs api per domain (`RuntimeProviderStrategyService`) | -| Interceptor chain | `[mockData, apiHeaders, cache]` | `[mockData, **apiBaseUrl**, apiHeaders, adminAuth, cache]` | -| Storefront read/write endpoints | see §3 | **unchanged in shape** | - -**Bottom line for backend:** the storefront read/write contract (categories, items, search, cart, reviews) is the **same as `main`**. What is *new* is (a) a `GET /bootstrap` document the frontend now depends on, and (b) two whole endpoint namespaces (`builder`, `backoffice`) that are declared but **empty** and served by client-side mocks today. - ---- - -## 2. How a request is built (B2B pipeline) - -Every storefront call goes through this chain: - -1. Code calls `this.http.get('/api/category')` (or `${apiConfig.getBaseUrl()}/category`). -2. **`apiBaseUrlInterceptor`** — if the URL starts with `/api`, rewrites it to the resolved tenant base via `ApiConfigService.toApiUrl()`. -3. **`apiHeadersInterceptor`** — on any API request, attaches: - - `X-Region` — `Moscow` / `ST. Petersburg` / `Yerevan` (from region id) - - `X-Language` — `RU` / `EN` / `AM` - - `Currency` — `RUB` (default) / `USD` / `AMD` - - `WebSessionID` — 32-char hex; from auth session if present, else a persisted anonymous id (`localStorage: web_session_id`) -4. Request goes out to the resolved absolute base. - -### Base-URL resolution (`ApiConfigService.getBaseUrl`) -Priority order: -1. localhost → `environment.localhostApiUrl` (`/api`, proxied by dev server) -2. `environment.tenantApiBaseUrls[tenantKey]` (e.g. `default`/`dexarmarket` → `https://api.dexarmarket.ru:445`) -3. `environment.tenantApiTemplate` → `https://{tenant}.api.dexarmarket.ru:445` -4. (opt-in) `bootstrap.apiEndpoints.website.baseUrl` / `bootstrap.tenant.apiBaseUrl` — only if `allowBootstrapApiOverride: true` **and** absolute -5. fallback `environment.apiUrl` - -> Backend impact: for a **new tenant**, either add it to `tenantApiBaseUrls`, or serve it at `https://.api.dexarmarket.ru:445` so the template matches. No frontend rebuild needed if the template host pattern holds. - ---- - -## 3. GET — reads (storefront, real HTTP today) - -Identical shapes to `main`. `ApiService` normalizes legacy **and** backOffice response formats (see `normalizeItem`/`normalizeCategory` — dual field names, `0x`-hex colours, `names[]`→translations, `imgs[]`→photos, Go-typo tolerance like `valuue`). - -| Method | Endpoint | Notes | -|---|---|---| -| `getCategories()` | `GET /category` | retry ×2 backoff | -| `getCategoryItems(id,count,skip)` | `GET /category/:id?count=&skip=` | | -| `getItem(id)` | `GET /items/:id` | rating/reviews/questions derived from item | -| `searchItems(q,count,skip,opts)` | `GET /searchitems?search=&count=&skip=&categoryIDs=&minPrice=&maxPrice=&tag=&sort=` | returns `{items,total}` | -| `getRandomItems(count,category?)` | `GET /items/randomitems?count=&category=` | featured/related | -| `ping()` | `GET /ping` | health | - -`GET /bootstrap` — **new, required in prod.** Returns the full `BootstrapConfig` (tenant, branding, theme, company, featureFlags, `apiEndpoints`, localization, seo, permissions, navigation, footer, pages, staticPages, widgetRegistry). Loaded once, cached (`ConfigService.loadBootstrap`, `shareReplay(1)`). Shape: `src/app/shared/models/config/bootstrap-config.model.ts`; canonical example: `src/assets/mock/bootstrap/bootstrap.json`. - -On **localhost** (or `useMockData:true`) the `MockBootstrapProvider` serves `bootstrap.json` instead of hitting `/bootstrap`, so the app runs fully offline (`npm run dexar`). - ---- - -## 4. POST — writes (storefront, real HTTP today) - -Unchanged from `main` (payment writes omitted per scope): - -| Method | Endpoint | Body | -|---|---|---| -| `addToCart(sessionId, items)` | `POST /websession/:sessionId` | `[{itemID,quantity,colour?,size?,price?}]` | -| `submitReview({...})` | `POST /items/:id/callback` | `{rating,comment,sessionID,timestamp}` | -| `submitQuestion({...})` | `POST /items/:id/questiion` | `{question,sessionID,timestamp}` — **note existing path typo, keep it** | -| `submitPurchaseEmail({...})` | `POST /purchase-email` | `{email,phone?,telegramUserId,items[]}` | - ---- - -## 5. New backend surface the frontend is waiting on - -Declared in the bootstrap model (`apiEndpoints.builder`, `apiEndpoints.backoffice`) but currently `{}` empty and served by **localStorage / in-memory gateways**. UI is done; wiring a real API = implement one gateway/provider against the existing interface and rebind its DI token — no page/component changes. - -### 5a. Builder (Project Editor) — **highest priority** -The editor edits the same `BootstrapConfig` the storefront consumes. Today `ProjectEditorIoService` only `JSON.stringify`s it (export) — **no HTTP, no persistence**. Needed: -- `GET /builder/bootstrap/draft` → current draft `BootstrapConfig` -- `PUT /builder/bootstrap/draft` ← save draft -- `POST /builder/bootstrap/publish` → promote draft to the live `GET /bootstrap` document -- `POST /builder/bootstrap/validate` (optional) → **server-side re-validation is mandatory**; the client validator is not a trust boundary - -### 5b. Backoffice (admin) — mock gateways awaiting real API -Each domain has a `*Gateway`/`*Provider` interface + a `*LocalGateway` mock bound via token. Implement `*ApiGateway` against the same interface, endpoints under `/backoffice/...`, models in `features/admin//models/*.model.ts`: -categories, products, orders, transactions, users/roles, dashboard metrics, monitoring feeds, media. - -Provider selection is centralized in `RuntimeProviderStrategyService` (`getBackofficeProviderMode`, `getProductProviderMode`, `getCategoryProviderMode`): returns `mock` only when `environment.useMockData`, else `api`. - -> Full per-domain punch list and model shapes: [`BACKEND.md`](BACKEND.md) §5–§16. Handoff framing: [`BACKEND-DIFF-VS-MAIN.md`](BACKEND-DIFF-VS-MAIN.md). - ---- - -## 6. Contract rules for the backend - -- **Do not change** existing storefront read/write shapes (categories, items, search, cart, reviews, questions) — frontend `main` and `B2B` both depend on them. -- **Serve `GET /bootstrap`** matching `bootstrap-config.model.ts`. Field-name tolerance exists on the client, but match the model to avoid normalization surprises. -- Respect request headers `X-Region`, `X-Language`, `Currency`, `WebSessionID` for localization/session-scoped responses. -- New namespaces `builder/*` and `backoffice/*` are yours to define; keep them consistent with the declared `apiEndpoints` block so they can be published via bootstrap. -- **Out of scope here:** auth (Telegram QR session) and payments (`/qr`, `/card`, `/cart` payment) — unchanged, see existing docs. - ---- - -## 7. Known reliability issue (not fixable in frontend) -Intermittent `502`/`504` on refresh/back-nav in prod originate from the upstream reverse proxy at `api.dexarmarket.ru:445` / `users.vitanova.network:456` — the frontend calls those absolute URLs directly. Needs DevOps/backend investigation of upstream health/timeouts around bootstrap + session-check. diff --git a/docs/BACKEND.md b/docs/BACKEND.md deleted file mode 100644 index c24b0bd..0000000 --- a/docs/BACKEND.md +++ /dev/null @@ -1,197 +0,0 @@ -# BACKEND - -This is the punch list for backend engineers. Every item below is a frontend behavior that is currently **mocked or local-only**, with the exact gap, the endpoint(s) needed, and which frontend files change once the endpoint exists. Read this file instead of diffing the whole repo against `main`. - -General contract rules (apply to everything below): tenant is resolved by request host, never a path/query param (`docs/ARCHITECTURE.md`); bootstrap/config responses must never include secrets; existing auth/payment/authorization contracts are frozen (ADR-010) — nothing here changes them. - -## 1. Auth — admin authorization gap (highest priority, security-relevant) - -**Current frontend behavior:** Admin login and customer login share **one** Telegram QR/session backend (`{authApiUrl}/users/sessions`) and **one** UI component (`TelegramLoginComponent`, `mode: 'customer' | 'admin'`). The only client-side difference is *where the resulting session id is stored*: `webSessionID` cookie (`SameSite=Lax`) for customers vs. `adminSessionID` cookie (`SameSite=Strict`) for admin, via separate `AuthService` / `AdminAuthService`. Admin API calls attach `AdminWebSessionID` via `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`). - -**The gap:** because both flows hit the identical Telegram session endpoint, the backend has **no concept of "this is an admin session"** at the moment the QR is scanned — it's an ordinary Telegram user session, indistinguishable from a customer's. The frontend only decides where to *store* the resulting id; it cannot and does not decide whether that Telegram user is actually allowed to act as an admin. Any Telegram user who completes the QR flow while the admin login UI is showing gets an `adminSessionID`. - -**What's needed:** server-side authorization check keyed off the session id (or a dedicated admin-scoped token) at the point admin API calls arrive with `AdminWebSessionID` — reject non-admin users. This must be enforced server-side; nothing on the frontend can substitute for it. - -**Frontend files that would change:** `src/app/core/admin-auth/admin-auth.service.ts`, `admin-auth-headers.interceptor.ts`, `admin-auth.guard.ts` — only if the auth response shape changes (e.g. a role claim to check client-side in addition to the server-side enforcement). - -**Also prepared, not wired:** `core/admin-auth/ed25519-verification.model.ts` defines `Ed25519VerificationService` (challenge/verify) for a future non-Telegram admin auth path. Current binding (`NoopEd25519VerificationService`, in `app.config.ts`) throws rather than silently accepting — safe to leave wired until a real challenge/verify endpoint exists. - -## 2. Bootstrap draft / publish persistence (second-highest priority) - -**Current frontend behavior:** The Project Editor (`docs/EDITOR.md`) edits the same `BootstrapConfig` the storefront consumes. -- Load: `GET /bootstrap` (existing, tenant by host). -- Save: in-memory + `localStorage` only (`ProjectEditorDraftStorageService`, key `projectEditor.draftBootstrap.v1`, scoped by `tenant.id`). Reload in another browser/tab and it's gone. -- Publish: applies the bootstrap in-memory via `PlatformRuntimeService.reloadFromBootstrap()` and flips a local `status` flag. **No backend call.** - -**Endpoints needed** (none exist yet; shapes are contracts, adjust to house conventions): - -``` -GET /builder/bootstrap/draft - -> tenant's current draft BootstrapConfig. 404/empty if none (draft = published). - -PUT /builder/bootstrap/draft - body: BootstrapConfig - -> persists the draft for this tenant. Does not affect GET /bootstrap (storefront-facing). - -POST /builder/bootstrap/publish - body: BootstrapConfig (or none, if publish always promotes the stored draft) - -> validates, then makes this BootstrapConfig what GET /bootstrap returns. Only endpoint - that affects the live storefront. - -POST /builder/bootstrap/validate (optional) - body: BootstrapConfig - -> { code: string, message: string }[], mirroring ProjectValidationIssue shape client-side. -``` - -Tenant identity: resolved by request host, same as every other endpoint — no `projectId` path param. One domain = one tenant = one draft = one published bootstrap. - -**Don't duplicate client validation, enforce it server-side too.** `ProjectValidator` (`features/project-editor/services/project-validator.service.ts`) blocks Publish client-side on: missing `branding.logoUrl`, empty `localization.supportedLocales`, invalid `tenant.websiteBaseUrl` (must be `http(s)://...`), duplicate static-page identifiers (slug, falling back to `route`), empty homepage sections, a homepage widget with no `type`, duplicate header nav links, non-hex `theme.palette` values, missing translations for a supported locale, and unknown layout/section-layout enum values. A malicious or stale client can bypass all of this — if `POST /builder/bootstrap/publish` is a trust boundary, re-run equivalent checks server-side. - -**Frontend files that would change:** `features/project-editor/facade/project-editor.facade.ts` (replace local save/publish with the new endpoints, keep the same public method signatures), `services/project-editor-draft-storage.service.ts` (becomes a fallback/offline cache rather than primary store). - -## 3. Static pages / `slug` vs `route` inconsistency - -**Gap:** `StaticPageConfig` requires `slug: string`, but at least one real bootstrap fixture (`src/assets/mock/bootstrap/bootstrap.json`) only populates `route` (e.g. `/about-us`) and leaves `slug` undefined. The frontend's duplicate-detection was patched to fall back to `route` when `slug` is empty, but the underlying data inconsistency remains. - -**Needed:** decide whether `slug` is backend-required or auto-derived from `route`, so both sides agree on one source of truth going forward. - -**Frontend files:** `shared/models/config/static-page.model.ts`, `features/content-management/services/*` (`ContentPageService` normalization), `features/project-editor/services/project-validator.service.ts`. - -## 4. Static pages / CMS persistence - -**Current frontend behavior:** `staticPages` are edited client-side in the Project Editor (create/delete page, toggle footer/header/sitemap visibility, edit slug/icon/order/translations/HTML) and only ever written into the in-memory/`localStorage` bootstrap draft above (item 2) — no dedicated backend for CMS content exists. - -**Endpoints needed:** -``` -GET /builder/content-pages -PUT /builder/content-pages -POST /builder/content-pages/import -GET /builder/content-pages/export -POST /builder/content-pages/validate -``` -Backend must also support content moderation/validation on publish (disallow dangerous tags/attributes) and revision history — the frontend only sanitizes at render time, not at authoring time (`MarketplaceHtmlEditorComponent` emits raw HTML with no sanitization by design; sanitization is a storefront-render concern, not an authoring concern). - -**Frontend files:** `features/content-management/facade/*`, `services/*`, `components/html-editor/*`, `features/project-editor/sections/footer-section.component.ts` (static page list editing today lives partly here too). - -## 5. Categories - -**Current behavior (Sprint 20):** `GET /category` (existing) still backs the public storefront via `CategoryDto -> CategoryMapper -> Category -> CategoryFacade`, unchanged. A full admin editor now exists at `features/admin/categories/` (list + create/edit, hierarchy, drag-and-drop reorder, soft delete/restore, draft/publish, SEO/translations — see `docs/ADMIN.md` "Sprint 20") but it runs entirely against `AdminCategoriesLocalGateway`, an in-memory cache seeded once from `BackofficeDataService.loadCategories()` (`CategoryCardConfig`, no hierarchy) — nothing persists across a page reload. - -**Gap:** no admin write path (create/update/delete/reorder categories) exists on the backend. `AdminCategory` also carries fields the current `CategoryDto`/`CategoryCardConfig` don't have yet: `parentId` (hierarchy), `slug`, `icon`, `imageUrl`, `status` (draft/published), `deletedAt` (soft delete), `seo`, per-locale `translations`. - -**Needed:** category CRUD endpoints matching the `AdminCategory` shape (`src/app/features/admin/categories/models/admin-category.model.ts`) plus a bulk reorder endpoint (`order` field) and a slug-uniqueness check (`GET /admin/categories/slug-taken?slug=...`, mirrors `AdminCategoriesGateway.isSlugTaken`). - -**Frontend files:** implement `AdminCategoriesApiGateway` against `AdminCategoriesGateway` (`services/admin-categories-gateway.interface.ts`) and rebind via an injection token (same swap pattern as `AdminDashboardMetricsGateway`/`BACKOFFICE_DATA_PROVIDER`) — the facade and pages don't change. Also still open: wire `admin/products`' category dropdown to `AdminCategoriesGateway` instead of its own `AdminProductsGateway.loadCategories()` (Sprint 21). - -## 6. Products - -**Current behavior (Sprint 21):** `features/admin/products/` is fully built (list + editor pages) against `AdminProductsLocalGateway` (swappable via an injection token, same pattern as everywhere else) — i.e. it's ready for a real API gateway, but one has never been implemented. Sprint 21 added `barcode`, `archived` (soft archive/restore), `variants` (`{name, price, quantity}[]`), `relatedProductIds`, and wired the category dropdown to `AdminCategoriesGateway` (see item 5) instead of its own seed. - -**Needed:** product CRUD endpoints matching the current `AdminProduct` shape (`src/app/features/admin/products/models/admin-product.model.ts`) — `itemID`, `name`, `price`, `currency`, `categoryID`, `visible`, `archived`, `discount`, `images`, `badges`, `media`, `specifications`/`attributes`, `variants`, `relatedProductIds`, plus a bulk endpoint matching `PATCH /items/bulk`-style semantics for `applyBulkVisibility`/`applyBulkDelete`. - -**Frontend files:** implement `AdminProductsApiGateway` alongside the existing `AdminProductsLocalGateway` and rebind the injection token — `features/admin/products/pages/*` and the facade do not change. - -## 7. Orders / revenue - -**Current behavior (Sprint 23):** `features/admin/orders/` now exists as an admin CRUD-ish surface (list/detail, status changes, refund request, cancel, notes, CSV export, print invoice) but runs entirely against `AdminOrdersLocalGateway`, which fabricates 24 synthetic in-memory orders — there is still no real order data anywhere in this system. The dashboard's Orders/Revenue cards (Sprint 19) still correctly render `pending-backend` rather than reading from this mock (they're intentionally not wired to it — the mock is order-management scaffolding, not a real metrics source). - -**Needed:** a real order domain — `AdminOrder` shape is in `src/app/features/admin/orders/models/admin-order.model.ts`. At minimum: order CRUD, status transitions with a timeline/audit trail, payment status, refund workflow, and a revenue aggregation endpoint for the dashboard cards. - -**Frontend files:** implement `AdminOrdersApiGateway` against `AdminOrdersGateway` (`services/admin-orders-gateway.interface.ts`) and rebind via an injection token — facade and pages don't change. - -## 8. Dashboard metrics - -**Current behavior:** `AdminDashboardMetricsGateway` (token `ADMIN_DASHBOARD_METRICS_GATEWAY`) defaults to `AdminDashboardMetricsLocalGateway`, which composes `BackofficeDataService.loadCategories()/loadProducts()` client-side into counts. Everything else on the dashboard (marketplace status, theme, languages, last publish/save, bootstrap version, active layout, enabled widgets, system health) is derived from `ProjectEditorFacade` state, not a metrics endpoint. - -**Needed:** a `/builder/dashboard/summary`-style endpoint returning real-time counts and trend deltas. - -**Frontend files:** implement `AdminDashboardMetricsGateway` (real API version) and rebind the token in DI config — `AdminDashboardFacade` and all dashboard card components are unaffected (`docs/ADMIN.md` has the full architecture). - -## 9. Recent Activity (admin dashboard) - -**Current behavior:** `AdminDashboardHistoryService` is `localStorage`-backed, scoped per tenant (`adminDashboard.activityHistory.v1`) — same limitation pattern as the draft storage in item 2. It will never show another editor's activity. - -**Needed:** a real audit-log endpoint (who changed what, when) that multiple admin sessions can read. - -**Frontend files:** `features/admin/dashboard/services/admin-dashboard-history.service.ts`, `facade/admin-dashboard.facade.ts`. - -## 10. Translations - -**Current behavior:** all UI strings are static, compiled into the three locale files (`src/app/i18n/{en,ru,hy}.ts`) via the `Translations` interface (`translations.ts`) and served by `TranslateService`/`TranslatePipe`. There is no backend-editable translation surface — bootstrap-level translatable content (nav labels, static-page translations, footer copyright) is edited per-tenant through the Project Editor and stored in the bootstrap itself (see item 2), which is separate from the compiled UI-chrome strings. - -**Gap:** UI-chrome strings (button labels, section titles, validation messages) require a frontend deploy to change or add a locale — there's no backend-driven UI-string catalog. This may be acceptable (frontend chrome vs. tenant content are different concerns) but is worth an explicit decision if a backend team expects to control all copy. - -**Frontend files:** `src/app/i18n/*` (if this becomes backend-driven, it's a new i18n loading mechanism, a larger change than any other item here). - -## 11. Search / autocomplete / trending (future-ready, not urgent) - -**Current behavior:** in-memory products/categories/tags power autocomplete and suggestions (`features/search/services/search-autocomplete.service.ts`); trending returns `null` (UI hides gracefully); search history is `LocalSearchHistoryRepository` (guest) with a `BackendSearchHistoryRepository` placeholder already defined but unimplemented. - -**Endpoints that would let this go live without frontend changes:** -``` -GET /search/suggestions?q={term} -GET /catalog/filters?category={id}&q={term} -``` -Plus, if authenticated history/wishlist/compare/saved-searches sync is wanted: `GET/POST/DELETE /me/wishlist`, `/me/compare`, `/me/saved-searches`, `/me/recently-viewed`. - -**Frontend files:** `features/search/services/search-autocomplete.service.ts`, `search-trending.service.ts`, `search-history.repository.ts` (swap `LocalSearchHistoryRepository` for `BackendSearchHistoryRepository`) — facade/UI unaffected by design. - -## 12. Product engagement (rating/reviews/questions) — partially real, confirm contract - -**Current behavior:** frontend already expects these endpoints and renders against them: `GET /products/{id}/rating`, `GET /products/{id}/reviews?page&pageSize`, `GET /products/{id}/questions?page&pageSize`, `POST /products/{id}/reviews` (rating/title/text/anonymous), `POST /products/{id}/questions` (text/anonymous). If these already exist, this is a contract-confirmation item, not new work; if not, they block the Reviews/Questions UI (`productPage.reviews`/`questions` config, already toggleable per tenant). - -**Frontend files:** none, if the contract matches what's documented in `docs/BOOTSTRAP.md`'s Product Engagement notes. - ---- - -## 13. Transactions (Sprint 24, mock/local) - -**Current behavior:** `features/admin/transactions/` exists (list, retry-failed, fraud flag, per-transaction audit log, CSV export) against `AdminTransactionsLocalGateway`, which derives one synthetic transaction per seeded mock order from item 7's `AdminOrdersLocalGateway` — no real payment/transaction data exists. - -**Needed:** a real payments/transactions domain (card, QR, cash-on-delivery), linked to orders, with retry semantics matching whatever the actual payment provider supports, and fraud-flag persistence. - -**Frontend files:** implement `AdminTransactionsApiGateway` against `AdminTransactionsGateway` (`services/admin-transactions-gateway.interface.ts`) and rebind via an injection token. - -## 14. Users, roles & permissions (Sprint 25, mock/local) - -**Current behavior:** `features/admin/users/` (users, built-in roles, invitations, per-user mock sessions, per-user audit log) against `AdminUsersLocalGateway` — fully synthetic, no backend. Passwordless login itself is real (`AdminAuthService`, Telegram QR, `docs/BACKEND.md` item 1) — only the roles/permissions/invitations/multi-session-listing layer on top is mocked. - -**Needed:** a real user/role domain tied to the eventual server-side admin-authorization enforcement in item 1 — role assignment, a real permission catalog, invitation emails, and genuine multi-device session listing (the current `AdminAuthService` only ever knows about the current browser's session). - -**Frontend files:** implement `AdminUsersApiGateway` against `AdminUsersGateway` (`services/admin-users-gateway.interface.ts`) and rebind via an injection token. - -## 15. Monitoring (Sprint 26, mock/local except Health) - -**Current behavior:** `features/admin/monitoring/` — Health section reads real data (`AdminDashboardFacade.healthChecks`, unchanged from Sprint 19). Everything else (audit/security/login/failed-login/API/error/warning event feed, queue depths, webhook deliveries) is synthetic, seeded once in `AdminMonitoringLocalGateway` — no logging, queue, or webhook infrastructure exists anywhere in this system. - -**Needed:** real structured logging with a query API (by category/level/actor/time range), real queue introspection (whatever job runner ships), and real webhook delivery tracking once webhooks exist as a feature at all. - -**Frontend files:** implement `AdminMonitoringApiGateway`-equivalent methods against a to-be-defined `AdminMonitoringGateway` interface (`AdminMonitoringLocalGateway` currently has no interface extracted — add one when a real implementation is built, mirroring the pattern used everywhere else in `admin/*`). - -## 16. Analytics - visitors/funnels/heatmaps (Sprint 27, no backend at all) - -**Current behavior:** `features/admin/analytics/` computes real revenue/orders/top-products aggregations from the existing mock order data (see item 7), but visitor traffic, conversion funnels, and heatmaps have zero data source anywhere in this system (no analytics/tracking pipeline, no event collection) — these render `pending-backend` badges rather than fabricated numbers. - -**Needed:** a traffic/event tracking pipeline (page views, sessions, conversion events) and a funnel/heatmap aggregation service, before this section of the Analytics page can show anything real. - -**Frontend files:** `features/admin/analytics/pages/admin-analytics-page.component.html` currently renders the pending-backend badge inline (no gateway method exists for this yet, unlike every other mocked domain in this doc). - -## 17. Sitemap generation (Sprint 28, static baseline only) - -**Current behavior:** `public/sitemap.xml` (new, Sprint 28) lists only the statically-known top-level marketplace routes (home/catalog/search/wishlist/compare) for the default `ru` locale segment, referenced from `public/robots.txt`'s `Sitemap:` directive. - -**Gap:** this is a multi-tenant, config-driven platform (`docs/ARCHITECTURE.md`) — supported locales, categories, products, and static pages are all resolved at runtime from each tenant's bootstrap config, not enumerable from the frontend at build time. A real per-tenant sitemap covering `/:lang/product/:id`, `/:lang/catalog/:categoryId`, and `/:lang/:staticPath` needs a backend/build-time job that reads the same bootstrap data source (categories/products/static pages, items 5/6/3-4 above) per tenant/domain and regenerates or serves this file dynamically — not something the SPA can produce correctly on its own. - -**Needed:** a build-time or server-side sitemap generator with access to the real per-tenant product/category/static-page lists and domain, likely alongside whatever eventually serves `bootstrap.json` per-domain server-side (item 2). - -## Known reliability issues - -### Production 502/504 Bad Gateway on refresh / back-navigation - -**Symptom:** production hits intermittent `502`/`504 Bad Gateway` responses specifically on page refresh and browser back-navigation. - -**Root cause (investigation so far):** `environment.production.ts` points the frontend at the backend API via **absolute URLs directly** (`apiUrl: 'https://api.dexarmarket.ru:445'`, `authApiUrl: 'https://users.vitanova.network:456'`), bypassing this repo's `nginx.conf` entirely — that config only proxies `/api` for the `lovero.store` tenant, not `dexarmarket.ru`. So the 502/504 originates from **that backend API's own reverse proxy** (ports 445/456, a separate server not in this repo), not from anything this repo controls. - -Refresh and back-navigation both re-fire session-check and bootstrap-load calls on mount (`AdminAuthService.checkSession()`, `ConfigService.loadBootstrap()`, `TelegramSessionApiService`), which is the likely trigger if that backend's app server or reverse proxy is crashing, overloaded, or misconfigured on those specific endpoints. - -**This is not fixable from this repo.** It needs DevOps/backend investigation on the reverse proxy and app server fronting `api.dexarmarket.ru:445` and `users.vitanova.network:456` — check upstream health, timeout settings, and concurrent-connection handling around session-check and bootstrap endpoints. diff --git a/docs/BOOTSTRAP.md b/docs/BOOTSTRAP.md deleted file mode 100644 index 4c13ff5..0000000 --- a/docs/BOOTSTRAP.md +++ /dev/null @@ -1,153 +0,0 @@ -# BOOTSTRAP - -The `BootstrapConfig` model (`src/app/shared/models/config/bootstrap-config.model.ts`) is the single JSON contract that drives the entire storefront for a tenant. `ConfigService` loads it once at startup via `GET /bootstrap` (tenant resolved server-side by request host); `PlatformRuntimeService` applies it and can re-apply an edited in-memory copy for preview. - -## Top-level shape - -```ts -interface BootstrapConfig { - schemaVersion: string; - generatedAt: string; - tenant: TenantConfig; // required - branding: BrandingConfig; // required - theme: ThemeConfig; // required - company: CompanyConfig; // required - featureFlags: FeatureFlagsConfig; // required - features?: MarketplaceFeaturesConfig; // optional, centralized feature toggles - apiEndpoints: ApiEndpointsConfig; // required - localization: LocalizationConfig; // required - seo: SeoConfig; // required - permissions: PermissionsConfig; // required - header?: HeaderConfig; - catalog?: CatalogConfig; - layout?: PlatformLayoutConfig; - navigation: NavigationConfig; // required - footer?: FooterConfig; - productPage?: ProductPageConfig; - userExperience?: UserExperienceConfig; - pages: PageConfig[]; // required - staticPages?: StaticPagesConfig; - widgetRegistry?: WidgetRegistryConfig; -} -``` - -## Field-by-field - -| Key | Meaning | -|---|---| -| `schemaVersion` | Contract version. Breaking changes require a bump; frontend must stay compatible within a minor line. | -| `generatedAt` | Payload generation timestamp. | -| `tenant` | `id`, `slug`, `host`, `name`, `defaultLocale`, `supportedLocales`, `defaultCurrency`, `websiteBaseUrl`, etc. Tenant is resolved by domain only — never re-derived on the frontend. | -| `branding` | `logoUrl`, `logoCompactUrl`, `faviconUrl`, `brandName`. | -| `theme` | `themeId`, `mode` (`light\|dark\|system`), `palette` (12 semantic colors), `typography`, `spacing`, `borderRadiusScale`, `shadows`, `iconSet`. See `docs/ARCHITECTURE.md` theme engine section. | -| `company` | Legal/contact info used in the footer: `companyName`, `address`, `contacts.phone`/`email`. | -| `featureFlags` | Simple boolean toggles: `wishlist`, `compare`, `reviews`, `comments`, `recommendations`, etc. | -| `features` | Newer centralized feature surface (`MarketplaceFeaturesConfig`) — wishlist/compare/reviews/comments/questions/recommendations/recentlyViewed/searchHistory/recentlySearched/ratings/share/brands/manufacturers/availability/discounts/badges. Resolvers fall back to `featureFlags`/`productPage`/`userExperience`/`catalog` for older bootstraps. | -| `apiEndpoints` | Public endpoint map the frontend's API layer reads (base URLs, paths, timeouts). Never contains secrets. | -| `localization` | `defaultLocale`, `supportedLocales`, optional `currencyByLocale`. | -| `seo` | `default.title`/`default.description` plus per-page SEO overrides. | -| `permissions` | Roles/permissions for admin surfaces (currently minimal; see `docs/BACKEND.md`). | -| `header` | Boolean toggles: `showLogo`, `showSearch`, `showCategories`, `showLanguages`, `showCart`, `showProfile`, `showWishlist`, `showCompare`, `showRegion`. | -| `catalog` | UI/feature config only (no product data) — see Catalog Config below. | -| `layout` | `PlatformLayoutConfig`: `{ type: 'default'|'sidebar-left'|'carousel-home'|'minimal', options?: Record }`. Global page-chrome mode. | -| `navigation` | `header[]` / `footer[]` link arrays: `id`, label (translatable), `route`, `order`, `visible`. | -| `footer` | Payment icons, social links, copyright (per-locale), static-page references. | -| `productPage` | Feature config only for the product detail page — rating/reviews/questions/tabs/relatedProducts/actions enablement, pagination size, mode. No review/question *data* lives here. | -| `userExperience` | Feature config only for wishlist/compare/recentlyViewed/share/savedSearches — flags and limits, never user-specific lists. | -| `pages` | Array of `PageConfig`: `id`, `key`, `route.path`, `sections: SectionConfig[]`. | -| `staticPages` | CMS-style informational/legal pages — see `staticPages` below. | -| `widgetRegistry` | Pointer/metadata for the widget manifest (see `docs/ARCHITECTURE.md` widget engine). | - -### Section config (`shared/models/config/section.model.ts`) - -```ts -type SectionLayoutStrategy = 'stack' | 'grid' | 'hero' | 'carousel' | 'split'; - -interface SectionConfig { - id: string; type: string; order: number; - layout?: { strategy?: SectionLayoutStrategy; columns?: number; gap?: string; align?: 'start'|'center'|'end'|'stretch' }; - visibility?: { desktop?: boolean; tablet?: boolean; mobile?: boolean }; - widgets: WidgetConfig[]; - featureFlag?: string; - visible?: boolean; -} -``` - -### Widget config (`shared/models/config/widget.model.ts`) - -Each widget has `id`, `type`, `version`, optional `title`/`subtitle`/`order`/`padding`/`visibility`/`animation`/`style`/`permissions`/`actions`/`featureFlag`/`visible`, and `props: Record` (widget-specific). Typed editors exist in the Project Editor for `hero`, `categories`, `product-collection`; everything else edits `props` as raw JSON (see `docs/EDITOR.md`). - -### Catalog Config (`shared/models/config/catalog-config.model.ts`) - -```ts -type CatalogLayoutModeConfig = 'grid'|'large-grid'|'compact-grid'|'grid-2'|'grid-3'|'grid-4'|'compact'|'list'; -type CatalogNavigationModeConfig = 'default'|'left-category-navigation'|'mega-category-layout'|'top-category-carousel'; -type CatalogLoadingStrategy = 'pagination'|'loadMore'|'infiniteScroll'; -``` -Plus `defaultSort`/`availableSorts` (`relevance|latest|price_asc|price_desc|rating|popular|discount`), `enabledFilters: string[]`, and `show*`/`*Enabled` booleans (breadcrumbs, category banner, subcategory chips, ratings, discounts, availability, suggestions, search history). This is feature-configuration only — no product/filter *data* lives in bootstrap. - -### `staticPages` - -Each entry: `id`, `slug`, `title`, `showInHeader`, `showInFooter`, `showInSitemap`, `icon`, `order`, `visibility`, `requiresAuthentication`, `footerGroup`, `translations[locale] = { title, html, seo }`. Rendered dynamically (no hardcoded page list); HTML is sanitized on render. **Known inconsistency:** the model requires `slug`, but the mock bootstrap only populates `route` for some pages — the frontend's duplicate-slug validator falls back to `route` when `slug` is empty (see `docs/BACKEND.md`). - -## Representative example (trimmed) - -```json -{ - "schemaVersion": "2.1.0", - "generatedAt": "2026-07-05T10:30:00Z", - "tenant": { - "id": "tenant-dexar-ru", "slug": "dexar-ru", "host": "dexarmarket.ru", - "name": "Dexar Market", "defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"], - "defaultCurrency": "RUB", "websiteBaseUrl": "https://dexarmarket.ru" - }, - "branding": { "logoUrl": "/assets/brand/logo.svg", "faviconUrl": "/assets/brand/favicon.ico", "brandName": "Dexar Market" }, - "theme": { - "themeId": "dexar-light", "mode": "light", - "palette": { "primary": "#2F6E5D", "secondary": "#8FA9A2", "backgroundPrimary": "#FFFFFF", "textPrimary": "#1F322D" }, - "typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 } - }, - "layout": { "type": "default" }, - "catalog": { "layout": "grid-4", "navigationMode": "default", "defaultSort": "relevance", "showRatings": true }, - "navigation": { - "header": [ { "id": "nav-home", "label": "Home", "route": "/", "order": 1, "visible": true } ], - "footer": [ { "id": "nav-privacy", "label": "Privacy", "route": "/privacy-policy", "order": 1, "visible": true } ] - }, - "pages": [ - { - "id": "page-home", "key": "home", "route": { "path": "/", "exact": true }, - "sections": [ - { - "id": "home-hero", "type": "hero", "order": 1, - "layout": { "strategy": "hero" }, - "widgets": [ { "id": "w-hero", "type": "hero", "version": "1.0.0", "props": { "title": "Welcome", "layout": "full-bleed" } } ] - }, - { - "id": "home-categories", "type": "categories", "order": 2, - "layout": { "strategy": "grid", "columns": 4 }, - "widgets": [ { "id": "w-categories", "type": "categories", "version": "1.0.0", "props": { "columns": 4 } } ] - } - ] - } - ], - "staticPages": [ - { "id": "static-about", "slug": "about", "title": { "en": "About Us" }, "showInFooter": true, "showInHeader": false, "translations": { "en": { "html": "

About Us

" } } } - ] -} -``` - -## How `ConfigService` / `PlatformRuntimeService` consume it - -1. `ConfigService.loadBootstrap()` fetches (or, in mock mode, reads local JSON under `src/assets/mock/bootstrap/`) and parses `BootstrapConfig`. -2. `PlatformRuntimeService` applies theme tokens as CSS variables, sets branding, and exposes the parsed pages/navigation/footer/static-pages to the rest of the app. -3. Section Engine / Widget Host render pages from `bootstrap.pages` on route match (see `docs/ARCHITECTURE.md`). -4. `PlatformRuntimeService.reloadFromBootstrap(next)` re-applies an entire new `BootstrapConfig` in-memory — this is what the Project Editor's Publish action (and Preview) use, without a full browser reload. - -## How the Project Editor edits it, and draft/publish/preview - -The Project Editor (`docs/EDITOR.md`) edits an in-memory copy of the exact same `BootstrapConfig` — there is no parallel editor model or DTO translation layer. Today: - -- **Load**: `GET /bootstrap` (same endpoint the storefront uses). -- **Save**: in-memory snapshot only, persisted to `localStorage` as a draft (`projectEditor.draftBootstrap.v1`, scoped by `tenant.id`) so it survives reloads on the same browser. -- **Publish**: runs `ProjectValidator`, then calls `PlatformRuntimeService.reloadFromBootstrap()` for live in-memory preview and flips a local `status` flag — **no backend call happens**. This is the largest gap covered in `docs/BACKEND.md`. -- **Preview**: same in-memory re-apply mechanism, without marking the state published. diff --git a/docs/EDITOR.md b/docs/EDITOR.md index 9899e03..6f9f076 100644 --- a/docs/EDITOR.md +++ b/docs/EDITOR.md @@ -2,7 +2,7 @@ Replaces the old `docs/Project-Editor.md` (content merged in below and extended with the Sprint 19 field-description/dropdown work). -The Project Editor (`src/app/features/project-editor/`) edits the tenant's `BootstrapConfig` (`docs/BOOTSTRAP.md`) directly — no parallel model. It is out of scope for products, categories, orders, or analytics management (those live under `features/admin/*`/`features/backoffice/*`, see `docs/ADMIN.md`). +The Project Editor (`src/app/features/project-editor/`) edits the tenant's `BootstrapConfig` (`docs/backend/BACKEND-INTEGRATION.md#4-bootstrap`) directly — no parallel model. It is out of scope for products, categories, orders, or analytics management (those live under `features/admin/*`/`features/backoffice/*`, see `docs/ADMIN.md`). ``` src/app/features/project-editor/ @@ -46,7 +46,7 @@ Route: `/edit/:section` or `/{lang}/edit/:section`. `/backoffice/static-pages` ( - **Reset section**: reverts one section's bootstrap keys (per `EDITOR_SECTION_BOOTSTRAP_KEYS` in `models/project-editor.model.ts`) to `originalBootstrap`. Confirmation required. - **Reset draft**: reverts the entire bootstrap to `originalBootstrap` and clears the persisted local draft. Confirmation required. - **Per-field reset is not implemented** — no per-field default registry exists; only section- and project-level reset. -- **No backend persistence exists for any of this today** — see `docs/BACKEND.md` item 2 for the endpoints needed. +- **No backend persistence exists for any of this today** — see `docs/backend/BACKEND-INTEGRATION.md#67-builder--bootstrap-draftpublishvalidate-planned-highest-priority` for the endpoints needed. ## Configuration schema, form engine, and validation architecture (Sprint X+1) @@ -88,7 +88,7 @@ No changes to `ProjectEditorIoService` (export/import), `ProjectEditorDraftStora ## Admin Authentication (QR reuse) -Admin login shares the exact same Telegram QR/session backend and `TelegramLoginComponent` as customer login (`mode: 'admin'` vs `'customer'`) — only the cookie name/`SameSite` policy, token storage keys, and guard differ. **Backend gap:** because both flows hit the same session endpoint, the backend cannot distinguish an admin scan from a customer scan today — real admin authorization must be enforced server-side. Full detail: `docs/BACKEND.md` item 1. +Admin login shares the exact same Telegram QR/session backend and `TelegramLoginComponent` as customer login (`mode: 'admin'` vs `'customer'`) — only the cookie name/`SameSite` policy, token storage keys, and guard differ. **Backend gap:** because both flows hit the same session endpoint, the backend cannot distinguish an admin scan from a customer scan today — real admin authorization must be enforced server-side. Full detail: `docs/backend/BACKEND-INTEGRATION.md#25-the-admin-authorization-gap-critical--security-relevant-unresolved`. ## Design system primitives (post-Sprint 30 redesign) @@ -125,7 +125,7 @@ Interaction feedback + motion applied consistently, all gated behind `prefers-re **HTML-mode validation (Sprint X+2):** switching from raw-HTML back to the visual surface now runs `schema/validators/primitives.validateHtml` (stack-based tag-balance check) first; a malformed edit (unclosed/mismatched tag) stays in code mode with an inline error instead of silently corrupting the visual editor. -**Caveat:** implemented on the deprecated `document.execCommand` API. It works in all current browsers today but is a legacy web API with no modern drop-in replacement; if a future browser drops it, this component needs a rewrite (e.g. a maintained rich-text library). By design it emits **raw, unsanitized** HTML — sanitization is a storefront-render concern, not an authoring one (see `docs/BACKEND.md` item 4 on server-side content moderation on publish; `StaticPageComponent` and `StaticPagePreviewComponent` both run content through `DomSanitizer` before render). +**Caveat:** implemented on the deprecated `document.execCommand` API. It works in all current browsers today but is a legacy web API with no modern drop-in replacement; if a future browser drops it, this component needs a rewrite (e.g. a maintained rich-text library). By design it emits **raw, unsanitized** HTML — sanitization is a storefront-render concern, not an authoring one (see `docs/backend/BACKEND-INTEGRATION.md#68-builder--content-pages--cms-planned` on server-side content moderation on publish; `StaticPageComponent` and `StaticPagePreviewComponent` both run content through `DomSanitizer` before render). ## Field-description / dropdown UX (Sprint 19+) diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 788a4a4..244ab03 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -51,4 +51,4 @@ src/app/ ## Dynamic widget/section rendering from bootstrap JSON -Full detail in `docs/ARCHITECTURE.md` and `docs/BOOTSTRAP.md`. Summary: `page config (bootstrap.pages) -> Section Engine (order/layout/visibility) -> Page Renderer -> Widget Host (resolves component via Widget Manifest + data via Data Source Resolver) -> widget component (props + resolved data only)`. Nothing in this pipeline calls an API directly except the Data Source Resolver, which delegates to `CategoryFacade`/`ProductFacade`. +Full detail in `docs/ARCHITECTURE.md` and `docs/backend/BACKEND-INTEGRATION.md#4-bootstrap`. Summary: `page config (bootstrap.pages) -> Section Engine (order/layout/visibility) -> Page Renderer -> Widget Host (resolves component via Widget Manifest + data via Data Source Resolver) -> widget component (props + resolved data only)`. Nothing in this pipeline calls an API directly except the Data Source Resolver, which delegates to `CategoryFacade`/`ProductFacade`. diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index b3069ec..f639376 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -28,7 +28,7 @@ Standards referenced below are enforced, not suggestions: `docs/architecture/fou 2. **Gateway interface** — `services/admin-dashboard-metrics.gateway.interface.ts`. An abstract contract (`AdminDashboardMetricsGateway`) for "however we get category/product counts" — deliberately decoupled from *how* (local computation vs. real API) so the facade never knows which implementation is active. -3. **Gateway implementation** — `services/admin-dashboard-metrics.local.gateway.ts`. `AdminDashboardMetricsLocalGateway implements AdminDashboardMetricsGateway`, composing `BackofficeDataService.loadCategories()/loadProducts()` (already used elsewhere) into counts. A future `AdminDashboardMetricsApiGateway` would implement the same interface against a real endpoint (`docs/BACKEND.md` item 8) — nothing above this layer changes when that happens. +3. **Gateway implementation** — `services/admin-dashboard-metrics.local.gateway.ts`. `AdminDashboardMetricsLocalGateway implements AdminDashboardMetricsGateway`, composing `BackofficeDataService.loadCategories()/loadProducts()` (already used elsewhere) into counts. A future `AdminDashboardMetricsApiGateway` would implement the same interface against a real endpoint (`docs/backend/BACKEND-INTEGRATION.md#615-backoffice--dashboard-metrics--recent-activity-planned`) — nothing above this layer changes when that happens. 4. **DI token** — `services/admin-dashboard-metrics-gateway.token.ts`. `const ADMIN_DASHBOARD_METRICS_GATEWAY = new InjectionToken(...)`, bound to the local gateway by default in `app.config.ts`. This is the swap point: rebinding this token to a real API gateway is the *only* change needed to go from mock to real data. @@ -56,5 +56,5 @@ Full narrative and known gaps: `docs/ADMIN.md`. 8. Build the container/page component that injects the facade and wires routing. 9. Add routes in `app.routes.ts`, with `adminAuthGuard` (or the relevant guard) if it's an admin surface. 10. Add every new user-facing string to `i18n/translations.ts` (interface) then `en.ts`/`ru.ts`/`hy.ts` — never hardcode copy in a template. -11. Document backend gaps (if any) in `docs/BACKEND.md` using the same "current behavior / gap / endpoint needed / files that change" structure as the existing entries. +11. Document backend gaps (if any) in `docs/backend/BACKEND-INTEGRATION.md` (§6, endpoints by domain) using the same CURRENT/PLANNED/FUTURE tagging as the existing entries. 12. Run `npm run arch:check` (import boundaries + circular dependencies) and `npx tsc -p tsconfig.app.json --noEmit` before committing. diff --git a/docs/PROJECT.md b/docs/PROJECT.md index 58a7a04..b4db58c 100644 --- a/docs/PROJECT.md +++ b/docs/PROJECT.md @@ -2,7 +2,7 @@ ## What this is -A configuration-driven, multi-tenant SaaS marketplace platform (Angular 18+, standalone components). One frontend codebase serves unlimited tenants ("marketplaces"). Tenant identity, theme, navigation, page/section/widget composition, and static content are all resolved from a per-tenant `bootstrap.json` fetched at runtime — no tenant-specific code paths exist in the frontend. See `docs/ARCHITECTURE.md` and `docs/BOOTSTRAP.md` for the mechanics. +A configuration-driven, multi-tenant SaaS marketplace platform (Angular 18+, standalone components). One frontend codebase serves unlimited tenants ("marketplaces"). Tenant identity, theme, navigation, page/section/widget composition, and static content are all resolved from a per-tenant `bootstrap.json` fetched at runtime — no tenant-specific code paths exist in the frontend. See `docs/ARCHITECTURE.md` and `docs/backend/BACKEND-INTEGRATION.md#4-bootstrap` for the mechanics. Every tenant conceptually has three surfaces on this one codebase: - **Website** — the public storefront (catalog, product pages, cart, static pages). @@ -15,14 +15,13 @@ Every tenant conceptually has three surfaces on this one codebase: - The frontend loads `GET /bootstrap` (tenant resolved server-side by host) and renders entirely from that JSON: theme, layout, navigation, pages, sections, widgets, static pages, feature flags. - Widgets never call APIs directly; they receive resolved data through facades/resolvers. - New tenants are onboarded by domain + bootstrap config + backend data, not by forking the frontend. -- Full contract: `docs/BOOTSTRAP.md`. +- Full contract: `docs/backend/BACKEND-INTEGRATION.md#4-bootstrap`. ## Doc index - **[ARCHITECTURE.md](ARCHITECTURE.md)** — layered architecture, container/facade/service pattern, bootstrap/theme/widget engines, links to the enforced ADRs. -- **[BACKEND.md](BACKEND.md)** — the backend punch list: every mocked/local-only feature, its gap, and the endpoint needed to make it real. Start here if you're a backend engineer picking up this project. +- **[backend/BACKEND-INTEGRATION.md](backend/BACKEND-INTEGRATION.md)** — the single canonical backend/API spec: architecture, auth, security, bootstrap, conventions, every endpoint (CURRENT/PLANNED/FUTURE), DTOs, state machines, validation, media, errors, localization, caching. Start here if you're a backend engineer picking up this project. - **[FRONTEND.md](FRONTEND.md)** — app structure, routing, i18n, theming, state management (signals/facades, no NgRx), dynamic rendering. -- **[BOOTSTRAP.md](BOOTSTRAP.md)** — the `BootstrapConfig` model, field-by-field, with a representative example JSON. - **[EDITOR.md](EDITOR.md)** — the Project Editor: every section, the save/publish/draft/reset model, and the field-description/dropdown UX. - **[StaticPages.md](StaticPages.md)** — the Static Pages module: full field reference, per-page draft/publish gating, CRUD/search/filter/bulk, rich text, device preview, nav integration (Sprint X+2). - **[PROJECT-STRUCTURE.md](PROJECT-STRUCTURE.md)** — folder-by-folder tour of `src/app/**` with a worked "add a new feature" example (admin dashboard). @@ -57,5 +56,5 @@ See root `CLAUDE.md` for the full Barry Cache workflow and memory policy. - **Sprint 19** shipped the production Admin Dashboard (`src/app/features/admin/dashboard/`) as the default `/​:lang/backoffice` landing page, wired the previously-unrouted `admin/products` feature into routing, and added `lastPublishedAt` tracking to `ProjectEditorFacade`. See `docs/ADMIN.md`. - **Sprint 18** added Project Editor autosave (localStorage draft), section/draft reset, admin QR-login reuse (shared Telegram session API/component, separate cookie/guard), and Ed25519 verification scaffolding (no crypto implemented yet — fails closed). See `docs/EDITOR.md`. -- Draft/publish for the Project Editor is still **frontend-only** (localStorage), with no backend persistence. This is the single largest backend gap — see `docs/BACKEND.md`. -- This documentation set (`docs/PROJECT.md`, `ARCHITECTURE.md`, `BACKEND.md`, `FRONTEND.md`, `BOOTSTRAP.md`, `EDITOR.md`, `PROJECT-STRUCTURE.md`) replaces ~35 previously scattered files under `docs/platform/`, `docs/backend-platform/`, and various one-off sprint reports, which have been consolidated and removed. +- Draft/publish for the Project Editor is still **frontend-only** (localStorage), with no backend persistence. This is the single largest backend gap — see `docs/backend/BACKEND-INTEGRATION.md#67-builder--bootstrap-draftpublishvalidate-planned-highest-priority`. +- This documentation set (`docs/PROJECT.md`, `ARCHITECTURE.md`, `backend/BACKEND-INTEGRATION.md`, `FRONTEND.md`, `EDITOR.md`, `PROJECT-STRUCTURE.md`) replaces ~35 previously scattered files under `docs/platform/`, `docs/backend-platform/`, and various one-off sprint reports, which have been consolidated and removed. diff --git a/docs/StaticPages.md b/docs/StaticPages.md index 1c5fd80..edcbb57 100644 --- a/docs/StaticPages.md +++ b/docs/StaticPages.md @@ -1,6 +1,6 @@ # Static Pages (Project Editor module) -Sprint X+2. Full-featured CRUD editor for tenant static content (About, Privacy, Terms, Contacts, custom pages, etc.), living inside the Project Editor at `/edit/static-pages`. Edits `bootstrap.staticPages` directly — the same model the storefront renders from (`docs/BOOTSTRAP.md`), no parallel content store. +Sprint X+2. Full-featured CRUD editor for tenant static content (About, Privacy, Terms, Contacts, custom pages, etc.), living inside the Project Editor at `/edit/static-pages`. Edits `bootstrap.staticPages` directly — the same model the storefront renders from (`docs/backend/BACKEND-INTEGRATION.md#46-staticpages`), no parallel content store. `/backoffice/static-pages` (Admin dashboard) redirects here rather than hosting a second CRUD UI over the same data. diff --git a/docs/architecture/backend/Backend-Platform-API-Spec.md b/docs/architecture/backend/Backend-Platform-API-Spec.md deleted file mode 100644 index d6935f7..0000000 --- a/docs/architecture/backend/Backend-Platform-API-Spec.md +++ /dev/null @@ -1,451 +0,0 @@ -# Backend Platform API Specification - -Status: Draft for implementation handoff -Date: 2026-07-03 -Scope: Marketplace Platform (Website + Builder + Backoffice) - -## 1. Tenant Resolution - -### Mechanism -- Backend resolves tenant from HTTP Host header. -- Frontend never sends tenant id or project key. -- Tenant resolution occurs before authorization and route handling. - -### Resolution Rules -1. Exact host match in tenant registry. -2. Alias host fallback. -3. Unknown host returns 404 tenant_not_found. - -### Validation -- Host must be present. -- Host must map to active tenant. -- Suspended tenant returns 403 tenant_suspended. - -## 2. Bootstrap API - -### Endpoint -- Method: GET -- Path: /bootstrap - -### Purpose -- Return complete tenant runtime configuration for frontend bootstrap. - -### Authorization -- Public for website runtime. -- Optional authenticated extensions for builder/backoffice context may be included via claims. - -### Request -- Headers: Host (required) -- Query: locale (optional) - -### Response -- 200 with BootstrapConfig payload. - -### Validation -- Schema version required. -- Required segments: tenant, branding, theme, featureFlags, navigation, pages. - -### Example Response -```json -{ - "schemaVersion": "1.0.0", - "generatedAt": "2026-07-03T00:00:00Z", - "tenant": { "id": "tenant-default-001", "slug": "default", "host": "default.local" }, - "branding": { "brandName": "Marketplace", "logoUrl": "/icons/icon-192x192.png", "faviconUrl": "/icons/icon-192x192.png" }, - "theme": { "themeId": "default-light", "mode": "light", "palette": { "primary": "#497671" }, "typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 }, "spacing": { "unit": 4, "scale": [0,4,8] }, "borderRadiusScale": { "md": "12px" }, "shadows": { "md": "0 4px 12px rgba(0,0,0,0.15)" }, "iconSet": "default" }, - "featureFlags": { "wishlist": true, "reviews": true }, - "navigation": { "header": [], "footer": [] }, - "pages": [] -} -``` - -## 3. Configuration API - -### Endpoint -- Method: GET -- Path: /configuration - -### Purpose -- Return complete editable configuration model for Builder. - -### Authorization -- Required: builder.read - -### Request -- Headers: Authorization bearer token - -### Response -- 200 configuration aggregate for tenant. - -### Validation -- Caller must belong to tenant context. -- Caller role must include builder permissions. - -### Example Response -```json -{ - "tenantId": "tenant-default-001", - "branding": { "brandName": "Marketplace" }, - "theme": { "themeId": "default-light" }, - "navigation": { "header": [], "footer": [] }, - "pages": [] -} -``` - -## 4. Website API - -### Endpoint -- Method: GET -- Path: /website/pages/{pageKey} - -### Purpose -- Return website page composition for runtime rendering. - -### Authorization -- Public. - -### Request -- Path: pageKey required - -### Response -- 200 PageConfig with sections/widgets. - -### Validation -- pageKey must exist for tenant. -- hidden pages return 404. - -### Example Response -```json -{ - "id": "page-home", - "key": "home", - "layout": "default-public", - "sections": [ - { - "id": "section-hero", - "type": "hero", - "order": 1, - "widgets": [ - { "id": "widget-hero-main", "type": "hero", "version": "1.0.0", "props": { "title": "Welcome" } } - ] - } - ] -} -``` - -## 5. Builder API - -### Endpoint -- Method: PUT -- Path: /builder/configuration - -### Purpose -- Save tenant configuration from Builder Sandbox. - -### Authorization -- Required: builder.write - -### Request -- Body: full or partial configuration document. - -### Response -- 200 updated configuration metadata. - -### Validation -- JSON schema validation. -- Widget types must be registered and supported. -- Route/path uniqueness checks. - -### Example Request -```json -{ - "branding": { "brandName": "Updated Brand" }, - "featureFlags": { "chat": true }, - "pages": [] -} -``` - -### Example Response -```json -{ - "version": 42, - "updatedAt": "2026-07-03T12:00:00Z", - "updatedBy": "user-100" -} -``` - -## 6. Backoffice API - -### Endpoint -- Method: GET -- Path: /backoffice/dashboard - -### Purpose -- Return dashboard aggregates for backoffice operations. - -### Authorization -- Required: backoffice.read - -### Request -- Optional query filters by date range. - -### Response -- KPIs and entity counts. - -### Validation -- Caller must belong to tenant. - -### Example Response -```json -{ - "ordersToday": 14, - "openOrders": 38, - "products": 1024, - "customers": 5600, - "inventoryAlerts": 12 -} -``` - -## 7. Products API - -### Endpoint -- GET /products -- GET /products/{id} -- POST /products -- PUT /products/{id} -- DELETE /products/{id} - -### Purpose -- Product catalog management for backoffice. - -### Authorization -- Read: backoffice.products.read -- Write: backoffice.products.write - -### Request -- Supports paging/filter/sort on GET /products. - -### Response -- Product entities aligned to UI contracts. - -### Validation -- SKU unique per tenant. -- Price and currency required. -- Visibility and status must be valid enum values. - -### Example Product -```json -{ - "id": "prod-001", - "sku": "SKU-001", - "title": "Wireless Headphones", - "price": { "amount": 149990, "currency": "RUB" }, - "stockStatus": "in_stock", - "visible": true -} -``` - -## 8. Categories API - -### Endpoint -- GET /categories -- POST /categories -- PUT /categories/{id} -- DELETE /categories/{id} - -### Purpose -- Category tree management. - -### Authorization -- Read: backoffice.categories.read -- Write: backoffice.categories.write - -### Validation -- Category id unique. -- Parent relation must not create cycles. - -### Example Category -```json -{ - "id": "cat-001", - "title": "Electronics", - "parentId": null, - "itemsCount": 120, - "visible": true -} -``` - -## 9. Orders API - -### Endpoint -- GET /orders -- GET /orders/{id} -- PATCH /orders/{id}/status - -### Purpose -- Order lifecycle tracking and updates. - -### Authorization -- Read: backoffice.orders.read -- Write: backoffice.orders.write - -### Validation -- Status transition must be legal according to state machine. - -### Example Response -```json -{ - "id": "ord-1001", - "status": "processing", - "total": { "amount": 9900, "currency": "RUB" }, - "createdAt": "2026-07-03T10:20:00Z" -} -``` - -## 10. Media API - -### Endpoint -- POST /media/upload -- GET /media/{id} -- DELETE /media/{id} - -### Purpose -- Media asset management for products/widgets/pages. - -### Authorization -- Required: backoffice.media.write for upload/delete. - -### Validation -- File size/type constraints. -- Malware scan required before publish. - -### Example Response -```json -{ - "id": "media-001", - "url": "https://cdn.example.com/tenant-default/media-001.jpg", - "mimeType": "image/jpeg" -} -``` - -## 11. Localization API - -### Endpoint -- GET /localization/dictionaries/{locale} -- PUT /localization/dictionaries/{locale} - -### Purpose -- Localization dictionary retrieval and updates. - -### Authorization -- Read: builder.localization.read -- Write: builder.localization.write - -### Validation -- Locale must be supported by tenant. -- Keys must be unique. - -### Example Response -```json -{ - "locale": "ru", - "version": "1.0.3", - "entries": { - "nav.home": "Главная", - "nav.cart": "Корзина" - } -} -``` - -## 12. Permissions API - -### Endpoint -- GET /permissions -- GET /roles -- PUT /roles/{role} - -### Purpose -- Permission definitions and role bindings. - -### Authorization -- Required: security.admin - -### Validation -- Role names unique. -- Permission keys must exist in definitions. - -### Example Response -```json -{ - "definitions": [ - { "key": "builder.pages.edit" }, - { "key": "backoffice.products.read" } - ], - "roles": [ - { "role": "builder_admin", "permissions": ["builder.pages.edit"] } - ] -} -``` - -## 13. Feature Flags API - -### Endpoint -- GET /feature-flags -- PUT /feature-flags - -### Purpose -- Tenant capability toggles for optional modules. - -### Authorization -- Read: builder.features.read -- Write: builder.features.write - -### Validation -- Flag keys must be from allowed registry. -- Non-boolean values rejected. - -### Example Response -```json -{ - "wishlist": true, - "compare": true, - "reviews": true, - "blog": false, - "chat": false, - "analytics": true, - "notifications": true, - "coupons": true, - "loyalty": false, - "giftCards": false, - "invoices": true -} -``` - -## Error Model (Common) - -### Structure -```json -{ - "code": "validation_error", - "message": "Validation failed", - "details": [ - { "field": "pages[0].route.path", "message": "Path already exists" } - ], - "traceId": "trc-123" -} -``` - -### Common Codes -- tenant_not_found -- tenant_suspended -- unauthorized -- forbidden -- validation_error -- conflict -- not_found -- internal_error - -## Contract Compatibility Note - -Authentication, payment, and authorization behavior and contracts in the current system are preserved as-is. -This document defines platform APIs around those stable integrations without changing their existing payload contracts. diff --git a/docs/architecture/foundation/adr/ADR-004-configuration-bootstrap-and-provider-abstraction.md b/docs/architecture/foundation/adr/ADR-004-configuration-bootstrap-and-provider-abstraction.md index a678048..7d3c5c2 100644 --- a/docs/architecture/foundation/adr/ADR-004-configuration-bootstrap-and-provider-abstraction.md +++ b/docs/architecture/foundation/adr/ADR-004-configuration-bootstrap-and-provider-abstraction.md @@ -1,36 +1,11 @@ # ADR-004: Configuration Bootstrap and Provider Abstraction -Status: Accepted +Status: Superseded Date: 2026-07-03 +Superseded by: `docs/backend/BACKEND-INTEGRATION.md` §4 (Bootstrap) and §14 (Backend replacement pattern) -## Context +## Original decision (preserved for history) -Configuration must initially come from mock JSON and later from backend API without changing consumers. +Configuration must initially come from mock JSON and later from backend API without changing consumers. `ConfigService` is the only configuration entrypoint; consumers depend on typed selectors only; provider implementation is swappable (`MockBootstrapProvider` / `ApiBootstrapProvider`); the frontend calls `GET /bootstrap` when the API provider is enabled and never passes a tenant id. -## Decision - -Introduce configuration provider abstraction behind ConfigService. - -- ConfigService is the only configuration entrypoint. -- Consumers depend on typed ConfigService selectors only. -- Provider implementation is swappable: - - MockBootstrapProvider - - ApiBootstrapProvider -- Frontend calls GET /bootstrap when API provider is enabled. -- Frontend does not pass tenant id. - -## Consequences - -Positive: - -- Source-agnostic configuration usage. -- Mock-to-backend transition with minimal change surface. - -Negative: - -- Requires strict prohibition of direct JSON imports in components/services. - -## Compliance Requirements - -- No code outside ConfigService may load bootstrap JSON. -- No page/widget/component may access configuration files directly. +This decision remains in effect. The full, verified contract — endpoint, caching, field-by-field DTO reference, and the generalized mock↔API provider-swap pattern this ADR introduced (now used by every admin domain, not just bootstrap) — lives in `docs/backend/BACKEND-INTEGRATION.md`. Read that document for current, code-verified detail; this file is kept only so ADR-numbered references in `docs/architecture/foundation/README.md` continue to resolve. diff --git a/docs/architecture/foundation/adr/ADR-010-backward-compatibility-for-auth-payment-authorization.md b/docs/architecture/foundation/adr/ADR-010-backward-compatibility-for-auth-payment-authorization.md index 80c20e4..aae98dd 100644 --- a/docs/architecture/foundation/adr/ADR-010-backward-compatibility-for-auth-payment-authorization.md +++ b/docs/architecture/foundation/adr/ADR-010-backward-compatibility-for-auth-payment-authorization.md @@ -1,35 +1,11 @@ # ADR-010: Backward Compatibility for Authentication, Payment, and Authorization -Status: Accepted +Status: Superseded Date: 2026-07-03 +Superseded by: `docs/backend/BACKEND-INTEGRATION.md` §2 (Authentication), §2.8 (Payments), §2.5 (admin authorization gap) -## Context +## Original decision (preserved for history) -Authentication and payment flows are proven and contract-sensitive. Platform refactoring must not break existing integrations. +Authentication and payment flows are proven and contract-sensitive. Platform refactoring must not break existing integrations. Freeze behavior and contracts for authentication flow, payment API interactions, and authorization logic. Allow only encapsulation and integration-layer isolation, not contract redesign. -## Decision - -Freeze behavior and contracts for: - -- Authentication flow. -- Payment API interactions. -- Authorization logic. - -Allow only encapsulation and integration-layer isolation, not contract redesign. - -## Consequences - -Positive: - -- Prevents regressions in critical commerce and access flows. -- Enables architecture modernization around stable core behavior. - -Negative: - -- Some suboptimal legacy internals may remain until controlled replacement strategy is approved. - -## Compliance Requirements - -- Existing auth/payment request/response contracts remain unchanged. -- Behavior-equivalent wrappers/adapters are allowed. -- Any change requires explicit ADR and compatibility test evidence. +This decision remains in effect. The full, verified contract — the Telegram QR/session flow, cookie policy, the frozen payment endpoint shapes, and the still-unresolved admin-authorization gap this ADR's constraint interacts with — lives in `docs/backend/BACKEND-INTEGRATION.md` §2 and §2.5–§2.8. Read that document for current, code-verified detail; this file is kept only so ADR-numbered references in `docs/architecture/foundation/README.md` continue to resolve. diff --git a/docs/backend/BACKEND-INTEGRATION.md b/docs/backend/BACKEND-INTEGRATION.md new file mode 100644 index 0000000..b96142a --- /dev/null +++ b/docs/backend/BACKEND-INTEGRATION.md @@ -0,0 +1,1744 @@ +# Backend Integration — Master Specification + +Status: canonical. This is the single authoritative backend/API contract for this Angular multi-tenant marketplace platform. It supersedes and replaces `docs/BACKEND.md`, `docs/BACKEND-INTEGRATION.md` (old), `docs/BACKEND-INTEGRATION-PROMPT.md`, `docs/BACKEND-DIFF-VS-MAIN.md`, `docs/architecture/backend/Backend-Platform-API-Spec.md`, and the API-contract portions of `docs/BOOTSTRAP.md`. Those files are deleted; this document is where all of that content now lives, verified against the current source tree on branch `B2B`. + +Audience: a backend engineer implementing this platform's API with no other context, and any frontend engineer who needs the ground truth for what the client sends and expects. + +## How to read this document + +Every endpoint, DTO, and behavior below is tagged with a status: + +- **CURRENT** — real HTTP call today, verified in code (file path cited). Shape is frozen; do not change it. +- **PLANNED** — declared in the bootstrap model (`apiEndpoints.builder` / `apiEndpoints.backoffice`) or implied by a gateway interface, but not wired to real HTTP yet. Served today by a `*LocalGateway` (in-memory/localStorage/IndexedDB mock) bound via an Angular DI token. Implementing the real endpoint means writing one `*ApiGateway` class against the documented interface and rebinding the token — no UI change. +- **FUTURE** — reserved contract only. Nothing exists client-side yet beyond a placeholder or a "coming soon" page. Speculative shape, subject to change once real requirements exist. + +Never mix these tags. If you can't find a field in the cited model file, it isn't real — ask rather than invent (see Assumptions below for the few places a judgment call was necessary). + +## Assumptions (only where the source code was genuinely ambiguous) + +1. **Bootstrap path.** Code call sites use `GET /bootstrap` relative to the resolved API base (`ApiConfigService.getBaseUrl()`), not `/api/v1/bootstrap`. This document uses `GET /bootstrap` throughout, consistent with `docs/architecture/backend/Backend-Platform-API-Spec.md`'s draft and every existing doc. If a backend team versions it, `GET /v1/bootstrap` is a compatible evolution — do not silently rename without a schema/version bump story (see §5). +2. **`builder/*` and `backoffice/*` path prefixes.** No literal string constant for these prefixes exists in code (the mock gateways never build a URL at all — they're pure in-memory/localStorage). The prefixes used throughout §6 follow the naming already established in `docs/BACKEND.md`/`docs/BACKEND-INTEGRATION-PROMPT.md` and match `ApiEndpointsConfig`'s `builder`/`backoffice` buckets (`src/app/shared/models/config/api-endpoints.model.ts`). Treat every concrete path in §6.7–§6.16 as a **PLANNED contract proposal**, not a verified literal string — the *shapes* (request/response bodies, field names, enums) are verified against the real TypeScript interfaces; the *paths* are a reasonable default you're free to adjust as long as `apiEndpoints.backoffice`/`apiEndpoints.builder` in the published bootstrap stays in sync with whatever you choose. +3. **Auth/payment endpoints are explicitly frozen** by ADR-010 and out of scope for change — documented here for completeness (§2, §6.2) but treat as read-only reference, not a request for redesign. + +--- + +## Table of contents + +1. [Architecture overview](#1-architecture-overview) +2. [Authentication](#2-authentication) +3. [Security](#3-security) +4. [Bootstrap](#4-bootstrap) +5. [API conventions](#5-api-conventions) +6. [Endpoints, by domain](#6-endpoints-by-domain) +7. [DTOs](#7-dtos) +8. [State machines](#8-state-machines) +9. [Validation](#9-validation) +10. [Media](#10-media) +11. [Errors](#11-errors) +12. [Localization](#12-localization) +13. [Caching](#13-caching) +14. [Backend replacement pattern](#14-backend-replacement-pattern) +15. [Future APIs](#15-future-apis) +16. [Sequence diagrams](#16-sequence-diagrams) +17. [Developer notes](#17-developer-notes) + +--- + +## 1. Architecture overview + +This is not a single marketplace — it is a multi-tenant platform. One Angular codebase serves unlimited tenants ("marketplaces"), each configured entirely at runtime from a backend-served `BootstrapConfig` document. No marketplace-specific code exists in the frontend (`docs/context/adrs/ADR-0001-marketplace-platform-vision.md`). + +Every tenant has three logical surfaces sharing one domain: + +- **Website** — the public storefront (catalog, product, cart, checkout-adjacent flows). +- **Builder** (Project Editor) — the tenant admin's visual editor for the same `BootstrapConfig` the storefront renders from. No parallel model. +- **Backoffice** — operational admin: products, categories, orders, transactions, users, monitoring, analytics, media. + +```mermaid +flowchart TD + Host["HTTP Host header"] --> TenantResolve["Tenant resolution (backend, before auth/routing)"] + TenantResolve --> Bootstrap["GET /bootstrap"] + Bootstrap --> ConfigService["ConfigService (Angular, loads once, shareReplay(1))"] + ConfigService --> Runtime["PlatformRuntimeService (theme, branding, nav, pages)"] + Runtime --> Website["Website (public storefront)"] + Runtime --> Builder["Builder / Project Editor (draft BootstrapConfig)"] + Runtime --> Backoffice["Backoffice (admin CRUD)"] + Website --> API["Storefront REST API (category/items/search/cart/reviews)"] + Builder --> BuilderAPI["Builder REST API (draft/publish)"] + Backoffice --> BackofficeAPI["Backoffice REST API (per-domain CRUD)"] + API --> DB[("Database")] + BuilderAPI --> DB + BackofficeAPI --> DB +``` + +### Request flow (every storefront call) + +1. Application code calls `this.http.get('/api/category')` or builds an absolute URL via `ApiConfigService`. +2. **`apiBaseUrlInterceptor`** (`src/app/interceptors/api-base-url.interceptor.ts`) rewrites any `/api/...` URL to the resolved tenant base via `ApiConfigService.toApiUrl()`. +3. **`apiHeadersInterceptor`** (`src/app/interceptors/api-headers.interceptor.ts`) attaches `X-Region`, `X-Language`, `Currency`, `WebSessionID` to every request whose URL resolves as an API request (`ApiConfigService.isApiRequest()`). +4. **`adminAuthHeadersInterceptor`** (`src/app/core/admin-auth/admin-auth-headers.interceptor.ts`) additionally attaches `AdminWebSessionID` and, if present, `Authorization: Bearer ` — but only when the URL contains `/admin/`. +5. **`cacheInterceptor`** (`src/app/interceptors/cache.interceptor.ts`) short-circuits GET requests to `/category`, `/category/:id`, `/items/:id` against an in-memory TTL cache (see §13). +6. **`mockDataInterceptor`** (`src/app/interceptors/mock-data.interceptor.ts` / `.production.ts`) — dev-only; when `environment.useMockData` is true, intercepts before any of the above and serves fixture JSON. Not part of the real contract; ignore for backend implementation. +7. Request reaches the resolved absolute base (see base-URL resolution below). + +Interceptor registration order (as configured, `mockData → apiBaseUrl → apiHeaders → adminAuth → cache`) matters: base-URL rewriting must happen before headers are attached (headers only attach to recognized API requests), and admin headers only attach to `/admin/` URLs so customer requests never carry an `AdminWebSessionID`. + +### Base-URL resolution (`ApiConfigService.getBaseUrl`, `src/app/core/config/api-config.service.ts`) + +Priority order, first match wins: + +1. **Localhost** → `environment.localhostApiUrl` (`/api`, proxied by the Angular dev server). +2. **`environment.tenantApiBaseUrls[tenantKey]`** — explicit per-tenant override map (e.g. `default`/`dexarmarket` → `https://api.dexarmarket.ru:445`). +3. **`environment.tenantApiTemplate`** — host-pattern template, e.g. `https://{tenant}.api.dexarmarket.ru:445`. +4. **Bootstrap override** (opt-in only) — `bootstrap.apiEndpoints.website.baseUrl` or `bootstrap.tenant.apiBaseUrl`, used only when `environment.allowBootstrapApiOverride === true` **and** the value is an absolute `http(s)://` URL. Disabled by default in both `environment.ts` and `environment.production.ts`. +5. **Fallback** → `environment.apiUrl`. + +Tenant identity itself comes from `TenantResolverService` (host-based), never a path or query parameter — the backend must resolve tenant from the `Host` header the same way, before authorization and routing (mirrors `docs/architecture/backend/Backend-Platform-API-Spec.md` §1). + +**Backend impact for a new tenant:** either register it in `tenantApiBaseUrls`, or serve it at the `tenantApiTemplate` host pattern — no frontend rebuild required if the template host pattern holds. + +### Layered feature architecture (why the UI never touches an endpoint directly) + +Every admin domain (categories, products, orders, transactions, users, monitoring, dashboard) follows: `*Gateway` interface → `*LocalGateway` (mock, bound today) / `*ApiGateway` (real, to be built) → DI token → Facade → Page components. See §14 for the full replacement mechanics. This is why implementing a backend for any one domain is additive: write one class, rebind one token, ship. + +--- + +## 2. Authentication + +**Scope note (ADR-010):** authentication, payment, and authorization *behavior and contracts* are frozen — this section documents the existing contract for completeness and integration correctness. It is not a request to redesign the auth flow. + +### 2.1 Why Telegram, why sessions, why not just JWT-on-login + +The platform authenticates end users (both customers and admins) via **Telegram**, not a password. A user scans a QR code (desktop) or taps a deep link (mobile) that opens the platform's Telegram bot with a `start` payload equal to a server-generated session id. The bot, on the backend side, associates that Telegram identity with the session id. The frontend never sees Telegram credentials — it only polls "is this session id now associated with a logged-in Telegram user?" This is why the flow is **session-first**: the session id exists *before* the user is authenticated, and authentication is something that happens to an already-existing session, not something that produces one. + +**Why not a JWT immediately on session creation:** the session id is created anonymously (no identity yet) so the frontend has something to encode into a QR code before any identity exists. A JWT would need a subject; there isn't one until the Telegram bot round-trip completes. + +**Why sessions (cookie) rather than only a bearer token in memory:** the customer and admin session ids are stored as cookies (`webSessionID`, `adminSessionID`) specifically so a page refresh or new tab doesn't lose login state — a pure in-memory token would not survive navigation. See §2.6 for exact cookie policy. + +**Why Telegram alone isn't enough for admin:** completing the Telegram QR flow only proves "this is a real Telegram account holder." It proves nothing about whether that account is *authorized* to act as a marketplace admin. See §2.5 — this is the platform's most significant open security gap today. + +### 2.2 The one session API (CURRENT) + +Both customer and admin login are, today, **the same backend endpoint**: `{authApiUrl}/users/sessions` (`environment.authApiUrl`, e.g. `https://users.vitanova.network:456`), called exclusively through `TelegramSessionApiService` (`src/app/services/telegram-session-api.service.ts`). There is no separate admin session endpoint. + +| Method | Endpoint | Purpose | Caller | +|---|---|---|---| +| `POST` | `{authApiUrl}/users/sessions` | Create an anonymous session, get the Telegram bot deep-link | `TelegramSessionApiService.createSession()` | +| `GET` | `{authApiUrl}/users/sessions/:webSessionID` | Poll session status (has the Telegram bot round-trip completed?) | `TelegramSessionApiService.checkSessionOnce()` | +| `DELETE` | `{authApiUrl}/users/sessions/:webSessionID` | Logout / invalidate session | `TelegramSessionApiService.logout()` | + +**`POST /users/sessions`** — request body `{ webSessionID: string }` (a client-generated GUID, `generateGuid()` from `src/app/shared/util/guid.util.ts`), header `WebSessionID: `. Response is normalized defensively (`normalizeWebSession`) against many possible field-name variants — the client tolerates `webSessionID`/`WebSessionID`/`webSessionId`/`sessionID`/`SessionID`/`sessionId`/`id`/`ID` for the session id field, and similar variant lists for `user`, `status`/`active`/`loggedIn`, `username`, `firstName`/`first_name`, `expiresAt`/`expires`. **Backend implementers: pick one canonical field name per concept; the client's tolerance is defensive, not a suggestion to send multiple.** + +The frontend generates the QR/deep-link URL itself (not from a backend field): `https://t.me/{telegramBot}?start={webSessionID}` for the QR/browser flow, `tg://resolve?domain={telegramBot}&start={webSessionID}` for the in-app deep link. `telegramBot` is a frontend environment constant (`environment.telegramBot`, e.g. `myAMLKYCBOT` / `DexarSupport_bot`), not backend-supplied. + +**`GET /users/sessions/:webSessionID`** — polled every 5 seconds (`POLL_INTERVAL_MS`, `QrLoginEngine`, `src/app/shared/qr-login/qr-login.engine.ts`), up to 100 polls (`MAX_POLLS`) before the client gives up and shows `expired`. Also re-checked on `visibilitychange`/`focus`/`pageshow` events (the "user returned from the Telegram app" recovery path — `openAppLogin()` navigates away via `window.location.href`, so the SPA needs to re-verify session state whenever the tab regains focus). Response shape normalizes to: + +```ts +// src/app/shared/models/auth.model.ts (AuthSession) +interface AuthSession { + sessionId: string; + userId: number | null; + username: string | null; + displayName: string; + active: boolean; + expires: string; // ISO timestamp +} +``` + +If the backend response omits an expiry, the client defaults to `now + 3600s` (`SESSION_MAX_AGE_SECONDS = 60 * 60`) — this is a client-side fallback only; the backend should always send a real `expiresAt`. + +**`DELETE /users/sessions/:webSessionID`** — best-effort; client clears local state regardless of response (`catchError(() => of(null))`), so a network failure on logout never blocks the UI from appearing logged out. + +### 2.3 Customer login (CURRENT) + +`AuthService` (`src/app/services/auth.service.ts`) wraps `TelegramSessionApiService` and owns: + +- Cookie `webSessionID`, `SameSite=Lax`, `Max-Age=3600`, `Secure` when served over HTTPS, `Path=/`. +- A `status` signal: `unknown | checking | authenticated | unauthenticated`. +- Auto session-refresh: schedules a re-check 60 seconds before `expires` (minimum 30s out), so an active tab silently re-validates before expiry rather than surprising the user with a sudden logout. +- On app init, always calls `checkSession()` once against whatever cookie is present. + +### 2.4 Admin login (CURRENT, same backend as customer) + +`AdminAuthService` (`src/app/core/admin-auth/admin-auth.service.ts`) is a **structurally identical** wrapper around the same `TelegramSessionApiService` — same `createSession`/`checkSessionOnce`/`logout` calls, same polling engine (`QrLoginEngine`) — with only the *storage* kept separate: + +| | Customer (`AuthService`) | Admin (`AdminAuthService`) | +|---|---|---| +| Cookie name | `webSessionID` | `adminSessionID` | +| Cookie `SameSite` | `Lax` | `Strict` | +| Extra token storage | — | `localStorage: adminToken` / `adminRefreshToken` (reserved for a future JWT pair, currently unused — see below) | +| Guard | (route-level, customer flows) | `adminAuthGuard` (`src/app/core/admin-auth/admin-auth.guard.ts`) | +| Header interceptor | `apiHeadersInterceptor` sets `WebSessionID` | `adminAuthHeadersInterceptor` sets `AdminWebSessionID` (+ `Authorization: Bearer` if a token exists) | + +**`AdminAuthService.getAdminToken()`/`setAdminTokens()`/`clearAdminTokens()`** exist and are wired into the `Authorization` header, but nothing currently calls `setAdminTokens()` — this is dead-but-ready plumbing for once the backend issues a real JWT access/refresh pair on top of the Telegram session. Treat this as a **PLANNED** extension point, not a current contract. + +**Dev-only bypass:** `AdminAuthService.devBypassLogin()` fabricates a local session (`dev-bypass-`) and activates it directly, skipping the QR flow — but is a runtime no-op when `environment.production === true` (checked at call time, not just build time). Reachable via `?devBypassAdmin=true` per `docs/BACKEND-DIFF-VS-MAIN.md`. Not a backend concern beyond knowing it exists — it never calls any endpoint. + +### 2.5 The admin authorization gap (CRITICAL — security-relevant, unresolved) + +Because admin and customer login hit the **identical** `POST /users/sessions` endpoint, **the backend currently has no way to know, at the moment the QR is scanned, that this is an admin login attempt versus a customer one.** The frontend's only decision is *where to store* the resulting session id (`adminSessionID` cookie vs. `webSessionID` cookie) — it cannot and does not decide, and cannot enforce, whether the Telegram user who completed the scan is actually authorized to act as an admin. + +**Concretely: any Telegram user who completes the QR flow while the admin login screen happens to be showing receives a valid `adminSessionID`.** There is currently no server-side check that rejects a non-admin Telegram user's session when it's used with `AdminWebSessionID`. + +**Required fix (server-side, not fixable from the frontend):** when an API call arrives carrying `AdminWebSessionID`, the backend must look up whether that Telegram user id is in the admin/role registry and reject (401/403) if not. This is a hard requirement before this system is production-safe for any tenant with real admin/customer separation. + +**Also prepared, not wired:** `Ed25519VerificationService` (`src/app/core/admin-auth/ed25519-verification.model.ts`) defines a `requestChallenge()`/`verify()` contract for a future non-Telegram, challenge/signature-based admin auth path (`Ed25519Challenge { nonce, timestamp, payload }` → `Ed25519SignedResponse { challenge, publicKey, signature }` → `Ed25519VerificationResult { valid, reason? }`). The current binding is `NoopEd25519VerificationService`, which **throws** rather than silently accepting anything — a deliberate fail-closed placeholder, safe to leave wired until a real challenge/verify endpoint exists. **FUTURE** — no endpoint exists for this today. + +### 2.6 Cookies, headers, and session identity summary + +| Concern | Customer | Admin | +|---|---|---| +| Cookie | `webSessionID` | `adminSessionID` | +| `SameSite` | `Lax` | `Strict` | +| `Secure` | yes, when `https:` | yes, when `https:` | +| `Max-Age` | 3600s | 3600s | +| Header sent on API calls | `WebSessionID` (all API requests) | `AdminWebSessionID` (only `/admin/` requests) + optional `Authorization: Bearer ` | +| Anonymous fallback | 32-char hex, `localStorage: web_session_id`, generated client-side if no session exists | none — unauthenticated admin routes redirect to login | + +### 2.7 Refresh, expiry, unauthorized flows + +- **Refresh:** not a token-refresh in the OAuth sense — it's a periodic re-poll of `GET /users/sessions/:id` scheduled ~60s before the session's `expires` timestamp. If still active, the client silently re-activates the session (same cookie, updated in-memory signal); if not, the client transitions to `unauthenticated` and clears the cookie. +- **Expired token flow:** `checkSessionOnce` returning `active: false` (or erroring) clears all local auth state (`clearAuthState('unauthenticated')`) — cookie removed, signals reset, refresh timer cancelled. No automatic re-login; the user must re-scan. +- **Unauthorized flow (admin):** `adminAuthGuard` blocks navigation and calls `requestLogin()` (shows the login dialog) rather than a hard redirect — consistent with this being an SPA-level gate, not a route change. + +### 2.8 Payments (frozen, documented for completeness) + +`ApiService` (`src/app/services/api.service.ts`) also exposes the payment surface — **frozen per ADR-010**, not to be changed: + +| Method | Endpoint | Notes | +|---|---|---| +| `createPayment()` | `POST {qrApiUrl}/qr` | QR dynamic payment creation. Headers `authorization-key`, `userid-value` optional. | +| `createCartPayment()` | `POST /cart` | Cart-scoped payment (card or QR), fixed `partnerqrID = 'web-97ec-9c57-4dde-9037-3a68f7f83750'`. | +| `checkCartPaymentStatus()` | `GET {qrApiUrl}/qr/dynamic/{partnerId}/:qrId` | | +| `checkCartCardPaymentStatus()` | `GET {qrApiUrl}/card/{partnerId}/:orderId` | | +| `checkPaymentStatus()` | `GET {qrApiUrl}/qr/dynamic/:partnerQrId/:qrId` | | + +`QrCreateRequest`/`QrCreateResponse`/`CartPaymentRequest`/`QrDynamicStatusResponse` interfaces are in `api.service.ts` lines 10–66 — reproduced in §7.9. The response normalizer tolerates many field-name casings (`qrId`/`qrID`, `nspkID`/`nspkId`, etc.) for the same reason as session responses: multiple backend versions/typos have shipped over time and the client absorbs them rather than breaking. + +--- + +## 3. Security + +### 3.1 Origin is not authentication + +The frontend sends no CSRF token and relies on cookies (`webSessionID`/`adminSessionID`, `SameSite=Lax`/`Strict`) plus header-based session ids (`WebSessionID`, `AdminWebSessionID`) as the actual authorization signal — **`Origin`/`Referer` headers must never be treated as an authentication mechanism.** They are trivially spoofable by any non-browser client (curl, a compromised browser extension, a malicious script running same-origin) and provide no proof of user identity or intent. Legitimate uses of `Origin` are limited to: (a) CORS allow-listing for browser-enforced preflight, and (b) defense-in-depth logging/anomaly detection — never as a substitute for verifying `WebSessionID`/`AdminWebSessionID` against a real, backend-held session record. + +### 3.2 CORS + +Not configured in this repository (no `nginx.conf` CORS block for the actual API hosts `api.dexarmarket.ru:445` / `users.vitanova.network:456` — those are separate servers, not part of this repo). Recommended production posture: +- Allow-list exact tenant storefront origins (from the tenant registry, not `*`). +- `Access-Control-Allow-Credentials: true` (cookies are in play). +- Preflight `OPTIONS` handling for any endpoint that receives custom headers (`X-Region`, `X-Language`, `Currency`, `WebSessionID`, `AdminWebSessionID`). + +### 3.3 Endpoint authorization tiers + +| Tier | Examples | Requirement | +|---|---|---| +| Public | `GET /bootstrap`, `GET /category`, `GET /items/:id`, `GET /searchitems` | No auth. Never leak per-user/per-session data through these. | +| Session-required (customer) | `POST /websession/:id` (cart), `POST /items/:id/callback` (review), `POST /purchase-email` | Valid `WebSessionID`; anonymous sessions are allowed (cart works for guests). | +| JWT/admin-session-required | `builder/*`, `backoffice/*` | Valid `AdminWebSessionID` **and** (§2.5) server-side verification that the underlying Telegram identity is actually provisioned as an admin. | +| Admin-role-required (finer-grained) | e.g. `users`/`roles` write endpoints vs. read-only `products` list | `AdminUser.roleId` → `AdminRole.permissions[]` (see §7.6) should gate specific mutations once a real permission model exists server-side. `PermissionsConfig` (`src/app/shared/models/config/permissions.model.ts`) is the bootstrap-level skeleton for this (`definitions: PermissionDefinition[]`, `roles: RolePermissions[]`) but is not yet enforced anywhere in the frontend beyond existing. + +### 3.4 Rate limiting, replay protection, CSRF + +None of these exist today, client- or server-side, for this platform. Recommendations for a production backend: +- **Rate limiting:** at minimum on `POST /users/sessions` (session creation is unauthenticated and cheap to hammer) and on write endpoints (`builder/bootstrap/publish`, any `backoffice/*` mutation). +- **Replay protection:** session ids are long-lived (1 hour) bearer-equivalent values sent in a custom header — treat header leakage (e.g. via logs, proxies) as a real risk; short expiry + refresh (already the pattern, §2.7) is the primary mitigation. +- **CSRF:** cookies are `SameSite=Lax` (customer) / `Strict` (admin), which meaningfully reduces cross-site request risk for the cookie itself, but the actual authorization value doubles as a **header** (`WebSessionID`/`AdminWebSessionID`) read from the cookie by JS and reattached — a state-changing endpoint that only checks the header, not the cookie, reopens CSRF risk from any origin that can trick a script into replaying a known session id. Recommend the backend validate that the header value matches a cookie-bound session, not just "does this header value exist in the sessions table." + +### 3.5 Bearer tokens, HTTPS, headers + +- `Authorization: Bearer ` is sent when `AdminAuthService.getAdminToken()` returns non-null (currently always null in practice — see §2.4). Once real JWTs are issued, use standard `Authorization: Bearer` semantics; don't invent a custom scheme. +- Every environment config uses `https://` for `apiUrl`/`authApiUrl`/`qrApiUrl` in production; only localhost dev proxies over `/api`. HTTPS is mandatory in production — cookies rely on `Secure` being meaningful. +- Custom headers the backend must read: `X-Region`, `X-Language`, `Currency`, `WebSessionID` (all requests); `AdminWebSessionID`, `Authorization` (admin requests only). + +### 3.6 Recommended production security checklist + +- Enforce admin authorization server-side per §2.5 — this is the single highest-priority item in this entire document. +- Validate `WebSessionID`/`AdminWebSessionID` against a real backend-held session, never trust the header value alone as proof. +- Rate-limit session creation and all write endpoints. +- Serve `GET /bootstrap` with no secrets, ever (see §4) — it is public by design. +- Populate `apiEndpoints.builder`/`apiEndpoints.backoffice` in bootstrap only with paths that are actually protected server-side; publishing an endpoint in bootstrap is not itself a security boundary. +- CORS allow-list per tenant host, not wildcard, when credentials are involved. + +--- + +## 4. Bootstrap + +### 4.1 Endpoint + +**CURRENT** — `GET /bootstrap`, resolved against the tenant API base (§1). No query params or body. Tenant resolved server-side from the `Host` header — **the frontend never sends a tenant id.** Loaded once at app startup by `ConfigService.loadBootstrap()` (`src/app/core/config/config.service.ts`) and cached via `shareReplay(1)` — repeat calls to `loadBootstrap()` within the same session return the cached observable unless `forceRefresh: true` is passed. + +On localhost (or when `environment.useMockData === true`), `MockBootstrapProvider` serves `src/assets/mock/bootstrap/bootstrap.json` instead of a real HTTP call, so the app runs fully offline (`npm run dexar`). This mock file is the **canonical field-for-field reference payload** — match it exactly to avoid client-side normalization surprises. `ApiBootstrapProvider` is the real-HTTP implementation, both behind `CONFIG_PROVIDER` DI token per ADR-004. + +### 4.2 Response caching + +No explicit `ETag`/`304` handling exists in the frontend today — the entire response is cached client-side for the session lifetime via `shareReplay(1)`. A backend implementer is free to add `ETag`/`Cache-Control` (recommended, see §13) since the client doesn't currently send conditional-request headers for this endpoint — adding them is additive and safe. + +### 4.3 Top-level shape + +Source: `src/app/shared/models/config/bootstrap-config.model.ts`. + +```ts +interface BootstrapConfig { + schemaVersion: string; + generatedAt: string; + tenant: TenantConfig; // required + branding: BrandingConfig; // required + theme: ThemeConfig; // required + company: CompanyConfig; // required + featureFlags: FeatureFlagsConfig; // required + features?: MarketplaceFeaturesConfig; // optional, newer centralized feature surface + apiEndpoints: ApiEndpointsConfig; // required + localization: LocalizationConfig; // required + seo: SeoConfig; // required + permissions: PermissionsConfig; // required + header?: HeaderConfig; + catalog?: CatalogConfig; + layout?: PlatformLayoutConfig; + navigation: NavigationConfig; // required + footer?: FooterConfig; + productPage?: ProductPageConfig; + userExperience?: UserExperienceConfig; + pages: PageConfig[]; // required + staticPages?: StaticPagesConfig; + widgetRegistry?: WidgetRegistryConfig; +} +``` + +**Rule (ADR-0001):** bootstrap contains only what's needed *before the app starts* — branding, theme, languages, navigation, footer/static-page references, homepage layout, enabled widgets/features. It must **never** contain products, orders, cart contents, or user-specific data. Product/category *data* is fetched separately (`GET /category`, `GET /items/:id`, etc.) — `catalog`/`productPage`/`userExperience` in bootstrap are feature/UI configuration only, never data. + +### 4.4 Field-by-field reference + +| Key | Source file | Notes | +|---|---|---| +| `schemaVersion` | — | Contract version string. Breaking changes require a bump; frontend must stay compatible within a minor line (see §5.1). | +| `generatedAt` | — | Payload generation timestamp, ISO 8601. | +| `tenant` | `tenant.model.ts` | `id` (UUID), `slug`, `code`, `host`, `name`, `websiteBaseUrl`, `builderBaseUrl`, `backofficeBaseUrl`, `defaultLocale`, `supportedLocales[]`, `defaultCurrency`, `supportedCurrencies[]`, `timezone`. Resolved by domain only — never re-derived client-side. | +| `branding` | `branding.model.ts` | `brandName`, `legalName`, `slogan?`, `logoUrl`, `logoCompactUrl?`, `faviconUrl`, `appIconUrl?`, `socialImageUrl?`, `galleryUrls?[]`, `supportEmail?`, `supportPhone?`. | +| `theme` | `theme.model.ts` | `themeId`, `mode: light\|dark\|system`, `palette` (12 semantic colors: primary/secondary/accent/success/warning/danger/info/textPrimary/textSecondary/backgroundPrimary/backgroundSecondary/border), `typography` (`primaryFontFamily`, `headingFontFamily?`, `baseFontSize`), `spacing` (`unit`, `scale[]`), `borderRadiusScale: Record`, `shadows: Record`, `iconSet`. | +| `company` | `company.model.ts` | `companyName`, `registrationNumber?`, `taxId?`, `address` (`country`, `region?`, `city`, `street?`, `postalCode?`), `contacts` (`email`, `phone?`, `telegram?`, `website?`, `additionalPhones?[]`, `additionalEmails?[]`). Used in footer legal/contact info. | +| `featureFlags` | `feature-flags.model.ts` | Boolean map with 14 named keys (`wishlist`, `compare`, `reviews`, `questions`, `comments`, `recommendations`, `blog`, `chat`, `analytics`, `notifications`, `coupons`, `loyalty`, `giftCards`, `invoices`) plus an open `[key: string]: boolean` index — new flags can be added without a model change. | +| `features` | `features-config.model.ts` | Newer, more granular `MarketplaceFeaturesConfig` (16 booleans: wishlist/compare/reviews/comments/questions/recommendations/recentlyViewed/searchHistory/recentlySearched/ratings/share/brands/manufacturers/availability/discounts/badges). Resolvers fall back to `featureFlags`/`productPage`/`userExperience`/`catalog` for older bootstraps lacking this field — **do not remove `featureFlags` when adding `features`; both must be sent for backward compatibility until every consumer is confirmed on the new surface.** | +| `apiEndpoints` | `api-endpoints.model.ts` | `{ bootstrap: ApiEndpointConfig, website: Record, builder: Record, backoffice: Record }` where `ApiEndpointConfig = { path, method, timeoutMs? }`. **Never contains secrets.** Populate `builder`/`backoffice` buckets as you implement each endpoint so the frontend can (eventually) discover them. | +| `localization` | `localization.model.ts` | `defaultLocale`, `supportedLocales[]`, `currencyByLocale: Record`, `dictionaries: LocalizationDictionaryRef[]` (`{locale, dictionaryUrl, version}`). | +| `seo` | `seo.model.ts` | `default: SeoPageConfig`, `byPageKey: Record`. `SeoPageConfig = { title, description, canonicalUrl?, robots?, metaTags?: {name?, property?, content}[] }`. | +| `permissions` | `permissions.model.ts` | `definitions: {key, description?}[]`, `roles: {role, permissions[]}[]`. Currently minimal/skeletal — real enforcement is server-side (§3.3, §2.5). | +| `header` | `header-config.model.ts` | All-boolean toggles: `showLogo`, `showSearch`, `showCategories`, `showLanguages`, `showCart`, `showProfile`, `showWishlist`, `showCompare`, `showRegion`, plus `sticky?: boolean`, `layout?: 'default'\|'centered'`. | +| `catalog` | `catalog-config.model.ts` | UI/feature config only, **no product data**. `layout` (8 grid/list modes), `loadingStrategy` (`pagination\|loadMore\|infiniteScroll`), `navigationMode` (4 modes), `defaultSort`/`availableSorts` (7 sort keys), `enabledFilters: string[]`, and 7 `show*`/`*Enabled` booleans. | +| `layout` | `layout.model.ts` | `{ type: 'default'\|'sidebar-left'\|'carousel-home'\|'minimal'\|string, options?: Record }`. Global page-chrome mode. | +| `navigation` | `navigation.model.ts` | `header: NavigationItemConfig[]`, `footer: NavigationItemConfig[] \| FooterNavigationGroupConfig[]` (two accepted shapes — flat list or grouped), optional `sidebar[]`. Each item: `id`, `label` (string or `{[locale]: string}`), `route?`, `type?`, `key?` (e.g. `'staticPage'` + a page id — see §12), `icon?`, `order?`, `visible?`, `visibleWhenFlags?: string[]`, `children?`. | +| `footer` | `footer-config.model.ts` | `logoUrl?`, `paymentIcons?: {src, alt, width?, height?}[]`, `copyrightText?` (string or per-locale), `columns?: FooterColumnConfig[]` (`{id, title, links: {id,label,pageKey?,url?}[]}`), `socialLinks?: {id,label,url,icon?}[]`. `legalPageKeys`/`staticPageKeys` are **deprecated**, superseded by `columns` — still read for already-saved configs, do not emit them for new tenants. | +| `productPage` | `product-page-config.model.ts` | Feature config only for PDP — `rating`, `reviews` (`pageSize`, `showSummary`, `mode: pages\|load-more`), `questions` (`pageSize`, `allowSubmission`), `tabs` (`items[]` from a fixed enum), `relatedProducts`, `actions` (`addToCart`,`buyNow`,`wishlist`,`compare`,`share`,`notifyMe`). No review/question *data* here. | +| `userExperience` | `user-experience-config.model.ts` | Feature config only — `wishlist` (`headerBadgeEnabled`), `compare` (`maxItems`, `hideIdenticalDefault`, `highlightDifferencesDefault`), `recentlyViewed` (`maxItems`, `widgetEnabled`), `share`, `continueBrowsing`, `savedSearches` (`maxItems`). Never user-specific lists — those are client-local today (§6.13). | +| `pages` | `page.model.ts` | `{id, key, title, route: {path, exact?}, layout: string \| PlatformLayoutConfig, sections: SectionConfig[], seoKey?, featureFlag?, visible?}`. | +| `staticPages` | `static-page.model.ts` | See §4.6. | +| `widgetRegistry` | `widget-registry.model.ts` | `{ manifestUrl: string }` — pointer to the widget manifest (see `docs/ARCHITECTURE.md` widget engine; not itself a backend endpoint documented here). | + +### 4.5 Section and widget config + +```ts +// section.model.ts +type SectionLayoutStrategy = 'stack' | 'grid' | 'hero' | 'carousel' | 'split'; + +interface SectionConfig { + id: string; type: string; order: number; + layout?: { strategy?: SectionLayoutStrategy; columns?: number; gap?: string; align?: 'start'|'center'|'end'|'stretch' }; + visibility?: { desktop?: boolean; tablet?: boolean; mobile?: boolean }; + widgets: WidgetConfig[]; + featureFlag?: string; + visible?: boolean; +} + +// widget.model.ts +interface WidgetConfig { + id: string; type: string; version: string; + title?: string; subtitle?: string; order?: number; padding?: string; + visibility?: { desktop?: boolean; tablet?: boolean; mobile?: boolean }; + animation?: { name?: string; duration?: string; delay?: string; timingFunction?: string }; + style?: Record; + permissions?: { requireAuthenticated?: boolean; roles?: string[]; permissions?: string[] }; + props: Record; + actions?: Record }>; + featureFlag?: string; + visible?: boolean; +} +``` + +Typed editors exist in the Project Editor for `hero`, `categories`, `product-collection` widget types; every other type edits `props` as raw JSON. + +### 4.6 `staticPages` + +```ts +// static-page.model.ts +type StaticPagesConfig = Record | LegacyStaticPageConfig[]; // both accepted + +interface StaticPageConfig { + id: string; + slug: string; + title: string | Record; + showInFooter?: boolean; showInHeader?: boolean; showInSitemap?: boolean; + icon?: string; order?: number; + visibility?: { desktop?: boolean; tablet?: boolean; mobile?: boolean }; + requiresAuthentication?: boolean; + footerGroup?: string; + translations?: Record; + html?: string | Record; + seo?: StaticPageSeoConfig; + visible?: boolean; + route?: string; + content?: Record; + enabled?: boolean; // master on/off switch + status?: 'draft' | 'published'; // per-page publish lifecycle, independent of whole-bootstrap publish + customTemplate?: string; + heroImage?: string; heroImageAlt?: string; heroImageCaption?: string; + thumbnail?: string; gallery?: string[]; + updatedAt?: string; +} +``` + +**Resolution rule:** a static page resolves on the storefront only when `enabled === true` **and** `status === 'published'` — independent of whether the surrounding bootstrap has been published. A page with `status: 'draft'` stays invisible even after the whole tenant config is published. Compatibility default when normalizing legacy data (no `enabled`/`status` present): `enabled: true, status: 'published'` — existing tenants are never silently un-published. + +**Known data inconsistency:** the model requires `slug`, but the reference mock (`src/assets/mock/bootstrap/bootstrap.json`) leaves `slug` empty for some pages and only populates `route` (e.g. `/about-us`). The frontend's duplicate-detection falls back to `route` when `slug` is empty. **Decide and standardize:** either always populate `slug` server-side, or formally make `slug` derivable from `route` — do not leave both optional indefinitely. + +### 4.7 Representative example (trimmed) + +```json +{ + "schemaVersion": "2.1.0", + "generatedAt": "2026-07-05T10:30:00Z", + "tenant": { + "id": "tenant-dexar-ru", "slug": "dexar-ru", "code": "dexar-ru", "host": "dexarmarket.ru", + "name": "Dexar Market", "websiteBaseUrl": "https://dexarmarket.ru", + "builderBaseUrl": "https://dexarmarket.ru/edit", "backofficeBaseUrl": "https://dexarmarket.ru/admin", + "defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"], + "defaultCurrency": "RUB", "supportedCurrencies": ["RUB", "USD", "AMD"], "timezone": "Europe/Moscow" + }, + "branding": { "brandName": "Dexar Market", "legalName": "OOO Dexar", "logoUrl": "/assets/brand/logo.svg", "faviconUrl": "/assets/brand/favicon.ico" }, + "theme": { + "themeId": "dexar-light", "mode": "light", + "palette": { "primary": "#2F6E5D", "secondary": "#8FA9A2", "accent": "#E7A33E", "success": "#2F9E44", "warning": "#F08C00", "danger": "#E03131", "info": "#1C7ED6", "textPrimary": "#1F322D", "textSecondary": "#5C6B66", "backgroundPrimary": "#FFFFFF", "backgroundSecondary": "#F4F6F5", "border": "#DDE3E1" }, + "typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 }, + "spacing": { "unit": 4, "scale": [0,4,8,12,16,24,32] }, + "borderRadiusScale": { "sm": "6px", "md": "12px", "lg": "20px" }, + "shadows": { "md": "0 4px 12px rgba(0,0,0,0.15)" }, + "iconSet": "default" + }, + "company": { "companyName": "OOO Dexar", "address": { "country": "RU", "city": "Moscow" }, "contacts": { "email": "info@dexarmarket.ru", "phone": "+7 926 459 31 57" } }, + "featureFlags": { "wishlist": true, "compare": true, "reviews": true, "questions": true, "comments": true, "recommendations": true, "blog": false, "chat": false, "analytics": true, "notifications": true, "coupons": false, "loyalty": false, "giftCards": false, "invoices": false }, + "apiEndpoints": { "bootstrap": { "path": "/bootstrap", "method": "GET" }, "website": {}, "builder": {}, "backoffice": {} }, + "localization": { "defaultLocale": "ru", "supportedLocales": ["ru","en","hy"], "currencyByLocale": { "ru": "RUB", "en": "USD", "hy": "AMD" }, "dictionaries": [] }, + "seo": { "default": { "title": "Dexar Market", "description": "Multi-category marketplace" }, "byPageKey": {} }, + "permissions": { "definitions": [], "roles": [] }, + "layout": { "type": "default" }, + "catalog": { "layout": "grid-4", "navigationMode": "default", "defaultSort": "relevance", "showRatings": true }, + "navigation": { + "header": [ { "id": "nav-home", "label": "Home", "route": "/", "order": 1, "visible": true } ], + "footer": [ { "id": "nav-privacy", "label": "Privacy", "route": "/privacy-policy", "order": 1, "visible": true } ] + }, + "pages": [ + { + "id": "page-home", "key": "home", "title": "Home", "route": { "path": "/", "exact": true }, "layout": "default", + "sections": [ + { "id": "home-hero", "type": "hero", "order": 1, "layout": { "strategy": "hero" }, + "widgets": [ { "id": "w-hero", "type": "hero", "version": "1.0.0", "props": { "title": "Welcome", "layout": "full-bleed" } } ] }, + { "id": "home-categories", "type": "categories", "order": 2, "layout": { "strategy": "grid", "columns": 4 }, + "widgets": [ { "id": "w-categories", "type": "categories", "version": "1.0.0", "props": { "columns": 4 } } ] } + ] + } + ], + "staticPages": { + "static-about": { "id": "static-about", "slug": "about", "title": { "en": "About Us" }, "showInFooter": true, "showInHeader": false, "enabled": true, "status": "published", "translations": { "en": { "html": "

About Us

" } } } + } +} +``` + +### 4.8 Lifecycle: how the frontend builds itself from it + +1. `ConfigService.loadBootstrap()` fetches (or, mock mode, reads local JSON) and parses `BootstrapConfig`. Cached via `shareReplay(1)`. +2. `PlatformRuntimeService` applies theme tokens as CSS variables, sets branding, and exposes parsed pages/navigation/footer/static-pages to the rest of the app. +3. The Section Engine / Widget Host render pages from `bootstrap.pages` on route match. +4. `PlatformRuntimeService.reloadFromBootstrap(next)` re-applies an entire new `BootstrapConfig` in-memory without a full page reload — this is what the Project Editor's Publish/Preview actions use (§6.7). + +The Project Editor edits an **in-memory copy of this exact same `BootstrapConfig`** — there is no parallel editor model or DTO translation layer (ADR-0001's core rule). + +--- + +## 5. API conventions + +### 5.1 Versioning + +No path-based versioning exists today (`/bootstrap`, not `/v1/bootstrap`) — `schemaVersion` inside the bootstrap payload is the actual contract version signal. Recommendation for new `builder/*`/`backoffice/*` endpoints: keep them unversioned in the path (matching existing convention) and rely on additive-only changes (new optional fields) within a schema-version line; bump `schemaVersion`'s major segment for breaking changes and coordinate a frontend release. + +### 5.2 Naming and pluralization + +Existing storefront endpoints are **inconsistently pluralized** by historical accident (`/category` singular-collection, `/items/:id` plural-collection, `/searchitems` compound) — **do not "fix" this**, it's a frozen contract (§6.1). For all **new** `builder/*`/`backoffice/*` endpoints, use plural resource collections (`/backoffice/products`, `/backoffice/orders`) consistently — this document's proposed paths in §6.7–§6.16 already follow that convention. + +### 5.3 Status codes + +| Code | Meaning | Used for | +|---|---|---| +| 200 | Success | GET, successful PUT/PATCH/POST that returns a body | +| 201 | Created | POST that creates a resource (e.g. `POST /backoffice/products`) | +| 204 | No Content | DELETE, or POST/PUT with no meaningful response body | +| 400 | Bad Request | Malformed request (not a validation failure — see 422) | +| 401 | Unauthorized | Missing/invalid session | +| 403 | Forbidden | Valid session, insufficient role/permission (including the admin-authorization gap fix, §2.5) | +| 404 | Not Found | Resource / tenant not found | +| 409 | Conflict | Slug/SKU uniqueness violation, concurrent-edit conflict | +| 422 | Unprocessable Entity | Validation failure (see §11 error shape) | +| 429 | Too Many Requests | Rate limit | +| 500 | Internal Server Error | Unhandled backend failure | +| 503 | Service Unavailable | Maintenance mode, dependency down (see §11.3) | + +### 5.4 Pagination + +Two patterns exist in the frontend's expectations, both real — pick per-domain, be consistent within a domain: + +- **Offset-based** (storefront): `count`/`skip` query params — `getCategoryItems(id, count=50, skip=0)`, `searchItems(..., count, skip)`, `getRandomItems(count)`. +- **Page-based** (admin lists): `page`/`pageSize` in filter objects (`AdminProductListFilters`, `AdminOrderListFilters`, `AdminReviewListFilters`, `AdminTransactionListFilters` all carry `page: number; pageSize: number`), response shape `{ items: T[], total: number, page: number, pageSize: number }` (`AdminProductsListResult`, `AdminOrdersListResult`, etc. — this exact 4-field envelope repeats across every paginated admin list; treat it as the standard admin list-response envelope for any new domain too). + +### 5.5 Sorting, filtering, searching + +- Storefront search (`GET /searchitems`) accepts `sort: relevance|price_asc|price_desc|popular|rating`, plus `categoryIDs` (comma-joined), `minPrice`, `maxPrice`, `tag`. +- Admin product list sort (`AdminProductSort`): `title|price|priority|stock|updated`. +- Every admin `*ListFilters` interface has a free-text `search: string` — treat it as a substring match across the domain's primary human-readable fields (name/title/email/order number as applicable) unless a domain-specific note says otherwise. + +### 5.6 PATCH vs PUT, DELETE semantics + +- **PUT** — full-resource replace or explicit "save current state" (`PUT /builder/bootstrap/draft` — whole document). +- **PATCH** — partial update by convention for status-only or single-field admin mutations (`updateStatus`, `setUserRole`, `setReviewVisible`, etc. — even though the gateway interfaces are transport-agnostic, model these as PATCH since they touch one field/aspect, not the whole entity). +- **DELETE** — categories/products/orders all expose an explicit `delete*` method returning `Observable` (→ `204`), **separate from** `archive*`/`restore*` (soft delete — see below). Reviews (`deleteReview`) and orders (`deleteOrder`) also have hard-delete methods distinct from any archive flow — confirm with product/legal whether hard delete should really be permitted for orders (audit-trail implications) before implementing it as literal data removal versus a tombstone. + +### 5.7 Soft delete + +Present as a first-class concept in two domains today: +- **Categories** (`AdminCategory.deletedAt: string | null` + `restoreCategory(id)`). +- **Products** (`archived: boolean` + `archiveProduct(id)`/`restoreProduct(id)` — boolean flag, not a timestamp). +- **Orders** (`archived: boolean` + `archiveOrder(id)`/`restoreOrder(id)`). + +No universal convention between `deletedAt: string|null` and `archived: boolean` — both are real, keep them domain-consistent with what's documented in §7 rather than unifying speculatively. + +### 5.8 Idempotency + +Not addressed anywhere in the current frontend contract. Recommend `Idempotency-Key` header support on `POST /builder/bootstrap/publish` and any payment-adjacent endpoint at minimum, since a retried publish/payment call must not double-apply. + +### 5.9 Response/error format + +See §11 for the single standard error envelope. Success responses have no forced envelope — most endpoints return the resource (or resource array) directly, not wrapped in `{ success, data }`. Follow the shapes documented per-endpoint in §6, not a generic wrapper. + +### 5.10 Dates, timezone, UUID + +- All timestamps in existing DTOs are ISO 8601 strings (`createdAt`, `updatedAt`, `expiresAt`, `generatedAt`, etc.) — never epoch numbers, except `AdminDashboardActivityEntry.timestamp: number` and `AdminMonitoringEvent`-adjacent internal caching (`Date.now()`), which are frontend-local, not wire-format precedent. +- `TenantConfig.timezone` (IANA string, e.g. `Europe/Moscow`) exists but nothing currently converts times server-side based on it — treat display-timezone conversion as a frontend concern unless otherwise decided. +- `TenantConfig.id: UUID` — `UUID`/`UrlString` are branded string types (`src/app/shared/types/primitive.types.ts`), i.e. plain strings at the wire level; no special serialization. + +### 5.11 LocaleMap and translatable fields + +The platform-wide rule (ADR-0001): **all user-facing text is translatable via a `translations.{lang}` shape, never a flat hardcoded field.** This repeats across the codebase as `Record` keyed by locale code (`ru`/`en`/`hy` in this deployment, but the model itself is not hardcoded to those three): `AdminProduct.translations: Record`, `AdminCategory.translations: Record`, `StaticPageConfig.translations: Record`, `NavigationItemConfig.label: string | Record`, `FooterConfig.copyrightText: string | Record`. When adding/removing a supported locale, every translatable object must expose/drop that locale's key — generically, never per-field hardcoding. + +### 5.12 Enums + +Enums throughout this document are TypeScript string-literal unions, not numeric — send/return the literal string values exactly as documented (e.g. `"processing"`, not `2`). + +--- + +## 6. Endpoints, by domain + +### 6.1 Storefront reads (CURRENT — frozen shapes, `src/app/services/api.service.ts`) + +`ApiService` normalizes both legacy and "backOffice" response variants defensively (dual field names, `0x`-hex colours → CSS hex, `names[]` → `translations`, `imgs[]` → `photos`, tolerance for the literal Go-side typo `valuue`). **Do not "fix" these variant shapes on the backend without confirming the frontend normalizer is updated in lockstep** — the normalizer exists precisely because multiple backend response shapes have shipped historically. + +| Method | Endpoint | Notes | +|---|---|---| +| `ping()` | `GET /ping` | Health check. | +| `getCategories()` | `GET /category` | Retried ×2 with exponential backoff (`retry({count:2, delay: 2^n * 500ms})`). Cached client-side (§13). | +| `getCategoryItems(id, count=50, skip=0)` | `GET /category/:id?count=&skip=` | Cached client-side. | +| `getItem(id)` | `GET /items/:id` | Rating/reviews/questions derived from the item payload itself (embedded `comments`/`questions`), not a separate call. Cached client-side. | +| `searchItems(search, count=50, skip=0, opts)` | `GET /searchitems?search=&count=&skip=&categoryIDs=&minPrice=&maxPrice=&tag=&sort=` | Returns `{ items: Item[], total: number }`. | +| `getRandomItems(count=5, categoryID?)` | `GET /items/randomitems?count=&category=` | Used for featured/related product rails. | + +### 6.2 Storefront writes (CURRENT — frozen shapes) + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| `addToCart(sessionId, items)` | `POST /websession/:sessionId` | `Array<{itemID, quantity, colour?, size?, price?}>` | provider-defined | +| `submitReview({...})` | `POST /items/:id/callback` | `{rating, comment, sessionID, timestamp}` | `{message: string}` | +| `submitQuestion({...})` | `POST /items/:id/questiion` | `{question, sessionID, timestamp}` | `{message: string}` — **note the literal path typo `questiion`, keep it; it is a frozen contract, not a bug to fix.** | +| `submitPurchaseEmail({...})` | `POST /purchase-email` | `{email, phone?, telegramUserId, items: [{itemID,name,price,currency,quantity?,delivery?}]}` | `{message: string}` | + +Payment endpoints: see §2.8. + +### 6.3 Bootstrap — see §4 in full. + +### 6.4 Authentication — see §2 in full. Summary table: + +| Method | Endpoint | Status | +|---|---|---| +| `POST {authApiUrl}/users/sessions` | Create session | CURRENT | +| `GET {authApiUrl}/users/sessions/:id` | Poll/check session | CURRENT | +| `DELETE {authApiUrl}/users/sessions/:id` | Logout | CURRENT | +| Ed25519 challenge/verify | — | FUTURE (§2.5) | + +### 6.5 Product engagement — rating / reviews / questions (CURRENT, per-product, confirm contract) + +The frontend already expects and renders against these; if they already exist on the backend, this is a contract-confirmation item, not new work: + +| Method | Endpoint | Notes | +|---|---|---| +| GET | `/products/{id}/rating` | Aggregate rating. | +| GET | `/products/{id}/reviews?page=&pageSize=` | Paginated per `productPage.reviews.pageSize` config. | +| GET | `/products/{id}/questions?page=&pageSize=` | Paginated per `productPage.questions.pageSize` config. | +| POST | `/products/{id}/reviews` | `{rating, title, text, anonymous}`. | +| POST | `/products/{id}/questions` | `{text, anonymous}`. | + +Toggled per-tenant via `bootstrap.productPage.reviews`/`.questions` (§4.4). If these don't exist yet, they block the Reviews/Questions UI. + +### 6.6 Search / autocomplete / trending (PLANNED) + +Current: in-memory products/categories/tags power autocomplete (`SearchAutocompleteService`); trending returns `null` (UI hides gracefully, never fabricates data); search history is `LocalSearchHistoryRepository` (guest, localStorage) with a `BackendSearchHistoryRepository` **placeholder class already defined but unimplemented** (`src/app/features/search/services/search-history.repository.ts` — both classes exist side by side, only the backend one is a no-op today). + +| Method | Endpoint | Status | +|---|---|---| +| GET | `/search/suggestions?q={term}` | PLANNED | +| GET | `/catalog/filters?category={id}&q={term}` | PLANNED | +| GET/POST/DELETE | `/me/wishlist`, `/me/compare`, `/me/saved-searches`, `/me/recently-viewed` | PLANNED — only needed if authenticated cross-device sync of these lists is wanted; today they're 100% localStorage (`LocalUserExperienceRepository`, keys `marketplace.ux.wishlist` etc.) | + +Swapping any of these live requires only replacing the repository implementation — facade/UI unaffected by design. + +### 6.7 Builder — Bootstrap draft/publish/validate (PLANNED, highest priority) + +The Project Editor edits the same `BootstrapConfig` the storefront reads (§4.8). Today: **load** is real (`GET /bootstrap`), **save** is `localStorage` only (`ProjectEditorDraftStorageService`, key `projectEditor.draftBootstrap.v1`, scoped by `tenant.id` — reload in another browser/tab and it's gone), **publish** re-applies the config in-memory via `PlatformRuntimeService.reloadFromBootstrap()` and flips a local `status` flag — **no backend call happens today.** `ProjectEditorIoService` today only does `JSON.stringify`/`JSON.parse` for manual export/import, no HTTP. + +| Method | Endpoint | Body | Response | Notes | +|---|---|---|---|---| +| GET | `/builder/bootstrap/draft` | — | `BootstrapConfig`, or 404/empty if none (draft = published) | Tenant resolved by host, no `projectId` param — one domain = one tenant = one draft = one published bootstrap. | +| PUT | `/builder/bootstrap/draft` | `BootstrapConfig` | 200, updated draft metadata | Persists the draft. **Does not** affect `GET /bootstrap` (storefront-facing) until publish. | +| POST | `/builder/bootstrap/publish` | `BootstrapConfig` (or none, if publish always promotes the stored draft) | 200 | Validates (§9.1), then makes this `BootstrapConfig` what `GET /bootstrap` returns. **Only** endpoint that affects the live storefront. | +| POST | `/builder/bootstrap/validate` | `BootstrapConfig` | `ProjectValidationIssue[]` (see §9.1 for the exact shape) | Optional but recommended — mirrors the client validator so the editor can preview server-side issues before attempting publish. | + +**Mandatory:** server-side re-validation on publish, equivalent to `ProjectValidator` (§9.1) at minimum. The client validator is not a trust boundary — a malicious or stale client can bypass all client-side checks. + +Frontend files that change once this exists: `features/project-editor/facade/project-editor.facade.ts` (replace local save/publish with these endpoints, same public method signatures), `services/project-editor-draft-storage.service.ts` (becomes a fallback/offline cache, not the primary store). + +### 6.8 Builder — Content pages / CMS (PLANNED) + +Static pages are today edited entirely client-side and written only into the in-memory/localStorage bootstrap draft (§6.7) — no dedicated backend exists. + +| Method | Endpoint | Notes | +|---|---|---| +| GET | `/builder/content-pages` | List, same shape as `bootstrap.staticPages`. | +| PUT | `/builder/content-pages` | Replace the set. | +| POST | `/builder/content-pages/import` | Bulk import. | +| GET | `/builder/content-pages/export` | Bulk export. | +| POST | `/builder/content-pages/validate` | Server-side HTML/SEO validation (§9.5). | + +Backend must also support **content moderation/validation on publish** (disallow dangerous tags/attributes) and **revision history** — the frontend only sanitizes at render time (`MarketplaceHtmlEditorComponent` emits raw HTML with no sanitization by design; sanitization is a storefront-render concern, not an authoring concern), never at authoring time. + +### 6.9 Backoffice — Categories (PLANNED) + +Interface: `AdminCategoriesGateway` (`src/app/features/admin/categories/services/admin-categories-gateway.interface.ts`). Today: `AdminCategoriesLocalGateway`, in-memory, seeded once from `BackofficeDataService.loadCategories()` — nothing persists across reload. `GET /category` (§6.1, storefront-facing) is unaffected and stays as-is. + +```ts +interface AdminCategoriesGateway { + loadCategories(filters: AdminCategoryListFilters): Observable; + loadCategory(id: string): Observable; + createCategory(category: AdminCategory): Observable; + updateCategory(category: AdminCategory): Observable; + deleteCategory(id: string): Observable; + restoreCategory(id: string): Observable; + isSlugTaken(slug: string, excludingId: string | null): Observable; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/backoffice/categories?search=&visibility=&includeDeleted=` | — | `AdminCategory[]` | +| GET | `/backoffice/categories/:id` | — | `AdminCategory \| 404` | +| POST | `/backoffice/categories` | `AdminCategory` | `201 AdminCategory` | +| PUT | `/backoffice/categories/:id` | `AdminCategory` | `200 AdminCategory` | +| DELETE | `/backoffice/categories/:id` | — | `204` (soft delete — sets `deletedAt`) | +| POST | `/backoffice/categories/:id/restore` | — | `200 AdminCategory` (clears `deletedAt`) | +| GET | `/backoffice/categories/slug-taken?slug=&excludingId=` | — | `{taken: boolean}` | +| PATCH | `/backoffice/categories/reorder` | `{id, order}[]` | `204` — bulk reorder endpoint, not in the gateway interface today but needed since `AdminCategory.order` exists and the editor drag-reorders | + +DTO: see §7.1. + +### 6.10 Backoffice — Products (PLANNED) + +Interface: `AdminProductsGateway` (`src/app/features/admin/products/services/admin-products-gateway.interface.ts`). Today: `AdminProductsLocalGateway`, in-memory. + +```ts +interface AdminProductsGateway { + loadProducts(filters: AdminProductListFilters): Observable; + loadProduct(id: string): Observable; + loadCategories(): Observable; + createProduct(product: AdminProduct): Observable; + updateProduct(product: AdminProduct): Observable; + deleteProduct(id: string): Observable; + duplicateProduct(id: string): Observable; + archiveProduct(id: string): Observable; + restoreProduct(id: string): Observable; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/backoffice/products?search=&categoryId=&visibility=&stock=&includeArchived=&sort=&page=&pageSize=` | — | `AdminProductsListResult` (`{items, total, page, pageSize}`) | +| GET | `/backoffice/products/:id` | — | `AdminProduct \| 404` | +| GET | `/backoffice/products/categories` | — | `AdminProductCategoryOption[]` (`{id, title}`) — **still wired to its own seed today, not `AdminCategoriesGateway`; unify when both backends exist.** | +| POST | `/backoffice/products` | `AdminProduct` | `201 AdminProduct` | +| PUT | `/backoffice/products/:id` | `AdminProduct` | `200 AdminProduct` | +| DELETE | `/backoffice/products/:id` | — | `204` (hard delete) | +| POST | `/backoffice/products/:id/duplicate` | — | `201 AdminProduct` | +| POST | `/backoffice/products/:id/archive` | — | `204` (soft archive) | +| POST | `/backoffice/products/:id/restore` | — | `200 AdminProduct` | +| PATCH | `/backoffice/products/bulk` | `{ids: string[], visible?: boolean, delete?: boolean}` | `204` — matches `applyBulkVisibility`/`applyBulkDelete` semantics referenced in the facade | + +DTO: see §7.2, including the production-matching flat-variant shape (`AdminProductVariant`) — this is the highest-fidelity DTO in this document; read §7.2 carefully before implementing. + +### 6.11 Backoffice — Orders (PLANNED) + +Interface: `AdminOrdersGateway`. Today: `AdminOrdersLocalGateway` fabricates 24 synthetic in-memory orders — **no real order data exists anywhere in this system today.** The dashboard's Orders/Revenue cards intentionally render `pending-backend` rather than reading from this mock — they are not wired to it on purpose (the mock is order-management UI scaffolding, not a metrics source). + +```ts +interface AdminOrdersGateway { + loadOrders(filters: AdminOrderListFilters): Observable; + loadOrder(id: string): Observable; + updateStatus(id: string, status: AdminOrderStatus, note: string): Observable; + requestRefund(id: string): Observable; + addNote(id: string, note: string, internal: boolean): Observable; + archiveOrder(id: string): Observable; + restoreOrder(id: string): Observable; + deleteOrder(id: string): Observable; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/backoffice/orders?search=&status=&page=&pageSize=` | — | `AdminOrdersListResult` | +| GET | `/backoffice/orders/:id` | — | `AdminOrder \| 404` | +| PATCH | `/backoffice/orders/:id/status` | `{status: AdminOrderStatus, note: string}` | `200 AdminOrder` — must enforce the order state machine (§8.1) | +| POST | `/backoffice/orders/:id/refund` | — | `200 AdminOrder` (sets `payment.status = 'refund_requested'`) | +| POST | `/backoffice/orders/:id/notes` | `{note: string, internal: boolean}` | `200 AdminOrder` | +| POST | `/backoffice/orders/:id/archive` | — | `200 AdminOrder` | +| POST | `/backoffice/orders/:id/restore` | — | `200 AdminOrder` | +| DELETE | `/backoffice/orders/:id` | — | `204` | +| GET | `/backoffice/orders/export.csv` | — | CSV — UI has a CSV-export action | +| GET | `/backoffice/orders/:id/invoice` | — | printable invoice — UI has a print-invoice action | +| GET | `/backoffice/dashboard/revenue-summary` | — | aggregation feeding the dashboard's currently-`pending-backend` Orders/Revenue cards | + +DTO: see §7.3, state machine: §8.1. + +### 6.12 Backoffice — Transactions (PLANNED) + +Interface: `AdminTransactionsGateway`. Today: `AdminTransactionsLocalGateway` derives one synthetic transaction per seeded mock order — **no real payment/transaction data exists.** + +```ts +interface AdminTransactionsGateway { + loadTransactions(filters: AdminTransactionListFilters): Observable; + retryFailed(id: string): Observable; + setFraudFlag(id: string, flagged: boolean): Observable; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/backoffice/transactions?search=&status=&type=&page=&pageSize=` | — | `AdminTransactionsListResult` | +| POST | `/backoffice/transactions/:id/retry` | — | `200 AdminTransaction` — semantics depend on whatever the real payment provider supports for retry | +| PATCH | `/backoffice/transactions/:id/fraud-flag` | `{flagged: boolean}` | `200 AdminTransaction` | +| GET | `/backoffice/transactions/export.csv` | — | CSV export | +| GET | `/backoffice/transactions/:id/audit` | — | `AdminTransactionAuditEntry[]` | + +Needed: a real payments/transactions domain (card, QR, cash-on-delivery), linked to orders, with fraud-flag persistence. DTO: §7.4. + +### 6.13 Backoffice — Users, roles, invitations (PLANNED) + +Interface: `AdminUsersGateway`. Today: `AdminUsersLocalGateway`, fully synthetic. **Passwordless login itself is real** (§2.4) — only the roles/permissions/invitations/multi-session-listing layer on top is mocked. + +```ts +interface AdminUsersGateway { + loadUsers(): Observable; + loadRoles(): Observable; + loadInvitations(): Observable; + loadSessions(userId: string): Observable; + loadAudit(userId: string): Observable; + setUserRole(userId: string, roleId: string): Observable; + setUserStatus(userId: string, status: AdminUserStatus): Observable; + inviteUser(email: string, roleId: string, scope: AdminUserScope): Observable; + revokeInvitation(id: string): Observable; + revokeSession(sessionId: string): Observable; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/backoffice/users` | — | `AdminUser[]` | +| GET | `/backoffice/roles` | — | `AdminRole[]` | +| GET | `/backoffice/invitations` | — | `AdminInvitation[]` | +| GET | `/backoffice/users/:id/sessions` | — | `AdminSession[]` — **must reflect real multi-device sessions**, unlike today's `AdminAuthService` which only knows the current browser's session | +| GET | `/backoffice/users/:id/audit` | — | `AdminUserAuditEntry[]` | +| PATCH | `/backoffice/users/:id/role` | `{roleId: string}` | `200 AdminUser` | +| PATCH | `/backoffice/users/:id/status` | `{status: AdminUserStatus}` | `200 AdminUser` | +| POST | `/backoffice/invitations` | `{email, roleId, scope}` | `201 AdminInvitation` — must trigger a real invitation email | +| DELETE | `/backoffice/invitations/:id` | — | `204` | +| DELETE | `/backoffice/sessions/:id` | — | `204` — revoke a specific device session | + +Ties directly to the §2.5 admin-authorization gap — role assignment here is meaningless until the auth layer actually enforces roles. DTO: §7.5–§7.6. + +### 6.14 Backoffice — Moderation (reviews & reports) (PLANNED) + +Interface: `AdminModerationGateway`. No `*LocalGateway` file distinct from the interface was found under a separate name in this pass beyond `admin-moderation-local.gateway.ts` — same pattern as everywhere else (mock today, swappable). + +```ts +interface AdminModerationGateway { + loadReviews(filters: AdminReviewListFilters): Observable; + loadReview(id: string): Observable; + setReviewStatus(id: string, status: AdminReviewStatus, note: string): Observable; + setReviewVisible(id: string, visible: boolean): Observable; + setReviewPinned(id: string, pinned: boolean): Observable; + setReviewFeatured(id: string, featured: boolean): Observable; + addModeratorNote(id: string, note: string): Observable; + deleteReview(id: string): Observable; + loadReports(): Observable; + setReportStatus(id: string, status: AdminReportStatus): Observable; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/backoffice/reviews?search=&status=&rating=&page=&pageSize=` | — | `AdminReviewsListResult` | +| GET | `/backoffice/reviews/:id` | — | `AdminReview \| 404` | +| PATCH | `/backoffice/reviews/:id/status` | `{status: AdminReviewStatus, note: string}` | `200 AdminReview` (state machine §8.4) | +| PATCH | `/backoffice/reviews/:id/visible` | `{visible: boolean}` | `200 AdminReview` | +| PATCH | `/backoffice/reviews/:id/pinned` | `{pinned: boolean}` | `200 AdminReview` | +| PATCH | `/backoffice/reviews/:id/featured` | `{featured: boolean}` | `200 AdminReview` | +| POST | `/backoffice/reviews/:id/notes` | `{note: string}` | `200 AdminReview` | +| DELETE | `/backoffice/reviews/:id` | — | `204` | +| GET | `/backoffice/reports` | — | `AdminReport[]` | +| PATCH | `/backoffice/reports/:id/status` | `{status: AdminReportStatus}` | `200 AdminReport` | + +DTO: §7.7. + +### 6.15 Backoffice — Dashboard metrics & recent activity (PLANNED) + +Interface: `AdminDashboardMetricsGateway`. Today: `AdminDashboardMetricsLocalGateway` composes `BackofficeDataService.loadCategories()/loadProducts()` client-side into counts; everything else on the dashboard (marketplace status, theme, languages, last publish/save, bootstrap version, active layout, enabled widgets, system health) is derived from `ProjectEditorFacade` state, not a metrics endpoint. Recent Activity is `AdminDashboardHistoryService`, `localStorage`-backed, scoped per tenant (`adminDashboard.activityHistory.v1`) — will never show another editor's activity. + +```ts +interface AdminDashboardMetricsGateway { + loadMetrics(): Observable; // today only { categoriesCount, productsCount } +} +``` + +| Method | Endpoint | Response | +|---|---|---| +| GET | `/backoffice/dashboard/summary` | Real-time counts and trend deltas — richer than today's `{categoriesCount, productsCount}` | +| GET | `/backoffice/dashboard/activity` | Real audit-log-backed activity feed, readable by multiple concurrent admin sessions (unlike today's per-browser localStorage) | + +DTO: §7.8. + +### 6.16 Backoffice — Monitoring (PLANNED except Health) + +Interface: `AdminMonitoringGateway`. Health section reads real data (`AdminDashboardFacade.healthChecks`) already — unaffected. Everything else (audit/security/login/failed-login/API/error/warning event feed, queue depths, webhook deliveries) is synthetic — **no logging, queue, or webhook infrastructure exists anywhere in this system today.** + +```ts +interface AdminMonitoringGateway { + loadEvents(filters: AdminMonitoringEventFilters): Observable; + loadQueues(): Observable; + loadWebhooks(): Observable; +} +``` + +| Method | Endpoint | Response | +|---|---|---| +| GET | `/backoffice/monitoring/events?category=&search=` | `AdminMonitoringEvent[]` — real structured logging with a query API by category/level/actor/time-range | +| GET | `/backoffice/monitoring/queues` | `AdminQueue[]` — real queue introspection, once a job runner exists | +| GET | `/backoffice/monitoring/webhooks` | `AdminWebhookDelivery[]` — real webhook delivery tracking, once webhooks exist as a feature at all (§15) | + +DTO: §7.9. + +### 6.17 Backoffice — Analytics (mostly FUTURE — no data source) + +`features/admin/analytics/` computes real revenue/orders/top-products aggregations from mock order data (§6.11), but visitor traffic, conversion funnels, and heatmaps have **zero data source anywhere in this system** — no analytics/tracking pipeline, no event collection. The page renders `pending-backend` badges rather than fabricated numbers; no gateway method exists for this yet, unlike every other mocked domain. + +| Method | Endpoint | Status | +|---|---|---| +| GET | `/backoffice/analytics/summary` | PLANNED — `AdminAnalyticsSummary`, revenue/orders/customers real once orders are real | +| GET | `/backoffice/analytics/traffic` | FUTURE — needs an actual tracking pipeline | +| GET | `/backoffice/analytics/funnels` | FUTURE | +| GET | `/backoffice/analytics/heatmaps` | FUTURE | + +DTO: §7.10. + +### 6.18 Media (PLANNED — ADR-0002) + +Domain model and contract are already spec'd in `docs/context/adrs/ADR-0002-media-manager-contract.md`; reproduced here as the canonical copy. `MediaRepository` (`src/app/core/media/media-repository.ts`) abstract class, two implementations selected via DI token: `MockMediaRepository` (IndexedDB — not localStorage, binary blobs need it) and `HttpMediaRepository` (to be built). + +```ts +abstract class MediaRepository { + abstract list(params?: MediaListParams): Promise; + abstract upload(file: File, options?: MediaUploadOptions): Promise; + abstract remove(id: string): Promise; + abstract update(id: string, patch: Partial>): Promise; + abstract listFolders(): Promise; +} +``` + +| Method | Endpoint | Body | Response | +|---|---|---|---| +| GET | `/media?page=&pageSize=&search=&folder=&tag=&kind=&sort=` | — | `MediaListResult` (`{items: MediaAsset[], total: number}`) | +| POST | `/media/upload` | multipart, `+ folder?, tags?[]` | `201 MediaAsset` | +| DELETE | `/media/:id` | — | `204` | +| PATCH | `/media/:id` | `{altText?, tags?, folder?, caption?, description?, decorative?}` | `200 MediaAsset` | +| GET | `/media/folders` | — | `string[]` | + +**Media never enters the Bootstrap model** — like products/orders/users, media assets are runtime admin data, not tenant configuration (ADR-0001). DTO: §7.11. + +### 6.19 Sitemap (FUTURE — static baseline only today) + +`public/sitemap.xml` currently lists only the statically-known top-level routes (home/catalog/search/wishlist/compare) for the default locale, referenced from `public/robots.txt`. This platform is multi-tenant and config-driven — supported locales, categories, products, static pages are all resolved at runtime per tenant, not enumerable from the frontend at build time. A real per-tenant sitemap covering `/:lang/product/:id`, `/:lang/catalog/:categoryId`, `/:lang/:staticPath` needs a build-time or server-side job reading the same per-tenant data source (categories/products/static pages) and regenerating/serving this file dynamically — not something the SPA can produce correctly on its own. + +--- + +## 7. DTOs + +Every interface below is lifted directly from its cited source file — field lists, optionality, and nullability are exact, not paraphrased. + +### 7.1 Categories — `src/app/features/admin/categories/models/admin-category.model.ts` + +```ts +type AdminCategoryStatus = 'draft' | 'published'; + +interface AdminCategoryTranslation { title?: string; description?: string; seoTitle?: string; seoDescription?: string; } +interface AdminCategorySeo { metaTitle: string; metaDescription: string; keywords: string; } +interface AdminCategoryAttribute { key: string; value: string; } + +interface AdminCategory { + id: string; + parentId: string | null; // hierarchy + title: string; + slug: string; + description: string; + icon: string; + imageUrl: string; + imageAlt: string; + order: number; + visible: boolean; + status: AdminCategoryStatus; // draft | published — generated field, editor-set + itemsCount: number; // generated field, backend-computed + translations: Record; + seo: AdminCategorySeo; + attributes: AdminCategoryAttribute[]; + deletedAt: string | null; // generated, soft-delete + createdAt: string; // generated + updatedAt: string; // generated +} + +interface AdminCategoryListFilters { search: string; visibility: 'all'|'visible'|'hidden'; includeDeleted: boolean; } +``` + +Note vs. the storefront-facing `CategoryDto`/`CategoryCardConfig` (`src/app/features/admin/**` seed source): those lack `parentId`, `slug`, `icon`, `imageUrl`, `status`, `deletedAt`, `seo`, `translations` — the admin model is a superset. Reconciling the two into one backend category table is a real design decision for the backend team; this document only certifies what the frontend needs, not how storage should be normalized. + +### 7.2 Products — `src/app/features/admin/products/models/admin-product.model.ts` + +```ts +type AdminProductStockStatus = 'in_stock' | 'low_stock' | 'out_of_stock'; +type AdminProductSort = 'title' | 'price' | 'priority' | 'stock' | 'updated'; + +interface AdminProductMedia { images: string[]; gallery: string[]; videos: string[]; } +interface AdminProductSpecification { key: string; value: string; } + +/** + * Matches the production variant shape: a flat list of attribute-value + * combinations, each priced per currency, e.g. + * {color:'0x8B4513', size:'S', price:62560, currency:'RUB', remaining:100}. + * The admin UI groups rows sharing the same attribute combo into one + * AdminProductVariant with multiple prices, then flattens back on save. + */ +interface AdminProductVariantPrice { currency: string; price: number; } +interface AdminProductVariant { + id: string; + attributes: Record; // e.g. { color: '0x8B4513', size: 'S' } + sku: string; + image: string; + remaining: number; + prices: AdminProductVariantPrice[]; +} +interface AdminProductVariantAttributeDef { + key: string; // lowercase, backend field name (e.g. 'color', 'size') + label: string; + isColor: boolean; + values: string[]; +} + +interface AdminProductAttribute { key: string; value: string; } +interface AdminProductTranslation { name?: string; shortDescription?: string; htmlDescription?: string; seoTitle?: string; seoDescription?: string; } +interface AdminProductSeo { metaTitle: string; metaDescription: string; keywords: string; } +interface AdminProductReview { id: string; author: string; rating: number; text: string; } +interface AdminProductQuestion { id: string; question: string; answer?: string; } + +interface AdminProduct { + id: string; name: string; slug: string; sku: string; barcode: string; brand: string; + categoryId: string; + visible: boolean; archived: boolean; priority: number; + media: AdminProductMedia; + price: number; discount: number; currency: string; quantity: number; + stockStatus: AdminProductStockStatus; availability: string; + shortDescription: string; htmlDescription: string; + specifications: AdminProductSpecification[]; + attributes: AdminProductAttribute[]; + variantAttributes: AdminProductVariantAttributeDef[]; + variants: AdminProductVariant[]; + relatedProductIds: string[]; + translations: Record; + seo: AdminProductSeo; + featured: boolean; recommended: boolean; isNew: boolean; bestseller: boolean; + badges: string[]; + reviews: AdminProductReview[]; questions: AdminProductQuestion[]; + createdAt: string; updatedAt: string; +} + +interface AdminProductListFilters { + search: string; categoryId: string | null; visibility: 'all'|'visible'|'hidden'; + stock: 'all' | AdminProductStockStatus; includeArchived: boolean; sort: AdminProductSort; + page: number; pageSize: number; +} +interface AdminProductsListResult { items: AdminProduct[]; total: number; page: number; pageSize: number; } +interface AdminProductCategoryOption { id: string; title: string; } +``` + +**Validation notes for the backend:** `sku` must be unique per tenant (§9.4). `variants[].sku` should also be unique among a product's own variants at minimum. `price`/`currency` required at the top level even when `variants` carries its own per-combination `prices` — top-level price is the "from" price shown on listing cards. + +### 7.3 Orders — `src/app/features/admin/orders/models/admin-order.model.ts` + +```ts +type AdminOrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded'; +type AdminOrderPaymentStatus = 'unpaid' | 'paid' | 'refund_requested' | 'refunded'; + +interface AdminOrderCustomer { name: string; email: string; phone: string; } +interface AdminOrderPayment { method: string; status: AdminOrderPaymentStatus; amount: number; currency: string; } +interface AdminOrderShipping { address: string; method: string; trackingNumber: string; } +interface AdminOrderItem { productId: string; name: string; quantity: number; price: number; } +interface AdminOrderTimelineEntry { status: AdminOrderStatus; timestamp: string; note: string; } + +interface AdminOrder { + id: string; orderNumber: string; status: AdminOrderStatus; + customer: AdminOrderCustomer; payment: AdminOrderPayment; shipping: AdminOrderShipping; + items: AdminOrderItem[]; total: number; currency: string; + notes: string; internalNotes: string; + timeline: AdminOrderTimelineEntry[]; // audit trail of every status change + archived: boolean; + createdAt: string; updatedAt: string; +} + +interface AdminOrderListFilters { search: string; status: 'all'|AdminOrderStatus; page: number; pageSize: number; } +interface AdminOrdersListResult { items: AdminOrder[]; total: number; page: number; pageSize: number; } +``` + +`AdminCustomer` (`src/app/features/admin/customers/models/admin-customer.model.ts`) is **not its own stored entity** — it's derived by grouping `AdminOrder` records by `customer.email`, since no dedicated customer gateway/backend exists: + +```ts +interface AdminCustomer { + email: string; name: string; phone: string; + orderCount: number; totalSpent: number; currency: string; + firstOrderAt: string; lastOrderAt: string; + addresses: string[]; orders: AdminOrder[]; +} +``` + +If/when a real customer domain is built, this becomes a real GET endpoint rather than a client-side aggregation — every field here should map 1:1 to a real column/aggregate, never fabricated. + +### 7.4 Transactions — `src/app/features/admin/transactions/models/admin-transaction.model.ts` + +```ts +type AdminTransactionType = 'payment' | 'refund' | 'qr_payment'; +type AdminTransactionStatus = 'pending' | 'success' | 'failed' | 'retried'; + +interface AdminTransactionAuditEntry { action: string; actor: string; timestamp: string; } + +interface AdminTransaction { + id: string; orderId: string; orderNumber: string; + type: AdminTransactionType; method: string; status: AdminTransactionStatus; + amount: number; currency: string; + fraudFlag: boolean; + audit: AdminTransactionAuditEntry[]; + createdAt: string; updatedAt: string; +} + +interface AdminTransactionListFilters { search: string; status: 'all'|AdminTransactionStatus; type: 'all'|AdminTransactionType; page: number; pageSize: number; } +interface AdminTransactionsListResult { items: AdminTransaction[]; total: number; page: number; pageSize: number; } +``` + +### 7.5 Users, roles, invitations, sessions — `src/app/features/admin/users/models/admin-user.model.ts` + +```ts +type AdminUserScope = 'marketplace' | 'office'; +type AdminUserStatus = 'active' | 'invited' | 'suspended'; +type AdminInvitationStatus = 'pending' | 'accepted' | 'expired' | 'revoked'; + +interface AdminRole { id: string; name: string; permissions: string[]; builtIn: boolean; } + +interface AdminUser { + id: string; name: string; telegramUsername: string; email: string; + scope: AdminUserScope; roleId: string; status: AdminUserStatus; + lastLoginAt: string | null; createdAt: string; +} + +interface AdminInvitation { id: string; email: string; roleId: string; scope: AdminUserScope; status: AdminInvitationStatus; invitedAt: string; expiresAt: string; } +interface AdminSession { id: string; userId: string; device: string; ip: string; lastActiveAt: string; current: boolean; } +interface AdminUserAuditEntry { action: string; actor: string; timestamp: string; } +``` + +### 7.6 Permissions (bootstrap-level skeleton) — `src/app/shared/models/config/permissions.model.ts` + +```ts +interface PermissionDefinition { key: string; description?: string; } +interface RolePermissions { role: string; permissions: string[]; } +interface PermissionsConfig { definitions: PermissionDefinition[]; roles: RolePermissions[]; } +``` + +### 7.7 Moderation — reviews & reports + +```ts +// admin-review.model.ts +type AdminReviewStatus = 'pending' | 'approved' | 'rejected' | 'spam'; +interface AdminReviewTimelineEntry { action: string; actor: string; note: string; timestamp: string; } +interface AdminReview { + id: string; productId: string; productName: string; + customerName: string; customerEmail: string; + rating: number; text: string; photos: string[]; + status: AdminReviewStatus; visible: boolean; pinned: boolean; featured: boolean; + reportCount: number; moderatorNotes: string; + timeline: AdminReviewTimelineEntry[]; + createdAt: string; updatedAt: string; +} +interface AdminReviewListFilters { search: string; status: 'all'|AdminReviewStatus; rating: 'all'|number; page: number; pageSize: number; } +interface AdminReviewsListResult { items: AdminReview[]; total: number; page: number; pageSize: number; } + +// admin-report.model.ts +type AdminReportTargetType = 'product' | 'review' | 'customer' | 'category' | 'unknown'; +type AdminReportStatus = 'open' | 'resolved' | 'dismissed'; +interface AdminReport { id: string; targetType: AdminReportTargetType; targetId: string; targetLabel: string; reason: string; reporterEmail: string; status: AdminReportStatus; createdAt: string; } +``` + +### 7.8 Dashboard — `src/app/features/admin/dashboard/models/admin-dashboard.model.ts` + +```ts +type AdminDashboardCardStatus = 'loading' | 'ready' | 'empty' | 'error' | 'pending-backend'; +interface AdminDashboardCardState { status: AdminDashboardCardStatus; value: T | null; } +interface AdminDashboardMetrics { categoriesCount: number; productsCount: number; } // today's real shape — expand per §6.15 + +type AdminDashboardHealthStatus = 'healthy' | 'attention' | 'unhealthy' | 'unknown' | 'loading'; +interface AdminDashboardHomeHealthCheck { code: string; labelKey: string; status: AdminDashboardHealthStatus; displayValue?: string | null; } +interface AdminDashboardActivityEntry { id: string; type: 'draft-saved'|'published'; timestamp: number; } +``` + +`pending-backend` is a real, intentional card status — it means "this sprint has no data source for this metric yet," rendered honestly instead of a fabricated number. Any new metrics endpoint should let the frontend distinguish "zero" from "no data source" the same way (e.g. omit the field, or use `null`, rather than sending `0`). + +### 7.9 Monitoring — `src/app/features/admin/monitoring/models/admin-monitoring.model.ts` + +```ts +type AdminMonitoringCategory = 'audit' | 'security' | 'login' | 'failed_login' | 'api' | 'error' | 'warning'; +type AdminMonitoringLevel = 'info' | 'warning' | 'error'; +type AdminQueueStatus = 'healthy' | 'degraded' | 'down'; +type AdminWebhookStatus = 'delivered' | 'failed' | 'pending'; + +interface AdminMonitoringEvent { id: string; category: AdminMonitoringCategory; level: AdminMonitoringLevel; message: string; actor: string; timestamp: string; } +interface AdminMonitoringEventFilters { category: 'all'|AdminMonitoringCategory; search: string; } +interface AdminQueue { name: string; depth: number; status: AdminQueueStatus; } +interface AdminWebhookDelivery { id: string; endpoint: string; event: string; status: AdminWebhookStatus; timestamp: string; } +``` + +Payment/QR types (`src/app/services/api.service.ts` lines 10–66, frozen — §2.8): + +```ts +interface QrCreateRequest { qrtype: 'QRDynamic'; amount: number; currency: 'RUB'; partnerqrID?: string; qrDescription?: string; Userid?: string; Reference?: string; RedirectUrl?: string; } +interface QrCreateResponse { qrId?: string; qrID?: string; nspkID?: string; nspkId?: string; nspkurl?: string; orderID?: string; url?: string; bankUrl?: string; status?: string; qrStatus?: string; qrExpirationDate?: string; payload?: string; Payload?: string; qrUrl?: string; partnerqrID?: string|number; partnerID?: string|number; partnerId?: string|number; PartnerID?: string|number; } +interface CartPaymentRequest { amount: number; currency: 'RUB'; siteuserID: string; siteorderID: string; redirectUrl: string; telegramUsername: string; paymentMethod: 'qr'|'card'; items: Array<{itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[]}>; } +interface QrDynamicStatusResponse { additionalInfo: string; paymentPurpose: string; amount: number; code: string; createDate: string; currency: string; order: string; status: string; qrId: string; transactionDate: string; transactionId: number; qrExpirationDate: string; } +``` + +### 7.10 Analytics — `src/app/features/admin/analytics/models/admin-analytics.model.ts` + +```ts +type AdminAnalyticsDateRange = 7 | 30 | 90; +interface AdminAnalyticsSummary { + revenueTotal: number; currency: string; ordersCount: number; avgOrderValue: number; + productsCount: number; categoriesCount: number; customersCount: number; + conversionRate: number | null; // null = unknown, no visitor/traffic tracking exists yet — never fabricated +} +interface AdminAnalyticsSeriesPoint { date: string; value: number; } +interface AdminAnalyticsTopProduct { productId: string; name: string; quantity: number; revenue: number; } +interface AdminLowStockProduct { productId: string; name: string; quantity: number; stockStatus: 'low_stock'|'out_of_stock'; } +interface AdminRecentActivityEntry { id: string; labelKey: string; timestamp: number; } +type AdminMarketplaceHealthStatus = 'healthy' | 'attention' | 'unhealthy' | 'unknown'; +interface AdminMarketplaceHealthCheck { code: string; labelKey: string; status: AdminMarketplaceHealthStatus; displayValue?: string | null; actionRoute?: string[]; } +interface AdminProductAnalyticsRow { productId: string; name: string; value: number; } +interface AdminProductAnalytics { topSelling: AdminAnalyticsTopProduct[]; mostReviewed: AdminProductAnalyticsRow[]; worstRated: AdminProductAnalyticsRow[]; hiddenCount: number; archivedCount: number; } +interface AdminCustomerAnalytics { newCustomers: number; returningCustomers: number; averageSpend: number; currency: string; retentionPercent: number | null; } +type AdminRecommendationSeverity = 'info' | 'warning' | 'critical'; +interface AdminRecommendationCard { id: string; labelKey: string; descriptionKey?: string; severity: AdminRecommendationSeverity; route: string[]; } +``` + +`conversionRate: number | null` and `retentionPercent: number | null` are the platform's explicit convention for "no data source yet" — `null` is a first-class, meaningful value here, not an oversight. Replicate this pattern for any new metric without a real source rather than defaulting to `0`. + +### 7.11 Media — `src/app/core/media/models/media-asset.model.ts` + +```ts +type MediaAssetKind = 'image' | 'svg' | 'pdf' | 'other'; +interface MediaAsset { + id: string; url: string; thumbnailUrl?: string; + filename: string; mimeType: string; size: number; + width?: number; height?: number; + altText?: Record; // translations.{lang} rule + caption?: string; description?: string; + decorative?: boolean; // suppresses missing-alt-text warning + tags?: string[]; folder?: string; + createdAt: string; +} +type MediaSort = 'recent' | 'name' | 'size'; +interface MediaListParams { page?: number; pageSize?: number; search?: string; folder?: string; tag?: string; kind?: MediaAssetKind; sort?: MediaSort; } +interface MediaUploadOptions { folder?: string; tags?: string[]; } +interface MediaListResult { items: MediaAsset[]; total: number; } +``` + +### 7.12 Bootstrap DTOs — see §4.3–§4.6 for the complete `BootstrapConfig` tree (not repeated here to avoid duplication). + +--- + +## 8. State machines + +### 8.1 Orders — `AdminOrderStatus` + +``` +pending → processing → shipped → delivered +pending → cancelled +processing → cancelled +shipped → delivered +delivered → refunded (via payment.status refund_requested → refunded, not a direct status jump) +* → refunded (only through the refund request flow, never a direct status write) +``` + +Invalid transitions to reject server-side: `delivered → pending`, `cancelled → *` (terminal), `refunded → *` (terminal), skipping `shipped` to jump straight `processing → delivered` is allowed by the model (no `shipped` requirement enforced client-side) but should be a deliberate backend policy decision, not silently permitted. Every transition must append an `AdminOrderTimelineEntry {status, timestamp, note}` — the timeline is the audit trail, never overwritten. + +### 8.2 Payment status — `AdminOrderPaymentStatus` + +``` +unpaid → paid +paid → refund_requested +refund_requested → refunded +refund_requested → paid (refund rejected, reverts) +``` + +### 8.3 Products — implicit via `visible`/`archived` booleans, not an enum + +``` +(new) → visible: true, archived: false +visible: true ⇄ visible: false (toggle, reversible, any time) +archived: false → archived: true (via archiveProduct — soft archive) +archived: true → archived: false (via restoreProduct) +archived: true → (hard delete) (via deleteProduct — irreversible) +``` + +`stockStatus` (`in_stock|low_stock|out_of_stock`) is derived from `quantity`, not independently settable — treat it as backend-computed, never accept it as client input on create/update. + +### 8.4 Reviews — `AdminReviewStatus` + +``` +pending → approved +pending → rejected +pending → spam +approved ⇄ rejected (moderator can reverse a decision) +* → spam (any state can be flagged spam) +``` + +Every status change should append an `AdminReviewTimelineEntry`. `visible`, `pinned`, `featured` are independent booleans layered on top of `status` — a `rejected` review should probably force `visible: false` server-side regardless of what the client sends (defense in depth), even though nothing in the current frontend enforces that coupling. + +### 8.5 Reports — `AdminReportStatus` + +``` +open → resolved +open → dismissed +resolved ⇄ dismissed (reversible, e.g. re-opened as a mistake) +``` + +### 8.6 Static pages — `status: 'draft' | 'published'` (per-page, independent of whole-bootstrap draft/publish) + +``` +draft → published +published → draft (unpublish) +``` + +Independent from the category/product `draft|published` pattern — no shared enum type, don't unify speculatively. + +### 8.7 Sessions — `AuthStatus` / `AdminAuthStatus` + +``` +unknown → checking → authenticated +unknown → checking → unauthenticated +authenticated → unauthenticated (logout, or expiry check fails) +``` + +### 8.8 Users — `AdminUserStatus` + +``` +invited → active (invitation accepted) +active ⇄ suspended +invited → (invitation revoked, never becomes a user) +``` + +### 8.9 Invitations — `AdminInvitationStatus` + +``` +pending → accepted +pending → expired (time-based, backend job) +pending → revoked (admin action) +``` + +--- + +## 9. Validation + +### 9.1 Backend vs. frontend validation — the client validator is not a trust boundary + +`ProjectValidator` (`src/app/features/project-editor/services/project-validator.service.ts`) runs entirely client-side and blocks the Publish button, but **a malicious or stale client can bypass all of it.** `POST /builder/bootstrap/publish` must re-run equivalent checks server-side. The exact issue shape to mirror: + +```ts +interface ProjectValidationIssue { + code: string; // stable machine code + message: string; // i18n key, not a rendered string + section?: string; // editor section for UI badges + fieldKey?: string; // schema field key for inline errors + severity: 'error' | 'warning'; // error blocks publishing; warning is advisory +} +``` + +Checks the client runs today (replicate server-side, at minimum the `error`-severity ones): + +| Code | Severity | Rule | +|---|---|---| +| `missing-logo` | error | `branding.logoUrl` must be non-empty | +| `no-languages` | error | `localization.supportedLocales` must be non-empty | +| `default-locale-not-supported` | error | `localization.defaultLocale` must be in `supportedLocales` | +| `invalid-url` | error | `tenant.websiteBaseUrl` must be `http(s)://...` if present | +| `duplicate-slugs` | error | static-page `slug` (or `route` fallback) must be unique | +| `duplicate-routes` | warning | `pages[].route.path` must be unique; static-page routes must not collide with page routes | +| `empty-homepage` | error | the `home` page must have ≥1 section | +| `missing-widget` | error | every homepage widget must have a non-empty `type` | +| `invalid-widget-config` | error | every widget on every page needs `id`, `type`, `version`, and an object `props` | +| `duplicate-nav-links` | error | `navigation.header` items must be unique by `(label, route)` | +| `invalid-colors` | error | every `theme.palette` value must be a valid hex color | +| `invalid-css` | warning | `