1 Commits

Author SHA1 Message Date
sdarbinyan
905c8be8c5 translations 2026-07-04 23:34:00 +04:00
1070 changed files with 33615 additions and 61404 deletions

View File

@@ -1,30 +0,0 @@
name: Architecture Governance
on:
push:
branches:
- '**'
pull_request:
jobs:
architecture:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install Dependencies
run: npm ci
- name: Enforce Boundaries
run: npm run arch:check
- name: Build
run: npm run build

32
.gitignore vendored
View File

@@ -7,9 +7,6 @@
/bazel-out /bazel-out
/files /files
changes.txt changes.txt
/agent
/agents
.agents
# Node # Node
/node_modules /node_modules
@@ -45,32 +42,3 @@ testem.log
# System files # System files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Claude Code worktrees/session state, graphify knowledge-graph output
.claude/
graphify-out/
<!-- barry-cache:start -->
.context-state/
.context-cache/
.barry-cache/
<!-- barry-cache:end -->
AGENTS.md
CLAUDE.md
GEMINI.md
llms.txt
.cursor/rules/barry-cache.mdc
.github/copilot-instructions.md
docs/context/INDEX.md
docs/context/LOG.md
docs/context/MAINTENANCE.md
docs/context/README.md
docs/context/adrs/README.md
docs/context/concepts/project-context-model.md
docs/context/schema/adr.schema.json
docs/context/schema/fact.schema.json
docs/context/schema/failure.schema.json
docs/context/schema/route.schema.json
docs/context/schema/strategy.schema.json
docs/context/schema/work-state.schema.json
docs/context/schema/workspace.schema.json

View File

@@ -1,124 +0,0 @@
{
"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": "<button class=\"ds-btn-primary\">Save changes</button>",
"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": "<button class=\"ds-btn-ghost\">Cancel</button>",
"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": "<div class=\"ds-card\"><h3 class=\"ds-card-title\">Product title</h3><p class=\"ds-card-body\">Supporting copy sits in Muted Pine Grey at a comfortable line height.</p></div>",
"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": "<label class=\"ds-field\"><span class=\"ds-field-label\">Store name</span><span class=\"ds-field-desc\">Shown in the storefront header.</span><input class=\"ds-input\" type=\"text\" placeholder=\"My marketplace\" /></label>",
"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": "<span class=\"ds-badge ds-badge-sale\">Sale</span>",
"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": "<span class=\"ds-tag\">Digital</span>",
"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."
]
}
}

View File

@@ -1,6 +0,0 @@
{
"files": ["src/index.html"],
"insertBefore": "</body>",
"commentSyntax": "html",
"cspChecked": true
}

View File

@@ -1,534 +0,0 @@
# Backend API Reference
One document, everyone reads it: product, backend, frontend, QA. It answers three questions for every domain — **what does the frontend already call**, **what shape does it send/expect**, and **is it real or mocked today**. Generated from the actual Angular frontend source (this repo has no backend code — it is a pure client consuming an external API), cross-checked against the frontend's own tolerant adapters, not aspirational.
Maturity tags used throughout:
| Tag | Meaning |
|---|---|
| **LIVE** | Real `HttpClient` call exists in code today, hits a real endpoint. |
| **MOCK-SWAPPABLE** | Interface + DI token exist; a real implementation can be dropped in without touching UI. May or may not have a real impl yet. |
| **MOCK-ONLY (no seam)** | A mock/local implementation exists but the facade injects the concrete mock class directly — no DI token. A backend needs a token introduced first before it can be wired in. |
| **LOCAL-ONLY** | Never talks to a backend by design — localStorage / in-memory / derived from bootstrap. |
---
## 1. Core principles
1. **No response envelope.** There is no `{ success, data, error }` wrapper anywhere. Every call is typed to the bare payload — `HttpClient.get<Item>(...)`, `get<Category[]>(...)`, `get<BootstrapConfig>(...)`. Success = the raw resource (object, array, or `{ items, total }` for lists). Do not wrap new endpoints in an envelope unless it's a deliberate, coordinated breaking change.
2. **No API versioning.** No `/v1/` segment, no `Accept-Version` header, anywhere. The only version field in the whole contract is `BootstrapConfig.schemaVersion`, and it's checked for presence only, not semantically enforced.
3. **No WebSocket / SSE.** Every "live" feeling feature (QR login polling, payment status) is plain `setInterval`/RxJS polling against a normal request/response endpoint.
4. **Tenant resolution is 100% by hostname, not by header or path.** `TenantResolverService` reads the first DNS label (skipping `www`) and uses it to pick a base URL. No `X-Tenant` header, no `/tenant/{id}/...` prefix, ever. Auth requests carry no tenant identifier either — origin is the only signal.
5. **Two independent API bases exist**, plus a third for auth:
- Marketplace/tenant API — `ApiConfigService.getBaseUrl()` — default `https://api.dexarmarket.ru:445` (or per-tenant subdomain), `/api` on localhost.
- Payment/QR API — `environment.qrApiUrl` = `https://qr.vitanova.network/api`.
- Session auth API — `environment.authApiUrl` (currently same host as the marketplace API).
6. **Two independent mock mechanisms coexist — don't conflate them.** (a) `mock-data.interceptor.ts` globally short-circuits a hardcoded URL list (`/ping`, `/users/sessions*`, `/category`, `/items/*`, `/searchitems`, `/cart`, `/qr*`, `/websession/*`) when `environment.useMockData=true` — off in both shipped environments today. (b) Per-domain DI-token factories (`CONFIG_PROVIDER`, `CATEGORY_REPOSITORY`, `PRODUCT_DATA_PROVIDER`, `BACKOFFICE_DATA_PROVIDER`, `ADMIN_CATEGORIES_GATEWAY`) pick a mock vs. real class per `RuntimeProviderStrategyService`. **`PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY` always resolve to the real API implementation regardless of mode** — their mock branch is dead code (`product-data-provider.token.ts:12-18`, `category-repository.token.ts:12-19`). `ADMIN_DASHBOARD_METRICS_GATEWAY` always resolves to the local/mock class the other direction — no real implementation is bound yet even though the token exists.
7. **GET retries:** `ApiService`/`ApiCategoryRepository` wrap reads in a shared `retry({ count: 2, delay: exponential from 500ms })` — expect up to 3 attempts per read before a caller sees a failure.
8. **Dead scaffolding, not missing files:** `src/app/core/error-handling/`, `src/app/core/guards/`, `src/app/core/interceptors/` each contain only a `.gitkeep` — reserved directory structure for a centralized error-handling layer that was never built. Every error today is handled ad hoc at the call site.
9. **Backend engineers should not "clean up" the tolerant adapters.** `ApiService.normalizeItem()`/`normalizeCategory()` and `TelegramSessionApiService.normalizeWebSession()` accept multiple historical field-name casings/aliases on purpose (see §7 Products). A payload landing anywhere inside that tolerance envelope works; a stricter renamed shape breaks the client.
10. **Nullable fields:** the frontend treats `null`, `undefined`, and an omitted key as the same "absent" signal everywhere except a handful of fields explicitly typed `T | null` (e.g. `AuthSession.userId`) where `null` specifically means "known to be absent." Omit or send `null` interchangeably elsewhere.
11. **Do not invent endpoints, fields, or business rules beyond what a real frontend call already implies.** Every open question below is flagged `Requires backend decision` with a recommended default — apply the default and move on unless it's flagged as a business/security decision.
---
## 2. Authentication
Two **independent, coexisting** mechanisms. Neither is a stand-in for the other; they authenticate different populations today.
### 2a. Telegram QR / session login — customer AND admin (LIVE)
Single mechanism for both; only client-side storage differs (separate cookie/signals per surface). Source: `src/app/services/telegram-session-api.service.ts`.
| Endpoint | Method | Auth | Body / Headers | Response |
|---|---|---|---|---|
| `/users/sessions` | POST | none | body `{ webSessionID }` (client-generated GUID) + header `WebSessionID: <same guid>` | `{ webSessionID, url }``url` is a `https://t.me/{bot}?start={id}` deep link |
| `/users/sessions/{id}` | GET | none | — | Session object, field-tolerant, normalized to `AuthSession` |
| `/users/sessions/{id}` | DELETE | none | header `WebSessionID: <id>` | ignored — client clears local state regardless of response |
```http
POST https://api.dexarmarket.ru:445/users/sessions
WebSessionID: 3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11
Content-Type: application/json
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
```
```json
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11", "url": "https://t.me/myAMLKYCBOT?start=3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
```
Poll response (field-tolerant — send real field names, the client accepts many aliases):
```json
{
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
"status": "active",
"user": { "id": 8823771, "username": "buyer_ivan", "firstName": "Ivan", "lastName": "P" },
"expiresAt": "2026-07-26T05:00:00Z"
}
```
Send a real `expiresAt`/`expires` — if absent, the client fabricates `now + 3600s`.
Client-side model (`src/app/models/auth.model.ts`):
```ts
interface AuthSession { sessionId: string; userId: number | null; username: string | null; displayName: string; active: boolean; expires: string; }
interface WebSessionStart { webSessionID: string; url: string; }
```
Expiry handling: `expires` drives a client timer that re-polls `GET /users/sessions/{id}` shortly before expiry; if the backend reports inactive, local state clears. There is no reactive 401 handling for this mechanism — expiry is only discovered on the next explicit poll.
### 2b. Ed25519 challenge/response admin auth (wired client-side, backend not implemented — calls 404 today)
Source: `src/app/core/auth/services/auth-api.service.ts`. Base `{authApiUrl}/api/admin/auth`.
| Endpoint | Method | Request | Response |
|---|---|---|---|
| `/challenge` | GET | — | `AuthChallenge { nonce, issuedAt, expiresAt }` |
| `/verify` | POST | `VerifySignatureRequest { publicKey, signature, nonce }` | `AuthTokenPair { token, refreshToken }` |
| `/refresh` | POST | `RefreshTokenRequest { refreshToken }` | `AuthTokenPair` |
| `/logout` | POST | `{ refreshToken }` | void |
JWT claims (`JwtClaims`, decode-only client-side — the frontend never verifies the signature, that's the backend's job on every request):
```ts
interface JwtClaims { sub: string; role: AdminRole; iat: number; exp: number; publicKey: string; }
type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
```
Storage: `localStorage['ed25519AdminToken']` (access), `localStorage['ed25519AdminRefreshToken']` (refresh, opaque, never decoded client-side).
**Header:** intended as standard `Authorization: Bearer <token>`, but the interceptor that would auto-attach it (`authInterceptor`) is **not registered** in `app.config.ts` today — no request currently attaches the bearer token automatically. `adminAuthHeadersInterceptor` sets it *if* a token happens to be in storage, but nothing populates one in the live flow yet.
**Refresh:** client proactively refreshes ~60s before `exp` via a scheduled timer, and (once `authInterceptor` is registered) would reactively refresh once on any 401 before giving up. Every `/refresh` response is expected to return a **new** `refreshToken` (rotation) — the backend should invalidate the one just used.
**Role → permission table** (`ROLE_PERMISSIONS`, coarse, enforced client-side only for UX — backend must independently authorize every mutation):
| Role | Permissions |
|---|---|
| `Owner` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage`, `settings.manage` |
| `Administrator` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage` |
| `Editor` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write` |
| `Support` | `backoffice.read` |
| `ReadOnly` | `backoffice.read`, `builder.read` |
**Known naming collision:** `AdminRole` is defined twice — the string union above (`core/auth/models/permission.model.ts`, the real JWT/auth contract) and an unrelated interface in `features/admin/users/models/admin-user.model.ts` (display-only labels in the Users admin page, not connected to auth). Treat the string union as the authoritative role for auth purposes; the interface needs a rename (e.g. `AdminUserRoleRecord`) — this is flagged, not yet fixed.
**Route guards:** `adminAuthGuard` (live, checks only "is there an active Telegram session," no role check) gates `/edit`, `/edit/:section`, `/backoffice`. `ed25519AuthGuard` and `permissionGuard(permission)` exist and are fully built but attached to **no route today** — dormant until Mechanism B cuts over. Every guard is a client-side UX gate only; the backend must independently verify authorization on every admin mutation regardless of what a guard decided.
**Open decision (business, not technical — ask a human):** whether Mechanism A is retired outright in favor of Mechanism B at cutover, or both run in parallel gated by role/tenant config.
---
## 3. Bootstrap — the runtime config document
The single payload that drives the entire multi-tenant storefront/builder/backoffice. Fetched once at app startup, held in memory; nearly every feature reads from it instead of a dedicated endpoint.
| | |
|---|---|
| Method / Route | `GET /bootstrap` (relative, rewritten onto the tenant base) |
| Auth | **None** — must be publicly cacheable per tenant, fetched before any login |
| Query / body | none |
```ts
interface BootstrapConfig {
schemaVersion: string; generatedAt: string;
tenant: TenantConfig; branding: BrandingConfig; theme: ThemeConfig; company: CompanyConfig;
featureFlags: FeatureFlagsConfig; features?: MarketplaceFeaturesConfig;
apiEndpoints: ApiEndpointsConfig; localization: LocalizationConfig; seo: SeoConfig;
permissions: PermissionsConfig; header?: HeaderConfig; catalog?: CatalogConfig;
layout?: PlatformLayoutConfig; navigation: NavigationConfig; footer?: FooterConfig;
productPage?: ProductPageConfig; userExperience?: UserExperienceConfig;
pages: PageConfig[]; staticPages?: StaticPagesConfig; widgetRegistry?: WidgetRegistryConfig;
}
```
Required top-level keys (must always be emitted): `schemaVersion, generatedAt, tenant, branding, theme, company, featureFlags, apiEndpoints, localization, seo, permissions, navigation, pages`. Everything marked `?` may be omitted — the client applies defaults.
`apiEndpoints.{website,builder,backoffice}` is where a tenant is meant to declare its per-surface endpoint paths at runtime (`Record<string, { path, method, timeoutMs? }>`) — **these are empty `{}` in the mock today; no builder/backoffice CRUD path exists as a hardcoded literal anywhere in the client.** Any concrete admin CRUD path in this document is a proposal, not a verified literal, until populated here.
Abridged real example (from `src/assets/mock/bootstrap/bootstrap.json`):
```json
{
"schemaVersion": "1.0.0",
"generatedAt": "2026-07-03T00:00:00Z",
"tenant": {
"id": "tenant-default-001", "slug": "default", "code": "DEFAULT", "host": "default.local",
"name": "Marketplace", "websiteBaseUrl": "https://marketplace.local",
"builderBaseUrl": "https://builder.marketplace.local", "backofficeBaseUrl": "https://backoffice.marketplace.local",
"defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"],
"defaultCurrency": "RUB", "supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
"timezone": "Europe/Moscow", "documentationUrl": "https://docs.marketplace.local"
},
"branding": { "brandName": "Marketplace", "logoUrl": "/icons/icon-192x192.png", "faviconUrl": "/favicon.ico", "supportEmail": "support@marketplace.local" },
"theme": {
"themeId": "default-light", "mode": "light",
"palette": { "primary": "#497671", "secondary": "#a1b4b5", "success": "#10b981", "warning": "#f59e0b", "danger": "#ef4444", "textPrimary": "#1e3c38", "backgroundPrimary": "#ffffff", "border": "#d3dad9" },
"typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 },
"spacing": { "unit": 4, "scale": [0, 4, 8, 12, 16, 24, 32, 48] }
},
"featureFlags": { "wishlist": true, "compare": true, "reviews": true, "blog": false, "chat": false, "coupons": true },
"apiEndpoints": { "bootstrap": { "path": "/bootstrap", "method": "GET", "timeoutMs": 10000 }, "website": {}, "builder": {}, "backoffice": {} },
"localization": { "defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"], "currencyByLocale": { "ru": "RUB", "en": "USD", "hy": "AMD" } },
"catalog": { "layout": "grid", "defaultSort": "relevance", "availableSorts": ["relevance", "latest", "price_asc", "price_desc", "rating", "popular", "discount"] },
"navigation": { "header": [{ "id": "nav-home", "labelKey": "nav.home", "route": "/", "order": 1 }], "footer": [{ "id": "footer-about", "labelKey": "nav.about", "route": "/about-us", "order": 1 }] },
"widgetRegistry": { "manifestUrl": "/assets/mock/bootstrap/widget-manifest.json" },
"pages": [{
"id": "page-home", "key": "home", "title": "Home", "route": { "path": "/", "exact": true }, "visible": true,
"sections": [{ "id": "section-hero", "type": "hero", "order": 1, "layout": { "strategy": "hero", "columns": 1 }, "widgets": [{ "id": "widget-hero-main", "type": "hero", "version": "1.0.0", "order": 1, "props": { "title": { "ru": "Добро пожаловать", "en": "Welcome" } } }] }]
}]
}
```
**Which provider fires:** mock (`GET /assets/mock/bootstrap/bootstrap.json`) when `useMockData=true`, or when `useMockBootstrapOnLocal=true` and host is localhost; otherwise real `GET /bootstrap`.
**No write path exists.** Publishing a marketplace (builder "Publish") only promotes an in-memory/localStorage draft signal today — nothing reaches a backend. See §8 Builder.
**Requires backend decision:** `X-Language`/`Accept-Language` pre-selection on this call (today the client always gets and holds the full multi-locale document); whether `schemaVersion` is ever semantically enforced (today presence-only); ETag/conditional-request caching (none exists); the entire draft→publish write path.
---
## 4. Pagination, sorting, filtering, search — conventions
**Two pagination styles coexist — support both, they are not interchangeable:**
- **Offset/count** (marketplace storefront reads) — query params `count` (page size, default 50) and `skip` (offset, default 0). `searchItems` returns `{ items, total }`; other list reads (`getCategoryItems`, `getRandomItems`) return a bare array with no total.
- **Page/pageSize** (admin lists, storefront engagement lists, media) — request `{ page, pageSize, ...filters }`, response `{ items, total, page, pageSize }`. Client derives `totalPages = ceil(total / pageSize)` itself.
No cursor/keyset pagination exists anywhere. No server-side page-size cap is enforced by the client (it just sends 50 as a default) — **requires backend decision** on max page size.
**Sorting:** enumerated in bootstrap `catalog.availableSorts`: `relevance | latest | price_asc | price_desc | rating | popular | discount` (7 values). The live `sort` query param on `GET /searchitems` only accepts a 5-value subset: `relevance | price_asc | price_desc | popular | rating``latest`/`discount` have no confirmed search-endpoint mapping. **Requires backend decision** to reconcile these two vocabularies, and to define wire encoding for admin-list sorting (no convention exists yet — admin CRUD is mock-only).
**Filtering:** storefront search accepts `categoryIDs` (comma-joined ints), `minPrice`, `maxPrice`, `tag`. Admin list filter objects (in-memory today, not confirmed wire contracts) all follow `{ search: string, <field>: 'all' | <enum>, page, pageSize }``'all'` is the "no filter on this facet" sentinel. **Requires backend decision:** whether `'all'` is sent literally or the param omitted.
**Search:** `GET /searchitems?search=<q>&count=&skip=[&categoryIDs&minPrice&maxPrice&tag&sort]``{ items, total }`. No dedicated autocomplete/suggestion/trending backend endpoint exists — those are derived client-side from already-loaded catalog data today.
---
## 5. Error model
**The frontend does not currently parse any backend error envelope for any real endpoint** — no interceptor inspects error responses; every caller reacts at the raw `HttpErrorResponse.status`/`.message` level. The one partial exception (Ed25519 admin auth) derives its error code from **HTTP status only**, ignoring any body field, which is itself a known bug (see below). Everything in this section is therefore a **recommended envelope to adopt going forward**, not something already wired end-to-end — apply it to new endpoints and treat the frontend gaps below as follow-up work, not something this doc can silently paper over.
### The envelope
```json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"status": 422,
"requestId": "b3f1c2a0-4e21-4d3a-9e77-1e8f6a2d9c11",
"details": [{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." }]
}
}
```
| Field | Required | Notes |
|---|---|---|
| `error.code` | yes | Stable, `UPPER_SNAKE_CASE`, never localized — this is what code should branch on, never `message`. |
| `error.message` | yes | Human-readable English fallback only. |
| `error.status` | yes | Mirrors the HTTP status. |
| `error.requestId` | recommended | Correlation id for support/ops, echoed in logs. |
| `error.details` | only on 422 | `{ field, code, message }[]` — matches the client's existing local-validation issue shape, so a future adapter can merge backend 422s into the same inline-error UI without inventing a second mechanism. |
### Status-by-status
| Status | `code` | Frontend reaction today |
|---|---|---|
| 401 | `UNAUTHENTICATED` | Ed25519 flow → generic "Unauthorized, sign in" screen. Customer Telegram auth: no 401 branch anywhere — session validity is only ever discovered by polling. Admin CRUD facades: none have ever seen a real 401 (all mock). |
| 403 | `FORBIDDEN` | Ed25519 flow → "Forbidden, back to dashboard." No tenant-vs-role distinction exists — both render identical copy. |
| 404 | `NOT_FOUND` | No code path distinguishes 404 from any other failure — a deleted product and a 500 render the identical generic empty-state today. |
| 409 | `CONFLICT` | Nothing reacts to 409 anywhere. Only related mechanism: `AdminCategoriesGateway.isSlugTaken()`, a proactive pre-check, not a 409 handler. |
| 422 | `VALIDATION_FAILED` + `details[]` | No admin form parses a backend validation body today (all mock). Client's own `ProjectEditorFacade.fieldError(fieldKey)` inline-error pattern is the convention to align a future adapter to. |
| 429 | `RATE_LIMITED` (+`retryAfterSeconds`) | **Zero handling anywhere** — no interceptor, facade, or component references 429 at all. |
| 500 | `INTERNAL_ERROR` | Falls into whatever generic catch-all a given caller has (retry-button empty state, or — for `LocationService.getRegions()` — silently falls back to 6 hardcoded regions with no visible error at all). |
| 503 (infra down) | `SERVICE_UNAVAILABLE` | Same "backend unavailable, retry" screen as 500, on the Ed25519 flow only. |
| 503 (maintenance) | `MAINTENANCE_MODE` (+`maintenanceUntil`) | **No maintenance-mode concept exists in the frontend at all today.** Same HTTP status as infra-down 503 — `error.code` is the only way to distinguish them. |
| 403 (tenant disabled) | `TENANT_DISABLED` | **No handling exists.** No code path today distinguishes "tenant exists but is disabled" from any other 403. |
| 401 (token expired) | `TOKEN_EXPIRED` | **Known bug, not just a gap:** the client has a dedicated "Session expired" screen wired and ready, but `toAuthErrorShape()` only reaches it via a no-refresh-token-present client-side branch — a *real* backend 401 on `/refresh` always renders the generic "Unauthorized" screen instead, because the mapping function ignores any body code and derives purely from HTTP status. Fix requires the backend to send `error.code: "TOKEN_EXPIRED"` **and** a small frontend change to prefer it. |
| 401 (bad signature) | `INVALID_SIGNATURE` | Same bug class as above — dedicated screen exists, unreachable from a real HTTP response for the identical reason. |
**Every admin backoffice list page** (Users/Orders/Monitoring/Moderation/Transactions/Products/Categories/Analytics/Customers/Dashboard) shares one generic pattern: a boolean `error` signal → "Something went wrong" + retry button. None of them branch on status or `code` today — every status above collapses into the same generic UI until facades are individually updated.
---
## 6. Marketplace / storefront API (LIVE)
Base: `ApiConfigService.getBaseUrl()`. Headers on every call (`apiHeadersInterceptor`): `X-Region`, `X-Language` (`ru→RU, en→EN, hy→AM`), `Currency` (default `RUB`), `WebSessionID`. Source: `src/app/services/api.service.ts`.
| Endpoint | Method | Params / Body | Response |
|---|---|---|---|
| `/ping` | GET | — | `{ message }` |
| `/category` | GET | — | `Category[]` (normalized) |
| `/category/{id}` | GET | `count`, `skip` | `Item[]` |
| `/items/{id}` | GET | — | `Item` |
| `/items/randomitems` | GET | `count`, `category?` | `Item[]` (featured/random) |
| `/searchitems` | GET | `search`, `count`, `skip`, `categoryIDs?`, `minPrice?`, `maxPrice?`, `tag?`, `sort?` | `{ items: Item[], total: number }` |
| `/websession/{sessionId}` | POST | item array | cart echo |
| `/items/{id}/callback` | POST | `{ rating, comment, sessionID, timestamp }` | `{ message }` — review |
| `/items/{id}/questiion` | POST | `{ question, sessionID, timestamp }` | `{ message }`**literal typo `questiion`, preserve it, matches the client** |
| `/purchase-email` | POST | `{ email, phone?, telegramUserId, items[] }` | `{ message }` |
| `/regions` | GET | — | `Region[]` — client falls back **silently** to 6 hardcoded regions on any error |
### 6.1 Products — the tolerance contract
The wire DTO `Item` (`src/app/models/item.model.ts`) is reconciled by `ApiService.normalizeItem()` — the single largest inline adapter in the codebase. It tolerates **two historical shapes at once**:
- `id` (string) ↔ `itemID` (numeric)
- `imgs[]``photos[]`
- `names[]``translations`
- `description` as a key/value array ↔ a plain string
- `comments``callbacks` (reviews)
- color `0xRRGGBB` → normalized `#RRGGBB`
- `remaining` count → a stock band
**A real backend can send either historical shape — do not invent a third, cleaner shape.** `normalizeCategory()` does the same job for categories.
### 6.2 Categories — two parallel stacks exist
- **Clean stack (real, LIVE):** `GET /category``CategoryDto[]` (`{ categoryID, names: [{lang,name}], subcategories: [...] }`) → `CategoryMapper` flattens the tree, dedupes by id, normalizes `am→hy` → domain `Category`.
- **Legacy stack:** the same `/category` response also feeds `ApiService.normalizeCategory()` → a *different* `Category` type (`src/app/models/category.model.ts`). **Two unrelated `Category` types exist in the codebase with the same name** — a known duplication, not a bug to silently fix on the backend side; just be aware both consume the same wire shape.
```json
[{ "categoryID": 12, "names": [{ "lang": "ru", "name": "Электроника" }, { "lang": "en", "name": "Electronics" }], "subcategories": [{ "categoryID": 34, "names": [{ "lang": "en", "name": "Phones" }] }] }]
```
---
## 7. Cart / Orders / Payments (LIVE)
Cart **contents** are LOCAL-ONLY (localStorage `marketplace_cart`, + Telegram CloudStorage in-app) — there is no backend cart. Checkout produces real payment + order calls.
| Endpoint | Method | Base | Body | Response |
|---|---|---|---|---|
| `/cart` | POST | marketplace | `CartPaymentRequest` | `QrCreateResponse` |
| `/orders` | POST | marketplace | `CreateOrderRequest` | `CreateOrderResponse` — fire-and-forget after payment succeeds, doesn't touch the payment call chain |
| `/qr` | POST | `qrApiUrl` | `QrCreateRequest` (headers `authorization-key`, `userid-value`) | `QrCreateResponse` |
| `/qr/dynamic/{partnerId}/{qrId}` | GET | `qrApiUrl` | — | `QrDynamicStatusResponse` |
| `/card/{partnerId}/{orderId}` | GET | `qrApiUrl` | — | `QrDynamicStatusResponse` |
Const `partnerId` = `web-97ec-9c57-4dde-9037-3a68f7f83750`.
```ts
interface CartPaymentRequest {
amount: number; currency: 'RUB'; siteuserID: string; siteorderID: string; redirectUrl: string;
telegramUsername: string; paymentMethod: 'qr' | 'card'; qrDescription?: string; customerID?: string;
items: Array<{ itemID: number; price: number; name: string; quantity?: number }>;
}
interface CreateOrderRequest {
items: Array<{ productId: string; name: string; quantity: number; price: number }>;
customer: { name: string; email: string; phone: string };
payment?: { method: string; currency: string };
shipping?: { address: string; method: string; trackingNumber: string };
}
interface CreateOrderResponse { id: string; orderNumber: string; status: string; total: number; currency: string; }
```
`QrCreateResponse` is deliberately alias-tolerant — many casings accepted for id/url/partner fields (`qrId`/`qrID`, `nspkurl`/`nspkId`, `partnerID`/`partnerId`/`PartnerID`, etc). Pick one canonical casing on the backend; the client resolves whichever it gets.
```http
POST https://api.dexarmarket.ru:445/cart
WebSessionID: 3f1c2a0e-
{ "amount": 4990, "currency": "RUB", "siteuserID": "8823771", "siteorderID": "order-2026-0007", "redirectUrl": "https://marketplace.local/checkout/done", "telegramUsername": "buyer_ivan", "paymentMethod": "qr", "items": [{ "itemID": 101, "price": 4990, "name": "Wireless Keyboard", "quantity": 1 }] }
```
```json
{ "qrId": "QR-77f0", "nspkurl": "https://qr.nspk.ru/AD10…", "status": "created", "qrExpirationDate": "2026-07-26T04:10:00Z" }
```
**Payments are frozen** — this call chain is explicitly out of scope for changes; document only, don't modify.
---
## 8. Admin (backoffice) domains
**Structural finding, the single most important fact in this section:** of 11 admin gateway domains, only **Categories** and **Dashboard-metrics** are bound through a DI token — a real backend can be dropped in for those two with zero facade changes. **Every other admin domain's facade injects its mock `*LocalGateway` class directly**, so a token has to be added before a real backend can be wired in at all, regardless of whether the endpoint itself is easy to build. Only one real admin HTTP implementation exists anywhere: `AdminCategoriesApiGateway`.
| Domain | Interface | Real impl? | DI token? | Facade | Seam status |
|---|---|---|---|---|---|
| Categories | `AdminCategoriesGateway` | **yes** (`admin-categories-api.gateway.ts`) | yes (`ADMIN_CATEGORIES_GATEWAY`) | `AdminCategoriesFacade` | MOCK-SWAPPABLE, done |
| Dashboard metrics | `AdminDashboardMetricsGateway` | no | yes (`ADMIN_DASHBOARD_METRICS_GATEWAY`) | `AdminDashboardFacade` | MOCK-SWAPPABLE, token only |
| Orders | `AdminOrdersGateway` | no | **none** | `AdminOrdersFacade` | MOCK-ONLY, no seam |
| Products | `AdminProductsGateway` | no | **none** | `AdminProductsFacade` | MOCK-ONLY, no seam |
| Users | `AdminUsersGateway` | no | **none** | `AdminUsersFacade` | MOCK-ONLY, no seam |
| Transactions | `AdminTransactionsGateway` | no | **none** | `AdminTransactionsFacade` | MOCK-ONLY, no seam |
| Monitoring | `AdminMonitoringGateway` | no | **none** | `AdminMonitoringFacade` | MOCK-ONLY, no seam |
| Moderation | `AdminModerationGateway` | no | **none** | `AdminModerationFacade` | MOCK-ONLY, no seam |
| Customers | *(none — derived)* | no | n/a | `AdminCustomersFacade` | derives from Orders' mock gateway |
| Analytics | *(none — derived)* | no | partial | `AdminAnalyticsFacade` | composes 5 other gateways, no data source |
| Media | abstract class `MediaRepository` | no | yes (class token) | `MediaLibraryFacade` | MOCK-SWAPPABLE |
### Gateway interface method contracts (what a real backend must satisfy)
- **Categories** — `loadCategories(filters)`, `loadCategory(id)`, `createCategory`, `updateCategory`, `deleteCategory`, `restoreCategory`, `isSlugTaken(slug, excludingId)`.
- **Dashboard metrics** — `loadMetrics(): AdminDashboardMetrics` (no params — a seller/scope filter would need a new parameter, no object to extend).
- **Orders** — `loadOrders(filters)`, `loadOrder(id)`, `updateStatus(id, status)`, `requestRefund(id)`, `addNote(id, note, internal)`, `archiveOrder`, `restoreOrder`, `deleteOrder`.
- **Products** — `loadProducts(filters)`, `loadProduct(id)`, `loadCategories()`, `createProduct`, `updateProduct`, `deleteProduct`, `duplicateProduct`, `archiveProduct`, `restoreProduct`.
- **Users** — `loadUsers`, `loadRoles`, `loadInvitations`, `loadSessions(userId)`, `loadAudit(userId)`, `setUserRole`, `setUserStatus`, `inviteUser(email, roleId, scope)`, `revokeInvitation`, `revokeSession`.
- **Transactions** — `loadTransactions(filters)`, `retryFailed(id)`, `setFraudFlag(id, flagged)`.
- **Monitoring** — `loadEvents(filters)`, `loadQueues()`, `loadWebhooks()`.
- **Moderation** — `loadReviews(filters)`, `loadReview(id)`, `setReviewStatus`, `setReviewVisible`, `setReviewPinned`, `setReviewFeatured`, `addModeratorNote`, `deleteReview`, `loadReports()`, `setReportStatus(id, status)`.
- **Media** — `list(params?)`, `upload(file, options?)`, `remove(id)`, `update(id, patch)`, `listFolders()`.
### The one real admin endpoint — Categories, exact paths
Base: `${apiConfig.getBaseUrl()}/backoffice/categories`. Mode-switched between this and the local mock via `getCategoryProviderMode()` — mock in local dev (no reachable backoffice API there), real API in production.
| Method | Path | Body | Response |
|---|---|---|---|
| GET | `/backoffice/categories?search=&visibility=&includeDeleted=` | — | `AdminCategory[]` |
| GET | `/backoffice/categories/{id}` | — | `AdminCategory \| null` (404 → null) |
| POST | `/backoffice/categories` | `AdminCategory` minus `{id, itemsCount, deletedAt, createdAt, updatedAt}` | `AdminCategory` |
| PUT | `/backoffice/categories/{id}` | full `AdminCategory` | `AdminCategory` |
| DELETE | `/backoffice/categories/{id}` | — | `void`**soft delete only, no hard delete exists** |
| POST | `/backoffice/categories/{id}/restore` | `{}` | `AdminCategory \| null` |
| GET | `/backoffice/categories/slug-taken?slug=&excludingId=` | — | `{ taken: boolean }` |
Use this exact path shape as the template for every other admin domain in §8.5's build order — it's the only one proven end-to-end.
### Worked example — Admin Orders (no mapper exists yet, backend has freedom here)
Unlike Categories/Products (which have a wire DTO to match), admin domains other than Categories have **no wire DTO and no mapper today** — the mock gateways build view models directly in memory. This means the JSON shape below is a *proposal* the new `AdminOrdersApiGateway` would map into the existing `AdminOrder` view model, not a shape already fixed by an adapter:
```json
{
"id": "ord_1042", "status": "processing", "paymentStatus": "paid",
"customer": { "id": "cus_88", "name": "…", "email": "…" },
"items": [{ "productId": "…", "title": "…", "qty": 2, "unitPrice": 1990 }],
"shipping": { "method": "…", "address": "…" },
"timeline": [{ "event": "created", "at": "2026-07-01T10:00:00Z" }]
}
```
The same "no mapper exists, write one inside the new `*ApiGateway`" note applies to Products, Users, Transactions, Monitoring, Moderation.
### Widget manifest (LIVE — static/remote JSON, separate from admin CRUD)
`GET <bootstrap.widgetRegistry.manifestUrl>` (default `/assets/mock/bootstrap/widget-manifest.json`), falls back to `{ widgets: [] }` on any error, never throws to the UI.
```json
{ "widgets": [{ "type": "hero", "version": "1.0.0", "componentKey": "HeroWidgetComponent", "supportedLayouts": ["hero"], "supportedDataSources": ["manual"], "settingsSchema": { "type": "object", "properties": { "title": { "type": "string" } } }, "defaultSettings": { "title": "Welcome" }, "enabled": true }] }
```
### Backoffice storefront cards (LIVE — distinct from admin CRUD above)
`GET /api/backoffice/products`, `GET /api/backoffice/categories` — feeds storefront product/category card widgets, not the admin panel.
---
## 9. Everything that is LOCAL-ONLY (no backend call exists at all)
Worth knowing explicitly, so nobody assumes a gateway swap will "just work" for these:
- **Content management / static pages (CMS)** — reads/writes `BootstrapConfig.staticPages` in-memory. No dedicated backend call. Publishing = writing bootstrap back, for which no client write call exists.
- **Project editor / builder** — edits an in-memory `BootstrapConfig`, persists drafts to `localStorage` only. "Publish" today only promotes the local draft signal. A builder API is declared only as an empty `apiEndpoints.builder: {}` placeholder in bootstrap.
- **Search** — `SearchFacade` is a client-side orchestration over the product/category providers (history, trending, autocomplete, cache all local). The only real backend traffic underneath it is `GET /searchitems`.
- **User experience (wishlist/compare/recently-viewed/saved-searches)** — fully denormalized objects in `localStorage`, guest-first. A DI token exists for a future authenticated repository, but nothing is bound to it — comment in code notes it "can be switched to authenticated repository later."
- **Diagnostics** — inspects runtime/bootstrap/widget state locally; the one live-ish probe is a `/ping` health check.
- **Cart contents** — see §7, real payment/order calls exist, cart *state* never round-trips to a backend.
---
## 10. Backend build order (dependency-driven, not document order)
1. **Auth + session** — blocks everything admin-gated.
2. **Bootstrap content** (branding/theme/nav/seo) — transport (`GET /bootstrap`) already works; the *content* is still default stubs. Tenant resolution depends on it.
3. **Categories** — already LIVE both storefront and admin; products reference categories.
4. **Products / catalog** — storefront reads are LIVE; admin Products CRUD is the first no-seam admin domain to build.
5. **Media** — products/categories editors reference media assets.
6. **Cart / Orders / Transactions** — checkout is LIVE; admin Orders CRUD, then Transactions (derives from Orders).
7. **Reviews / Moderation** — customer writes are LIVE; admin Moderation gates them.
8. **Users / roles / invitations** — independent of commerce, needs auth.
9. **Dashboard metrics, then Monitoring** — operational visibility layers.
10. **Analytics — last.** Needs orders/products/moderation real *and* a tracking pipeline that doesn't exist yet anywhere (not just a missing endpoint — no data source at all).
11. **Builder draft/publish + CMS** — net-new write paths, can proceed in parallel once bootstrap content (step 2) is real.
12. **User-experience sync, search suggestions** — enhancements over already-working local features.
Per-domain migration pattern for the six no-seam admin domains (Orders, Products, Users, Transactions, Monitoring, Moderation): add a DI token → switch the facade to inject the token instead of the concrete mock class → implement the `*ApiGateway` (contains the DTO→view-model mapper) → bind the token → retire or keep the mock behind the existing `useMockData` flag. This is the exact pattern already proven by Categories — replicate it, don't redesign it per domain.
---
## 11. Known discrepancies to reconcile before/while building
- **`AdminRole` defined twice** with unrelated shapes (§2b) — auth string-union vs. Users-page display interface.
- **`Category` defined twice** (§6.2) — legacy vs. clean-stack, both fed by the same `/category` response.
- **Duplicate search models** under `features/search/models/` and `core/search/models/`.
- **`submitQuestion` endpoint path has a literal typo** (`questiion`, not `question`) — this matches the real backend spec, do not "fix" it.
- **The Ed25519 error-code bug** (§5) — `TOKEN_EXPIRED`/`INVALID_SIGNATURE` screens are fully built and unreachable from real HTTP responses today because the client only reads HTTP status, never a body code. Needs a coordinated backend + frontend fix, not backend alone.
- **`ADMIN_DASHBOARD_METRICS_GATEWAY` and `USER_EXPERIENCE_REPOSITORY` token factories return the mock/local class in every mode** — a real implementation must be written *and* explicitly bound; the seam existing does not mean a real backend is one line away.
For open product/business decisions this document deliberately does not resolve (rate limiting posture, refresh-token reuse detection, tenant-scoped auth, API versioning scheme, etc.), see [GAPS-AND-IMPROVEMENTS.md](GAPS-AND-IMPROVEMENTS.md).
---
## 12. Frontend-blocked TODOs — needs backend
Raised during the Phase 0 security hardening pass (see the sprint plan). Each of these has a client-side mitigation already in place where one exists, but none of them close the actual gap without a backend change.
### 12.1 Admin role claim on the session
**Gap:** `adminAuthGuard` (Mechanism A, Telegram/QR) only checks "is there an active session" — the session API has no concept of admin role at all, so the frontend cannot enforce permissions server-authoritatively. Client mitigation: `AdminPermissionsService` derives a cosmetic permission set by matching the Telegram username against the mock Users domain locally — this is UI-only and trivially bypassed by calling the API directly.
**Ask:** either (a) add a `role` field to the existing `GET /users/sessions/{id}` response when the session belongs to a registered admin, or (b) finish Mechanism B (Ed25519 challenge/response, already wired client-side, `/challenge` and `/verify` currently 404) so the JWT `role` claim becomes real. Whichever is chosen, every admin-mutating endpoint must independently authorize the request — a role claim on the session is necessary but not sufficient.
Proposed minimal shape for option (a), added to the existing poll response (§2a):
```json
{
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
"status": "active",
"user": { "id": 8823771, "username": "buyer_ivan", "firstName": "Ivan", "lastName": "P" },
"expiresAt": "2026-07-26T05:00:00Z",
"adminRole": "admin"
}
```
`adminRole` absent/null → treat as non-admin regardless of what `/backoffice/**` UI is reachable client-side.
### 12.2 HttpOnly session cookie
**Gap:** the customer session cookie (`webSessionID`, `services/auth.service.ts`) is set via `document.cookie` from the frontend, which means it cannot be `HttpOnly` — only a `Set-Cookie` response header from the backend can set that flag, and JS-set cookies are readable by any injected script. Client mitigation: CSP hardened on all three nginx tenant blocks (was missing entirely on two of three) as defense-in-depth, but this does not close the gap.
**Ask:** `POST /users/sessions` and `GET /users/sessions/{id}` issue the session id via `Set-Cookie: webSessionID=…; HttpOnly; Secure; SameSite=Lax; Max-Age=…` instead of (or in addition to, during migration) returning it in the JSON body. Once that ships, the frontend stops writing `document.cookie` itself and relies on the browser sending the cookie automatically; `credentials: 'include'` needs enabling on the relevant HTTP calls.
### 12.3 Server-side order pricing
**Gap:** `POST` order creation (§7) let the client send a computed, discount-applied `price` per line item with no server-side revalidation. Client fix already shipped: `CreateOrderRequest.items` no longer sends `price` — only `{ productId, name, quantity }`.
**Ask:** the order-creation endpoint must price every line item itself by looking up `productId` in its own catalog (applying whatever discount/promo logic is authoritative server-side), and reject/[400] if the resulting total doesn't reconcile with what the client displayed (or just recompute and use the server total as-of-record, ignoring any client total entirely). Example of the request shape now sent:
```json
{
"items": [{ "productId": "prod_1042", "name": "Sample Product", "quantity": 2 }],
"customer": { "name": "Ivan P", "email": "ivan@example.com", "phone": "79991234567" },
"payment": { "method": "card", "currency": "RUB" }
}
```
Separately, `createCartPayment()` (payment-gateway charge creation) still sends a client-computed `amount` — that field can't simply be dropped, since it's what tells the payment provider how much to charge. That endpoint must independently revalidate `amount` against its own pricing before creating the charge, and reject on mismatch.
### 12.4 Real order audit trail
**Gap:** `AdminOrder` had no actor/audit field at all. Client fix already shipped: `AdminOrderTimelineEntry.actor` now exists and is populated from the signed-in admin's display name in the local mock gateway — but that's client-only bookkeeping with no server-side record.
**Ask:** when admin Orders CRUD gets a real backend (§10, step 6), every mutating endpoint (`updateStatus`, `requestRefund`, `addNote`, etc.) should record who performed the action server-side (from the authenticated session/JWT, not a client-supplied field) and return it in the order/timeline response:
```json
{
"timeline": [
{ "status": "processing", "timestamp": "2026-08-13T10:15:00Z", "eventKey": "statusChanged", "actor": "anna@dexar.market" }
]
}
```
`actor` must be derived server-side from the authenticated caller, never trusted from the request body.
### 12.5 Back-in-stock ("Notify Me") subscription
**Gap:** the "Notify Me" button on out-of-stock products had no real subscription mechanism at all - it just toggled wishlist. Client fix already shipped: `notifyMe()` now calls `POST /items/{id}/notify-me` and, if that fails (today it always will - the endpoint doesn't exist), falls back to a local-only record in `localStorage['restockSubscriptions']` so the request isn't silently dropped while waiting on the backend. The shopper sees the same confirmation either way.
**Ask:** implement `POST /items/{id}/notify-me`, plus whatever mechanism actually sends the notification once the item restocks (Telegram message, most likely, given the rest of the auth stack). Request body sent today:
```json
{ "telegramUserId": "8823771" }
```
`telegramUserId` may be `null` for a non-Telegram web session - decide whether to also accept an email address as an alternative identifier (the frontend has no email capture on this flow today, so that would need a small frontend addition too). Once this ships, the frontend's localStorage fallback becomes purely a resilience path rather than the common case, and could optionally sync any locally-queued subscriptions on next successful call.
### 12.6 Trending search terms
**Gap:** `SearchTrendingService.loadTrending()` is a stub returning `of(null)` - no trending-searches endpoint exists. It already degrades gracefully (UI hides the trending section rather than showing an error), so this is purely a missing-feature gap, not a bug.
**Ask:** an endpoint returning the top N search queries over some recent window, e.g.:
```json
{ "trending": [{ "query": "wireless earbuds", "count": 214 }, { "query": "winter jacket", "count": 187 }] }
```
Once it exists, wire `loadTrending()` to it and map `query` -> `SearchSuggestion.title/text`.

View File

@@ -1,223 +0,0 @@
# Gaps & Improvements
Findings only — nothing in this document has been fixed as part of writing it. Sourced from re-verifying prior audits against current source, plus a fresh automated review pass across the storefront (website) and backoffice (admin). Organized by the lens each finding matters most to; several findings matter to more than one role and are cross-referenced rather than duplicated.
---
## As a Customer / End User
1. **Ed25519 admin-auth "session expired" and "invalid signature" recovery screens are dead UI.** Both are fully built and wired, but `toAuthErrorShape()` (`core/auth/services/auth.service.ts:110-118`) derives the error code from HTTP status only, never a body-level code — so a real backend 401 always shows the generic "Unauthorized" screen instead. Also an [Engineering](#as-backend--api-engineer) and [Backend](#as-backend--api-engineer) item.
2. **Dark mode selector does nothing.** The light/dark/system dropdown saves correctly, but no CSS anywhere reads the `data-theme-mode` attribute it sets — picking anything but Light changes nothing visually.
3. **"Site Layout" selector (Theme section) has no effect.** `layout.type` is edited but page rendering only ever reads each page's own `layout`, never the top-level selector.
4. **Footer "Contacts" link has nothing behind it.** No static-page content exists for it at all in the bootstrap data (unlike other footer legal pages, which are populated).
5. **Product pages get no per-product SEO.** `SeoService.setItemMeta(item)` — the method that would set per-product Open Graph/canonical tags — exists but is **never called anywhere in the codebase**. Every product page ships only the site-wide default meta tags.
6. **`og:locale` is hardcoded to `'ru_RU'`** in both SEO meta-tag code paths, regardless of the active locale — a real gap for EN/HY visitors' social-share previews.
7. **No structured data (JSON-LD) and no sitemap generation exist anywhere** — confirmed absent, not partially built. Sitemap is backend-only work; JSON-LD would need net-new frontend code.
8. **Checkout's payment-description fallback is a hardcoded Russian string** (`'Покупка на Маркетплейсе'`) used as a last resort when no brand name or hostname is available — single-tenant-framed wording in a multi-tenant product.
9. **Brand color contrast fails WCAG AA.** `--border-color` measures 1.241.42:1 against a 3:1 UI-component requirement in every theme; `--success`/`--warning`/`--error`/`--info-color` fail 4.5:1 when used as plain text. Real palette colors, not a token bug — see [Accessibility](#as-accessibility-reviewer).
10. **`stars.component.scss:10` uses a literal hex color** (`#cdd6d5`) with no design token behind it — any future palette change will silently miss this one glyph.
11. **No multi-vendor cart handling exists.** Checkout is one inline flow producing exactly one order from one payment popup; a cart with items from multiple sellers has no defined behavior (relevant the moment Seller Management ships beyond its current disabled-by-default placeholder).
---
## As Product Owner / Business
1. **Backend completion is ~10%.** Only Categories has a real HTTP implementation on the admin side; every other domain (Orders, Products, Users, Transactions, Monitoring, Moderation, Analytics, Customers) runs entirely on mock data today. The frontend is feature-complete against that mock data; production readiness is blocked entirely on backend work, not frontend polish.
2. **The admin role model is decorative.** `AdminRole`/permissions exist in code, but **nothing gates any button, page, or action on them anywhere in the app.** Anyone who passes admin authentication has full access regardless of their assigned role. This is a real authorization gap, not a display nicety, and should be scoped before any real admin backend goes live with multiple operators.
3. **Payment options are limited to QR and card via one custom flow** — no additional providers (wallets, buy-now-pay-later) are wired or planned; needs a business decision on which providers, if any, before integration work starts.
4. **Advanced analytics (traffic, funnels, heatmaps) has no data source at all** — not a missing endpoint, a missing tracking pipeline. Flagged as the single largest ("XL") remaining backend effort, deliberately last in the build order because it depends on every other commerce domain being real first.
5. **Seller Management has eight cross-linked documents for a capability that is disabled by default and has zero backend bytes.** Real risk if it proceeds: at least three of those documents independently restate the same undecided "Unified vs. Split Orders" question — a decision change means updating multiple documents in sync, not one. Worth a consolidation pass before backend implementation starts.
6. **Two competing "seller" type shapes exist with no conversion between them** (`SellerConfig` in bootstrap models vs. `Seller`/`SellerBranding` in the domain layer) — self-flagged during Seller Management design work, restated here as unresolved. Recommend resolving (pick one, or document a mapping) before real backend work on that capability begins.
7. **No reusable feature-flag/capability-guard service exists**, despite one being promised by an existing ADR. The one current consumer of `sellerManagement.enabled` hand-rolls the check inline; every future flag will either duplicate that pattern or need the promised service built retroactively under time pressure.
8. **The Seller Management "enabled" code path has never been manually exercised**, even once — every verification claim about it was tested with the flag at its real-world value, `false`. Low risk today (nothing renders differently yet), but worth a fixture-based test the first time any enabled-state UI is actually built.
9. **Two large lazy-loaded bundle chunks remain unaddressed**: `project-editor` (~1.0 MB) and `catalog-container` (~330375 kB). No mechanical split has been found; needs a dedicated profiling pass, ideally under real backend latency rather than instant mock responses.
---
## As QA / Test Engineering
1. **Automated test coverage is thin relative to the stated 80% target.** 11 spec files exist repo-wide (up from 5 before this cycle's test-foundation sprint); measured baseline is ~32% statements, ~19% branches, ~22% functions, ~33% lines. This is an honest foundation, not a coverage floor — no CI gate is set on it yet, deliberately, until a real floor number can be justified.
2. **Zero E2E tests exist anywhere in the repo.** No Playwright/Cypress/equivalent config found. Critical flows (storefront checkout, admin CRUD, builder draft→publish→live) have no automated regression coverage beyond the unit/facade specs added this cycle.
3. **Several "verified live" claims in prior audits were actually code-inspection only**, not real authenticated click-throughs — consistently because `/edit`, `/edit/:section`, and `/backoffice` require Telegram admin login, which cannot be completed in the automated environment those passes ran in. Worth flagging to a human tester before trusting those UI claims as fully proven: the manifest-aware layout picker (Sprint E), and multiple backoffice-auth-gated wording checks (Monitoring, Reports) among them.
4. **No real screen-reader software pass (NVDA/VoiceOver) has ever been performed anywhere in the app** — every accessibility claim in every audit to date is based on automated accessibility-tree inspection only (`role`, `aria-*` attribute presence), never an actual screen-reader session. This is a repo-wide gap, not specific to one page.
5. **A reactive-signal staleness bug was found and fixed in Seller Management's enabled-flag read** (it read the bootstrap snapshot once at construction instead of reactively) — worth a general regression-test pattern for any future flag/config read that should track `bootstrapRevision()`, since this bug class is easy to reintroduce and was invisible until specifically looked for.
6. **No facade-level tests exist yet for cart/checkout, moderation, or most admin domains** (Orders, Products, Users, Transactions, Monitoring were explicitly scoped out of this cycle's test-foundation sprint to stay within its time budget) — these are exactly the domains about to get real backends, so they carry the most regression risk with the least current coverage.
---
## As Backend / API Engineer
See [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md) for the full contract. Structural gaps worth flagging here specifically:
1. **Only 2 of 11 admin gateway domains (Categories, Dashboard-metrics) have a DI-token seam.** The other 9 — Orders, Products, Users, Transactions, Monitoring, Moderation, plus derived Customers/Analytics — inject their mock gateway class directly. A token has to be added to each before any real backend can be bound, independent of how easy that domain's actual endpoint is to build.
2. **`AdminRole` is defined twice with unrelated shapes** (auth string-union vs. a Users-page display interface) — needs a naming reconciliation before the real role table is built.
3. **Two unrelated `Category` types exist**, both fed by the same `/category` response, both still in active use.
4. **Duplicate search models** exist under two different module paths.
5. **The error envelope is entirely a proposal** — no interceptor in the app inspects error response bodies today; every error reaction happens at the raw HTTP-status level. Adopting an envelope is a net-new build for both sides, not a preservation of existing behavior.
6. **429 (rate limiting) has zero client-side handling anywhere** — no interceptor, facade, or component references it. If the backend rate-limits, today's frontend has no graceful path for that response.
7. **No API versioning scheme has been decided** — no version segment, no version header, anywhere in the client.
8. **Centralized error-handling scaffolding exists but was never built.** `src/app/core/error-handling/`, `src/app/core/guards/`, and `src/app/core/interceptors/` each contain only a `.gitkeep` file — someone planned a shared error-handling layer, and every caller still handles failures ad hoc at the call site instead. Worth building once real backends start returning the error envelope in [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md), rather than adding another one-off handler per facade.
9. **Admin Reports and Seller Management pages have zero data wiring of any kind** — not even a mock gateway call. Reports reuses `AdminAnalyticsFacade` (itself mock-derived) for its numbers; Seller Management is a static placeholder page with no `HttpClient` reference anywhere. Neither is currently a "swap the gateway" job — Reports inherits whatever Analytics becomes, Seller Management has no data layer to swap yet.
10. **Two per-domain provider tokens have a dead mock branch, silently.** `PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY` always resolve to the real API implementation regardless of `useMockData` — there is no mock class bound to either token. Anyone toggling mock mode expecting storefront products/categories to mock out will be surprised; only Bootstrap, Backoffice-widget-data, and Admin-Categories actually respect the mock/api switch.
---
## As Accessibility Reviewer
1. **Brand color contrast genuinely fails WCAG AA** — see [Product Owner item 9 above](#as-product-owner--business) for the numbers. This requires a theme-owner sign-off before any fix ships, since it changes brand appearance, not just token values.
2. **No screen-reader software testing has ever been performed on this codebase** — every existing accessibility verification (including in this review) is automated accessibility-tree inspection, never a real NVDA/VoiceOver session. Recommend at least one manual pass on the highest-traffic flows (checkout, product page, admin login) before treating any part of the app as accessibility-verified end to end.
3. **Known past pattern worth re-checking elsewhere:** a raw `<textarea>` (no dedicated shared textarea component exists in the codebase) previously shipped without its `aria-label`/label association wired correctly in one place (Seller Management's Message field, since fixed). Any other raw `<textarea>` usage in the app should be checked for the same gap, since the shared `app-input` component handles this automatically but plain textareas do not.
---
## As Engineering / Tech Debt
1. **`navigation.header`** (header top-nav list) is editable in the builder but has zero runtime consumer — the header's actual menu comes from a different source entirely. Needs a product decision on positioning/behavior before it's real feature work, not a wiring fix.
2. **`catalog.navigationMode`** renders an intentional placeholder — confirmed not a bug, but the alternate nav UIs it implies (mega-menu, top-carousel, left-nav) don't exist yet if ever wanted.
3. **Angular 22 upgrade is researched but not started** (~23.5 days estimated, needs a dependency fix and Node version bump first). Explicitly recommended as its own dedicated session, never bundled with feature work.
4. **`MarketplaceRef` and `TenantConfig` both represent "a marketplace" from two different vantage points** — a deliberate, documented distinction today, but worth consolidating if a third marketplace-shaped type is ever proposed.
5. **`sellerId` fields are typed as bare `string` instead of the `UUID` alias** used everywhere else in the newer sellers domain — zero functional impact, pure convention drift, cheap to fix opportunistically.
6. **No shared breadcrumb component exists anywhere** — the only breadcrumb logic in the entire storefront is one local signal inside the catalog container, duplicated conceptually wherever a future breadcrumb might be needed.
7. **Bootstrap `apiEndpoints.{website,builder,backoffice}` are empty objects in the mock today** — meaning no builder or backoffice CRUD path exists as a literal anywhere in the client. Any concrete path documented for those domains is a proposal until this is populated.
---
## Cross-cutting / process
- **Documentation was spread across 60+ overlapping markdown files** at the time of this review (status trackers, sprint plans, multiple overlapping backend specs, several audit reports referencing each other) — consolidated as part of this same pass into this file, [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md), and whatever the team chooses to keep going forward. Recommend a lighter-weight doc set than before: one living gaps list (this file), one backend contract, source-of-truth code — not a new pile of point-in-time sprint reports.
- **A running list of "Requires backend decision" items with recommended defaults already exists** inside `BACKEND-API-REFERENCE.md` and the source material behind it — treat those as the first thing to walk through with the client/backend team, since most already have a suggested default and don't need a meeting, only a sign-off.
---
## Automated code review — website (storefront)
*Findings from a fresh source-level pass over `src/app/pages`, `src/app/features/website`, `src/app/features/search`, `src/app/widgets`, and the project-editor/builder. Each item includes a file:line reference and the role it matters most to.*
### Cart / Checkout
- `[Product Owner]` No dedicated checkout feature exists — `src/app/features/website/checkout/` and `src/app/features/website/cart/` are empty `.gitkeep` placeholders; the entire cart/payment flow lives in the legacy `src/app/pages/cart/`.
- `[Engineering]` The post-payment email/phone capture flow (`submitEmail`, `onEmailInput`, `onPhoneInput`, `validateEmail`, `validatePhone`) is dead code — the template has no matching `<input>` anywhere, so these methods are never invoked (`src/app/pages/cart/cart.component.ts:507-729`).
- `[Product Owner]` Because that form is unreachable, `recordOrder()` always submits the backoffice order with an empty `email`/`phone` for every purchase (`src/app/pages/cart/cart.component.ts:418-441`).
- `[Engineering]` `autoSubmitPurchase()` schedules navigation to home via `setTimeout(…, 0)` unconditionally at the top of the method, before checking for a Telegram user ID or waiting on the `submitPurchaseEmail` call — navigation fires regardless of submission success or failure (`cart.component.ts:443-454`).
- `[User]` When no Telegram user ID is available, `autoSubmitPurchase()` only logs to console and returns silently — no toast/notification, even as the popup is already closing (`cart.component.ts:450-454`).
- `[Engineering / Security]` Payment `currency` is hardcoded to `'RUB'` in both `createPayment()` and `recordOrder()`, ignoring `LanguageService.currentCurrency()` — the total shown to the user can diverge from the currency actually sent to the payment gateway (`cart.component.ts:257,436`).
- `[Security]` `amount` and per-item prices sent to `POST /cart` are computed entirely client-side from cart data in localStorage/Telegram CloudStorage, with no server round-trip to re-verify live prices before payment creation — a tampered local cart could request payment at an incorrect amount unless the backend independently revalidates (`cart.component.ts:253-266`, `services/cart.service.ts:133-190`).
- `[QA]` `copyPaymentLink()` failure path only does `console.error` — no visible feedback that "copy link" failed (`cart.component.ts:495-505`).
- `[Engineering]` Hardcoded Russian fallback string `'Покупка на Маркетплейсе'` used as the QR payment description when no brand name/hostname resolves — bypasses i18n entirely (`cart.component.ts:592-604`). See also [Customer item 8](#as-a-customer--end-user).
- `[QA]` Cart quantity increases (`increaseQuantity`, `CartService.updateQuantity`/`addItem`) never validate against the item's available stock — a user can raise a cart line past what's in stock with no cap or warning (`cart.component.ts:119-121`, `cart.service.ts:206-256`).
- `[Accessibility]` Swipe-to-reveal-delete on mobile cart rows is touch-only; a fallback always-visible delete button exists, but nothing makes the swipe *state itself* keyboard-reachable (`cart.component.ts:140-163`).
- `[Product Owner]` Terms-of-service links (public offer, return policy, guarantee, privacy policy) are plain placeholder text on the cart page, explicitly flagged in-code as needing real backend-configured links (`cart.component.html:147-152`).
### Auth
- `[Security]` The customer web session id is stored in a plain, non-`HttpOnly` cookie set via `document.cookie` — readable by any script, exfiltratable via XSS (`services/auth.service.ts:173-180`).
### Search
- `[i18n]` `SearchFacade.popularSearches` is a hardcoded English list ("Smartphones", "Sneakers", …) never routed through translation — renders in English regardless of active locale (`features/search/facade/search.facade.ts:58-91`).
- `[Product Owner]` `SearchTrendingService.loadTrending()` is a stub that always returns `null` ("Endpoint not available yet") — trending searches are non-functional end to end, and it overwrites the (already-hardcoded) fallback popular list with an empty one every time (`features/search/services/search-trending.service.ts:7-10`).
### Catalog
- `[Performance]` Catalog fetches at most 200 products per category in one call, then filters/sorts/paginates entirely client-side over that fixed batch — categories with more than 200 products silently truncate with no indication, and every filter/sort change re-processes the whole in-memory array instead of re-querying (`features/website/catalog/containers/catalog-container.component.ts:160,675-699`).
- `[Engineering / tech-debt]` `restoreContinueBrowsing()` is fully implemented (restores search/sort/layout/scroll) but never called anywhere — "continue browsing" state is saved on every interaction but never actually restored (`catalog-container.component.ts:901-929`).
- `[Engineering]` Non-numeric category route tokens are resolved by fetching the entire category tree and slugifying titles client-side — fragile against duplicate or renamed category titles, and loads the full tree just to resolve one slug (`catalog-container.component.ts:943-959`).
- `[Performance]` Price-range filter inputs trigger a full catalog recompute + URL sync on every keystroke, with no debounce (unlike the search box) (`features/website/catalog/components/filters-panel/filters-panel.component.ts:80-94`).
- `[QA]` Range filter min/max inputs have no validation preventing `min > max` — an inverted range silently yields zero results with no explicit error state (`filters-panel.component.ts:80-94`, `.html:56-76`).
### Product details
- `[User]` The "Notify Me" button (shown when out of stock) is wired to `toggleWishlist()` — it does not create any real back-in-stock subscription, just relabels the wishlist button (`features/website/product/containers/product-details-container.component.ts:356-358`, `product-actions.component.html:22-24`).
- `[Engineering]` "Buy Now" calls `addToCart()` (which resolves item data via an async, subscribed call inside `CartService.addItem`) and immediately navigates to `/cart` without awaiting completion — the cart page can render before the item has actually been added (`product-details-container.component.ts:306-320`, `cart.service.ts:206-243`).
### Product cards / search results
- `[Engineering / tech-debt]` The "Quick View" button on every search-results product card emits `quickViewPlaceholder`, but no parent component anywhere binds that output — clicking it is a dead end (`components/product-card/product-card.component.html:23-25`, `.ts:83-87`, `catalog/components/search-results/search-results.component.html:18-33`).
### Wishlist / Compare
- `[i18n]` The Compare table renders `product.name`/description fields/color/size straight off the raw product object instead of through `getTranslatedField()` — names and specs on the Compare page always show the source language regardless of active locale (`features/website/user-experience/compare/components/compare-table.component.ts:42-58`).
- `[Product Owner]` Wishlist, compare, recently-viewed, and saved searches are all localStorage-only via `USER_EXPERIENCE_REPOSITORY`, with no server sync to the authenticated Telegram session — data is lost on device change or storage clear despite the app having real login (`facades/platform/user-experience.facade.ts:1-137`).
### Widgets / Page builder
- `[Engineering]` `DataSourceResolverService.resolve()` has no `catchError`/fallback on any branch — an API failure while loading a widget's data propagates as an unhandled Observable error (`widgets/resolvers/data-source-resolver.service.ts:20-52`).
- `[User]` Because of the above, a single failing widget just silently fails to render (stays blank) — no retry, error message, or loading skeleton anywhere in the chain (`layouts/containers/dynamic-page-layout.component.ts:55-61`, `dynamic-renderer/widget-host/widget-host.service.ts:23-58`).
- `[Engineering / tech-debt]` `'html'`, `'banner'`, and `'partners'` widget types have full data-resolution logic written but no entry in `APPROVED_WIDGET_COMPONENTS` — configuring one of these renders `UnknownWidgetComponent` on the live storefront instead of real content (`widgets/registry/widget-registry.bootstrap.service.ts:9-17`, `data-source-resolver.service.ts:218-257`).
- `[Accessibility]` The hero widget carousel auto-advances every 5s whenever `data.autoplay` is set, with no pause/stop control exposed to the user — only the CSS entrance animation respects `prefers-reduced-motion`, the autoplay timer itself does not (WCAG 2.2.2 risk) (`widgets/ui/hero-widget.component.ts:292-301`).
### Static / CMS pages
- `[User]` `loadByKey`/`loadByPath` subscribe with only a `next` handler, no `error` callback — if bootstrap loading fails, `loading` stays `true` forever and the page shows an infinite spinner with no error state (`pages/static-page/static-page.component.ts:76-92`).
### i18n / Performance
- `[Performance]` `TranslatePipe` is declared `pure: false`, so every `| translate` binding (hundreds across templates — 40+ on the cart page alone) re-evaluates on every change-detection cycle instead of only when the language changes — a real cost at scale even under `OnPush` components (`i18n/translate.pipe.ts:4-14`).
## Automated code review — backoffice (admin)
*Findings from a fresh source-level pass over every `src/app/features/admin/*` module and `src/app/features/backoffice`. Each item includes a file:line reference and the role it matters most to.*
### Security / Authorization
- `[Security]` `adminAuthGuard` only checks `isAuthenticated()` — there is no role/permission check anywhere in the codebase (zero matches for a permission check project-wide). Any authenticated admin, including a seeded "viewer" role, can perform every destructive action: delete products/orders, change any user's role, refund orders (`core/admin-auth/admin-auth.guard.ts:6-15`). Same root cause as [Product Owner item 2](#as-product-owner--business).
- `[Security]` `AdminRole.permissions` arrays (owner/admin/editor/viewer) exist only as display labels — nothing gates a button, route, or action on them (`features/admin/users/services/admin-users-local.gateway.ts:7-12`, `admin-users-page.component.ts:59-64`).
- `[Security]` Nothing prevents suspending or demoting the last remaining `owner`-role user — `setStatus`/`setRole` apply unconditionally (`features/admin/users/facade/admin-users.facade.ts:35-41`).
- `[Security]` Category slug-uniqueness check **fails open**: on API error, `isSlugTaken` swallows the error and returns `false` ("not taken"), letting a duplicate/conflicting slug through silently instead of blocking submission (`features/admin/categories/services/admin-categories-api.gateway.ts:56-65`).
### Audit / Traceability
- `[Security]` Audit/timeline entries hardcode `actor: 'admin'` as a literal string instead of the real authenticated admin's identity — the "who did this" record is meaningless the moment more than one admin uses the system. Affects role/status changes, transaction retry/fraud-flag, and review moderation (`features/admin/users/services/admin-users-local.gateway.ts:93-94`; `transactions/services/admin-transactions-local.gateway.ts:41,50`; `moderation/services/admin-moderation-local.gateway.ts:64,74`).
- `[Security]` Orders have no actor/audit field at all — `AdminOrderTimelineEntry` has no `actor` property, so cancel/refund/status-change history records *what* changed but never *who* changed it, for the most financially sensitive module in the app (`features/admin/orders/models/admin-order.model.ts:32-36`).
### Destructive actions with missing/inconsistent confirmation
- `[Admin-operator]` Product delete (single row and bulk) removes the product permanently with **zero confirmation of any kind**, not even a native `confirm()` (`features/admin/products/components/admin-products-list.component.html:134,161` → `admin-products.facade.ts:316-317`; bulk: `admin-products-list-page.component.ts:36` → `facade.ts:175-178`).
- `[Admin-operator]` Order bulk-delete removes selected orders permanently with zero confirmation — destroys financial records in one click (`features/admin/orders/pages/admin-orders-list-page.component.html:48` → `admin-orders.facade.ts:134-140`).
- `[Admin-operator]` The review bulk-action button is labeled **"archive"** (implying reversible) but actually calls `deleteReview`, which permanently splices the review out — no confirmation dialog (`moderation/pages/admin-reviews-list-page.component.html:56` → `admin-moderation.facade.ts:148-154` → `admin-moderation-local.gateway.ts:94-97`).
- `[Admin-operator]` Category bulk-delete has no confirmation, inconsistent with the same module's single-delete flow, which does confirm (`categories/pages/admin-categories-list-page.component.ts:40` vs `76-84`).
- `[QA]` The order-detail "Change status" dropdown bypasses the confirm-gated Cancel/Refund buttons next to it — picking `cancelled`/`refunded` from the dropdown applies immediately with no confirmation (`orders/pages/admin-order-detail-page.component.html:49` vs `55-57`).
- `[QA]` Terminal order statuses aren't enforced in the UI — the status dropdown stays active after an order reaches `cancelled`/`refunded`, so a terminal order can be silently moved back to any other status (`admin-order-detail-page.component.html:47-58`).
- `[Accessibility / Engineering]` Confirmation UX is implemented three inconsistent ways across the app: native `window.confirm()`/`alert()` (Categories, Orders cancel/refund, Users suspend), a themed dialog component (Media Library only), and nothing at all (Products, Orders/Reviews bulk-delete) (`categories/pages/admin-categories-list-page.component.ts:78,81`; `orders/pages/admin-order-detail-page.component.ts:80,86`; `users/pages/admin-users-page.component.ts:43`; vs `features/backoffice/media/media-library-page.component.html:126-140`).
### Missing/incorrect loading, empty, error states
- `[QA]` Order-detail and Customer-detail pages collapse "loading," "not found," and "error" into one static "Loading…" string shown forever if the record isn't found or the request errors — `loadDetail` has no `error` handler (`orders/pages/admin-order-detail-page.component.html:89-91`, facade `admin-orders.facade.ts:98-100`; `customers/pages/admin-customer-detail-page.component.html:51-53`).
- `[QA]` Products, Categories, Orders, Customers, and Transactions facades swallow load errors into an empty array with no distinct `error` signal — a genuine API failure renders as "No results found" rather than an error-with-retry state, unlike Users/Monitoring/Analytics, which do track error separately (`products/facade/admin-products.facade.ts:101-114`; `customers/facade/admin-customers.facade.ts:42-54`; `transactions/facade/admin-transactions.facade.ts:15-29`).
- `[QA]` The Reports page never checks `facade.error()` even though `AdminAnalyticsFacade` exposes it — on load failure it silently renders 0%/0 stats instead of an error message (`admin/reports/pages/admin-reports-page.component.html:1-32`, facade `admin-analytics.facade.ts:41,77`).
- `[Engineering]` Save/mutate calls across Products, Categories, and Users subscribe with only a `next` handler — a failed save/delete/role-change fails completely silently with no user-facing feedback (`products/facade/admin-products.facade.ts:305-314`; `categories/facade/admin-categories.facade.ts:314-332`; `users/facade/admin-users.facade.ts:35-50`).
### Fake/stubbed data presented as real
- `[Engineering / Product Owner]` Monitoring's security/audit events, queue depths, and webhook deliveries are entirely synthetic (seeded fake generators, hardcoded queue states, modulo-based fake webhook statuses) with no connection to any real backend, yet presented as a live security/audit surface (`monitoring/services/admin-monitoring-local.gateway.ts:14-92`).
- `[Admin-operator]` The Monitoring page loads once on construction with no polling/auto-refresh and no manual refresh control (only a retry-on-error button) (`monitoring/pages/admin-monitoring-page.component.ts:39-41`; template `:79`).
- `[Admin-operator]` The topbar global search input has no `(input)`/`(keyup.enter)` binding and no handler anywhere — it is entirely decorative (`shell/admin-layout.component.html:118`).
- `[Admin-operator]` The notification bell always opens a panel showing a static "no notifications" message — no notification data source is wired up at all (`shell/admin-layout.component.html:141-157`).
- `[Product Owner]` The Dashboard's "images without alt text" health check is hardcoded to `status: 'unknown'` with no code path that could ever resolve it — a permanent placeholder sitting alongside real, resolvable checks (`dashboard/facade/admin-dashboard.facade.ts:132`).
- `[Product Owner]` "Request Refund" doesn't touch any payment processor — it only flips `payment.status` to `refund_requested`; actually completing the refund means separately picking "refunded" from the unrelated status dropdown, with no workflow linking the two (`orders/services/admin-orders-local.gateway.ts:43-50`).
### Incomplete modules
- `[Product Owner]` Settings (`/backoffice/settings`) is fully routed and linked in nav but contains exactly one control (density toggle to localStorage) — no store/payment/tax/shipping/notification settings exist despite the nav entry implying a general settings page.
- `[Product Owner]` Seller Management's "Request Access" form submission is mocked with a bare `setTimeout` — no backend call exists, "Learn more" is static copy. Confirms [Product Owner item 5-8](#as-product-owner--business) are still accurate against current source.
- `[Product Owner]` Customer-detail "Notes" card always shows a static "notes unavailable" message — no way to add customer notes at all, unlike Orders, which supports both customer-facing and internal notes (`customers/pages/admin-customer-detail-page.component.html:28-31`).
- `[Product Owner]` "Reports" duplicates "Analytics" 1:1 — both consume the same facade and independently re-run the same nested aggregation — but Reports exposes only 3 CSV export buttons. Unclear differentiation between the two nav entries for an operator (`reports/pages/admin-reports-page.component.ts:15-20`).
### Missing validation
- `[QA / Product Owner]` The product create/edit form has **zero client-side validation** — no required-field checks (name, price, SKU), no min/max on price/discount/quantity. A completely empty or negative-priced product can be saved with no warning (`products/components/admin-product-form.component.ts`, entire file; save path `admin-products.facade.ts:305-314`).
### Performance
- `[Performance]` `AdminAnalyticsFacade.load()` chains four nested subscriptions (orders → products → categories → reviews) instead of `forkJoin`/`combineLatest`, with no cancellation of in-flight requests — rapidly toggling the date-range filter can let a stale response overwrite a newer one (`analytics/facade/admin-analytics.facade.ts:65-130`).
- `[Performance]` Dashboard-stat computations across Orders, Customers, Transactions, and Analytics each independently re-fetch the entire order list with `pageSize: 100000` rather than sharing one cached read (`orders/facade/admin-orders.facade.ts:166-169`; `customers/facade/admin-customers.facade.ts:44,57`; `analytics/facade/admin-analytics.facade.ts:79,92`).
### Engineering / tech-debt
- `[Engineering]` `AdminCustomersFacade.buildCustomers()` treats `customerOrders[0]` as the customer's "latest" order with no local sort — correctness depends entirely on the orders gateway happening to already return descending-by-`createdAt` order; swapping in a differently-ordered real gateway would silently corrupt "last order" with no compiler or runtime signal (`customers/facade/admin-customers.facade.ts:24-39` vs `orders/services/admin-orders-local.gateway.ts:20`).
- `[Engineering]` Bulk operations (Orders, Categories, Products, Moderation) fire N independent gateway calls in a loop with no `forkJoin`, no per-item error handling, and no aggregate loading indicator — one failed item in a batch gives the user no signal at all (`moderation/facade/admin-moderation.facade.ts:133-154`; `orders/facade/admin-orders.facade.ts:119-140`; `categories/facade/admin-categories.facade.ts:391-397`).
### Doc-drift confirmed against the Seller-Management audit
- `[Engineering]` `Seller-Management-Backoffice-Readiness-Audit.md` stated Settings "No route exists... Skipped, nothing to audit" — now stale: `/backoffice/settings` is a real routed page with no `comingSoon` flag (`shell/admin-nav.model.ts:48`, `app.routes.ts:266-274`).
- `[Engineering]` Since that audit, `AdminOrder` and `AdminProduct` have both gained an explicit, unread `sellerId?: string` groundwork field — partially updates the audit's "no injection point" framing at the order level, though its core point (no per-item seller attribution on `AdminOrderItem`) remains accurate (`orders/models/admin-order.model.ts:54-59`; `products/models/admin-product.model.ts:115-120`).

374
README.md Normal file
View File

@@ -0,0 +1,374 @@
# Dexar Market (Multi-Brand Marketplace)
A modern, responsive marketplace application built with Angular 20 that supports multiple brands from a single codebase.
## 🎨 Multi-Brand Support
This project supports **two brands** with the same codebase:
- **Dexar Market** - Purple theme (`http://localhost:4200`)
- **Novo Market** - Green theme (`http://localhost:4201`)
Each brand has its own:
- Colors and themes
- Logos and branding
- Environment configuration
- Production builds
## Features
- 🎨 **Multi-Brand Architecture** - Single codebase, multiple brands
- 📱 **Fully Responsive** - Optimized for desktop, tablet, and mobile devices
- 🏪 **Category Browsing** - Hierarchical category navigation
- ♾️ **Infinite Scroll** - Seamless product loading in categories and search
- 🔍 **Real-time Search** - Debounced search with live results
- 🛒 **Shopping Cart** - API-managed cart with quantity support
- 📞 **Phone Collection** - Russian phone number formatting and validation
-**Product Reviews** - Display ratings, reviews, and Q&A
- 💳 **Payment Integration** - Telegram Web App payment flow
- 📧 **Email Notifications** - Purchase confirmation emails
- 📱 **PWA Support** - Progressive Web App with offline support
- 🔔 **Service Worker** - Smart caching for better performance
- 🎨 **Modern UI** - Clean, intuitive interface with smooth animations
## Tech Stack
- **Angular 21** - Latest Angular with standalone components and signals
- **TypeScript** - Type-safe development
- **SCSS** - Modular styling with theme-based architecture
- **RxJS** - Reactive programming for API calls
- **Signals** - Angular signals for reactive state management
- **Telegram Web App** - Integration with Telegram Mini Apps
- **PWA** - Service workers and offline support
## Quick Start
### Development
**Run Dexar Market (Purple):**
```bash
npm start
# or
npm run start:dexar
```
Open: http://localhost:4200
**Run Novo Market (Green):**
```bash
npm run start:novo
```
Open: http://localhost:4201
### Production Build
**Build Dexar Market:**
```bash
npm run build:dexar
```
Output: `dist/dexarmarket/`
**Build Novo Market:**
```bash
npm run build:novo
```
Output: `dist/novomarket/`
## Project Structure
```
src/
├── app/
│ ├── components/
│ │ ├── header/ # Brand-aware header
│ │ ├── footer/ # Brand-aware footer
│ │ └── logo/ # Dynamic logo component
│ ├── models/
│ │ ├── category.model.ts # Category interface
│ │ └── item.model.ts # Item, Photo, Callback, Question
│ ├── pages/
│ │ ├── home/ # Categories overview
│ │ ├── category/ # Product listing with infinite scroll
│ │ ├── item-detail/ # Product details
│ │ ├── search/ # Search with infinite scroll
│ │ ├── cart/ # Shopping cart with checkout
│ │ ├── info/ # About, contacts, FAQ, etc.
│ │ └── legal/ # Legal documents
│ ├── services/
│ │ ├── api.service.ts # HTTP API integration
│ │ ├── cart.service.ts # Cart state management (signals)
│ │ └── telegram.service.ts # Telegram WebApp integration
│ └── interceptors/
│ └── cache.interceptor.ts # API caching
├── environments/
│ ├── environment.ts # Dexar development
│ ├── environment.production.ts # Dexar production
│ ├── environment.novo.ts # Novo development
│ └── environment.novo.production.ts # Novo production
├── styles/
│ ├── themes/
│ │ ├── dexar.theme.scss # Purple theme
│ │ └── novo.theme.scss # Green theme
│ └── shared-legal.scss # Shared legal page styles
├── index.html # Dexar HTML
└── index.novo.html # Novo HTML
```
## API Endpoints
**Base URL:** Configured per environment
### Health Check
- `GET /ping` - Server availability check
### Categories
- `GET /category` - Get all categories (hierarchical)
### Items
- `GET /category/:categoryID?count=50&skip=100` - Get items in category (paginated)
- `GET /items?search=query&count=50&skip=100` - Search items (paginated)
### Cart
- `GET /cart` - Get cart items with quantities
- `POST /cart` - Add item `{ itemID: number, quantity?: number }`
- `PATCH /cart` - Update quantity `{ itemID: number, quantity: number }`
- `DELETE /cart` - Remove items `[itemID1, itemID2, ...]`
### Payment
- `POST /payment/create` - Create payment intent
- `POST /purchase-email` - Send purchase confirmation
See [docs/API_CHANGES_REQUIRED.md](docs/API_CHANGES_REQUIRED.md) for detailed API specifications.
## Environment Configuration
Each brand has development and production environments:
### Dexar Market
**Development** (`environment.ts`):
```typescript
{
production: false,
brandName: 'Dexar Market',
apiUrl: '/api', // Uses proxy
// ... other config
}
```
**Production** (`environment.production.ts`):
```typescript
{
production: true,
brandName: 'Dexar Market',
apiUrl: 'https://api.dexarmarket.ru',
// ... other config
}
```
### Novo Market
**Development** (`environment.novo.ts`):
```typescript
{
production: false,
brandName: 'novo Market',
apiUrl: '/api', // Uses proxy
// ... other config
}
```
**Production** (`environment.novo.production.ts`):
```typescript
{
production: true,
brandName: 'novo Market',
apiUrl: 'https://api.novomarket.ru', // To be configured
// ... other config
}
```
## Deployment
### Prerequisites
1. Node.js 18+ and npm installed
2. Backend API running and accessible
3. Domain names configured (dexarmarket.ru, novomarket.ru)
### Build for Production
**For Dexar Market:**
```bash
npm run build:dexar
```
Output: `dist/dexarmarket/`
**For Novo Market:**
```bash
npm run build:novo
```
Output: `dist/novomarket/`
### Nginx Configuration
When deploying to production, you **must** configure nginx to handle Angular routing properly.
**Example nginx config (Dexar):**
```nginx
server {
listen 80;
server_name dexarmarket.ru www.dexarmarket.ru;
root /var/www/dexarmarket;
index index.html;
# Angular routing support
location / {
try_files $uri $uri/ /index.html;
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
**For Novo Market**, use the same config with `novomarket.ru` and `/var/www/novomarket`.
### SSL Setup
Enable HTTPS with Let's Encrypt:
```bash
sudo certbot --nginx -d dexarmarket.ru -d www.dexarmarket.ru
sudo certbot --nginx -d novomarket.ru -d www.novomarket.ru
```
### Deploy Steps
1. Build the project:
```bash
npm run build:dexar
npm run build:novo
```
2. Upload to server:
```bash
scp -r dist/dexarmarket/* user@server:/var/www/dexarmarket/
scp -r dist/novomarket/* user@server:/var/www/novomarket/
```
3. Configure nginx (see above)
4. Reload nginx:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
### Important Notes
- The `try_files $uri $uri/ /index.html;` directive is **critical** for Angular routing
- Without it, direct URL access or page refreshes will cause 404 errors
- Each brand needs its own server block with separate domain
- Update API URLs in production environment files before building
## PWA (Progressive Web App)
The application includes PWA support with:
- Service worker for offline caching
- Install prompts on mobile devices
- Brand-specific app icons and manifests
- Background sync capabilities
**Manifests:**
- Dexar: `public/manifest.webmanifest`
- Novo: `public/manifest.novo.webmanifest`
**Configuration:** `ngsw-config.json`
## Development
### Angular CLI Commands
**Generate a new component:**
```bash
ng generate component component-name
```
**For a complete list of schematics:**
```bash
ng generate --help
```
### Running Tests
**Unit tests:**
```bash
ng test
```
**E2E tests:**
```bash
ng e2e
```
## Documentation
Comprehensive documentation is available in the `docs/` folder:
- **[MULTI_BRAND.md](docs/MULTI_BRAND.md)** - Multi-brand architecture guide
- **[QUICK_START_NOVO.md](docs/QUICK_START_NOVO.md)** - Quick start for Novo brand
- **[API_CHANGES_REQUIRED.md](docs/API_CHANGES_REQUIRED.md)** - Backend API requirements
- **[DEPLOYMENT.md](docs/DEPLOYMENT.md)** - Deployment instructions
- **[PWA_SETUP.md](docs/PWA_SETUP.md)** - PWA configuration guide
- **[IMPLEMENTATION.md](docs/IMPLEMENTATION.md)** - Implementation details
- **[RECOMMENDATIONS.md](docs/RECOMMENDATIONS.md)** - Roadmap and improvements
- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Common issues and solutions
## Telegram Integration
The marketplace is designed to work as a Telegram Mini App:
1. Cart data is stored on backend per Telegram user
2. Payment flow uses Telegram's payment system
3. Deep linking support for sharing products
4. Telegram user info auto-collection
## Browser Compatibility
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Mobile browsers (iOS Safari, Chrome Mobile)
## Known Issues & Limitations
1. **Cart quantity support** - Backend needs to implement quantity fields (see [API_CHANGES_REQUIRED.md](docs/API_CHANGES_REQUIRED.md))
2. **Novo brand assets** - Logo and custom images need to be added
3. **Legal documents** - Need real company details for Novo brand before deployment
## Contributing
When contributing, please:
1. Follow the existing code style (use Prettier)
2. Write unit tests for new features
3. Update documentation as needed
4. Test both Dexar and Novo brands before committing
## License
Proprietary - All rights reserved
## Support
For technical support or questions:
- Email: dev@dexarmarket.ru
- Telegram: @dexarmarket
## Additional Resources
- [Angular CLI Documentation](https://angular.dev/tools/cli)
- [Angular Docs](https://angular.dev)
- [Telegram Web Apps](https://core.telegram.org/bots/webapps)

View File

@@ -28,11 +28,6 @@
{ {
"glob": "**/*", "glob": "**/*",
"input": "public" "input": "public"
},
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
} }
], ],
"styles": [ "styles": [
@@ -58,8 +53,8 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "700kB", "maximumWarning": "600kB",
"maximumError": "1.5MB" "maximumError": "1MB"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
@@ -91,6 +86,146 @@
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
},
"novo": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.novo.ts"
},
{
"replace": "src/app/brands/brand-routes.ts",
"with": "src/app/brands/brand-routes.novo.ts"
},
{
"replace": "src/app/interceptors/mock-data.interceptor.ts",
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
}
],
"index": "src/index.novo.html",
"styles": [
"src/styles.scss",
"src/styles/themes/novo.theme.scss"
],
"outputPath": "dist/novomarket",
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"novo-production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.novo.production.ts"
},
{
"replace": "src/app/brands/brand-routes.ts",
"with": "src/app/brands/brand-routes.novo.ts"
}
],
"index": "src/index.novo.html",
"styles": [
"src/styles.scss",
"src/styles/themes/novo.theme.scss"
],
"outputPath": "dist/novomarket",
"budgets": [
{
"type": "initial",
"maximumWarning": "600kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "40kB",
"maximumError": "50kB"
}
],
"outputHashing": "all",
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": true
},
"fonts": {
"inline": true
}
},
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"serviceWorker": "ngsw-config.json"
},
"lavero": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.lavero.ts"
},
{
"replace": "src/app/brands/brand-routes.ts",
"with": "src/app/brands/brand-routes.lavero.ts"
},
{
"replace": "src/app/interceptors/mock-data.interceptor.ts",
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
}
],
"index": "src/index.lavero.html",
"styles": [
"src/styles.scss",
"src/styles/themes/lavero.theme.scss"
],
"outputPath": "dist/laveromarket",
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"lavero-production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.lavero.production.ts"
},
{
"replace": "src/app/brands/brand-routes.ts",
"with": "src/app/brands/brand-routes.lavero.ts"
}
],
"index": "src/index.lavero.html",
"styles": [
"src/styles.scss",
"src/styles/themes/lavero.theme.scss"
],
"outputPath": "dist/laveromarket",
"budgets": [
{
"type": "initial",
"maximumWarning": "600kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "40kB",
"maximumError": "50kB"
}
],
"outputHashing": "all",
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": true
},
"fonts": {
"inline": true
}
},
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"serviceWorker": "ngsw-config.json"
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@@ -98,10 +233,13 @@
"serve": { "serve": {
"options": { "options": {
"allowedHosts": [ "allowedHosts": [
"novo.market",
"dexarmarket.ru", "dexarmarket.ru",
"dexar.market", "dexar.market",
"localhost" "localhost",
] "lovero.store"
],
"proxyConfig": "proxy.conf.json"
}, },
"builder": "@angular/build:dev-server", "builder": "@angular/build:dev-server",
"configurations": { "configurations": {
@@ -109,40 +247,27 @@
"buildTarget": "Dexarmarket:build:production" "buildTarget": "Dexarmarket:build:production"
}, },
"development": { "development": {
"proxyConfig": "proxy.conf.json",
"buildTarget": "Dexarmarket:build:development" "buildTarget": "Dexarmarket:build:development"
},
"novo": {
"buildTarget": "Dexarmarket:build:novo",
"proxyConfig": "proxy.conf.novo.json"
},
"novo-production": {
"buildTarget": "Dexarmarket:build:novo-production"
},
"lavero": {
"buildTarget": "Dexarmarket:build:lavero",
"proxyConfig": "proxy.conf.lavero.json"
},
"lavero-production": {
"buildTarget": "Dexarmarket:build:lavero-production"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular/build:extract-i18n" "builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
},
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
}
],
"styles": [
"src/styles.scss"
]
}
} }
} }
} }

View File

@@ -0,0 +1,726 @@
# Complete Backend API Documentation
> **Last updated:** February 2026
> **Frontend:** Angular 21 · Dual-brand (Dexar + Novo)
> **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice
---
## Base URLs
| Brand | Dev | Production |
|--------|----------------------------------|----------------------------------|
| Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` |
| Novo | `https://api.novo.market:444` | `https://api.novo.market:444` |
---
## Global HTTP Headers
The frontend **automatically attaches** two custom headers to **every API request** via an interceptor. The backend should read these headers and use them to filter/translate responses accordingly.
| Header | Example Value | Description |
|---------------|---------------|------------------------------------------------------------|
| `X-Region` | `moscow` | Region ID selected by the user. **Absent** = global (all). |
| `X-Language` | `ru` | Active UI language: `ru`, `en`, or `hy`. |
### Backend behavior
- **`X-Region`**: If present, filter items/categories to only those available in that region. If absent, return everything (global catalog).
- **`X-Language`**: If present, return translated `name`, `description`, etc. for categories/items when translations exist. If absent or `ru`, use russians defaults.
### CORS requirements for these headers
```
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
```
---
## 1. Health Check
### `GET /ping`
Simple health check.
**Response `200`:**
```json
{ "message": "pong" }
```
---
## 2. Catalog — Categories
### `GET /category`
Returns all top-level categories. Respects `X-Region` and `X-Language` headers.
**Response `200`:**
```json
[
{
"categoryID": 1,
"name": "Электроника",
"parentID": 0,
"icon": "https://...",
"wideBanner": "https://...",
"itemCount": 42,
"priority": 10,
"id": "cat_abc123",
"visible": true,
"img": "https://...",
"projectId": "proj_xyz",
"subcategories": [
{
"id": "sub_001",
"name": "Смартфоны",
"visible": true,
"priority": 5,
"img": "https://...",
"categoryId": "cat_abc123",
"parentId": "cat_abc123",
"itemCount": 20,
"hasItems": true,
"subcategories": []
}
]
}
]
```
**Category object:**
| Field | Type | Required | Description |
|------------------|---------------|----------|----------------------------------------------------|
| `categoryID` | number | yes | Legacy numeric ID |
| `name` | string | yes | Category display name (translated if `X-Language`) |
| `parentID` | number | yes | Parent category ID (`0` = top-level) |
| `icon` | string | no | Category icon URL |
| `wideBanner` | string | no | Wide banner image URL |
| `itemCount` | number | no | Number of items in category |
| `priority` | number | no | Sort priority (higher = first) |
| `id` | string | no | BackOffice string ID |
| `visible` | boolean | no | Whether category is shown (`true` default) |
| `img` | string | no | BackOffice image URL (maps to `icon`) |
| `projectId` | string | no | BackOffice project reference |
| `subcategories` | Subcategory[] | no | Nested subcategories |
**Subcategory object:**
| Field | Type | Required | Description |
|------------------|---------------|----------|------------------------------------|
| `id` | string | yes | Subcategory ID |
| `name` | string | yes | Display name |
| `visible` | boolean | no | Whether visible |
| `priority` | number | no | Sort priority |
| `img` | string | no | Image URL |
| `categoryId` | string | yes | Parent category ID |
| `parentId` | string | yes | Direct parent ID |
| `itemCount` | number | no | Number of items |
| `hasItems` | boolean | no | Whether has any items |
| `subcategories` | Subcategory[] | no | Nested children |
---
### `GET /category/:categoryID`
Returns items in a specific category. Respects `X-Region` and `X-Language` headers.
**Query params:**
| Param | Type | Default | Description |
|----------|--------|---------|--------------------|
| `count` | number | `50` | Items per page |
| `skip` | number | `0` | Offset for paging |
**Response `200`:** Array of [Item](#item-object) objects.
---
## 3. Items
### `GET /item/:itemID`
Returns a single item. Respects `X-Region` and `X-Language` headers.
**Response `200`:** A single [Item](#item-object) object.
---
### `GET /searchitems`
Full-text search across items. Respects `X-Region` and `X-Language` headers.
**Query params:**
| Param | Type | Default | Description |
|----------|--------|---------|----------------------|
| `search` | string | — | Search query (required) |
| `count` | number | `50` | Items per page |
| `skip` | number | `0` | Offset for paging |
**Response `200`:**
```json
{
"items": [ /* Item objects */ ],
"total": 128,
"count": 50,
"skip": 0
}
```
---
### `GET /randomitems`
Returns random items for carousel/recommendations. Respects `X-Region` and `X-Language` headers.
**Query params:**
| Param | Type | Default | Description |
|------------|--------|---------|------------------------------------|
| `count` | number | `5` | Number of items to return |
| `category` | number | — | Optional: limit to this category |
**Response `200`:** Array of [Item](#item-object) objects.
---
### Item Object
The backend can return items in **either** legacy format or BackOffice format. The frontend normalizes both.
```json
{
"categoryID": 1,
"itemID": 123,
"name": "iPhone 15 Pro",
"photos": [{ "url": "https://..." }],
"description": "Описание товара",
"currency": "RUB",
"price": 89990,
"discount": 10,
"remainings": "high",
"rating": 4.5,
"callbacks": [
{
"rating": 5,
"content": "Отличный товар!",
"userID": "user_123",
"timestamp": "2026-02-01T12:00:00Z"
}
],
"questions": [
{
"question": "Есть ли гарантия?",
"answer": "Да, 12 месяцев",
"upvotes": 5,
"downvotes": 0
}
],
"id": "item_abc123",
"visible": true,
"priority": 10,
"imgs": ["https://img1.jpg", "https://img2.jpg"],
"tags": ["new", "popular"],
"badges": ["bestseller", "sale"],
"simpleDescription": "Краткое описание",
"descriptionFields": [
{ "key": "Процессор", "value": "A17 Pro" },
{ "key": "Память", "value": "256 GB" }
],
"subcategoryId": "sub_001",
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short description",
"description": [
{ "key": "Processor", "value": "A17 Pro" }
]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կարcheck check check"
}
},
"comments": [
{
"id": "cmt_001",
"text": "Отличный товар!",
"author": "user_123",
"stars": 5,
"createdAt": "2026-02-01T12:00:00Z"
}
],
"quantity": 50
}
```
**Full Item fields:**
| Field | Type | Required | Description |
|---------------------|-------------------|----------|------------------------------------------------------------|
| `categoryID` | number | yes | Category this item belongs to |
| `itemID` | number | yes | Legacy numeric item ID |
| `name` | string | yes | Item display name |
| `photos` | Photo[] | no | Legacy photo array `[{ url }]` |
| `description` | string | yes | Text description |
| `currency` | string | yes | Currency code (default: `RUB`) |
| `price` | number | yes | Price in the currency's smallest display unit |
| `discount` | number | yes | Discount percentage (`0``100`) |
| `remainings` | string | no | Stock level: `high`, `medium`, `low`, `out` |
| `rating` | number | yes | Average rating (`0``5`) |
| `callbacks` | Review[] | no | Legacy reviews (alias for reviews) |
| `questions` | Question[] | no | Q&A entries |
| `id` | string | no | BackOffice string ID |
| `visible` | boolean | no | Whether item is visible (`true` default) |
| `priority` | number | no | Sort priority (higher = first) |
| `imgs` | string[] | no | BackOffice image URLs (maps to `photos`) |
| `tags` | string[] | no | Item tags for filtering |
| `badges` | string[] | no | Display badges (`bestseller`, `sale`, etc.) |
| `simpleDescription` | string | no | Short plain-text description |
| `descriptionFields` | DescriptionField[]| no | Structured `[{ key, value }]` descriptions |
| `subcategoryId` | string | no | BackOffice subcategory reference |
| `translations` | Record | no | Translations keyed by lang code (see below) |
| `comments` | Comment[] | no | BackOffice comments format |
| `quantity` | number | no | Numeric stock count (maps to `remainings` on frontend) |
**Nested types:**
| Type | Fields |
|--------------------|-----------------------------------------------------------------|
| `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` |
| `DescriptionField` | `key: string`, `value: string` |
| `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` |
| `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` |
| `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` |
| `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` |
---
## 4. Cart
### `POST /cart` — Add item to cart
**Request body:**
```json
{ "itemID": 123, "quantity": 1 }
```
**Response `200`:**
```json
{ "message": "Added to cart" }
```
---
### `PATCH /cart` — Update item quantity
**Request body:**
```json
{ "itemID": 123, "quantity": 3 }
```
**Response `200`:**
```json
{ "message": "Updated" }
```
---
### `DELETE /cart` — Remove items from cart
**Request body:** Array of item IDs
```json
[123, 456]
```
**Response `200`:**
```json
{ "message": "Removed" }
```
---
### `GET /cart` — Get cart contents
**Response `200`:** Array of [Item](#item-object) objects (each with `quantity` field).
---
## 5. Payments (SBP / QR)
### `POST /cart` — Create payment (SBP QR)
> Note: Same endpoint as add-to-cart but with different body schema.
**Request body:**
```json
{
"amount": 89990,
"currency": "RUB",
"siteuserID": "tg_123456789",
"siteorderID": "order_abc123",
"redirectUrl": "",
"telegramUsername": "john_doe",
"items": [
{ "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" }
]
}
```
**Response `200`:**
```json
{
"qrId": "qr_abc123",
"qrStatus": "CREATED",
"qrExpirationDate": "2026-02-28T13:00:00Z",
"payload": "https://qr.nspk.ru/...",
"qrUrl": "https://qr.nspk.ru/..."
}
```
---
### `GET /qr/payment/:qrId` — Check payment status
**Response `200`:**
```json
{
"additionalInfo": "",
"paymentPurpose": "Order #order_abc123",
"amount": 89990,
"code": "SUCCESS",
"createDate": "2026-02-28T12:00:00Z",
"currency": "RUB",
"order": "order_abc123",
"paymentStatus": "COMPLETED",
"qrId": "qr_abc123",
"transactionDate": "2026-02-28T12:01:00Z",
"transactionId": 999,
"qrExpirationDate": "2026-02-28T13:00:00Z",
"phoneNumber": "+7XXXXXXXXXX"
}
```
| `paymentStatus` values | Meaning |
|------------------------|---------------------------|
| `CREATED` | QR generated, not paid |
| `WAITING` | Payment in progress |
| `COMPLETED` | Payment successful |
| `EXPIRED` | QR code expired |
| `CANCELLED` | Payment cancelled |
---
### `POST /purchase-email` — Submit email after payment
**Request body:**
```json
{
"email": "user@example.com",
"telegramUserId": "123456789",
"items": [
{ "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" }
]
}
```
**Response `200`:**
```json
{ "message": "Email sent" }
```
---
## 6. Reviews / Comments
### `POST /comment` — Submit a review
**Request body:**
```json
{
"itemID": 123,
"rating": 5,
"comment": "Great product!",
"username": "john_doe",
"userId": 123456789,
"timestamp": "2026-02-28T12:00:00Z"
}
```
**Response `200`:**
```json
{ "message": "Review submitted" }
```
---
## 7. Regions
### `GET /regions` — List available regions
Returns regions where the marketplace operates.
**Response `200`:**
```json
[
{
"id": "moscow",
"city": "Москва",
"country": "Россия",
"countryCode": "RU",
"timezone": "Europe/Moscow"
},
{
"id": "spb",
"city": "Санкт-Петербург",
"country": "Россия",
"countryCode": "RU",
"timezone": "Europe/Moscow"
},
{
"id": "yerevan",
"city": "Ереван",
"country": "Армения",
"countryCode": "AM",
"timezone": "Asia/Yerevan"
}
]
```
**Region object:**
| Field | Type | Required | Description |
|---------------|--------|----------|--------------------------|
| `id` | string | yes | Unique region identifier |
| `city` | string | yes | City name (display) |
| `country` | string | yes | Country name |
| `countryCode` | string | yes | ISO 3166-1 alpha-2 |
| `timezone` | string | no | IANA timezone |
> **Fallback:** If this endpoint is down, the frontend uses 6 hardcoded defaults: Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi.
---
## 8. Authentication (Telegram Login)
Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None).
All auth endpoints must include `withCredentials: true` CORS support.
### Auth flow
```
1. User clicks "Checkout" → not authenticated → login dialog shown
2. User clicks "Log in with Telegram" → opens https://t.me/{bot}?start=auth_{callback}
3. User starts the bot in Telegram
4. Bot sends user data → backend /auth/telegram/callback
5. Backend creates session → sets Set-Cookie
6. Frontend polls GET /auth/session every 3s
7. Session detected → dialog closes → checkout proceeds
```
---
### `GET /auth/session` — Check current session
**Request:** Cookies only (session cookie set by backend).
**Response `200`** (authenticated):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": true,
"expiresAt": "2026-03-01T12:00:00Z"
}
```
**Response `200`** (expired):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": false,
"expiresAt": "2026-02-27T12:00:00Z"
}
```
**Response `401`** (no session):
```json
{ "error": "No active session" }
```
**AuthSession object:**
| Field | Type | Required | Description |
|------------------|---------|----------|--------------------------------------------|
| `sessionId` | string | yes | Unique session ID |
| `telegramUserId` | number | yes | Telegram user ID |
| `username` | string? | no | Telegram @username (can be null) |
| `displayName` | string | yes | User display name (first + last) |
| `active` | boolean | yes | Whether session is valid |
| `expiresAt` | string | yes | ISO 8601 expiration datetime |
---
### `GET /auth/telegram/callback` — Telegram bot auth callback
Called by the Telegram bot after user authenticates.
**Request body (from bot):**
```json
{
"id": 123456789,
"first_name": "John",
"last_name": "Doe",
"username": "john_doe",
"photo_url": "https://t.me/i/userpic/...",
"auth_date": 1709100000,
"hash": "abc123def456..."
}
```
**Response:** Must set a session cookie and return:
```json
{
"sessionId": "sess_abc123",
"message": "Authenticated successfully"
}
```
**Cookie requirements:**
| Attribute | Value | Notes |
|------------|----------------|--------------------------------------------|
| `HttpOnly` | `true` | Not accessible via JS |
| `Secure` | `true` | HTTPS only |
| `SameSite` | `None` | Required for cross-origin (API ≠ frontend) |
| `Path` | `/` | |
| `Max-Age` | `86400` (24h) | Or as needed |
---
### `POST /auth/logout` — End session
**Request:** Cookies only, empty body `{}`
**Response `200`:**
```json
{ "message": "Logged out" }
```
Must clear/invalidate the session cookie.
---
### Session refresh
The frontend re-checks the session **60 seconds before `expiresAt`**. If the backend supports sliding expiration, it can reset the cookie's `Max-Age` on each `GET /auth/session`.
---
## 9. i18n / Translations
The frontend supports 3 languages: **Russian (ru)**, **English (en)**, **Armenian (hy)**.
The active language is sent via the `X-Language` HTTP header on every request.
### What the backend should do with `X-Language`
1. **Categories & items**: If `translations` field exists for the requested language, return the translated `name`, `description`, etc. OR the backend can apply translations server-side and return already-translated fields.
2. **The `translations` field** on items (optional approach):
```json
{
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short desc in English",
"description": [{ "key": "Processor", "value": "A17 Pro" }]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կarcheck check"
}
}
}
```
3. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default.
---
## 10. CORS Configuration
For auth cookies and custom headers to work, the backend CORS config must include:
```
Access-Control-Allow-Origin: https://dexarmarket.ru (NOT wildcard *)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
```
> **Important:** `Access-Control-Allow-Origin` cannot be `*` when `Allow-Credentials: true`. Must be the exact frontend origin.
**Allowed origins:**
- `https://dexarmarket.ru`
- `https://novo.market`
- `http://localhost:4200` (dev)
- `http://localhost:4201` (dev, Novo)
---
## 11. Telegram Bot Setup
Each brand needs its own bot:
- **Dexar:** `@dexarmarket_bot`
- **Novo:** `@novomarket_bot`
The bot should:
1. Listen for `/start auth_{callbackUrl}` command
2. Extract the callback URL
3. Send the user's Telegram data (`id`, `first_name`, `username`, etc.) to that callback URL
4. The callback URL is `{apiUrl}/auth/telegram/callback`
---
## Complete Endpoint Reference
### New endpoints
| Method | Path | Description | Auth |
|--------|---------------------------|----------------------------|----------|
| `GET` | `/regions` | List available regions | No |
| `GET` | `/auth/session` | Check current session | Cookie |
| `GET` | `/auth/telegram/callback` | Telegram bot auth callback | No (bot) |
| `POST` | `/auth/logout` | End session | Cookie |
### Existing endpoints
| Method | Path | Description | Auth | Headers |
|----------|-----------------------|-------------------------|------|--------------------|
| `GET` | `/ping` | Health check | No | — |
| `GET` | `/category` | List categories | No | X-Region, X-Language |
| `GET` | `/category/:id` | Items in category | No | X-Region, X-Language |
| `GET` | `/item/:id` | Single item | No | X-Region, X-Language |
| `GET` | `/searchitems` | Search items | No | X-Region, X-Language |
| `GET` | `/randomitems` | Random items | No | X-Region, X-Language |
| `POST` | `/cart` | Add to cart / Payment | No* | — |
| `PATCH` | `/cart` | Update cart quantity | No* | — |
| `DELETE` | `/cart` | Remove from cart | No* | — |
| `GET` | `/cart` | Get cart contents | No* | — |
| `POST` | `/comment` | Submit review | No | — |
| `GET` | `/qr/payment/:qrId` | Check payment status | No | — |
| `POST` | `/purchase-email` | Submit email after pay | No | — |
> \* Cart/payment endpoints may use the session cookie if available for order association, but don't strictly require auth. The frontend enforces auth before checkout.

726
docs/API_DOCS_RU.md Normal file
View File

@@ -0,0 +1,726 @@
# Полная документация Backend API
> **Последнее обновление:** Февраль 2026
> **Фронтенд:** Angular 21 · Два бренда (Dexar + Novo)
> **Охватывает:** Каталог, Корзина, Оплата, Отзывы, Регионы, Авторизация, i18n, BackOffice
---
## Базовые URL
| Бренд | Dev | Production |
|--------|----------------------------------|----------------------------------|
| Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` |
| Novo | `https://api.novo.market:444` | `https://api.novo.market:444` |
---
## Глобальные HTTP-заголовки
Фронтенд **автоматически добавляет** два кастомных заголовка к **каждому API-запросу** через interceptor. Бэкенд должен читать эти заголовки и использовать для фильтрации/перевода ответов.
| Заголовок | Пример значения | Описание |
|---------------|-----------------|-------------------------------------------------------------------|
| `X-Region` | `moscow` | ID региона, выбранного пользователем. **Отсутствует** = все регионы. |
| `X-Language` | `ru` | Активный язык интерфейса: `ru`, `en` или `hy`. |
### Поведение бэкенда
- **`X-Region`**: Если присутствует — фильтровать товары/категории только по этому региону. Если отсутствует — возвращать всё (глобальный каталог).
- **`X-Language`**: Если присутствует — возвращать переведённые `name`, `description` и т.д., если переводы существуют. Если отсутствует или `ru` — возвращать на русском (по умолчанию).
### Требования CORS для этих заголовков
```
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
```
---
## 1. Проверка состояния
### `GET /ping`
Простая проверка работоспособности.
**Ответ `200`:**
```json
{ "message": "pong" }
```
---
## 2. Каталог — Категории
### `GET /category`
Возвращает все категории верхнего уровня. Учитывает заголовки `X-Region` и `X-Language`.
**Ответ `200`:**
```json
[
{
"categoryID": 1,
"name": "Электроника",
"parentID": 0,
"icon": "https://...",
"wideBanner": "https://...",
"itemCount": 42,
"priority": 10,
"id": "cat_abc123",
"visible": true,
"img": "https://...",
"projectId": "proj_xyz",
"subcategories": [
{
"id": "sub_001",
"name": "Смартфоны",
"visible": true,
"priority": 5,
"img": "https://...",
"categoryId": "cat_abc123",
"parentId": "cat_abc123",
"itemCount": 20,
"hasItems": true,
"subcategories": []
}
]
}
]
```
**Объект Category:**
| Поле | Тип | Обязат. | Описание |
|------------------|---------------|---------|-----------------------------------------------------|
| `categoryID` | number | да | Числовой ID (legacy) |
| `name` | string | да | Название категории (переведённое если `X-Language`) |
| `parentID` | number | да | ID родительской категории (`0` = верхний уровень) |
| `icon` | string | нет | URL иконки категории |
| `wideBanner` | string | нет | URL широкого баннера |
| `itemCount` | number | нет | Количество товаров в категории |
| `priority` | number | нет | Приоритет сортировки (больше = выше) |
| `id` | string | нет | Строковый ID из BackOffice |
| `visible` | boolean | нет | Видима ли категория (по умолч. `true`) |
| `img` | string | нет | URL изображения из BackOffice (маппится на `icon`) |
| `projectId` | string | нет | Ссылка на проект в BackOffice |
| `subcategories` | Subcategory[] | нет | Вложенные подкатегории |
**Объект Subcategory:**
| Поле | Тип | Обязат. | Описание |
|------------------|---------------|---------|----------------------------------|
| `id` | string | да | ID подкатегории |
| `name` | string | да | Отображаемое название |
| `visible` | boolean | нет | Видима ли |
| `priority` | number | нет | Приоритет сортировки |
| `img` | string | нет | URL изображения |
| `categoryId` | string | да | ID родительской категории |
| `parentId` | string | да | ID прямого родителя |
| `itemCount` | number | нет | Количество товаров |
| `hasItems` | boolean | нет | Есть ли товары |
| `subcategories` | Subcategory[] | нет | Вложенные дочерние подкатегории |
---
### `GET /category/:categoryID`
Возвращает товары определённой категории. Учитывает заголовки `X-Region` и `X-Language`.
**Query-параметры:**
| Параметр | Тип | По умолч. | Описание |
|----------|--------|-----------|-----------------------|
| `count` | number | `50` | Товаров на страницу |
| `skip` | number | `0` | Смещение для пагинации |
**Ответ `200`:** Массив объектов [Item](#объект-item).
---
## 3. Товары
### `GET /item/:itemID`
Возвращает один товар. Учитывает заголовки `X-Region` и `X-Language`.
**Ответ `200`:** Один объект [Item](#объект-item).
---
### `GET /searchitems`
Полнотекстовый поиск по товарам. Учитывает заголовки `X-Region` и `X-Language`.
**Query-параметры:**
| Параметр | Тип | По умолч. | Описание |
|----------|--------|-----------|-------------------------------|
| `search` | string | — | Поисковый запрос (обязателен) |
| `count` | number | `50` | Товаров на страницу |
| `skip` | number | `0` | Смещение для пагинации |
**Ответ `200`:**
```json
{
"items": [ /* объекты Item */ ],
"total": 128,
"count": 50,
"skip": 0
}
```
---
### `GET /randomitems`
Возвращает случайные товары для карусели/рекомендаций. Учитывает заголовки `X-Region` и `X-Language`.
**Query-параметры:**
| Параметр | Тип | По умолч. | Описание |
|------------|--------|-----------|--------------------------------------|
| `count` | number | `5` | Количество товаров |
| `category` | number | — | Ограничить данной категорией (опц.) |
**Ответ `200`:** Массив объектов [Item](#объект-item).
---
### Объект Item
Бэкенд может возвращать товары в **любом** из двух форматов — legacy или BackOffice. Фронтенд нормализует оба варианта.
```json
{
"categoryID": 1,
"itemID": 123,
"name": "iPhone 15 Pro",
"photos": [{ "url": "https://..." }],
"description": "Описание товара",
"currency": "RUB",
"price": 89990,
"discount": 10,
"remainings": "high",
"rating": 4.5,
"callbacks": [
{
"rating": 5,
"content": "Отличный товар!",
"userID": "user_123",
"timestamp": "2026-02-01T12:00:00Z"
}
],
"questions": [
{
"question": "Есть ли гарантия?",
"answer": "Да, 12 месяцев",
"upvotes": 5,
"downvotes": 0
}
],
"id": "item_abc123",
"visible": true,
"priority": 10,
"imgs": ["https://img1.jpg", "https://img2.jpg"],
"tags": ["new", "popular"],
"badges": ["bestseller", "sale"],
"simpleDescription": "Краткое описание",
"descriptionFields": [
{ "key": "Процессор", "value": "A17 Pro" },
{ "key": "Память", "value": "256 GB" }
],
"subcategoryId": "sub_001",
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short description",
"description": [
{ "key": "Processor", "value": "A17 Pro" }
]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կարcheck check"
}
},
"comments": [
{
"id": "cmt_001",
"text": "Отличный товар!",
"author": "user_123",
"stars": 5,
"createdAt": "2026-02-01T12:00:00Z"
}
],
"quantity": 50
}
```
**Все поля Item:**
| Поле | Тип | Обязат. | Описание |
|---------------------|-------------------|---------|-----------------------------------------------------------|
| `categoryID` | number | да | Категория, к которой принадлежит товар |
| `itemID` | number | да | Числовой ID товара (legacy) |
| `name` | string | да | Название товара |
| `photos` | Photo[] | нет | Массив фотографий `[{ url }]` (legacy) |
| `description` | string | да | Текстовое описание |
| `currency` | string | да | Код валюты (по умолч. `RUB`) |
| `price` | number | да | Цена |
| `discount` | number | да | Процент скидки (`0``100`) |
| `remainings` | string | нет | Уровень остатка: `high`, `medium`, `low`, `out` |
| `rating` | number | да | Средний рейтинг (`0``5`) |
| `callbacks` | Review[] | нет | Отзывы (legacy формат) |
| `questions` | Question[] | нет | Вопросы и ответы |
| `id` | string | нет | Строковый ID из BackOffice |
| `visible` | boolean | нет | Виден ли товар (по умолч. `true`) |
| `priority` | number | нет | Приоритет сортировки (больше = выше) |
| `imgs` | string[] | нет | URL картинок из BackOffice (маппится на `photos`) |
| `tags` | string[] | нет | Теги для фильтрации |
| `badges` | string[] | нет | Бейджи (`bestseller`, `sale` и т.д.) |
| `simpleDescription` | string | нет | Краткое текстовое описание |
| `descriptionFields` | DescriptionField[]| нет | Структурированное описание `[{ key, value }]` |
| `subcategoryId` | string | нет | Ссылка на подкатегорию из BackOffice |
| `translations` | Record | нет | Переводы по ключу языка (см. ниже) |
| `comments` | Comment[] | нет | Комментарии в формате BackOffice |
| `quantity` | number | нет | Числовое кол-во на складе (маппится на `remainings`) |
**Вложенные типы:**
| Тип | Поля |
|--------------------|-----------------------------------------------------------------|
| `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` |
| `DescriptionField` | `key: string`, `value: string` |
| `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` |
| `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` |
| `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` |
| `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` |
---
## 4. Корзина
### `POST /cart` — Добавить товар в корзину
**Тело запроса:**
```json
{ "itemID": 123, "quantity": 1 }
```
**Ответ `200`:**
```json
{ "message": "Added to cart" }
```
---
### `PATCH /cart` — Обновить количество товара
**Тело запроса:**
```json
{ "itemID": 123, "quantity": 3 }
```
**Ответ `200`:**
```json
{ "message": "Updated" }
```
---
### `DELETE /cart` — Удалить товары из корзины
**Тело запроса:** Массив ID товаров
```json
[123, 456]
```
**Ответ `200`:**
```json
{ "message": "Removed" }
```
---
### `GET /cart` — Получить содержимое корзины
**Ответ `200`:** Массив объектов [Item](#объект-item) (каждый с полем `quantity`).
---
## 5. Оплата (СБП / QR)
### `POST /cart` — Создать платёж (СБП QR)
> Примечание: Тот же эндпоинт что и добавление в корзину, но с другой схемой тела запроса.
**Тело запроса:**
```json
{
"amount": 89990,
"currency": "RUB",
"siteuserID": "tg_123456789",
"siteorderID": "order_abc123",
"redirectUrl": "",
"telegramUsername": "john_doe",
"items": [
{ "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" }
]
}
```
**Ответ `200`:**
```json
{
"qrId": "qr_abc123",
"qrStatus": "CREATED",
"qrExpirationDate": "2026-02-28T13:00:00Z",
"payload": "https://qr.nspk.ru/...",
"qrUrl": "https://qr.nspk.ru/..."
}
```
---
### `GET /qr/payment/:qrId` — Проверить статус оплаты
**Ответ `200`:**
```json
{
"additionalInfo": "",
"paymentPurpose": "Order #order_abc123",
"amount": 89990,
"code": "SUCCESS",
"createDate": "2026-02-28T12:00:00Z",
"currency": "RUB",
"order": "order_abc123",
"paymentStatus": "COMPLETED",
"qrId": "qr_abc123",
"transactionDate": "2026-02-28T12:01:00Z",
"transactionId": 999,
"qrExpirationDate": "2026-02-28T13:00:00Z",
"phoneNumber": "+7XXXXXXXXXX"
}
```
| Значение `paymentStatus` | Значение |
|--------------------------|------------------------------|
| `CREATED` | QR создан, не оплачен |
| `WAITING` | Оплата в процессе |
| `COMPLETED` | Оплата успешна |
| `EXPIRED` | QR-код истёк |
| `CANCELLED` | Оплата отменена |
---
### `POST /purchase-email` — Отправить email после оплаты
**Тело запроса:**
```json
{
"email": "user@example.com",
"telegramUserId": "123456789",
"items": [
{ "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" }
]
}
```
**Ответ `200`:**
```json
{ "message": "Email sent" }
```
---
## 6. Отзывы / Комментарии
### `POST /comment` — Оставить отзыв
**Тело запроса:**
```json
{
"itemID": 123,
"rating": 5,
"comment": "Отличный товар!",
"username": "john_doe",
"userId": 123456789,
"timestamp": "2026-02-28T12:00:00Z"
}
```
**Ответ `200`:**
```json
{ "message": "Review submitted" }
```
---
## 7. Регионы
### `GET /regions` — Список доступных регионов
Возвращает регионы, в которых работает маркетплейс.
**Ответ `200`:**
```json
[
{
"id": "moscow",
"city": "Москва",
"country": "Россия",
"countryCode": "RU",
"timezone": "Europe/Moscow"
},
{
"id": "spb",
"city": "Санкт-Петербург",
"country": "Россия",
"countryCode": "RU",
"timezone": "Europe/Moscow"
},
{
"id": "yerevan",
"city": "Ереван",
"country": "Армения",
"countryCode": "AM",
"timezone": "Asia/Yerevan"
}
]
```
**Объект Region:**
| Поле | Тип | Обязат. | Описание |
|---------------|--------|---------|----------------------------------|
| `id` | string | да | Уникальный идентификатор региона |
| `city` | string | да | Название города |
| `country` | string | да | Название страны |
| `countryCode` | string | да | Код страны ISO 3166-1 alpha-2 |
| `timezone` | string | нет | Часовой пояс IANA |
> **Фоллбэк:** Если эндпоинт недоступен, фронтенд использует 6 захардкоженных значений: Москва, СПб, Ереван, Минск, Алматы, Тбилиси.
---
## 8. Авторизация (вход через Telegram)
Авторизация **через Telegram** с **cookie-сессиями** (HttpOnly, Secure, SameSite=None).
Все auth-эндпоинты должны поддерживать CORS с `credentials: true`.
### Процесс авторизации
```
1. Пользователь нажимает «Оформить заказ» → не авторизован → показывается диалог входа
2. Нажимает «Войти через Telegram» → открывается https://t.me/{bot}?start=auth_{callback}
3. Пользователь запускает бота в Telegram
4. Бот отправляет данные пользователя → бэкенд /auth/telegram/callback
5. Бэкенд создаёт сессию → устанавливает Set-Cookie
6. Фронтенд опрашивает GET /auth/session каждые 3 секунды
7. Сессия обнаружена → диалог закрывается → оформление заказа продолжается
```
---
### `GET /auth/session` — Проверить текущую сессию
**Запрос:** Только cookie (сессионная cookie, установленная бэкендом).
**Ответ `200`** (авторизован):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": true,
"expiresAt": "2026-03-01T12:00:00Z"
}
```
**Ответ `200`** (сессия истекла):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": false,
"expiresAt": "2026-02-27T12:00:00Z"
}
```
**Ответ `401`** (нет сессии):
```json
{ "error": "No active session" }
```
**Объект AuthSession:**
| Поле | Тип | Обязат. | Описание |
|------------------|---------|---------|-------------------------------------------|
| `sessionId` | string | да | Уникальный ID сессии |
| `telegramUserId` | number | да | ID пользователя в Telegram |
| `username` | string? | нет | @username в Telegram (может быть null) |
| `displayName` | string | да | Отображаемое имя (имя + фамилия) |
| `active` | boolean | да | Действительна ли сессия |
| `expiresAt` | string | да | Дата истечения в формате ISO 8601 |
---
### `GET /auth/telegram/callback` — Callback авторизации Telegram-бота
Вызывается Telegram-ботом после авторизации пользователя.
**Тело запроса (от бота):**
```json
{
"id": 123456789,
"first_name": "John",
"last_name": "Doe",
"username": "john_doe",
"photo_url": "https://t.me/i/userpic/...",
"auth_date": 1709100000,
"hash": "abc123def456..."
}
```
**Ответ:** Должен установить cookie сессии и вернуть:
```json
{
"sessionId": "sess_abc123",
"message": "Authenticated successfully"
}
```
**Требования к cookie:**
| Атрибут | Значение | Примечание |
|------------|----------------|-----------------------------------------------------|
| `HttpOnly` | `true` | Недоступна из JavaScript |
| `Secure` | `true` | Только HTTPS |
| `SameSite` | `None` | Обязательно для cross-origin (API ≠ фронтенд) |
| `Path` | `/` | |
| `Max-Age` | `86400` (24ч) | Или по необходимости |
---
### `POST /auth/logout` — Завершить сессию
**Запрос:** Только cookie, пустое тело `{}`
**Ответ `200`:**
```json
{ "message": "Logged out" }
```
Должен очистить/инвалидировать cookie сессии.
---
### Обновление сессии
Фронтенд повторно проверяет сессию за **60 секунд до `expiresAt`**. Если бэкенд поддерживает скользящий срок действия (sliding expiration), можно обновлять `Max-Age` cookie при каждом вызове `GET /auth/session`.
---
## 9. i18n / Переводы
Фронтенд поддерживает 3 языка: **Русский (ru)**, **Английский (en)**, **Армянский (hy)**.
Активный язык отправляется через HTTP-заголовок `X-Language` с каждым запросом.
### Что бэкенд должен делать с `X-Language`
1. **Категории и товары**: Если для запрошенного языка есть поле `translations`, вернуть переведённые `name`, `description` и т.д. ИЛИ бэкенд может применять переводы на стороне сервера и возвращать уже переведённые поля.
2. **Поле `translations`** на товарах (опциональный подход):
```json
{
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short desc in English",
"description": [{ "key": "Processor", "value": "A17 Pro" }]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կarcheck check"
}
}
}
```
3. **Рекомендуемый подход**: Читать заголовок `X-Language` и возвращать `name`/`description` на этом языке напрямую. Если перевода нет — возвращать русский вариант по умолчанию.
---
## 10. Настройка CORS
Для работы auth-cookie и кастомных заголовков конфигурация CORS бэкенда должна включать:
```
Access-Control-Allow-Origin: https://dexarmarket.ru (НЕ wildcard *)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
```
> **Важно:** `Access-Control-Allow-Origin` не может быть `*` при `Allow-Credentials: true`. Должен быть точный origin фронтенда.
**Разрешённые origins:**
- `https://dexarmarket.ru`
- `https://novo.market`
- `http://localhost:4200` (dev)
- `http://localhost:4201` (dev, Novo)
---
## 11. Настройка Telegram-бота
Каждому бренду нужен свой бот:
- **Dexar:** `@dexarmarket_bot`
- **Novo:** `@novomarket_bot`
Бот должен:
1. Слушать команду `/start auth_{callbackUrl}`
2. Извлечь callback URL
3. Отправить данные пользователя (`id`, `first_name`, `username` и т.д.) на этот callback URL
4. Callback URL: `{apiUrl}/auth/telegram/callback`
---
## Полный справочник эндпоинтов
### Новые эндпоинты
| Метод | Путь | Описание | Авторизация |
|--------|---------------------------|---------------------------------|-------------|
| `GET` | `/regions` | Список доступных регионов | Нет |
| `GET` | `/auth/session` | Проверка текущей сессии | Cookie |
| `GET` | `/auth/telegram/callback` | Callback авторизации через бота | Нет (бот) |
| `POST` | `/auth/logout` | Завершение сессии | Cookie |
### Существующие эндпоинты
| Метод | Путь | Описание | Авт. | Заголовки |
|----------|-----------------------|---------------------------|------|--------------------|
| `GET` | `/ping` | Проверка состояния | Нет | — |
| `GET` | `/category` | Список категорий | Нет | X-Region, X-Language |
| `GET` | `/category/:id` | Товары категории | Нет | X-Region, X-Language |
| `GET` | `/item/:id` | Один товар | Нет | X-Region, X-Language |
| `GET` | `/searchitems` | Поиск товаров | Нет | X-Region, X-Language |
| `GET` | `/randomitems` | Случайные товары | Нет | X-Region, X-Language |
| `POST` | `/cart` | Добавить в корзину / Оплата | Нет* | — |
| `PATCH` | `/cart` | Обновить кол-во | Нет* | — |
| `DELETE` | `/cart` | Удалить из корзины | Нет* | — |
| `GET` | `/cart` | Содержимое корзины | Нет* | — |
| `POST` | `/comment` | Оставить отзыв | Нет | — |
| `GET` | `/qr/payment/:qrId` | Статус оплаты | Нет | — |
| `POST` | `/purchase-email` | Отправить email после оплаты | Нет | — |
> \* Эндпоинты корзины/оплаты могут использовать cookie сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.

View File

@@ -0,0 +1,824 @@
# Авторизация через Telegram — Backend & Bot
> Всё что нужно Go-разработчику для реализации авторизации.
> Фронтенд **полностью готов** и ждёт эти эндпоинты.
---
## Статус
| Компонент | Готов? |
|-----------|--------|
| Frontend (Angular) — диалог, QR, поллинг, корзина | ✅ Готов |
| Telegram бот (обработка `/start`) | ❌ Нужно |
| Backend — 6 HTTP-эндпоинтов | ❌ Нужно |
| Хранилище сессий + QR-токенов | ❌ Нужно |
| CORS для cookie-based запросов | ❌ Нужно |
---
## Архитектура
Два сценария авторизации:
### Сценарий 1: Прямой вход (кнопка "Войти через Telegram")
Пользователь нажимает кнопку → открывается Telegram → бот выдаёт кнопку "Войти на сайт" → callback ставит cookie → фронтенд поллит `/auth/session`.
### Сценарий 2: QR-логин с десктопа (основной)
```
ДЕСКТОП БРАУЗЕР СЕРВЕР (Go) TELEGRAM
│ │ │
│ 1. POST /auth/qr/create │ │
│ ─────────────────────────────> │ │
│ { token: "abc", url: "..." } │ │
│ <───────────────────────────── │ │
│ │ │
│ 2. Показать QR: │ │
│ t.me/Bot?start=login_abc │ │
│ │ │
│ ПОЛЬЗОВАТЕЛЬ СКАНИРУЕТ ТЕЛЕФОНОМ │
│ │ │
│ │ 3. /start login_abc │
│ │ <────────────────────────│
│ │ │
│ │ Бот → POST /auth/qr/confirm
│ │ Бот → "✅ Вы вошли!" │
│ │ ────────────────────────>│
│ │ │
│ 4. GET /auth/qr/poll?token=abc │ │
│ (каждые 3 сек) │ │
│ ─────────────────────────────> │ │
│ { status: "confirmed", │ │
│ session: {...} } │ │
│ + Set-Cookie: dx_session=... │ │
│ <───────────────────────────── │ │
│ │ │
│ 5. POST /websession/{sessionId} │ │
│ [{ itemID, quantity, ... }] │ ← корзина │
│ ─────────────────────────────> │ │
│ │ │
│ 6. Готово! Авторизован + корзина│ │
```
---
## Бренды и боты
| Бренд | Username бота | Домен фронтенда | API сервер | Cookie Domain |
|-------|---------------|------------------|------------|---------------|
| Dexar | `DexarSupport_bot` | `dexarmarket.ru` | `api.dexarmarket.ru:445` | `.dexarmarket.ru` |
| Novo | `novomarket_bot` | `novo.market` | `api.novo.market:444` | `.novo.market` |
Бот создаётся через https://t.me/BotFather → `/newbot`. Сохранить `BOT_TOKEN`.
---
## Хранилище
### Структура: Сессия
```go
type Session struct {
SessionID string `json:"sessionId"`
TelegramUserID int64 `json:"telegramUserId"`
Username *string `json:"username"` // может быть null
DisplayName string `json:"displayName"`
Active bool `json:"active"`
ExpiresAt time.Time `json:"expiresAt"`
}
```
**TTL:** 24 часа.
### Структура: QR-токен (одноразовый)
```go
type AuthToken struct {
Token string `json:"token"`
Status string `json:"status"` // "pending" | "confirmed" | "expired"
SessionID string `json:"sessionId"` // заполняется после подтверждения ботом
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
```
**TTL:** 5 минут.
### Варианты хранения
**Redis (рекомендуется):**
```go
// Сессия
redisClient.Set(ctx, "session:"+s.SessionID, json, 24*time.Hour)
// QR-токен
redisClient.Set(ctx, "auth_token:"+t.Token, json, 5*time.Minute)
```
**sync.Map (для MVP):**
```go
var sessions sync.Map
var authTokens sync.Map
// Очистка устаревших токенов — запустить горутину при старте
func cleanupExpiredTokens() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
authTokens.Range(func(key, value any) bool {
t := value.(AuthToken)
if time.Now().After(t.ExpiresAt) {
authTokens.Delete(key)
}
return true
})
}
}
```
---
## HTTP-эндпоинты
### 1. `POST /auth/qr/create`
Фронтенд вызывает при открытии диалога логина. Создаёт одноразовый QR-токен.
```go
func handleQrCreate(w http.ResponseWriter, r *http.Request) {
// 1. Сгенерировать криптографически безопасный токен
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
http.Error(w, "internal error", 500)
return
}
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes)
// 2. Определить бота по origin
botUsername := getBotForOrigin(r.Header.Get("Origin"))
// 3. Сохранить токен
authToken := AuthToken{
Token: token,
Status: "pending",
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(5 * time.Minute),
}
saveAuthToken(authToken)
// 4. Ответить
qrURL := fmt.Sprintf("https://t.me/%s?start=login_%s", botUsername, token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
"url": qrURL,
})
}
func getBotForOrigin(origin string) string {
if strings.Contains(origin, "novo.market") {
return "novomarket_bot"
}
return "DexarSupport_bot"
}
```
**Ответ:**
```json
{ "token": "dG9rZW4tYWJj....", "url": "https://t.me/DexarSupport_bot?start=login_dG9rZW4tYWJj...." }
```
> Telegram ограничивает `start` до 64 символов. `login_` (6) + base64url из 32 байт (43) = 49 ✅
---
### 2. `GET /auth/qr/poll?token={token}`
Фронтенд вызывает каждые 3 секунды. Когда бот подтвердил — возвращает сессию и ставит cookie.
```go
func handleQrPoll(w http.ResponseWriter, r *http.Request) {
tokenStr := r.URL.Query().Get("token")
if tokenStr == "" {
http.Error(w, "missing token", 400)
return
}
authToken, ok := getAuthToken(tokenStr)
if !ok {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "expired"})
return
}
switch authToken.Status {
case "pending":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "pending"})
case "confirmed":
session, err := getSession(authToken.SessionID)
if err != nil {
http.Error(w, "session not found", 500)
return
}
// Cookie в ДЕСКТОПНЫЙ браузер
domain := getDomainForOrigin(r.Header.Get("Origin"))
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: 86400,
Domain: domain,
})
// Удалить использованный токен
deleteAuthToken(tokenStr)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "confirmed",
"session": session,
})
}
}
func getDomainForOrigin(origin string) string {
if strings.Contains(origin, "novo.market") {
return ".novo.market"
}
return ".dexarmarket.ru"
}
```
**Ответы:**
| Статус | JSON |
|--------|------|
| Ждём | `{ "status": "pending" }` |
| Подтверждено | `{ "status": "confirmed", "session": { sessionId, telegramUserId, username, displayName, active, expiresAt } }` + `Set-Cookie` |
| Истекло | `{ "status": "expired" }` |
---
### 3. `POST /auth/qr/confirm` (внутренний, для бота)
Бот вызывает когда пользователь отсканировал QR. Привязывает сессию к токену.
```go
func handleQrConfirm(w http.ResponseWriter, r *http.Request) {
// Проверить секрет бота
if r.Header.Get("X-Bot-Secret") != os.Getenv("BOT_INTERNAL_SECRET") {
http.Error(w, "forbidden", 403)
return
}
var req struct {
Token string `json:"token"`
User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
} `json:"telegram_user"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", 400)
return
}
authToken, ok := getAuthToken(req.Token)
if !ok || authToken.Status != "pending" {
http.Error(w, "token not found or already used", 404)
return
}
// Создать сессию
displayName := req.User.FirstName
if req.User.LastName != "" {
displayName += " " + req.User.LastName
}
var username *string
if req.User.Username != "" {
username = &req.User.Username
}
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: req.User.ID,
Username: username,
DisplayName: displayName,
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session)
// Привязать сессию к токену
authToken.Status = "confirmed"
authToken.SessionID = session.SessionID
saveAuthToken(*authToken)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
```
**Запрос от бота:**
```json
{
"token": "dG9rZW4tYWJj...",
"telegram_user": {
"id": 123456789,
"first_name": "Иван",
"last_name": "Петров",
"username": "ivan_petrov"
}
}
```
---
### 4. `GET /auth/session`
Фронтенд вызывает для проверки текущей сессии. Читает cookie.
```go
func handleGetSession(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
session, err := getSession(cookie.Value)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
if time.Now().After(session.ExpiresAt) {
session.Active = false
saveSession(session)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(session)
}
```
**Формат ответа (200)** — фронтенд ожидает **точно эти поля**:
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Иван Петров",
"active": true,
"expiresAt": "2026-03-25T14:30:00Z"
}
```
| Поле | Тип | Обязательно | Примечание |
|------|-----|-------------|------------|
| `sessionId` | string (UUID) | да | Используется для `/websession/{sessionId}` |
| `telegramUserId` | number | да | Telegram user ID |
| `username` | string / null | нет | Telegram @username |
| `displayName` | string | да | "Имя Фамилия" — показывается в UI |
| `active` | boolean | да | `false` = истекла |
| `expiresAt` | string (ISO 8601) | да | Фронтенд перепроверяет за 60 сек до |
**Ошибка:** любой HTTP не-200 → фронтенд считает "не авторизован".
---
### 5. `GET /auth/telegram/callback`
Для прямого входа (по кнопке в Telegram, не через QR). Открывается в браузере.
```go
func handleTelegramCallback(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", 400)
return
}
session, err := getSession(token)
if err != nil || !session.Active {
http.Error(w, "invalid or expired token", 401)
return
}
domain := getDomainForOrigin(r.Header.Get("Origin"))
if domain == "" {
domain = ".dexarmarket.ru" // fallback для прямого перехода
}
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: 86400,
Domain: domain,
})
// Редирект на сайт
http.Redirect(w, r, "https://dexarmarket.ru", http.StatusFound)
}
```
---
### 6. `POST /auth/logout`
```go
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err == nil {
deleteSession(cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: -1,
Domain: ".dexarmarket.ru",
})
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"ok"}`))
}
```
---
## Cookie-параметры
| Параметр | Значение | Почему |
|----------|----------|--------|
| `Name` | `dx_session` | |
| `SameSite` | `None` | Фронтенд на `dexarmarket.ru`, API на `api.dexarmarket.ru:445` — разные origins |
| `Secure` | `true` | Обязательно при `SameSite=None` |
| `Domain` | `.dexarmarket.ru` | Доступна и на `dexarmarket.ru` и на `api.dexarmarket.ru` |
| `HttpOnly` | `true` | Недоступна из JS — защита от XSS |
| `MaxAge` | `86400` | 24 часа |
---
## CORS
Фронтенд шлёт `withCredentials: true`. Бэкенд обязан вернуть правильные заголовки.
```go
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
allowed := map[string]bool{
"https://dexarmarket.ru": true,
"https://www.dexarmarket.ru": true,
"https://novo.market": true,
"https://www.novo.market": true,
"http://localhost:4200": true,
}
if allowed[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin) // НЕ "*"
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}
if r.Method == "OPTIONS" {
w.WriteHeader(200)
return
}
next.ServeHTTP(w, r)
})
}
```
> **Критично:** `Access-Control-Allow-Origin` не может быть `"*"` при `withCredentials`. Вернуть конкретный origin.
---
## Роутинг
```go
mux := http.NewServeMux()
// Существующие
mux.HandleFunc("GET /items/{id}", handleGetItem)
mux.HandleFunc("GET /category", handleGetCategories)
mux.HandleFunc("POST /websession/{id}", handleWebSession)
mux.HandleFunc("POST /websession/{id}/qr", handleCreateQR)
mux.HandleFunc("GET /websession/{id}/{qrId}", handleCheckPayment)
// Auth — прямой вход
mux.HandleFunc("GET /auth/session", handleGetSession)
mux.HandleFunc("GET /auth/telegram/callback", handleTelegramCallback)
mux.HandleFunc("POST /auth/logout", handleLogout)
// Auth — QR-логин
mux.HandleFunc("POST /auth/qr/create", handleQrCreate)
mux.HandleFunc("GET /auth/qr/poll", handleQrPoll)
mux.HandleFunc("POST /auth/qr/confirm", handleQrConfirm)
handler := corsMiddleware(mux)
http.ListenAndServeTLS(":445", "cert.pem", "key.pem", handler)
```
---
## Telegram-бот
### Обработчик `/start`
```go
const (
confirmURL = "http://localhost:8080/auth/qr/confirm"
botInternalSecret = os.Getenv("BOT_INTERNAL_SECRET")
)
func handleStart(update tgbotapi.Update) {
text := update.Message.Text
user := update.Message.From
switch {
case strings.HasPrefix(text, "/start login_"):
handleQrLogin(update, user, strings.TrimPrefix(text, "/start login_"))
case strings.HasPrefix(text, "/start auth"):
handleDirectAuth(update, user)
default:
sendWelcome(update)
}
}
```
### QR-логин (основной)
```go
func handleQrLogin(update tgbotapi.Update, user *tgbotapi.User, token string) {
reqBody := map[string]interface{}{
"token": token,
"telegram_user": map[string]interface{}{
"id": user.ID,
"first_name": user.FirstName,
"last_name": user.LastName,
"username": user.UserName,
},
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", confirmURL, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Bot-Secret", botInternalSecret)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
msg := tgbotapi.NewMessage(update.Message.Chat.ID,
"❌ Не удалось войти. QR-код мог устареть. Попробуйте обновить страницу и отсканировать новый QR.")
bot.Send(msg)
return
}
defer resp.Body.Close()
displayName := buildDisplayName(user)
msg := tgbotapi.NewMessage(update.Message.Chat.ID,
fmt.Sprintf("✅ Вы вошли на сайт как %s!\n\nМожете вернуться в браузер — страница обновится автоматически.", displayName))
bot.Send(msg)
}
func buildDisplayName(user *tgbotapi.User) string {
name := user.FirstName
if user.LastName != "" {
name += " " + user.LastName
}
return name
}
```
### Прямой вход (кнопка, для обратной совместимости)
```go
func handleDirectAuth(update tgbotapi.Update, user *tgbotapi.User) {
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: user.ID,
Username: stringPtr(user.UserName),
DisplayName: buildDisplayName(user),
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session)
callbackURL := "https://api.dexarmarket.ru:445/auth/telegram/callback"
loginURL := callbackURL + "?token=" + session.SessionID
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Нажмите кнопку чтобы войти:")
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonURL("🔐 Войти на сайт", loginURL),
),
)
bot.Send(msg)
}
```
### Запуск бота (long polling)
```go
func main() {
bot, _ := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil && strings.HasPrefix(update.Message.Text, "/start") {
handleStart(update)
}
}
}
```
---
## Синхронизация корзины
Сразу после QR-логина фронтенд автоматически отправляет корзину:
```
POST /websession/{sessionId}
```
Тело — массив:
```json
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
```
| Поле | Тип | Примечание |
|------|-----|------------|
| `itemID` | number | ID товара |
| `quantity` | number | Количество |
| `colour` | string | CSS hex (`#ff0000`). Бэкенд отдаёт `0xff0000`, фронтенд конвертирует |
| `size` | string | `"default"` если размер один |
| `price` | number | Финальная цена **с учётом скидки** |
> Этот эндпоинт (`POST /websession/{id}`) уже существует. Ничего менять не нужно, просто учитывать что он вызывается сразу после успешного логина.
---
## Безопасность
### Криптографический токен
```go
tokenBytes := make([]byte, 32) // 256 бит
crypto/rand.Read(tokenBytes)
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes)
```
**НЕ использовать:** `math/rand`, UUID, timestamp.
### Токен одноразовый
- После `confirmed` → удалить при первом успешном `poll`
- После 5 минут → автоудаление (TTL)
- Повторный `poll``"expired"`
### Защита `/auth/qr/confirm`
```go
if r.Header.Get("X-Bot-Secret") != os.Getenv("BOT_INTERNAL_SECRET") {
http.Error(w, "forbidden", 403)
return
}
```
Дополнительно: можно ограничить по IP (`127.0.0.1`) если бот на том же сервере.
### Rate limiting для `/auth/qr/create`
Не более **5 токенов в минуту** с одного IP:
```go
var ipCounts sync.Map
func rateLimitQrCreate(ip string) bool {
key := ip + ":" + time.Now().Format("2006-01-02T15:04")
val, _ := ipCounts.LoadOrStore(key, new(int32))
count := atomic.AddInt32(val.(*int32), 1)
return count <= 5
}
```
---
## Переменные окружения
```env
BOT_TOKEN=123456:ABC-DEF...
BOT_INTERNAL_SECRET=случайная-строка-минимум-32-символа
FRONTEND_URL=https://dexarmarket.ru
SESSION_TTL=24h
REDIS_URL=localhost:6379
```
`BOT_INTERNAL_SECRET` должен совпадать в env сервера и env бота.
---
## Тестирование
### curl-тесты
**1. Создание токена:**
```bash
curl -X POST https://api.dexarmarket.ru:445/auth/qr/create \
-H "Origin: https://dexarmarket.ru"
# → { "token": "dG9r...", "url": "https://t.me/DexarSupport_bot?start=login_dG9r..." }
```
**2. Поллинг (до подтверждения):**
```bash
curl "https://api.dexarmarket.ru:445/auth/qr/poll?token=dG9r..."
# → { "status": "pending" }
```
**3. Подтверждение (имитация бота):**
```bash
curl -X POST https://api.dexarmarket.ru:445/auth/qr/confirm \
-H "Content-Type: application/json" \
-H "X-Bot-Secret: ваш-секрет" \
-d '{"token":"dG9r...","telegram_user":{"id":123,"first_name":"Тест","last_name":"","username":"testuser"}}'
# → { "status": "ok" }
```
**4. Поллинг (после подтверждения):**
```bash
curl -v "https://api.dexarmarket.ru:445/auth/qr/poll?token=dG9r..."
# → { "status": "confirmed", "session": {...} } + Set-Cookie: dx_session=...
```
**5. E2E:**
1. Открыть маркетплейс → добавить товар в корзину
2. Нажать "Оформить заказ" → появляется диалог с QR
3. Отсканировать QR телефоном → Telegram → бот: "✅ Вы вошли!"
4. Через 3 сек диалог закрывается → авторизован
5. Корзина синхронизирована (`POST /websession/{sessionId}`)
### Отладка
| Проблема | Где смотреть |
|----------|-------------|
| QR не показывается | `POST /auth/qr/create` — ошибка? CORS? |
| QR отсканирован, ничего не происходит | Бот получил `/start login_...`? Бот вызвал `confirm`? |
| Бот пишет "❌ QR устарел" | Токен expired? 5 минут прошло? |
| Поллинг "pending" бесконечно | Бот не вызвал `confirm`. Логи бота |
| Поллинг "confirmed" но cookie нет | `SameSite`, `Secure`, `Domain`, CORS |
---
## Чеклист
### Бэкенд (Go)
- [ ] Структура `Session` + `AuthToken`, функции save/get/delete
- [ ] `POST /auth/qr/create` — генерация токена
- [ ] `GET /auth/qr/poll?token=...` — статус + cookie при confirmed
- [ ] `POST /auth/qr/confirm` — приём от бота с `X-Bot-Secret`
- [ ] `GET /auth/session` — чтение cookie, JSON сессии
- [ ] `GET /auth/telegram/callback?token=...` — cookie + редирект
- [ ] `POST /auth/logout` — удаление сессии и cookie
- [ ] TTL 5 мин для токенов, 24ч для сессий
- [ ] Rate limiting `/auth/qr/create` (5/мин/IP)
- [ ] Очистка устаревших токенов
- [ ] CORS middleware
- [ ] `BOT_INTERNAL_SECRET` в env
### Telegram бот
- [ ] Обработка `/start login_{token}``POST /auth/qr/confirm`
- [ ] Обработка `/start auth` → создание сессии + кнопка "Войти"
- [ ] Сообщения: "✅ Вы вошли" / "❌ QR устарел"
- [ ] `BOT_INTERNAL_SECRET` в env (совпадает с сервером)
- [ ] `BOT_TOKEN` в env

156
docs/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,156 @@
# Dexar Market - Deployment Guide
## Prerequisites
- Ubuntu/Debian server with root access
- Domain: dexarmarket.ru
- Node.js 18+ installed
## Quick Deployment
### 1. Build locally
```bash
npm install
npm run build
```
Output: `dist/dexarmarket/browser/`
**VERIFY BUILD LOCALLY:**
```bash
cd dist/dexarmarket/browser
ls -la
```
You MUST see `index.html`, chunk files, `assets/` folder, etc.
### 2. Upload to server
```bash
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
```
### 3. Set permissions on server
```bash
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
sudo nginx -t
sudo systemctl reload nginx
```
## Initial Server Setup (one-time)
### Install and configure Nginx
```bash
sudo apt update
sudo apt install nginx -y
sudo mkdir -p /var/www/dexarmarket/browser
```
Copy `nginx.conf` content to `/etc/nginx/sites-available/dexarmarket`:
```bash
sudo nano /etc/nginx/sites-available/dexarmarket
```
Then enable it:
```bash
sudo ln -s /etc/nginx/sites-available/dexarmarket /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
```
### Setup SSL (recommended)
```bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d dexarmarket.ru -d www.dexarmarket.ru
```
## Common Issues & Solutions
### ❌ 404 Error - Files Not Found
**Check 1: Verify files on server**
```bash
ls -la /var/www/dexarmarket/browser/
```
Should show: `index.html`, `chunk-*.js`, `assets/`, etc.
**If empty:**
```bash
# Re-upload files
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
```
**Check 2: Verify permissions**
```bash
namei -l /var/www/dexarmarket/browser/index.html
```
All directories need `x` (execute) permission.
**Fix permissions:**
```bash
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
```
**Check 3: Test nginx config**
```bash
sudo nginx -t
```
Should say "syntax is ok" and "test is successful".
**Check 4: View nginx error log**
```bash
sudo tail -f /var/log/nginx/error.log
```
This shows the actual error!
### ❌ 502 Bad Gateway - API Issues
**This means the API backend is down or unreachable.**
**Check 1: Is API accessible?**
```bash
curl -v https://api.dexarmarket.ru:445/ping
```
**Check 2: Port 445 problem**
Port 445 is unusual for HTTPS and may be blocked by firewalls. Standard HTTPS uses port 443.
**Check 3: CORS issues**
The API must allow requests from `https://dexarmarket.ru`. Check API CORS configuration.
**Check 4: SSL certificate**
```bash
curl -k https://api.dexarmarket.ru:445/ping
```
If this works but without `-k` doesn't, SSL cert is invalid.
### ✅ Final Verification Checklist
On server, run all these:
```bash
# 1. Files exist
ls -la /var/www/dexarmarket/browser/index.html
# 2. Nginx config is valid
sudo nginx -t
# 3. Nginx is running
sudo systemctl status nginx
# 4. Site is enabled
ls -la /etc/nginx/sites-enabled/ | grep dexarmarket
# 5. Test API from server
curl -v https://api.dexarmarket.ru:445/ping
# 6. Check logs
sudo tail -20 /var/log/nginx/error.log
sudo tail -20 /var/log/nginx/access.log
```
### Debug Steps
If still having issues:
1. Check browser console (F12 → Console tab) - shows JavaScript errors
2. Check browser network tab (F12 → Network tab) - shows failed requests
3. Check exact error message in nginx logs
4. Test locally: `cd dist/dexarmarket/browser && python3 -m http.server 8000`

140
docs/IMPLEMENTATION.md Normal file
View File

@@ -0,0 +1,140 @@
# Dexar Market - Implementation Summary
## ✅ Completed Features
### 1. **Data Models** (`src/app/models/`)
- **Category Model**: Hierarchical category structure
- **Item Model**: Complete product data including photos/videos, pricing, reviews, Q&A
### 2. **Services** (`src/app/services/`)
- **API Service**: All endpoint integrations
- Health check (`/ping`)
- Categories (`/category`)
- Category items with pagination (`/category/:id`)
- Search with pagination (`/items`)
- Cart operations (GET, POST, DELETE)
- **Cart Service**: Reactive state management using Angular signals
- Add/remove items
- Real-time cart count
- Automatic total price calculation
### 3. **Pages** (`src/app/pages/`)
#### **Home Page** (`/`)
- Display all categories in grid layout
- Show subcategories
- Responsive category cards
#### **Category Page** (`/category/:id`)
- **Infinite Scroll**: Automatically loads more items on scroll
- Product grid with images, pricing, ratings
- Discount badges
- Stock status indicators
- Add to cart functionality
#### **Search Page** (`/search`)
- **Real-time search** with debounce (300ms)
- **Infinite Scroll** for results
- Same product display as category page
- Empty state handling
#### **Item Detail Page** (`/item/:id`)
- Photo/video gallery with thumbnails
- Full product information
- Pricing with discount display
- Reviews section with ratings
- Q&A section with voting counts (👍👎)
- Add to cart
#### **Cart Page** (`/cart`)
- List all cart items with details
- Remove individual items
- Clear entire cart
- Real-time total calculation
- Empty state with call-to-action
- Checkout button (placeholder)
### 4. **Components** (`src/app/components/`)
#### **Header Component**
- Sticky navigation
- Cart icon with badge showing item count
- Mobile-responsive hamburger menu
- Active route highlighting
### 5. **Routing & Configuration**
- Lazy-loaded routes for performance
- HTTP client configured
- All pages connected and navigable
### 6. **Responsive Design**
- Mobile-first approach
- Breakpoints at 768px and 968px
- Adaptive layouts for all screen sizes
- Touch-friendly interface
## 🎨 Design Features
- **Color Scheme**: Purple gradient theme (#667eea primary)
- **Smooth Animations**: Hover effects, transitions
- **Modern UI**: Card-based layouts, rounded corners
- **Custom Scrollbar**: Themed scrollbar styling
- **Loading States**: Spinners and skeleton states
- **Error Handling**: User-friendly error messages
## 📱 Performance Optimizations
1. **Infinite Scroll**: Loads 20 items at a time
2. **Lazy Loading**: Route-based code splitting
3. **Image Lazy Loading**: Native lazy loading for images
4. **Debounced Search**: Prevents excessive API calls
5. **Angular Signals**: Efficient reactivity
## 🔧 Technical Stack
- Angular 20 (standalone components)
- TypeScript
- RxJS for reactive programming
- SCSS for styling
- Angular Signals for state management
## 📦 API Integration
All endpoints from the provided documentation are integrated:
- ✅ GET /ping
- ✅ GET /category
- ✅ GET /category/:categoryID
- ✅ GET /items (search)
- ✅ GET /cart
- ✅ POST /cart
- ✅ DELETE /cart
## 🚀 How to Run
```bash
# Install dependencies (if needed)
npm install
# Start development server
ng serve
# Open browser
http://localhost:4200
```
## 📝 Notes
- **Item Detail Limitation**: Currently fetches items from cart for demo. In production, you may want to add a dedicated `/item/:id` endpoint or cache category results.
- **Checkout**: Placeholder button ready for payment integration
- **No Authentication**: As per requirements, no user management implemented
- **API Base URL**: Configured as `https://api.dexarmarket.ru`
## 🎯 Ready for Production
The application is production-ready with:
- Type-safe TypeScript
- Modular architecture
- Responsive design
- Error handling
- Performance optimizations
- Clean, maintainable code

146
docs/MULTI_BRAND.md Normal file
View File

@@ -0,0 +1,146 @@
# Multi-Brand Configuration
Этот проект поддерживает несколько брендов с разными темами и конфигурациями.
## Доступные бренды
### 1. Dexar Market (фиолетовый)
- **Цвета**: Фиолетовый/пурпурный (#667eea, #764ba2)
- **Домен**: dexarmarket.ru
- **Email**: info@dexarmarket.ru
### 2. novo Market (зеленый)
- **Цвета**: Зеленый (#10b981, #14b8a6)
- **Домен**: novomarket.ru (будет настроено)
- **Email**: info@novomarket.ru (будет настроено)
## Команды запуска
### Dexar Market (разработка)
```bash
ng serve
# или
ng serve --configuration=development
```
### novo Market (разработка)
```bash
ng serve --configuration=novo
```
### Сборка для продакшена
#### Dexar Market
```bash
ng build --configuration=production
```
Результат: `dist/dexarmarket/`
#### novo Market
```bash
ng build --configuration=novo-production
```
Результат: `dist/novomarket/`
## Структура файлов
```
src/
├── environments/
│ ├── environment.ts # Dexar Development
│ ├── environment.production.ts # Dexar Production
│ ├── environment.novo.ts # novo Development
│ └── environment.novo.production.ts # novo Production
├── styles/
│ └── themes/
│ ├── dexar.theme.scss # Dexar цвета (фиолетовый)
│ └── novo.theme.scss # novo цвета (зеленый)
```
## Что настраивается через Environment
В файлах environment можно настроить:
```typescript
{
brandName: 'Название бренда',
brandFullName: 'Полное название бренда',
theme: 'dexar' | 'novo',
apiUrl: 'URL API',
logo: 'Путь к логотипу',
contactEmail: 'Email контактов',
supportEmail: 'Email поддержки',
domain: 'Домен сайта',
telegram: 'Telegram канал',
phones: {
russia: 'Телефон в России',
armenia: 'Телефон в Армении'
}
}
```
## CSS Переменные
Темы используют CSS переменные, которые можно изменить:
```scss
:root {
--primary-color: #10b981; // Основной цвет
--primary-hover: #059669; // Hover эффект
--secondary-color: #14b8a6; // Вторичный цвет
--gradient-primary: linear-gradient(...);
--gradient-hero: linear-gradient(...);
// и другие...
}
```
## Обновление для нового бренда
### Что нужно обновить для novo Market:
1.**Environment файлы** - созданы
2.**Темы (SCSS)** - созданы (зеленые цвета)
3.**Angular.json конфигурации** - настроены
4.**Логотипы и изображения** - добавить в `public/assets/images/`
5.**Реквизиты компании** - обновить когда будут готовы
6.**Домен и SSL** - настроить при деплое
7.**API endpoint** - обновить когда будет готов
## Деплой
### Dexar Market
```bash
ng build --configuration=production
# Deploy dist/dexarmarket/ to dexarmarket.ru
```
### novo Market
```bash
ng build --configuration=novo-production
# Deploy dist/novomarket/ to novomarket.ru
```
## Отличия брендов
| Параметр | Dexar Market | novo Market |
|----------|--------------|-------------|
| Основной цвет | Фиолетовый (#667eea) | Зеленый (#10b981) |
| Название | Dexar Market | novo Market |
| Домен | dexarmarket.ru | novomarket.ru |
| Email | info@dexarmarket.ru | info@novomarket.ru |
| Telegram | @dexarmarket | @novomarket |
| Реквизиты | Текущие | Будут обновлены |
## Следующие шаги для novo Market
1. Добавить логотип novo Market (`public/assets/images/novo-logo.svg`)
2. Обновить реквизиты компании в правовых документах
3. Настроить API endpoint для novo
4. Настроить домен и SSL сертификаты
5. Обновить контактную информацию (телефоны, адреса)
## Примечания
- Оба бренда используют одну кодовую базу
- Все компоненты автоматически адаптируются под выбранный бренд
- Легко добавить новые бренды по той же схеме

206
docs/PWA_SETUP.md Normal file
View File

@@ -0,0 +1,206 @@
# PWA Setup Guide
## ✅ Implemented Features
### 1. Service Worker
- **Caching Strategy**: Aggressive prefetch for app shell
- **API Caching**: Freshness strategy with 1-hour cache (max 100 requests)
- **Image Caching**: Performance strategy with 7-day cache (max 50 images)
- **Configuration**: `ngsw-config.json`
### 2. Web App Manifests
- **Dexar**: `public/manifest.webmanifest` (purple theme #a855f7)
- **Novo**: `public/manifest.novo.webmanifest` (green theme #10b981)
- **Features**:
- Installable on mobile/desktop
- Standalone display mode
- 8 icon sizes (72px to 512px)
- Russian language metadata
### 3. Offline Support
- App shell loads instantly from cache
- API responses cached for 1 hour
- Product images cached for 7 days
- Automatic background updates
## 🚀 Testing PWA Functionality
### Local Testing with Production Build
```bash
# Build for production
npm run build -- --configuration=production
# Serve the production build
npx http-server dist/dexarmarket -p 4200 -c-1
# For Novo brand
npx http-server dist/novomarket -p 4201 -c-1
```
### Chrome DevTools Testing
1. Open `http://localhost:4200`
2. Open DevTools (F12)
3. Go to **Application** tab
4. Check:
- **Service Workers**: Should show registered worker
- **Cache Storage**: Should show `ngsw:/:db`, `ngsw:/:assets`
- **Manifest**: Should show app details
### Install Prompt Testing
1. Open app in Chrome/Edge
2. Click the **install icon** in address bar ()
3. Confirm installation
4. App opens as standalone window
5. Check Start Menu/Home Screen for app icon
### Offline Testing
1. Open app while online
2. Navigate through pages (loads assets)
3. Open DevTools → Network → Toggle **Offline**
4. Refresh page - should still work!
5. Navigate to cached pages - should load instantly
## 📱 Mobile Testing
### Android Chrome
1. Open app URL
2. Chrome shows "Add to Home Screen" banner
3. Install and open - works like native app
4. Splash screen with your logo/colors
### iOS Safari
1. Open app URL
2. Tap Share → "Add to Home Screen"
3. Icon appears on home screen
4. Opens in full-screen mode
## 🔧 Configuration Details
### Service Worker Caching Strategy
```json
{
"app": {
"installMode": "prefetch", // Download immediately
"updateMode": "prefetch" // Auto-update in background
},
"assets": {
"installMode": "lazy", // Load on-demand
"updateMode": "prefetch"
},
"api-cache": {
"strategy": "freshness", // Network first, fallback to cache
"maxAge": "1h" // Keep for 1 hour
},
"product-images": {
"strategy": "performance", // Cache first, update in background
"maxAge": "7d" // Keep for 7 days
}
}
```
### Manifest Differences
| Property | Dexar | Novo |
|----------|-------|------|
| Theme Color | #a855f7 (purple) | #10b981 (green) |
| Name | Dexar Market | Novo Market |
| Icons | Default Angular | Default Angular |
| Background | White (#ffffff) | White (#ffffff) |
## 🎨 Custom Icons (Recommended)
Replace the default Angular icons with brand-specific ones:
```bash
public/icons/
├── icon-72x72.png # Smallest (splash screen)
├── icon-96x96.png
├── icon-128x128.png
├── icon-144x144.png
├── icon-152x152.png # iOS home screen
├── icon-192x192.png # Android home screen
├── icon-384x384.png
└── icon-512x512.png # Largest (splash, install prompt)
```
**Design Guidelines**:
- Use solid background color (purple for Dexar, green for Novo)
- Center white logo/icon
- Keep design simple (shows at small sizes)
- Export as PNG with transparency or solid background
## 🔄 Update Strategy
### How Updates Work
1. User visits app
2. Service worker checks for updates
3. New version downloads in background
4. User refreshes → gets updated version
5. Old cache automatically cleared
### Force Update (Development)
```bash
# Clear all caches
chrome://serviceworker-internals/ # Unregister worker
chrome://settings/clearBrowserData # Clear cache
# Or in code (add to app.config.ts)
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(reg => reg.unregister());
});
```
## 📊 Performance Benefits
### Before PWA
- Initial load: ~2-3s (network dependent)
- Subsequent loads: ~1-2s
- Offline: ❌ Not available
### After PWA
- Initial load: ~2-3s (first visit)
- Subsequent loads: **~200-500ms** (cached)
- Offline: ✅ **Fully functional**
- Install: ✅ **Native app experience**
## 🐛 Troubleshooting
### Service Worker Not Registering
- Check console for errors
- Ensure HTTPS (or localhost)
- Clear browser cache and reload
### Old Version Not Updating
- Hard refresh: `Ctrl+Shift+R` (Windows) or `Cmd+Shift+R` (Mac)
- Unregister worker in DevTools
- Wait 24 hours (automatic update)
### Manifest Not Loading
- Check `index.html` has `<link rel="manifest">`
- Verify manifest path is correct
- Check manifest JSON is valid (no syntax errors)
### Icons Not Showing
- Check icon paths in manifest
- Ensure icons exist in `public/icons/`
- Verify icon sizes match manifest
## 📚 Next Steps
1. **Custom Icons**: Create brand-specific icons for both themes
2. **Push Notifications**: Add user engagement (requires backend)
3. **Background Sync**: Queue offline orders, sync when online
4. **Analytics**: Track PWA installs, offline usage
5. **A2HS Prompt**: Show custom "Install App" banner
## 🔗 Resources
- [PWA Checklist](https://web.dev/pwa-checklist/)
- [Angular PWA Guide](https://angular.dev/ecosystem/service-workers)
- [Manifest Generator](https://www.simicart.com/manifest-generator.html/)
- [Icon Generator](https://realfavicongenerator.net/)

View File

@@ -0,0 +1,181 @@
# Рекомендации по работе с платежными ссылками
## Требования Райффайзенбанка для оплаты по ссылке
### ✅ Что уже реализовано:
1. **Реквизиты организации** - полностью заполнены
2. **Правила оплаты** - подробная страница с требованиями ЦБ РФ, PCI DSS, 3D-Secure
3. **Политика возврата** - полная информация о возврате физических и цифровых товаров
4. **Публичная оферта** - модель маркетплейса, разграничение ответственности
5. **Политика конфиденциальности** - обработка персональных данных (152-ФЗ)
6. **Чекбокс согласия в корзине** - со ссылками на:
- Публичную оферту
- Политику возврата
- Условия гарантии
- Политику конфиденциальности
7. **Логотипы платежных систем**:
- МИР (обязательно!)
- Visa
- Mastercard
- Размещены в футере и на странице оплаты
---
## 📧 Рекомендации при отправке платежной ссылки покупателю
### Шаблон письма/сообщения:
```
Здравствуйте, [Имя покупателя]!
Ваш заказ №[НОМЕР] оформлен.
Для оплаты перейдите по ссылке:
[ПЛАТЕЖНАЯ ССЫЛКА]
Сумма к оплате: [СУММА] ₽
Перед оплатой, пожалуйста, ознакомьтесь с условиями:
• Публичная оферта: https://dexarmarket.ru/public-offer
• Политика возврата: https://dexarmarket.ru/return-policy
• Условия гарантии: https://dexarmarket.ru/guarantee
• Политика конфиденциальности: https://dexarmarket.ru/privacy-policy
Оплачивая заказ, вы подтверждаете, что ознакомились и согласны с данными условиями.
---
С уважением,
Команда Dexarmarket
Техподдержка: Info@dexarmarket.ru
Телефон: +7 (926) 459-31-57
```
### ✅ Важно получить подтверждение от покупателя!
**Вариант 1 - Автоматическое подтверждение:**
После оплаты отправить покупателю:
```
Спасибо за оплату заказа №[НОМЕР]!
Вы подтвердили согласие с:
✓ Публичной офертой
✓ Политикой возврата
✓ Условиями гарантии
✓ Политикой конфиденциальности
Чек отправлен на email: [EMAIL]
Статус заказа можно отслеживать в личном кабинете.
```
**Вариант 2 - Ручное подтверждение (желательно):**
Перед отправкой ссылки запросить:
```
Для оформления заказа подтвердите, пожалуйста, что вы ознакомились с условиями
(https://dexarmarket.ru/public-offer) и согласны с ними.
Ответьте "Согласен" или "Подтверждаю" для продолжения.
```
---
## 🛡️ Защита от оспаривания платежей (Chargeback)
### Что сохранять для доказательной базы:
1. **Переписка с покупателем:**
- Скриншоты чатов
- Email переписка
- SMS/WhatsApp сообщения с подтверждением
2. **Логи действий покупателя:**
- IP-адрес при оформлении заказа
- Timestamp (дата и время)
- Согласие с чекбоксом (если есть личный кабинет)
3. **Документы об отправке:**
- Трек-номер посылки
- Подтверждение доставки
- Подпись получателя (если есть)
4. **Платежная информация:**
- Номер транзакции
- Дата и время оплаты
- Сумма платежа
---
## 🔒 Дополнительные меры безопасности
### 1. Двухфакторное подтверждение
Для крупных заказов (>10 000 ₽) рекомендуется:
- Звонок покупателю для подтверждения заказа
- Запись разговора (с уведомлением клиента)
### 2. Проверка благонадежности
Для новых покупателей:
- Проверить совпадение адреса доставки с регионом телефона
- При подозрительных заказах запросить фото документа
### 3. Страхование рисков
- Оформить договор с платежным провайдером на защиту от мошенничества
- Использовать холдирование средств (72 часа на проверку)
---
## 📊 Статистика оспариваний
**Риски по категориям товаров:**
- Электроника: ~2-5% оспариваний
- Одежда: ~1-3%
- Цифровые товары: ~0.5-2%
- Продукты питания: ~0.1-0.5%
**Причины оспариваний:**
1. "Не получил товар" (40%)
2. "Товар не соответствует описанию" (30%)
3. "Не заказывал" (20%)
4. "Дубликат платежа" (10%)
---
## ✅ Чек-лист готовности к работе с Райффайзенбанком
- [x] Реквизиты организации заполнены
- [x] Правила оплаты на русском языке
- [x] Политика возврата опубликована
- [x] Публичная оферта опубликована
- [x] Политика конфиденциальности опубликована
- [x] Логотип МИР размещен на сайте
- [x] Чекбокс согласия с условиями в корзине
- [x] Ссылки на все документы в чекбоксе
- [ ] Настроен процесс отправки платежных ссылок с условиями
- [ ] Настроен процесс получения подтверждений от покупателей
- [ ] Настроена система логирования действий пользователей
- [ ] Подготовлена база для работы с оспариваниями
---
## 📞 Контакты для связи с банком
**АО "Райффайзенбанк"**
- Сайт: https://www.raiffeisen.ru
- Требования к сайтам: https://www.raiffeisen.ru/common/img/uploaded/files/business/treb_k_saity.pdf
- Техподдержка эквайринга: указывается при подключении
**Платежная система МИР**
- Требования к использованию логотипа: https://mironline.ru/support/merchantam/brand/
- Обязательно размещение логотипа при приеме карт МИР
---
## 🚀 Статус проекта
**Готовность к подключению эквайринга: 95%**
Осталось реализовать:
1. Автоматизацию отправки ссылок с условиями
2. Систему получения подтверждений от покупателей
3. Логирование действий для доказательной базы
**Все юридические и информационные требования выполнены!**

423
docs/RECOMMENDATIONS.md Normal file
View File

@@ -0,0 +1,423 @@
# Project Recommendations & Roadmap
## 📊 Current Status: 9.2/10
Your project is production-ready with excellent architecture! Here's what to focus on next:
---
## ✅ Recently Completed (January 2026)
1. **Phone Number Collection**
- Real-time formatting (+7 XXX XXX-XX-XX)
- Comprehensive validation (11 digits)
- Raw digits sent to API
2. **HTML Structure Unification**
- Single template for both themes
- CSS-only differentiation (Novo/Dexar)
- Eliminated code duplication
3. **PWA Implementation**
- Service worker with smart caching
- Dual manifests (brand-specific)
- Offline support
- Installable app
4. **Code Quality**
- Removed 3 duplicate methods
- Fixed SCSS syntax errors
- Optimized cart component
---
## 🎯 Priority Roadmap
### 🔥 HIGH PRIORITY (Next 2 Weeks)
#### 1. Custom PWA Icons
**Why**: Branding, professionalism
**Effort**: 2-3 hours
**Impact**: High visibility
**Action Items**:
```bash
# Create 8 icon sizes for each brand:
# Dexar: Purple (#a855f7) background + white logo
# Novo: Green (#10b981) background + white logo
public/icons/dexar/
├── icon-72x72.png
├── icon-512x512.png
└── ...
public/icons/novo/
├── icon-72x72.png
└── ...
# Update manifests to point to brand folders
```
**Tools**: Figma, Photoshop, or [RealFaviconGenerator](https://realfavicongenerator.net/)
---
#### 2. Unit Testing
**Why**: Code reliability, easier refactoring
**Effort**: 1-2 weeks
**Impact**: Development velocity, bug reduction
**Target Coverage**: 80%+
**Priority Test Files**:
```typescript
// 1. Services (highest ROI)
cart.service.spec.ts // Test signal updates, cart logic
api.service.spec.ts // Mock HTTP calls
telegram.service.spec.ts // Test WebApp initialization
// 2. Components (critical paths)
cart.component.spec.ts // Payment flow, validation
header.component.spec.ts // Cart count, navigation
item-detail.component.spec.ts // Add to cart, variant selection
// 3. Interceptors
cache.interceptor.spec.ts // Verify caching logic
```
**Quick Start**:
```bash
# Generate test with Angular CLI
ng test --code-coverage
# Write first test
describe('CartService', () => {
it('should add item to cart', () => {
service.addToCart(mockItem, mockVariant);
expect(service.cartItems().length).toBe(1);
});
});
```
---
#### 3. Error Boundary & User Feedback
**Why**: Graceful failures, better UX
**Effort**: 1 day
**Impact**: User trust, reduced support tickets
**Implementation**:
```typescript
// src/app/services/error-handler.service.ts
@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
showError(message: string) {
// Show toast notification
// Log to analytics
// Optionally send to backend
}
}
// Usage in cart.component.ts
this.apiService.createPayment(data).subscribe({
next: (response) => { /* handle success */ },
error: (err) => {
this.errorHandler.showError(
'Не удалось создать платеж. Попробуйте позже.'
);
console.error(err);
}
});
```
**Add Toast Library**:
```bash
npm install ngx-toastr --save
```
---
### ⚡ MEDIUM PRIORITY (Next Month)
#### 4. E2E Testing
**Why**: Catch integration bugs, confidence in releases
**Effort**: 3-5 days
**Impact**: Release quality
**Recommended**: [Playwright](https://playwright.dev/) (better than Cypress for modern apps)
```bash
npm install @playwright/test --save-dev
npx playwright install
```
**Critical Test Scenarios**:
1. Browse categories → View item → Add to cart → Checkout
2. Search product → Filter results → Add to cart
3. Empty cart → Add items → Remove items
4. Payment flow (mock SBP QR code response)
5. Email/phone validation on success screen
---
#### 5. Analytics Integration
**Why**: Data-driven decisions, understand users
**Effort**: 1 day
**Impact**: Business insights
**Recommended Setup**:
```typescript
// Yandex Metrica (best for Russian market)
<!-- index.html -->
<script>
(function(m,e,t,r,i,k,a){
// Yandex Metrica snippet
})(window, document, "yandex_metrica_callbacks2");
</script>
// Track events
yaCounter12345678.reachGoal('ADD_TO_CART', {
product_id: item.id,
price: variant.price
});
```
**Key Metrics to Track**:
- Product views
- Add to cart events
- Checkout initiation
- Payment success/failure
- Search queries
- PWA installs
---
#### 6. Performance Optimization
**Why**: Better UX, SEO, conversion rates
**Effort**: 2-3 days
**Impact**: User satisfaction
**Action Items**:
```typescript
// 1. Image Optimization
// Use WebP format with fallbacks
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Product">
</picture>
// 2. Lazy Load Images
<img loading="lazy" src="product.jpg">
// 3. Preload Critical Assets
// index.html
<link rel="preload" href="logo.svg" as="image">
// 4. Virtual Scrolling for Long Lists
// npm install @angular/cdk
<cdk-virtual-scroll-viewport itemSize="150">
@for (item of items; track item.id) {
<div>{{ item.title }}</div>
}
</cdk-virtual-scroll-viewport>
```
**Measure First**:
```bash
# Lighthouse audit
npm install -g lighthouse
lighthouse http://localhost:4200 --view
# Target scores:
# Performance: 90+
# Accessibility: 95+
# Best Practices: 100
# SEO: 90+
```
---
### 🔮 FUTURE ENHANCEMENTS (Next Quarter)
#### 7. Push Notifications
**Why**: Re-engage users, promote offers
**Effort**: 1 week (needs backend)
**Impact**: Retention, sales
**Requirements**:
- Firebase Cloud Messaging (FCM)
- Backend endpoint to send notifications
- User permission flow
---
#### 8. Background Sync
**Why**: Queue orders offline, sync when online
**Effort**: 2-3 days
**Impact**: Offline-first experience
```typescript
// Register background sync
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('sync-orders');
});
// ngsw-config.json - already set up!
// Your PWA is ready for this
```
---
#### 9. Advanced Features
**Effort**: Varies
**Impact**: Competitive advantage
- **Product Recommendations**: "You might also like..."
- **Recently Viewed**: Track browsing history
- **Wishlist**: Save items for later
- **Price Alerts**: Notify when price drops
- **Social Sharing**: Share products on Telegram/VK
- **Dark Mode**: Theme switcher
- **Multi-language**: Support English, etc.
---
## 🛠️ Technical Debt & Improvements
### Quick Wins (< 1 hour each)
1. **Environment Variables for API URLs**
```typescript
// Don't hardcode API URLs
// Use environment.apiUrl consistently
```
2. **Content Security Policy (CSP)**
```nginx
# nginx.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';";
```
3. **Rate Limiting**
```typescript
// Prevent API spam
import { debounceTime } from 'rxjs';
searchQuery$.pipe(
debounceTime(300)
).subscribe(/* search */);
```
4. **Loading States**
```html
<!-- Show skeletons while loading -->
@if (loading()) {
<div class="skeleton"></div>
} @else {
<div>{{ content }}</div>
}
```
5. **SEO Meta Tags**
```typescript
// Use Angular's Meta service
constructor(private meta: Meta) {}
ngOnInit() {
this.meta.updateTag({
name: 'description',
content: this.product.description
});
}
```
---
## 📈 Success Metrics
### Before Optimizations
- Test Coverage: ~10%
- Lighthouse Score: ~85
- Error Tracking: Console only
- Analytics: None
- PWA: ❌
### After Optimizations (Target)
- Test Coverage: **80%+**
- Lighthouse Score: **95+**
- Error Tracking: ✅ Centralized
- Analytics: ✅ Yandex Metrica
- PWA: ✅ **Fully functional**
- User Engagement: **+30%** (with push notifications)
---
## 🎓 Learning Resources
### Testing
- [Angular Testing Guide](https://angular.dev/guide/testing)
- [Testing Library](https://testing-library.com/docs/angular-testing-library/intro/)
### Performance
- [Web.dev Performance](https://web.dev/performance/)
- [Angular Performance Checklist](https://github.com/mgechev/angular-performance-checklist)
### PWA
- [PWA Workshop](https://web.dev/learn/pwa/)
- [Workbox](https://developer.chrome.com/docs/workbox/) (service worker library)
### Analytics
- [Yandex Metrica Guide](https://yandex.ru/support/metrica/)
- [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4)
---
## 💡 Pro Tips
1. **Ship Frequently**: Deploy small updates often
2. **Monitor Production**: Set up error tracking (Sentry, Rollbar)
3. **User Feedback**: Add feedback button in app
4. **A/B Testing**: Test different checkout flows
5. **Mobile First**: 70%+ of e-commerce is mobile
6. **Accessibility**: Test with screen readers
7. **Security**: Regular dependency updates (`npm audit fix`)
---
## 🚀 Next Actions (This Week)
```bash
# Day 1: PWA Icons
1. Design icons for both brands
2. Update manifests
3. Test installation on mobile
# Day 2-3: Error Handling
1. Install ngx-toastr
2. Add ErrorHandlerService
3. Update all API calls with error handling
# Day 4-5: First Unit Tests
1. Set up testing utilities
2. Write tests for CartService
3. Write tests for cart validation logic
4. Run coverage report: npm test -- --code-coverage
# Weekend: Analytics
1. Set up Yandex Metrica
2. Add tracking to key events
3. Monitor dashboard
```
---
## 💬 Questions?
If you need help with any of these tasks:
1. Ask for specific code examples
2. Request architectural guidance
3. Need library recommendations
4. Want code reviews
Your project is already excellent - these improvements will make it world-class! 🌟

View File

@@ -0,0 +1,327 @@
# Telegram UserAuth Backend Contract
This document extracts the existing Telegram login flow into a repo-neutral contract for reuse in other projects.
The UI behavior, payloads, polling cadence, and session model stay the same. Only route names and cookie naming are generalized.
## Endpoint Renaming
| Current app contract | Reusable contract |
|---|---|
| `GET /auth/session` | `GET /userauth/session` |
| `POST /auth/qr/create` | `POST /userauth/qr/create` |
| `GET /auth/qr/poll?token=...` | `GET /userauth/qr/poll?token=...` |
| `POST /auth/qr/confirm` | `POST /userauth/qr/confirm` |
| `GET /auth/telegram/callback` | `GET /userauth/telegram/callback` |
| `POST /auth/logout` | `POST /userauth/logout` |
| `POST /websession/{sessionId}` | `POST /usersession/{sessionId}` |
| Cookie `dx_session` | Cookie `userauth_session` |
## Flow Summary
There are two supported flows.
### 1. Direct login from button
1. Frontend opens `https://t.me/{botUsername}?start=auth_{callbackUrl}`.
2. Telegram bot creates a session and sends the user a login button.
3. The button points to `GET /userauth/telegram/callback?token={sessionId}`.
4. Backend sets `userauth_session` cookie and redirects back to the storefront.
5. Frontend calls `GET /userauth/session` and becomes authenticated.
### 2. QR login from desktop
1. Frontend opens dialog.
2. Frontend calls `POST /userauth/qr/create`.
3. Backend returns `{ token, url }` where `url` is a Telegram deep link.
4. Frontend renders a QR from that URL.
5. User scans QR and bot calls `POST /userauth/qr/confirm`.
6. Frontend polls `GET /userauth/qr/poll?token=...` every 3 seconds.
7. When status becomes `confirmed`, backend returns session payload and sets the cookie.
8. Frontend syncs local cart using `POST /usersession/{sessionId}`.
## Session Shape
The frontend expects this exact response shape for the authenticated session.
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
```
| Field | Type | Required | Notes |
|---|---|---|---|
| `sessionId` | string | yes | Session identifier used in cart sync |
| `telegramUserId` | number | yes | Telegram user ID |
| `username` | string or null | no | Telegram username |
| `displayName` | string | yes | User-facing full name |
| `active` | boolean | yes | `false` means expired session |
| `expiresAt` | ISO 8601 string | yes | Used by frontend refresh scheduling |
Recommended TTL:
- Session TTL: 24 hours
- QR token TTL: 5 minutes
## HTTP Contract
### `POST /userauth/qr/create`
Creates a one-time QR login token when the dialog opens.
Request body:
```json
{}
```
Response `200`:
```json
{
"token": "dG9rZW4tYWJjMTIz",
"url": "https://t.me/userauth_bot?start=login_dG9rZW4tYWJjMTIz"
}
```
Requirements:
- Generate a cryptographically secure token.
- Save token with status `pending`.
- Return a Telegram deep link in `url`.
- Rate limit to 5 requests per minute per IP.
### `GET /userauth/qr/poll?token={token}`
Called every 3 seconds until confirmation or expiration.
Possible responses:
Pending:
```json
{ "status": "pending" }
```
Confirmed:
```json
{
"status": "confirmed",
"session": {
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
}
```
Expired:
```json
{ "status": "expired" }
```
Behavior:
- If confirmed, set cookie `userauth_session` in the response.
- Delete or invalidate the QR token after the first successful confirmed poll.
- If token is unknown or expired, return `status: "expired"`.
### `POST /userauth/qr/confirm`
Internal endpoint called by the Telegram bot after the user scans the QR code.
Required header:
```text
X-Bot-Secret: <shared secret between bot and backend>
```
Request body:
```json
{
"token": "dG9rZW4tYWJjMTIz",
"telegram_user": {
"id": 123456789,
"first_name": "Ivan",
"last_name": "Petrov",
"username": "ivan_petrov"
}
}
```
Response `200`:
```json
{ "status": "ok" }
```
Behavior:
- Validate `X-Bot-Secret`.
- Validate token exists and is still `pending`.
- Create a user session.
- Store session ID on the QR token.
- Mark QR token as `confirmed`.
### `GET /userauth/session`
Returns the currently active session based on the cookie.
Frontend behavior depends on this endpoint in two places:
- initial auth check on app startup
- fallback polling if QR token creation fails
Response `200`:
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
```
Error handling:
- Any non-200 response is treated by the frontend as unauthenticated.
### `GET /userauth/telegram/callback?token={sessionId}`
Used for direct Telegram login from the primary button flow.
Behavior:
- Read the `token` query param.
- Resolve it to a valid active session.
- Set cookie `userauth_session`.
- Redirect user to the storefront URL.
### `POST /userauth/logout`
Clears the backend session and expires the cookie.
Request body:
```json
{}
```
Response `200`:
```json
{ "message": "ok" }
```
### `POST /usersession/{sessionId}`
Synchronizes local cart immediately after successful login.
Request body:
```json
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
```
Notes:
- This payload is unchanged from the existing implementation.
- `price` is already discounted on the frontend side.
- The frontend skips the call if cart is empty.
## Telegram Deep Link Format
Direct login link format:
```text
https://t.me/{botUsername}?start=auth_{urlEncodedCallbackUrl}
```
QR login link format:
```text
https://t.me/{botUsername}?start=login_{qrToken}
```
Important limit:
- Telegram limits the `start` payload to 64 characters.
- A base64url encoding of 32 random bytes plus `login_` fits safely.
## Cookie Requirements
Use these cookie settings for the frontend to work correctly across site and API origins.
| Property | Value |
|---|---|
| Name | `userauth_session` |
| Path | `/` |
| HttpOnly | `true` |
| Secure | `true` |
| SameSite | `None` |
| MaxAge | `86400` |
| Domain | your shared parent domain, for example `.example.com` |
## CORS Requirements
Because the frontend sends credentials, backend must return an explicit origin.
Required headers:
```text
Access-Control-Allow-Origin: https://your-frontend.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```
Do not use `*` for `Access-Control-Allow-Origin` together with credentials.
## Frontend Runtime Expectations
The current dialog behavior is fixed and should be preserved by backend responses.
- QR polling interval: every 3 seconds
- QR expiration on frontend: after 100 checks
- If QR creation fails, frontend falls back to direct login URL and session polling
- After login, frontend closes the dialog and re-checks session
## Minimal Backend Checklist
- Implement all six `userauth` endpoints and the `usersession` sync endpoint.
- Store sessions for 24 hours.
- Store QR tokens for 5 minutes.
- Protect `POST /userauth/qr/confirm` with `X-Bot-Secret`.
- Set `userauth_session` cookie on confirmed QR poll and direct callback.
- Return the exact session JSON shape.
- Support credentialed CORS.
## Bot Checklist
- Handle `/start login_{token}` and call `POST /userauth/qr/confirm`.
- Handle `/start auth_{callbackUrl}` and provide a button that opens the callback URL.
- Send success and expiration messages back to the user.
- Share the same `X-Bot-Secret` value with backend.

193
docs/TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,193 @@
# 🔧 Troubleshooting Guide for 404 and 502 Errors
## Quick Diagnosis
Run these commands on your Ubuntu server to diagnose the issue:
```bash
# 1. Check if files exist
ls -la /var/www/dexarmarket/browser/index.html
# 2. Check nginx config syntax
sudo nginx -t
# 3. Check nginx error logs (THIS IS MOST IMPORTANT!)
sudo tail -30 /var/log/nginx/error.log
# 4. Check if nginx is running
sudo systemctl status nginx
# 5. Test API from server
curl -v https://api.dexarmarket.ru:445/ping
```
## Error: 404 Not Found
### Cause: Files not uploaded or wrong path
**Solution 1: Verify files are on server**
```bash
ls -la /var/www/dexarmarket/browser/
```
Should show:
- `index.html`
- `main-*.js`
- `chunk-*.js`
- `polyfills-*.js`
- `styles-*.css`
- `assets/` folder
**If files are missing:**
```bash
# From your local machine:
cd F:\dx\marketplace\Dexarmarket
npm run build
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
```
**Solution 2: Fix permissions**
```bash
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
```
**Solution 3: Check nginx config is loaded**
```bash
# Check which config is active
ls -la /etc/nginx/sites-enabled/
# Should show symlink to dexarmarket config
# If not:
sudo ln -s /etc/nginx/sites-available/dexarmarket /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
**Solution 4: Verify nginx root path**
```bash
sudo cat /etc/nginx/sites-available/dexarmarket | grep root
```
Should show: `root /var/www/dexarmarket/browser;`
## Error: 502 Bad Gateway
### This means the API backend (https://api.dexarmarket.ru:445) is unreachable
**Solution 1: Check if API is running**
```bash
# From Ubuntu server:
curl -v https://api.dexarmarket.ru:445/ping
# If this fails, your API backend is down!
```
**Solution 2: Port 445 is blocked**
Port 445 is typically blocked by many firewalls because it's used for SMB file sharing.
**Check from browser console (F12):**
- Open browser Developer Tools (F12)
- Go to Console tab
- Look for errors like: `net::ERR_CONNECTION_REFUSED` or `net::ERR_SSL_PROTOCOL_ERROR`
**Possible fixes:**
- Use standard port 443 for HTTPS
- Or use port 8443, 8080, or other non-standard but common ports
- Configure firewall to allow port 445
**Solution 3: CORS issues**
The API must have CORS headers allowing requests from `https://dexarmarket.ru`
Check API response headers:
```bash
curl -v -H "Origin: https://dexarmarket.ru" https://api.dexarmarket.ru:445/ping
```
Should include headers like:
```
Access-Control-Allow-Origin: https://dexarmarket.ru
```
**Solution 4: SSL Certificate issues**
```bash
# Test with SSL verification disabled
curl -k https://api.dexarmarket.ru:445/ping
# If this works but normal curl doesn't, SSL cert is invalid
```
## Still Not Working?
### Get detailed error information:
**1. Browser Console (JavaScript errors)**
```
F12 → Console tab
Look for red errors
```
**2. Browser Network Tab (Failed requests)**
```
F12 → Network tab
Reload page
Look for red (failed) requests
Click on failed request to see details
```
**3. Nginx Error Log (Server-side errors)**
```bash
sudo tail -50 /var/log/nginx/error.log
```
**4. Nginx Access Log (See what requests come in)**
```bash
sudo tail -50 /var/log/nginx/access.log
```
**5. Test Build Locally**
```bash
cd F:\dx\marketplace\Dexarmarket\dist\dexarmarket\browser
python -m http.server 8000
# Visit http://localhost:8000
```
If local test works, the issue is with deployment, not the build.
## Common Mistakes
**Uploading to wrong directory**
- Correct: `/var/www/dexarmarket/browser/`
- Wrong: `/var/www/dexarmarket/` (missing browser/)
**Wrong permissions**
```bash
# Must be readable by www-data
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
```
**Nginx config not reloaded**
```bash
# After ANY change to nginx config:
sudo nginx -t
sudo systemctl reload nginx
```
**Old files cached**
```bash
# Clear browser cache: Ctrl+Shift+R (hard refresh)
```
**API port blocked**
- Port 445 is unusual and often blocked
- Consider using port 443 (standard HTTPS)
## Contact Information for Support
When asking for help, provide:
1. Output of `sudo nginx -t`
2. Last 30 lines of nginx error log: `sudo tail -30 /var/log/nginx/error.log`
3. Browser console errors (F12 → Console)
4. Result of `curl -v https://api.dexarmarket.ru:445/ping` from server
5. Screenshot of browser Network tab showing failed request

View File

@@ -1,2 +0,0 @@
{"id":"MM-20260715T000000Z-0001","subject":"media-backend","predicate":"is","object":"not implemented yet; /media routes to BackofficeComingSoonPageComponent; GET /media, POST /media/upload, DELETE /media/:id, PATCH /media/:id are the documented backend gap","src":["docs/context/adrs/ADR-0002-media-manager-contract.md","src/app/app.routes.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-15T00:00:00Z","tags":["media-manager","backend-gap"]}
{"id":"MM-20260715T000000Z-0002","subject":"media-storage","predicate":"is-implemented-by","object":"MediaRepository interface with MockMediaRepository (IndexedDB-backed, interim) and HttpMediaRepository (future) selected via DI token; media assets never enter the Bootstrap model","src":["docs/context/adrs/ADR-0002-media-manager-contract.md"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-15T00:00:00Z","tags":["media-manager","repository-pattern"]}

View File

@@ -1,6 +0,0 @@
{"id":"PV-20260713T000000Z-0001","subject":"platform","predicate":"is-architected-as","object":"multi-tenant marketplace platform powering unlimited marketplaces from one codebase, driven entirely by backend bootstrap configuration","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"decision","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["architecture","multi-tenant"]}
{"id":"PV-20260713T000000Z-0002","subject":"frontend","predicate":"must-not","object":"contain marketplace-specific code, hardcoded marketplace data, or environment-flag-driven UI","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["frontend","constraint"]}
{"id":"PV-20260713T000000Z-0003","subject":"bootstrap","predicate":"must-only-contain","object":"data needed before app start (branding, languages, homepage layout, navigation, enabled widgets, footer pages) and must never contain products, orders, cart, or users","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["bootstrap","constraint"]}
{"id":"PV-20260713T000000Z-0004","subject":"translatable-fields","predicate":"must-be-modeled-as","object":"generic translations.{lang} map so adding/removing a language automatically exposes/removes translation fields across all translatable objects","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["i18n","constraint"]}
{"id":"PV-20260713T000000Z-0005","subject":"admin-app","predicate":"is-isolated-from","object":"marketplace storefront bundle: admin code never ships to storefront and vice versa, though they may share a domain","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["admin","security"]}
{"id":"PV-20260713T000000Z-0006","subject":"widgets","predicate":"must-not-own","object":"page spacing or page width; the renderer owns sections, spacing, and page width, widgets own only their internal layout","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["widgets","layout"]}

View File

@@ -1,6 +0,0 @@
{"id":"PE-20260713T010000Z-0001","subject":"project-editor-routing","predicate":"is","object":"flat routes under /edit/:section (no projectId — a project is the domain-resolved tenant); /builder and /project-editor redirect to /edit/general","src":["docs/superpowers/specs/2026-07-13-marketplace-project-editor-sprint16-design.md","src/app/app.routes.ts"],"status":"active","kind":"decision","updated_at":"2026-07-13T01:00:00Z","confidence":"high","tags":["project-editor","routing"]}
{"id":"PE-20260713T010000Z-0002","subject":"locale-sync","predicate":"is-implemented-by","object":"LocaleSyncService, which generically adds/removes a locale key across static page translations and navigation labels without per-field hardcoding","src":["src/app/features/project-editor/services/locale-sync.service.ts"],"status":"active","kind":"implemented","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","i18n"]}
{"id":"PE-20260713T010000Z-0003","subject":"draft-publish-flow","predicate":"is","object":"client-side only (ProjectEditorFacade.status/dirty/save/publish) because no backend draft/publish endpoint exists yet; PUT /builder/bootstrap/draft and POST /builder/bootstrap/publish are the documented backend gap","src":["docs/Project-Editor.md","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","backend-gap"]}
{"id":"PE-20260713T010000Z-0004","subject":"html-editing","predicate":"uses","object":"MarketplaceHtmlEditorComponent, a contentEditable + toolbar component with no external rich-text dependency; emits raw HTML, never sanitizes during editing","src":["src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","html-editor"]}
{"id":"PE-20260713T010000Z-0005","subject":"navigation-tab","predicate":"supports","object":"header navigation and flat-list footer navigation (add/remove/reorder/edit); grouped-column footer navigation is read-only until a future sprint","src":["src/app/features/project-editor/sections/navigation-section.component.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","navigation"]}
{"id":"PE-20260716T220000Z-0006","subject":"config-schema-and-validation","predicate":"is-implemented-by","object":"a field-schema registry (schema/editor-schema.ts, EditorSchemaService) driving centralized, severity-tagged validation (ProjectValidator composing pure schema/validators/primitives functions) and debounced undo/redo (schema/history.util) in ProjectEditorFacade; section templates stay hand-authored (metadata-augmented, not schema-rendered)","src":["docs/context/adrs/ADR-0002-project-editor-config-schema-and-validation-engine.md","src/app/features/project-editor/schema/editor-schema.ts","src/app/features/project-editor/services/project-validator.service.ts","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-16T22:00:00Z","tags":["project-editor","schema","validation","undo-redo"]}

View File

@@ -0,0 +1,551 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telegram Login Dialog</title>
<style>
:root {
--bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%);
--bg-card: #ffffff;
--bg-hover: #f0f0f0;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--accent-color: #497671;
--accent-light: rgba(73, 118, 113, 0.1);
--telegram: #2aabee;
--telegram-hover: #229ed9;
--border: #e8e8e8;
--shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: var(--text-primary);
background: var(--bg-page);
}
.page {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(320px, 448px) minmax(320px, 560px);
gap: 32px;
padding: 40px 32px;
align-items: center;
justify-content: center;
}
.panel {
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 28px;
padding: 24px;
box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12);
backdrop-filter: blur(14px);
}
.info h1 {
margin: 0 0 12px;
font-size: 32px;
line-height: 1.1;
}
.info p {
margin: 0 0 18px;
color: var(--text-secondary);
line-height: 1.6;
}
.state-switcher {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0 24px;
}
.state-switcher button {
border: 1px solid #cfd8e3;
border-radius: 999px;
background: #fff;
color: var(--text-primary);
padding: 10px 14px;
font-size: 14px;
cursor: pointer;
transition: 0.2s ease;
}
.state-switcher button.active {
border-color: var(--accent-color);
background: var(--accent-light);
color: var(--accent-color);
}
.api-grid {
display: grid;
gap: 12px;
margin-top: 20px;
}
.api-card {
background: #fff;
border: 1px solid #eef2f7;
border-radius: 16px;
padding: 14px 16px;
}
.api-card strong {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.api-card code {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #f3f7fb;
color: #21425f;
font-size: 13px;
}
.api-card p {
margin: 8px 0 0;
font-size: 13px;
}
.login-overlay {
position: relative;
min-height: 700px;
border-radius: 28px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
padding: 16px;
}
.login-dialog {
position: relative;
background: var(--bg-card);
border-radius: 20px;
padding: 32px 28px;
max-width: 400px;
width: 100%;
text-align: center;
box-shadow: var(--shadow);
animation: scaleIn 0.25s ease;
}
.close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: var(--bg-hover);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #e0e0e0;
color: #333;
}
.login-icon {
margin: 0 auto 16px;
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--accent-light);
color: var(--accent-color);
display: flex;
align-items: center;
justify-content: center;
}
.login-dialog h2 {
margin: 0 0 8px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.login-desc {
margin: 0 0 24px;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.telegram-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 14px 24px;
border: none;
border-radius: 12px;
background: var(--telegram);
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.telegram-btn:hover {
background: var(--telegram-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
}
.telegram-btn:active {
transform: translateY(0);
}
.tg-icon {
flex-shrink: 0;
}
.qr-section {
margin-top: 20px;
}
.qr-hint {
margin: 0 0 12px;
font-size: 13px;
color: #999;
}
.qr-container {
display: inline-flex;
padding: 12px;
background: #fff;
border-radius: 12px;
border: 1px solid var(--border);
}
.qr-container img {
display: block;
border-radius: 4px;
}
.qr-loading {
align-items: center;
justify-content: center;
width: 204px;
height: 204px;
}
.qr-loading .spinner,
.login-status .spinner {
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.qr-loading .spinner {
width: 32px;
height: 32px;
border: 3px solid #e0e0e0;
border-top-color: var(--accent-color);
}
.qr-expired {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 204px;
height: 204px;
cursor: pointer;
color: #999;
transition: color 0.2s ease;
}
.qr-expired:hover {
color: var(--accent-color);
}
.qr-expired span {
font-size: 13px;
}
.login-note {
margin: 16px 0 0;
font-size: 12px;
color: #999;
line-height: 1.4;
}
.login-status {
display: none;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px;
color: var(--text-secondary);
font-size: 14px;
}
.login-status .spinner {
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-top-color: var(--accent-color);
}
.dialog-content[data-state="checking"] .login-status {
display: flex;
}
.dialog-content[data-state="checking"] .action-block,
.dialog-content[data-state="loading"] .qr-ready,
.dialog-content[data-state="loading"] .qr-expired,
.dialog-content[data-state="expired"] .qr-ready,
.dialog-content[data-state="expired"] .qr-loading,
.dialog-content[data-state="error"] .qr-loading,
.dialog-content[data-state="error"] .qr-expired,
.dialog-content[data-state="checking"] .qr-section,
.dialog-content[data-state="checking"] .login-note {
display: none;
}
.dialog-content[data-state="ready"] .qr-loading,
.dialog-content[data-state="ready"] .qr-expired,
.dialog-content[data-state="ready"] .qr-error,
.dialog-content[data-state="loading"] .qr-ready,
.dialog-content[data-state="loading"] .qr-expired,
.dialog-content[data-state="loading"] .qr-error,
.dialog-content[data-state="expired"] .qr-loading,
.dialog-content[data-state="expired"] .qr-ready,
.dialog-content[data-state="expired"] .qr-error,
.dialog-content[data-state="error"] .qr-loading,
.dialog-content[data-state="error"] .qr-expired {
display: none;
}
.dialog-content[data-state="error"] .qr-ready {
display: inline-flex;
}
.metadata {
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid #e9edf2;
font-size: 13px;
color: var(--text-secondary);
}
.metadata ul {
margin: 10px 0 0;
padding-left: 18px;
}
.metadata li + li {
margin-top: 6px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 980px) {
.page {
grid-template-columns: 1fr;
padding: 24px 16px 32px;
}
.login-overlay {
min-height: 560px;
}
}
@media (max-width: 480px) {
.panel {
border-radius: 22px;
padding: 18px;
}
.login-dialog {
padding: 24px 20px;
border-radius: 16px;
}
.qr-container img {
width: 140px;
height: 140px;
}
.qr-loading,
.qr-expired {
width: 164px;
height: 164px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="panel info">
<h1>Telegram Login Dialog</h1>
<p>
Standalone extraction of the current login popup: same layout, same visual states,
same QR flow, but with reusable neutral endpoint names for moving into a separate repo.
</p>
<div class="state-switcher" aria-label="Dialog state switcher">
<button class="active" data-state-btn="ready" type="button">Ready</button>
<button data-state-btn="loading" type="button">QR Loading</button>
<button data-state-btn="checking" type="button">Checking</button>
<button data-state-btn="expired" type="button">Expired</button>
<button data-state-btn="error" type="button">Fallback</button>
</div>
<div class="api-grid">
<div class="api-card">
<strong>Start QR session</strong>
<code>POST /userauth/qr/create</code>
<p>Returns a one-time token and Telegram deeplink for the QR image.</p>
</div>
<div class="api-card">
<strong>Poll QR confirmation</strong>
<code>GET /userauth/qr/poll?token=...</code>
<p>Returns pending, confirmed, or expired. On confirmed, also returns the user session.</p>
</div>
<div class="api-card">
<strong>Read current session</strong>
<code>GET /userauth/session</code>
<p>Cookie-based session lookup used for initial auth check and fallback polling.</p>
</div>
<div class="api-card">
<strong>Sync cart after login</strong>
<code>POST /usersession/{sessionId}</code>
<p>Existing cart payload is preserved. Only the namespace is generalized for reuse.</p>
</div>
</div>
<div class="metadata">
<strong>Behavior kept intact</strong>
<ul>
<li>Open Telegram directly from the primary button.</li>
<li>Show QR immediately and poll every 3 seconds.</li>
<li>Expire the QR after 100 checks and allow manual refresh.</li>
<li>Re-check cookie session if QR creation fails.</li>
</ul>
</div>
</section>
<section class="panel">
<div class="login-overlay">
<div class="login-dialog">
<button class="close-btn" type="button" aria-label="Close dialog">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
</button>
<div class="dialog-content" data-state="ready" id="dialog-content">
<div class="login-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
</div>
<h2>Login required</h2>
<p class="login-desc">Please log in via Telegram to proceed with your order.</p>
<div class="login-status checking">
<div class="spinner"></div>
<span>Checking...</span>
</div>
<div class="action-block">
<button class="telegram-btn" type="button">
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"></path>
</svg>
Log in with Telegram
</button>
<div class="qr-section">
<p class="qr-hint">Or scan the QR code</p>
<div class="qr-container qr-loading">
<div class="spinner"></div>
</div>
<div class="qr-container qr-ready">
<img
src="https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=https%3A%2F%2Ft.me%2Fuserauth_bot%3Fstart%3Dlogin_sample_token"
alt="QR Code"
width="180"
height="180"
loading="eager"
/>
</div>
<div class="qr-container qr-expired" role="button" tabindex="0" aria-label="Refresh QR code">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 4v6h6M23 20v-6h-6"></path>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
</svg>
<span>QR code expired. Click to refresh</span>
</div>
</div>
<p class="login-note">You will be redirected back after login.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<script>
const content = document.getElementById('dialog-content');
const buttons = document.querySelectorAll('[data-state-btn]');
buttons.forEach((button) => {
button.addEventListener('click', () => {
const state = button.getAttribute('data-state-btn');
content.setAttribute('data-state', state);
buttons.forEach((candidate) => candidate.classList.remove('active'));
button.classList.add('active');
});
});
</script>
</body>
</html>

View File

@@ -1,32 +0,0 @@
// Karma configuration for `ng test` (@angular/build:karma builder).
// A headless, sandbox-free Chrome launcher so the suite runs in CI and in
// restricted/dev environments where Chrome isn't on PATH. CHROME_BIN falls
// back to the default Windows install path when the env var isn't set.
process.env.CHROME_BIN =
process.env.CHROME_BIN || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
module.exports = function (config) {
config.set({
frameworks: ['jasmine'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
],
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
},
},
reporters: ['progress', 'coverage'],
coverageReporter: {
dir: require('path').join(__dirname, 'coverage'),
subdir: '.',
reporters: [{ type: 'text-summary' }, { type: 'html' }, { type: 'lcovonly' }],
},
restartOnFileChange: true,
});
};

View File

@@ -7,7 +7,7 @@ server {
# Angular routing - serve index.html for all routes # Angular routing - serve index.html for all routes
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html =404;
} }
# Static assets caching # Static assets caching
@@ -55,7 +55,7 @@ server {
# Angular routing # Angular routing
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html =404;
} }
# Proxy API calls to backend # Proxy API calls to backend
@@ -93,93 +93,4 @@ server {
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;" always;
}
# Template for onboarding a new marketplace tenant.
# Replace NEWMARKETPLACE.EXAMPLE.COM, /var/www/newmarketplace, and the
# api.newmarketplace.example.com:443 proxy target with the real values,
# then rename this block's server_name/root before deploying.
#
# --- SPA routing (read before you skip this) ---
# This is an Angular app with client-side routing (all page navigation - the
# admin dashboard, project editor, catalog, product pages, etc. - happens in
# the browser, not via new server requests). Every URL the app owns
# (/:lang/backoffice/dashboard, /:lang/edit/general, /:lang/catalog/5, ...)
# must fall through to index.html on a fresh request (page refresh, typed
# URL, browser back/forward after a full reload) so Angular's router can take
# over client-side. `try_files $uri $uri/ /index.html;` below is what makes
# that work: nginx tries the literal file, then the directory, then falls
# back to index.html for anything that isn't a real static asset. If you ever
# see a raw nginx 404 page on refresh/back-navigation (not a blank app, an
# actual nginx error page), this fallback is missing or misconfigured for
# that server block - it is NOT an Angular or JS problem.
#
# --- Two ways the frontend talks to its API - pick one per tenant ---
# 1) Proxied (what this template and the lovero.store block above do):
# the frontend calls a relative `/api/...` path, and nginx proxies it to
# the real backend below. Browser never sees the backend host/port.
# 2) Direct (what the dexarmarket.ru production build does): the frontend's
# `environment.production.ts` sets `apiUrl`/`authApiUrl` to an absolute
# URL (e.g. `https://api.dexarmarket.ru:445`) and calls that directly -
# this nginx config is never involved in API calls at all for that tenant.
# If a tenant using pattern (2) reports 502/504 Bad Gateway on refresh or
# back-navigation, it is NOT this file - the app re-fires session-check and
# bootstrap-load calls on every route change/refresh, and a 502/504 means the
# *backend's own* reverse proxy/app server (the one fronting that absolute
# apiUrl/authApiUrl host) is down, overloaded, or timing out. Check that
# backend's own nginx/app logs, not this one.
server {
listen 80;
server_name newmarketplace.example.com www.newmarketplace.example.com;
root /var/www/newmarketplace/browser;
index index.html;
# Angular routing - serve index.html for all routes (client-side router
# handles /edit, /:lang/edit/:section, etc. once index.html is served)
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to backend - only needed if this tenant uses the
# relative `/api` pattern (see comment above); delete this block if the
# tenant's environment.*.ts uses an absolute apiUrl instead.
location /api {
proxy_pass https://api.newmarketplace.example.com:443;
proxy_set_header Host api.newmarketplace.example.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
rewrite ^/api(/.*)$ $1 break;
proxy_ssl_verify off;
}
# Static assets caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Don't cache index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_min_length 1000;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;" always;
} }

3936
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,49 +5,38 @@
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"dexar": "ng serve --configuration=development --port 4200", "dexar": "ng serve --configuration=development --port 4200",
"novo": "ng serve --configuration=novo --port 4201 --proxy-config proxy.conf.novo.json",
"start:dexar": "ng serve --configuration=development --port 4200", "start:dexar": "ng serve --configuration=development --port 4200",
"start:novo": "ng serve --configuration=novo --port 4201",
"build": "ng build", "build": "ng build",
"build:dexar": "ng build --configuration=production", "build:dexar": "ng build --configuration=production",
"test": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox", "build:novo": "ng build --configuration=novo-production",
"test:coverage": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"arch:check:boundaries": "node tools/architecture/check-boundaries.mjs", "lavero": "ng serve --configuration=lavero --port 4202 --proxy-config proxy.conf.lavero.json",
"arch:check:cycles": "npx --yes madge --circular --extensions ts src/app --ts-config tsconfig.app.json", "start:lavero": "ng serve --configuration=lavero --port 4202",
"arch:check": "npm run arch:check:boundaries ; npm run arch:check:cycles", "build:lavero": "ng build --configuration=lavero-production"
"barry": "barry-cache",
"barry:validate": "barry-cache validate",
"barry:resume": "barry-cache resume",
"barry:finalize": "barry-cache finalize",
"barry:failure": "barry-cache failure"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "22.0.8", "@angular/animations": "21.1.5",
"@angular/cdk": "22.0.6", "@angular/cdk": "21.1.5",
"@angular/common": "22.0.8", "@angular/common": "21.1.5",
"@angular/compiler": "22.0.8", "@angular/compiler": "21.1.5",
"@angular/core": "22.0.8", "@angular/core": "21.1.5",
"@angular/forms": "22.0.8", "@angular/forms": "21.1.5",
"@angular/platform-browser": "22.0.8", "@angular/platform-browser": "21.1.5",
"@angular/router": "22.0.8", "@angular/router": "21.1.5",
"@angular/service-worker": "22.0.8", "@angular/service-worker": "21.1.5",
"primeicons": "^7.0.0",
"primeng": "^21.0.3",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.8.0", "tslib": "^2.8.0",
"zone.js": "~0.16.0" "zone.js": "~0.16.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "22.0.8", "@angular/build": "21.1.5",
"@angular/cli": "22.0.8", "@angular/cli": "21.1.5",
"@angular/compiler-cli": "22.0.8", "@angular/compiler-cli": "21.1.5",
"@types/jasmine": "~5.1.0", "typescript": "~5.9.3"
"barry-cache": "^0.9.3",
"istanbul-lib-instrument": "^6.0.3",
"jasmine-core": "~5.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~6.0.3"
} }
} }

View File

@@ -1,8 +1,11 @@
{ {
"/api": { "/api": {
"target": "https://novo.market", "target": "https://api.dexarmarket.ru:445",
"secure": false, "secure": false,
"changeOrigin": true, "changeOrigin": true,
"pathRewrite": {
"^/api": ""
},
"logLevel": "debug" "logLevel": "debug"
} }
} }

11
proxy.conf.lavero.json Normal file
View File

@@ -0,0 +1,11 @@
{
"/api": {
"target": "https://api.lovero.store:555",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
},
"logLevel": "debug"
}
}

View File

@@ -0,0 +1,11 @@
{
"/api": {
"target": "https://api.lovero.store:555",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
},
"logLevel": "debug"
}
}

11
proxy.conf.novo.json Normal file
View File

@@ -0,0 +1,11 @@
{
"/api": {
"target": "https://api.novo.market:444",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
},
"logLevel": "debug"
}
}

View File

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" role="img" aria-label="No image available">
<rect width="400" height="300" fill="#e5e7eb"/>
<g fill="none" stroke="#9ca3af" stroke-width="2">
<rect x="40" y="40" width="320" height="220" rx="8"/>
<path d="M40 220 L140 130 L200 180 L260 110 L360 220" />
<circle cx="140" cy="100" r="20"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 380 B

View File

@@ -1,325 +0,0 @@
{
"schemaVersion": "1.0.0",
"generatedAt": "2026-07-03T00:00:00Z",
"tenant": {
"id": "tenant-default-001",
"slug": "default",
"code": "DEFAULT",
"host": "default.local",
"name": "Marketplace",
"websiteBaseUrl": "https://marketplace.local",
"builderBaseUrl": "https://builder.marketplace.local",
"backofficeBaseUrl": "https://backoffice.marketplace.local",
"defaultLocale": "ru",
"supportedLocales": ["ru", "en", "hy"],
"defaultCurrency": "RUB",
"supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
"timezone": "Europe/Moscow"
},
"branding": {
"brandName": "Marketplace",
"legalName": "Marketplace LLC",
"slogan": "Digital commerce marketplace",
"logoUrl": "/icons/icon-192x192.png",
"logoCompactUrl": "/icons/icon-192x192.png",
"faviconUrl": "/favicon.ico",
"appIconUrl": "/icons/icon-192x192.png",
"supportEmail": "support@marketplace.local",
"supportPhone": "+7-900-000-00-00"
},
"theme": {
"themeId": "default-light",
"mode": "light",
"palette": {
"primary": "#497671",
"secondary": "#a1b4b5",
"accent": "#a7ceca",
"success": "#10b981",
"warning": "#f59e0b",
"danger": "#ef4444",
"info": "#3b82f6",
"textPrimary": "#1e3c38",
"textSecondary": "#667a77",
"backgroundPrimary": "#ffffff",
"backgroundSecondary": "#f5f5f5",
"border": "#d3dad9"
},
"typography": {
"primaryFontFamily": "DM Sans, sans-serif",
"headingFontFamily": "DM Sans, sans-serif",
"baseFontSize": 16
},
"spacing": {
"unit": 4,
"scale": [0, 4, 8, 12, 16, 24, 32, 48]
},
"borderRadiusScale": {
"sm": "8px",
"md": "12px",
"lg": "16px",
"xl": "22px"
},
"shadows": {
"sm": "0 2px 8px rgba(0,0,0,0.1)",
"md": "0 4px 12px rgba(0,0,0,0.15)",
"lg": "0 12px 32px rgba(73,118,113,0.2)"
},
"iconSet": "default"
},
"company": {
"companyName": "Marketplace LLC",
"registrationNumber": "1027700000000",
"taxId": "7700000000",
"address": {
"country": "Russia",
"region": "Moscow",
"city": "Moscow",
"street": "Tverskaya 1",
"postalCode": "125009"
},
"contacts": {
"email": "support@marketplace.local",
"phone": "+7-900-000-00-00",
"telegram": "@marketplace_support",
"website": "https://marketplace.local"
}
},
"featureFlags": {
"wishlist": true,
"compare": true,
"reviews": true,
"blog": false,
"chat": false,
"analytics": true,
"notifications": true,
"coupons": true,
"loyalty": false,
"giftCards": false,
"invoices": true
},
"apiEndpoints": {
"bootstrap": {
"path": "/bootstrap",
"method": "GET",
"timeoutMs": 10000
},
"website": {},
"builder": {},
"backoffice": {}
},
"localization": {
"defaultLocale": "ru",
"supportedLocales": ["ru", "en", "hy"],
"currencyByLocale": {
"ru": "RUB",
"en": "USD",
"hy": "AMD"
},
"dictionaries": [
{
"locale": "ru",
"dictionaryUrl": "/assets/i18n/ru.json",
"version": "1.0.0"
},
{
"locale": "en",
"dictionaryUrl": "/assets/i18n/en.json",
"version": "1.0.0"
},
{
"locale": "hy",
"dictionaryUrl": "/assets/i18n/hy.json",
"version": "1.0.0"
}
]
},
"seo": {
"default": {
"title": "Marketplace",
"description": "Digital commerce marketplace",
"robots": "index,follow"
},
"byPageKey": {
"home": {
"title": "Marketplace - Home",
"description": "Digital commerce marketplace",
"canonicalUrl": "https://marketplace.local/",
"robots": "index,follow"
}
}
},
"permissions": {
"definitions": [
{
"key": "builder.pages.edit",
"description": "Edit pages in builder"
},
{
"key": "backoffice.products.read",
"description": "Read products in backoffice"
}
],
"roles": [
{
"role": "builder_admin",
"permissions": ["builder.pages.edit"]
},
{
"role": "backoffice_manager",
"permissions": ["backoffice.products.read"]
}
]
},
"navigation": {
"header": [
{
"id": "nav-home",
"labelKey": "nav.home",
"route": "/",
"icon": "home",
"order": 1
},
{
"id": "nav-search",
"labelKey": "nav.search",
"route": "/search",
"icon": "search",
"order": 2
},
{
"id": "nav-cart",
"labelKey": "nav.cart",
"route": "/cart",
"icon": "cart",
"order": 3
}
],
"footer": [
{
"id": "footer-about",
"labelKey": "nav.about",
"route": "/about",
"order": 1
},
{
"id": "footer-contacts",
"labelKey": "nav.contacts",
"route": "/contacts",
"order": 2
},
{
"id": "footer-privacy",
"labelKey": "nav.privacy",
"route": "/privacy-policy",
"order": 3
}
]
},
"pages": [
{
"id": "page-home",
"key": "home",
"title": "Home",
"route": {
"path": "/",
"exact": true
},
"layout": "default-public",
"seoKey": "home",
"visible": true,
"sections": [
{
"id": "section-hero",
"type": "hero",
"order": 1,
"layout": {
"strategy": "hero",
"columns": 1,
"gap": "1.5rem",
"align": "stretch"
},
"visibility": {
"desktop": true,
"tablet": true,
"mobile": true
},
"visible": true,
"widgets": [
{
"id": "widget-hero-main",
"type": "hero",
"version": "1.0.0",
"visible": true,
"props": {
"title": "Welcome to Marketplace Platform",
"subtitle": "Configuration-driven multi-tenant commerce",
"ctaLabel": "Start Shopping"
}
}
]
},
{
"id": "section-categories",
"type": "categories",
"order": 2,
"layout": {
"strategy": "grid",
"columns": 1,
"gap": "1.5rem",
"align": "stretch"
},
"visibility": {
"desktop": true,
"tablet": true,
"mobile": true
},
"visible": true,
"widgets": [
{
"id": "widget-categories-root",
"type": "categories",
"version": "1.0.0",
"visible": true,
"props": {
"title": "Categories",
"source": "root",
"emptyMessage": "No categories available"
}
}
]
},
{
"id": "section-featured-products",
"type": "product-collection",
"order": 3,
"layout": {
"strategy": "carousel",
"columns": 1,
"gap": "1rem",
"align": "stretch"
},
"visibility": {
"desktop": true,
"tablet": true,
"mobile": true
},
"visible": true,
"widgets": [
{
"id": "widget-featured-products",
"type": "product-collection",
"version": "1.0.0",
"visible": true,
"props": {
"title": "Featured Products",
"source": "featured",
"count": 8,
"actionLabel": "Select"
}
}
]
}
]
}
]
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "Marketplace - Интернет-магазин", "name": "Novo Market - Интернет-магазин",
"short_name": "Marketplace", "short_name": "Novo",
"description": "Интернет-магазин цифровых товаров и услуг", "description": "Novo Market - ваш онлайн магазин качественных товаров с доставкой",
"theme_color": "#10b981", "theme_color": "#10b981",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone", "display": "standalone",
@@ -11,9 +11,9 @@
"categories": ["shopping", "lifestyle"], "categories": ["shopping", "lifestyle"],
"icons": [ "icons": [
{ {
"src": "icons/icon-192x192.png", "src": "assets/images/novo-favicon.svg",
"sizes": "192x192", "sizes": "any",
"type": "image/png", "type": "image/svg+xml",
"purpose": "any" "purpose": "any"
}, },
{ {

View File

@@ -1,6 +1,6 @@
{ {
"name": "Marketplace - Интернет-магазин", "name": "Dexar Market - Интернет-магазин",
"short_name": "Marketplace", "short_name": "Dexar Market",
"description": "Интернет-магазин цифровых товаров и услуг", "description": "Интернет-магазин цифровых товаров и услуг",
"display": "standalone", "display": "standalone",
"orientation": "portrait-primary", "orientation": "portrait-primary",
@@ -11,9 +11,9 @@
"categories": ["shopping", "marketplace"], "categories": ["shopping", "marketplace"],
"icons": [ "icons": [
{ {
"src": "icons/icon-192x192.png", "src": "assets/images/dexar-favicon.svg",
"sizes": "192x192", "sizes": "any",
"type": "image/png", "type": "image/svg+xml",
"purpose": "any" "purpose": "any"
}, },
{ {

View File

@@ -1,20 +1,9 @@
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: https://dexarmarket.ru/sitemap.xml
# Block access to cart (user-specific data) # Block access to cart (user-specific data)
Disallow: /cart Disallow: /cart
# Block admin/backoffice and internal diagnostics
Disallow: /*/backoffice
Disallow: /*/edit
Disallow: /*/project-editor
Disallow: /__diagnostics
# Crawl delay for polite crawling # Crawl delay for polite crawling
Crawl-delay: 1 Crawl-delay: 1
# Static baseline sitemap (home/catalog/search/wishlist/compare only) - see
# public/sitemap.xml's own header comment for what this does and does not
# cover (no per-tenant product/category/static-page URLs yet - needs a
# backend/build-time generator, documented in docs/backend/BACKEND-INTEGRATION.md#619-sitemap-future--static-baseline-only-today).
Sitemap: /sitemap.xml

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Static baseline sitemap - top-level, statically-known marketplace routes
only (home, catalog, search, wishlist, compare) for the site's default
locale segment ('ru', see app.routes.ts's `redirectTo: 'ru'` fallback).
Known limitation (documented, not faked): this is a multi-tenant,
config-driven platform (docs/ARCHITECTURE.md) - supported locales,
categories, products, and static pages are all resolved at runtime from
the tenant's bootstrap config, not enumerable at build time from the
frontend alone. 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
and regenerates this file (or serves it dynamically) per tenant/domain -
see docs/backend/BACKEND-INTEGRATION.md#619-sitemap-future--static-baseline-only-today. Until that exists, this static file is a reasonable
floor, not the full picture.
-->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>/ru</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>/ru/catalog</loc>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>/ru/search</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>/ru/wishlist</loc>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>/ru/compare</loc>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -1,59 +0,0 @@
{
"version": 1,
"skills": {
"angular-developer": {
"source": "angular/skills",
"sourceType": "github",
"skillPath": "angular-developer/SKILL.md",
"computedHash": "62e087c9cf0dc17f4ca4fed9f451f65605f43e4427016eb799409d6da39a0a87"
},
"cavecrew": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/cavecrew/SKILL.md",
"computedHash": "9633c1391fa246091ce68ea522c0e424b2bc93aeb69fc44221a30b53e8a2c23d"
},
"caveman": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman/SKILL.md",
"computedHash": "723fb2a8bec1156c0f0b5bf020cc739ed09702b7726ec6377480038871339f6e"
},
"caveman-commit": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-commit/SKILL.md",
"computedHash": "f028652defd5fdeddcce2994083cb1a7b201ee827bba8e2495546ee159fca3de"
},
"caveman-compress": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-compress/SKILL.md",
"computedHash": "1055abaf7cb2f8c0ca78b64101b84dfc910d9819733ed9f0277b661441797aeb"
},
"caveman-help": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-help/SKILL.md",
"computedHash": "4dba39eea07a050108d47940b39600bc8f45489201ecff0ccf03627180fd8e50"
},
"caveman-review": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-review/SKILL.md",
"computedHash": "b9091dbc51de0f3710ea818fd4d638539f8c1784f8fda931eb159c44861e702e"
},
"caveman-stats": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-stats/SKILL.md",
"computedHash": "331f720e2fa97b68cacdae44384878071e8cac6013479edea68f4c8eca308852"
},
"design-taste-frontend": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/taste-skill/SKILL.md",
"computedHash": "899b84384f74f540ea5284d9b2e9234e050998b42eacc805410b518d4226c0b3"
}
}
}

View File

@@ -1,18 +1,12 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router'; import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { provideHttpClient, withInterceptors, withXhr } from '@angular/common/http'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { cacheInterceptor } from './interceptors/cache.interceptor'; import { cacheInterceptor } from './interceptors/cache.interceptor';
import { apiBaseUrlInterceptor } from './interceptors/api-base-url.interceptor';
import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor'; import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor';
import { mockDataInterceptor } from './interceptors/mock-data.interceptor'; import { mockDataInterceptor } from './interceptors/mock-data.interceptor';
import { adminAuthHeadersInterceptor } from './core/admin-auth/admin-auth-headers.interceptor';
import { Ed25519VerificationService } from './core/admin-auth/ed25519-verification.model';
import { NoopEd25519VerificationService } from './core/admin-auth/noop-ed25519-verification.service';
import { provideServiceWorker } from '@angular/service-worker'; import { provideServiceWorker } from '@angular/service-worker';
import { MediaRepository } from './core/media/media-repository';
import { MockMediaRepository } from './core/media/mock-media-repository.service';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -22,11 +16,9 @@ export const appConfig: ApplicationConfig = {
routes, routes,
withInMemoryScrolling({ scrollPositionRestoration: 'top' }) withInMemoryScrolling({ scrollPositionRestoration: 'top' })
), ),
provideHttpClient(withXhr(), provideHttpClient(
withInterceptors([mockDataInterceptor, apiBaseUrlInterceptor, apiHeadersInterceptor, adminAuthHeadersInterceptor, cacheInterceptor]) withInterceptors([mockDataInterceptor, apiHeadersInterceptor, cacheInterceptor])
), ),
{ provide: Ed25519VerificationService, useClass: NoopEd25519VerificationService },
{ provide: MediaRepository, useClass: MockMediaRepository },
provideServiceWorker('ngsw-worker.js', { provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(), enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000' registrationStrategy: 'registerWhenStable:30000'

View File

@@ -10,24 +10,18 @@
<p>{{ 'app.serverError' | translate }}</p> <p>{{ 'app.serverError' | translate }}</p>
<button class="retry-btn" (click)="retryConnection()">{{ 'app.retryConnection' | translate }}</button> <button class="retry-btn" (click)="retryConnection()">{{ 'app.retryConnection' | translate }}</button>
</div> </div>
} @else if (isAdminRoute()) {
<router-outlet></router-outlet>
<app-telegram-login mode="admin" />
} @else { } @else {
<a class="skip-link" href="#main-content">{{ 'app.skipToContent' | translate }}</a>
<app-header></app-header> <app-header></app-header>
<main id="main-content" class="main-content" tabindex="-1"> <main class="main-content">
@if (!isHomePage()) { @if (!isHomePage()) {
<app-back-button /> <app-back-button />
} }
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<app-floating-notifications />
@defer (on viewport) { @defer (on viewport) {
<app-footer></app-footer> <app-footer></app-footer>
} @placeholder { } @placeholder {
<div class="footer-placeholder" aria-hidden="true"></div> <div class="footer-placeholder" aria-hidden="true"></div>
} }
<!-- <app-telegram-login /> --> <!-- <app-telegram-login /> -->
<app-telegram-login mode="admin" />
} }

View File

@@ -1,11 +1,6 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { brandInfoRoutes, brandLegalRoutes } from './brands/brand-routes';
import { languageGuard } from './guards/language.guard'; import { languageGuard } from './guards/language.guard';
import { projectEditorDirtyGuard } from './features/project-editor/guards/project-editor-dirty.guard';
import { adminAuthGuard, requireAdminPermission } from './core/admin-auth/admin-auth.guard';
import { authRoutes } from './core/auth/auth.routes';
import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard';
import { adminProductDirtyGuard } from './features/admin/products/guards/admin-product-dirty.guard';
import { environment } from '../environments/environment';
// Core routes (same across all brands) // Core routes (same across all brands)
const coreRoutes: Routes = [ const coreRoutes: Routes = [
@@ -13,328 +8,37 @@ const coreRoutes: Routes = [
path: '', path: '',
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent) loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
}, },
{
path: 'catalog',
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
},
{
path: 'catalog/:id',
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
},
{ {
path: 'category/:id', path: 'category/:id',
redirectTo: 'catalog/:id', loadComponent: () => import('./pages/category/subcategories.component').then(m => m.SubcategoriesComponent)
pathMatch: 'full'
}, },
{ {
path: 'category/:id/items', path: 'category/:id/items',
redirectTo: 'catalog/:id', loadComponent: () => import('./pages/category/category.component').then(m => m.CategoryComponent)
pathMatch: 'full'
},
{
path: 'product/:id',
loadComponent: () => import('./features/website/product/containers/product-details-container.component').then(m => m.ProductDetailsContainerComponent)
}, },
{ {
path: 'item/:id', path: 'item/:id',
redirectTo: 'product/:id', loadComponent: () => import('./pages/item-detail/item-detail.component').then(m => m.ItemDetailComponent)
pathMatch: 'full'
}, },
{ {
path: 'search', path: 'search',
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent) loadComponent: () => import('./pages/search/search.component').then(m => m.SearchComponent)
},
{
path: 'edit',
canActivate: [adminAuthGuard],
loadComponent: () => import('./features/project-editor/pages/builder-overview-page.component').then(m => m.BuilderOverviewPageComponent)
},
{
path: 'edit/:section',
canActivate: [adminAuthGuard],
loadComponent: () => import('./features/project-editor/pages/project-editor-page.component').then(m => m.ProjectEditorPageComponent),
canDeactivate: [projectEditorDirtyGuard]
},
{
path: 'backoffice',
canActivate: [adminAuthGuard],
loadComponent: () => import('./features/admin/shell/admin-layout.component').then(m => m.AdminLayoutComponent),
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: 'dashboard',
loadComponent: () => import('./features/admin/dashboard/pages/admin-dashboard-page.component').then(m => m.AdminDashboardPageComponent),
data: {
titleKey: 'adminShell.pages.dashboard.title',
descriptionKey: 'adminShell.pages.dashboard.description',
breadcrumb: [{ labelKey: 'adminShell.pages.dashboard.title' }]
}
},
{
path: 'products',
loadComponent: () => import('./features/admin/products/pages/admin-products-list-page.component').then(m => m.AdminProductsListPageComponent),
data: {
titleKey: 'adminShell.pages.products.title',
descriptionKey: 'adminShell.pages.products.description',
breadcrumb: [{ labelKey: 'adminShell.nav.products' }]
}
},
{
path: 'products/create',
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
canDeactivate: [adminProductDirtyGuard],
data: {
titleKey: 'adminShell.pages.productCreate.title',
descriptionKey: 'adminShell.pages.productCreate.description',
breadcrumb: [{ labelKey: 'adminShell.nav.products', path: ['products'] }, { labelKey: 'adminShell.pages.productCreate.title' }]
}
},
{
path: 'products/:id/edit',
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
canDeactivate: [adminProductDirtyGuard],
data: {
titleKey: 'adminShell.pages.productEdit.title',
descriptionKey: 'adminShell.pages.productEdit.description',
breadcrumb: [{ labelKey: 'adminShell.nav.products', path: ['products'] }, { labelKey: 'adminShell.pages.productEdit.title' }]
}
},
{
path: 'products/:id/duplicate',
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
canDeactivate: [adminProductDirtyGuard],
data: {
titleKey: 'adminShell.pages.productDuplicate.title',
descriptionKey: 'adminShell.pages.productDuplicate.description',
breadcrumb: [{ labelKey: 'adminShell.nav.products', path: ['products'] }, { labelKey: 'adminShell.pages.productDuplicate.title' }]
}
},
{
path: 'categories',
loadComponent: () => import('./features/admin/categories/pages/admin-categories-list-page.component').then(m => m.AdminCategoriesListPageComponent),
data: {
titleKey: 'adminShell.pages.categories.title',
descriptionKey: 'adminShell.pages.categories.description',
breadcrumb: [{ labelKey: 'adminShell.nav.categories' }]
}
},
{
path: 'categories/create',
loadComponent: () => import('./features/admin/categories/pages/admin-category-editor-page.component').then(m => m.AdminCategoryEditorPageComponent),
canDeactivate: [adminCategoryDirtyGuard],
data: {
titleKey: 'adminShell.pages.categoryCreate.title',
descriptionKey: 'adminShell.pages.categoryCreate.description',
breadcrumb: [{ labelKey: 'adminShell.nav.categories', path: ['categories'] }, { labelKey: 'adminShell.pages.categoryCreate.title' }]
}
},
{
path: 'categories/:id/edit',
loadComponent: () => import('./features/admin/categories/pages/admin-category-editor-page.component').then(m => m.AdminCategoryEditorPageComponent),
canDeactivate: [adminCategoryDirtyGuard],
data: {
titleKey: 'adminShell.pages.categoryEdit.title',
descriptionKey: 'adminShell.pages.categoryEdit.description',
breadcrumb: [{ labelKey: 'adminShell.nav.categories', path: ['categories'] }, { labelKey: 'adminShell.pages.categoryEdit.title' }]
}
},
{
// Static Pages is a first-class Project Editor module (Sprint X+2), not a
// separate backoffice CRUD surface - redirect here rather than build a
// second UI over the same bootstrap.staticPages data.
path: 'static-pages',
redirectTo: '/edit/static-pages',
pathMatch: 'full'
},
{
path: 'transactions',
loadComponent: () => import('./features/admin/transactions/pages/admin-transactions-list-page.component').then(m => m.AdminTransactionsListPageComponent),
data: {
titleKey: 'adminShell.pages.transactions.title',
descriptionKey: 'adminShell.pages.transactions.description',
breadcrumb: [{ labelKey: 'adminShell.nav.transactions' }]
}
},
{
path: 'orders',
loadComponent: () => import('./features/admin/orders/pages/admin-orders-list-page.component').then(m => m.AdminOrdersListPageComponent),
data: {
titleKey: 'adminShell.pages.orders.title',
descriptionKey: 'adminShell.pages.orders.description',
breadcrumb: [{ labelKey: 'adminShell.nav.orders' }]
}
},
{
path: 'orders/:id',
loadComponent: () => import('./features/admin/orders/pages/admin-order-detail-page.component').then(m => m.AdminOrderDetailPageComponent),
data: {
titleKey: 'adminShell.pages.orderDetail.title',
descriptionKey: 'adminShell.pages.orderDetail.description',
breadcrumb: [{ labelKey: 'adminShell.nav.orders', path: ['orders'] }, { labelKey: 'adminShell.pages.orderDetail.title' }]
}
},
{
path: 'customers',
loadComponent: () => import('./features/admin/customers/pages/admin-customers-list-page.component').then(m => m.AdminCustomersListPageComponent),
data: {
titleKey: 'adminShell.pages.customers.title',
descriptionKey: 'adminShell.pages.customers.description',
breadcrumb: [{ labelKey: 'adminShell.nav.customers' }]
}
},
{
path: 'customers/:email',
loadComponent: () => import('./features/admin/customers/pages/admin-customer-detail-page.component').then(m => m.AdminCustomerDetailPageComponent),
data: {
titleKey: 'adminShell.pages.customerDetail.title',
descriptionKey: 'adminShell.pages.customerDetail.description',
breadcrumb: [{ labelKey: 'adminShell.nav.customers', path: ['customers'] }, { labelKey: 'adminShell.pages.customerDetail.title' }]
}
},
{
path: 'moderation',
loadComponent: () => import('./features/admin/moderation/pages/admin-reviews-list-page.component').then(m => m.AdminReviewsListPageComponent),
data: {
titleKey: 'adminShell.pages.moderation.title',
descriptionKey: 'adminShell.pages.moderation.description',
breadcrumb: [{ labelKey: 'adminShell.nav.moderation' }]
}
},
{
path: 'moderation/reports',
loadComponent: () => import('./features/admin/moderation/pages/admin-reports-list-page.component').then(m => m.AdminReportsListPageComponent),
data: {
titleKey: 'adminShell.pages.reportsQueue.title',
descriptionKey: 'adminShell.pages.reportsQueue.description',
breadcrumb: [{ labelKey: 'adminShell.nav.moderation', path: ['moderation'] }, { labelKey: 'adminShell.pages.reportsQueue.title' }]
}
},
{
path: 'moderation/:id',
loadComponent: () => import('./features/admin/moderation/pages/admin-review-detail-page.component').then(m => m.AdminReviewDetailPageComponent),
data: {
titleKey: 'adminShell.pages.reviewDetail.title',
descriptionKey: 'adminShell.pages.reviewDetail.description',
breadcrumb: [{ labelKey: 'adminShell.nav.moderation', path: ['moderation'] }, { labelKey: 'adminShell.pages.reviewDetail.title' }]
}
},
{
path: 'media',
loadComponent: () => import('./features/backoffice/media/media-library-page.component').then(m => m.MediaLibraryPageComponent),
data: {
titleKey: 'adminShell.pages.media.title',
descriptionKey: 'adminShell.pages.media.description',
breadcrumb: [{ labelKey: 'adminShell.nav.mediaLibrary' }]
}
},
{
path: 'users',
canActivate: [requireAdminPermission('users.manage')],
loadComponent: () => import('./features/admin/users/pages/admin-users-page.component').then(m => m.AdminUsersPageComponent),
data: {
titleKey: 'adminShell.pages.users.title',
descriptionKey: 'adminShell.pages.users.description',
breadcrumb: [{ labelKey: 'adminShell.nav.users' }]
}
},
{
path: 'monitoring',
loadComponent: () => import('./features/admin/monitoring/pages/admin-monitoring-page.component').then(m => m.AdminMonitoringPageComponent),
data: {
titleKey: 'adminShell.pages.monitoring.title',
descriptionKey: 'adminShell.pages.monitoring.description',
breadcrumb: [{ labelKey: 'adminShell.nav.monitoring' }]
}
},
{
path: 'analytics',
loadComponent: () => import('./features/admin/analytics/pages/admin-analytics-page.component').then(m => m.AdminAnalyticsPageComponent),
data: {
titleKey: 'adminShell.pages.analytics.title',
descriptionKey: 'adminShell.pages.analytics.description',
breadcrumb: [{ labelKey: 'adminShell.nav.analytics' }]
}
},
{
path: 'reports',
loadComponent: () => import('./features/admin/reports/pages/admin-reports-page.component').then(m => m.AdminReportsPageComponent),
data: {
titleKey: 'adminShell.pages.reports.title',
descriptionKey: 'adminShell.pages.reports.description',
breadcrumb: [{ labelKey: 'adminShell.nav.reports' }]
}
},
{
path: 'settings',
loadComponent: () => import('./features/admin/settings/pages/admin-settings-page.component').then(m => m.AdminSettingsPageComponent),
data: {
titleKey: 'adminShell.pages.settings.title',
descriptionKey: 'adminShell.pages.settings.description',
breadcrumb: [{ labelKey: 'adminShell.nav.settings' }]
}
},
{
path: 'partners/seller-management',
loadComponent: () => import('./features/admin/seller-management/pages/admin-seller-management-page.component').then(m => m.AdminSellerManagementPageComponent),
data: {
titleKey: 'adminShell.pages.sellerManagement.title',
descriptionKey: 'adminShell.pages.sellerManagement.description',
breadcrumb: [{ labelKey: 'adminShell.nav.sellerManagement' }]
}
},
{ path: '**', redirectTo: 'dashboard' }
]
},
{
path: 'builder',
redirectTo: 'edit',
pathMatch: 'full'
},
{
path: 'project-editor',
redirectTo: 'edit',
pathMatch: 'full'
},
{
path: 'wishlist',
loadComponent: () => import('./features/website/user-experience/wishlist/containers/wishlist-page.component').then(m => m.WishlistPageComponent)
},
{
path: 'compare',
loadComponent: () => import('./features/website/user-experience/compare/containers/compare-page.component').then(m => m.ComparePageComponent)
}, },
{ {
path: 'cart', path: 'cart',
loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent) loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent)
},
{
path: 'page/:key',
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
},
{
path: ':staticPath',
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
} }
]; ];
// TODO(CMS): Resolve informational/legal pages from backend content configuration here. // All routes sit under a :lang prefix (e.g. /ru/cart, /en/item/5)
// Disabled hardcoded pages: about, contacts, faq, delivery, guarantee,
// company-details, payment-terms, return-policy, public-offer, privacy-policy.
const cmsContentRoutes: Routes = [];
// All routes sit under a :lang prefix (e.g. /ru/cart, /en/product/5)
export const routes: Routes = [ export const routes: Routes = [
...(environment.production ? [] : [{
path: '__diagnostics',
loadComponent: () => import('./features/diagnostics/components/diagnostics-page.component').then(m => m.DiagnosticsPageComponent)
}]),
...authRoutes,
{ {
path: ':lang', path: ':lang',
canActivate: [languageGuard], canActivate: [languageGuard],
children: [ children: [
...coreRoutes, ...coreRoutes,
...cmsContentRoutes, ...brandInfoRoutes,
...brandLegalRoutes,
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' }
] ]
}, },

View File

@@ -43,7 +43,7 @@
.server-error-overlay h2 { .server-error-overlay h2 {
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
font-size: var(--font-size-2xl, 1.25rem); font-size: 1.25rem;
} }
.server-error-overlay p { .server-error-overlay p {
@@ -55,10 +55,10 @@
.retry-btn { .retry-btn {
padding: 0.75rem 2rem; padding: 0.75rem 2rem;
border: none; border: none;
border-radius: var(--radius-sm, 8px); border-radius: 8px;
background: var(--primary-color, #007bff); background: var(--primary-color, #007bff);
color: #fff; color: #fff;
font-size: var(--font-size-lg, 1rem); font-size: 1rem;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s; transition: opacity 0.2s;

View File

@@ -1,59 +1,43 @@
import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd } from '@angular/router'; import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
import { BackButtonComponent } from './components/back-button/back-button.component'; import { BackButtonComponent } from './components/back-button/back-button.component';
import { interval, concat } from 'rxjs'; import { interval, concat } from 'rxjs';
import { filter, first } from 'rxjs/operators'; import { filter, first } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { environment } from '../environments/environment';
import { SwUpdate } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { TranslatePipe } from './i18n/translate.pipe'; import { TranslatePipe } from './i18n/translate.pipe';
import { TranslateService } from './i18n/translate.service'; import { TranslateService } from './i18n/translate.service';
import { PlatformRuntimeService } from './core/runtime/platform-runtime.service';
import { UiRuntimeFacade } from './facades/runtime/ui-runtime.facade';
import { ApiHealthService } from './services/api-health.service';
import { SeoService } from './services/seo.service';
import { FloatingNotificationsComponent } from './features/website/user-experience/components/floating-notifications/floating-notifications.component';
import { AdminAuthService } from './core/admin-auth/admin-auth.service';
import { AuthService } from './services/auth.service';
import { TelegramLoginComponent } from './components/telegram-login/telegram-login.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, TranslatePipe, FloatingNotificationsComponent, TelegramLoginComponent], imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, TranslatePipe],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.scss', styleUrl: './app.scss'
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class App implements OnInit { export class App implements OnInit {
protected title = ''; protected title = environment.brandName;
isHomePage = signal(true); isHomePage = signal(true);
isAdminRoute = signal(false);
checkingServer = signal(true); checkingServer = signal(true);
serverAvailable = signal(false); serverAvailable = signal(false);
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
private http = inject(HttpClient);
private titleService = inject(Title); private titleService = inject(Title);
private swUpdate = inject(SwUpdate); private swUpdate = inject(SwUpdate);
private appRef = inject(ApplicationRef); private appRef = inject(ApplicationRef);
private router = inject(Router); private router = inject(Router);
private i18n = inject(TranslateService); private i18n = inject(TranslateService);
private platformRuntime = inject(PlatformRuntimeService);
private uiRuntime = inject(UiRuntimeFacade);
private apiHealth = inject(ApiHealthService);
private seoService = inject(SeoService);
private authService = inject(AuthService);
private adminAuthService = inject(AdminAuthService);
ngOnInit(): void { ngOnInit(): void {
this.platformRuntime.initialize(); this.titleService.setTitle(`${environment.brandFullName} - ${this.i18n.t('app.pageTitle')}`);
this.title = this.uiRuntime.marketplaceName();
this.titleService.setTitle(`${this.uiRuntime.marketplaceDisplayName()} - ${this.i18n.t('app.pageTitle')}`);
this.checkServerHealth(); this.checkServerHealth();
this.setupAutoUpdates(); this.setupAutoUpdates();
this.openLoginDialogsFromTestModeQueryParams();
// Track route changes to show/hide back button // Track route changes to show/hide back button
this.router.events this.router.events
@@ -66,16 +50,12 @@ export class App implements OnInit {
const url = navEnd.urlAfterRedirects || navEnd.url; const url = navEnd.urlAfterRedirects || navEnd.url;
// Home pages: /ru, /en, /hy (with or without trailing slash) // Home pages: /ru, /en, /hy (with or without trailing slash)
this.isHomePage.set(/^\/[a-z]{2}\/?$/.test(url) || url === '/' || url === ''); this.isHomePage.set(/^\/[a-z]{2}\/?$/.test(url) || url === '/' || url === '');
// Admin backoffice and the Marketplace Builder (/edit) own their own
// shells (AdminLayoutComponent / ProjectEditorPageComponent's sidebar) -
// the storefront header/back-button/footer never render on either.
this.isAdminRoute.set(/^\/[a-z]{2}\/(backoffice|edit)(\/|$|\?)/.test(url));
}); });
} }
private checkServerHealth(): void { private checkServerHealth(): void {
this.checkingServer.set(true); this.checkingServer.set(true);
this.apiHealth.ping() this.http.get<{ message: string }>(`${environment.apiUrl}/ping`)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
@@ -93,29 +73,6 @@ export class App implements OnInit {
this.checkServerHealth(); this.checkServerHealth();
} }
/**
* ?login=true / ?adminLogin=true open the respective login dialog for
* manual testing. ?devBypassAdmin=true skips the QR flow entirely and
* activates a fake local admin session - dev builds only, no effect (and
* no-ops server-side too, see AdminAuthService.devBypassLogin) in
* production. No effect when the params are absent.
*/
private openLoginDialogsFromTestModeQueryParams(): void {
if (typeof window === 'undefined') {
return;
}
const params = new URLSearchParams(window.location.search);
if (params.get('login') === 'true') {
this.authService.requestLogin();
}
if (params.get('adminLogin') === 'true') {
this.adminAuthService.requestLogin();
}
if (params.get('devBypassAdmin') === 'true') {
this.adminAuthService.devBypassLogin();
}
}
private setupAutoUpdates(): void { private setupAutoUpdates(): void {
if (!this.swUpdate.isEnabled) { if (!this.swUpdate.isEnabled) {
return; return;

View File

@@ -0,0 +1,49 @@
// Lavero brand routes
// Loaded via angular.json fileReplacements when building for novo
import { Routes } from '@angular/router';
export const brandInfoRoutes: Routes = [
{
path: 'about',
loadComponent: () => import('./lavero/pages/info/about/about.component').then(m => m.AboutLaveroComponent)
},
{
path: 'contacts',
loadComponent: () => import('./lavero/pages/info/contacts/contacts.component').then(m => m.ContactsLaveroComponent)
},
{
path: 'faq',
loadComponent: () => import('./lavero/pages/info/faq/faq.component').then(m => m.FaqLaveroComponent)
},
{
path: 'delivery',
loadComponent: () => import('./lavero/pages/info/delivery/delivery.component').then(m => m.DeliveryLaveroComponent)
},
{
path: 'guarantee',
loadComponent: () => import('./lavero/pages/info/guarantee/guarantee.component').then(m => m.GuaranteeLaveroComponent)
}
];
export const brandLegalRoutes: Routes = [
{
path: 'company-details',
loadComponent: () => import('./lavero/pages/legal/company-details/company-details.component').then(m => m.CompanyDetailsLaveroComponent)
},
{
path: 'payment-terms',
loadComponent: () => import('./lavero/pages/legal/payment-terms/payment-terms.component').then(m => m.PaymentTermsLaveroComponent)
},
{
path: 'return-policy',
loadComponent: () => import('./lavero/pages/legal/return-policy/return-policy.component').then(m => m.ReturnPolicyLaveroComponent)
},
{
path: 'public-offer',
loadComponent: () => import('./lavero/pages/legal/public-offer/public-offer.component').then(m => m.PublicOfferLaveroComponent)
},
{
path: 'privacy-policy',
loadComponent: () => import('./lavero/pages/legal/privacy-policy/privacy-policy.component').then(m => m.PrivacyPolicyLaveroComponent)
}
];

View File

@@ -0,0 +1,49 @@
// Novo brand routes
// Loaded via angular.json fileReplacements when building for novo
import { Routes } from '@angular/router';
export const brandInfoRoutes: Routes = [
{
path: 'about',
loadComponent: () => import('./novo/pages/info/about/about.component').then(m => m.AboutNovoComponent)
},
{
path: 'contacts',
loadComponent: () => import('./novo/pages/info/contacts/contacts.component').then(m => m.ContactsNovoComponent)
},
{
path: 'faq',
loadComponent: () => import('./novo/pages/info/faq/faq.component').then(m => m.FaqNovoComponent)
},
{
path: 'delivery',
loadComponent: () => import('./novo/pages/info/delivery/delivery.component').then(m => m.DeliveryNovoComponent)
},
{
path: 'guarantee',
loadComponent: () => import('./novo/pages/info/guarantee/guarantee.component').then(m => m.GuaranteeNovoComponent)
}
];
export const brandLegalRoutes: Routes = [
{
path: 'company-details',
loadComponent: () => import('./novo/pages/legal/company-details/company-details.component').then(m => m.CompanyDetailsNovoComponent)
},
{
path: 'payment-terms',
loadComponent: () => import('./novo/pages/legal/payment-terms/payment-terms.component').then(m => m.PaymentTermsNovoComponent)
},
{
path: 'return-policy',
loadComponent: () => import('./novo/pages/legal/return-policy/return-policy.component').then(m => m.ReturnPolicyNovoComponent)
},
{
path: 'public-offer',
loadComponent: () => import('./novo/pages/legal/public-offer/public-offer.component').then(m => m.PublicOfferNovoComponent)
},
{
path: 'privacy-policy',
loadComponent: () => import('./novo/pages/legal/privacy-policy/privacy-policy.component').then(m => m.PrivacyPolicyNovoComponent)
}
];

View File

@@ -0,0 +1,49 @@
// Default brand routes (Dexar)
// This file is swapped via angular.json fileReplacements for each brand
import { Routes } from '@angular/router';
export const brandInfoRoutes: Routes = [
{
path: 'about',
loadComponent: () => import('../pages/info/about/about.component').then(m => m.AboutComponent)
},
{
path: 'contacts',
loadComponent: () => import('../pages/info/contacts/contacts.component').then(m => m.ContactsComponent)
},
{
path: 'faq',
loadComponent: () => import('../pages/info/faq/faq.component').then(m => m.FaqComponent)
},
{
path: 'delivery',
loadComponent: () => import('../pages/info/delivery/delivery.component').then(m => m.DeliveryComponent)
},
{
path: 'guarantee',
loadComponent: () => import('../pages/info/guarantee/guarantee.component').then(m => m.GuaranteeComponent)
}
];
export const brandLegalRoutes: Routes = [
{
path: 'company-details',
loadComponent: () => import('../pages/legal/company-details/company-details.component').then(m => m.CompanyDetailsComponent)
},
{
path: 'payment-terms',
loadComponent: () => import('../pages/legal/payment-terms/payment-terms.component').then(m => m.PaymentTermsComponent)
},
{
path: 'return-policy',
loadComponent: () => import('../pages/legal/return-policy/return-policy.component').then(m => m.ReturnPolicyComponent)
},
{
path: 'public-offer',
loadComponent: () => import('../pages/legal/public-offer/public-offer.component').then(m => m.PublicOfferComponent)
},
{
path: 'privacy-policy',
loadComponent: () => import('../pages/legal/privacy-policy/privacy-policy.component').then(m => m.PrivacyPolicyComponent)
}
];

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-about-lavero-ru /> }
@case ('en') { <app-about-lavero-en /> }
@case ('hy') { <app-about-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { AboutLaveroRuComponent } from './ru/about-ru.component';
import { AboutLaveroEnComponent } from './en/about-en.component';
import { AboutLaveroHyComponent } from './hy/about-hy.component';
@Component({
selector: 'app-about-lavero',
imports: [AboutLaveroRuComponent, AboutLaveroEnComponent, AboutLaveroHyComponent],
templateUrl: './about.component.html',
styleUrls: ['../../../../../pages/info/about/about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AboutLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

View File

@@ -0,0 +1,98 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>About Us</h1>
<p class="subtitle">A modern marketplace for your convenience</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🚀</div>
<h3>Who We Are</h3>
<p>We are a rapidly growing marketplace connecting sellers and buyers from different countries. Our platform creates convenient conditions for safe trading of various goods and services.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🎯</div>
<h3>Our Mission</h3>
<p>To create a simple and profitable ecosystem for businesses and buyers, where everyone finds the best deals.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🌍</div>
<h3>Geography</h3>
<p>We operate in Russia, Armenia, UAE, Turkey, China, Kazakhstan, Kyrgyzstan, and other countries.</p>
</div>
<div class="info-card wide">
<div class="card-icon">💼</div>
<h3>For Business</h3>
<ul class="compact-list">
<li>Easy product listing</li>
<li>Ready-made audience</li>
<li>Convenient tools</li>
<li>Technical support</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🛍️</div>
<h3>For Buyers</h3>
<ul class="compact-list">
<li>Wide selection of products</li>
<li>Competitive prices</li>
<li>Safe purchases</li>
<li>Fast delivery</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🔒</div>
<h3>Our Values</h3>
<div class="features-list">
<div class="feature">✓ Transparency</div>
<div class="feature">✓ Reliability</div>
<div class="feature">✓ Innovation</div>
<div class="feature">✓ Customer Service</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📈</div>
<h3>Our Journey</h3>
<div class="timeline">
<div class="timeline-item">
<strong>2024</strong>
<p>Platform launch in Armenia</p>
</div>
<div class="timeline-item">
<strong>2025</strong>
<p>Expansion to the Russian market</p>
</div>
<div class="timeline-item">
<strong>Today</strong>
<p>International expansion</p>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">🏢</div>
<h3>Company Details</h3>
<p><strong>Company:</strong> «LAVERO» LLC</p>
<p><strong>Director:</strong> GEVORG MATEVOSYAN</p>
<p><strong>TIN (ՀVHH):</strong> 03590442</p>
<p><strong>Registration No.:</strong> 999.110.1583686</p>
<p><strong>Address:</strong> ARMENIA, KOTAYK, ABOVYAN, VERIN PTGHNI, 3rd Street, 28</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Contact Us</h3>
<a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
<p class="support-note">We are always in touch</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-about-lavero-en',
templateUrl: './about-en.component.html',
styleUrls: ['../../../../../../pages/info/about/about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AboutLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,98 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Մեր մասին</h1>
<p class="subtitle">Զամանակակից մարկեթփլեյս ձեր հարմարության համար</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🚀</div>
<h3>Ովքեր ենք</h3>
<p>Մենք դինամիկ զարգացող մարկեթփլեյս ենք, որը միավորում է վաճառողներին և գնորդներին տարբեր երկրներից։ Մեր հարթակը ստեղծում է հարմար պայմաններ տարբեր ապրանքների և ծառայությունների անվտանգ առևտրի համար։</p>
</div>
<div class="info-card wide">
<div class="card-icon">🎯</div>
<h3>Մեր առաքելությունը</h3>
<p>Ստեղծել պարզ և շահավետ էկոհամակարգ բիզնեսի և գնորդների համար, որտեղ բոլորը գտնեն լավագույն առաջարկները։</p>
</div>
<div class="info-card wide">
<div class="card-icon">🌍</div>
<h3>Աշխարհագրություն</h3>
<p>Մենք աշխատում ենք Ռուսաստանում, Հայաստանում, ԱՀԷ-ում, Թուրքիայում, Չինաստանում, Ղազախստանում, Ղրղզստանում և այլ երկրներում։</p>
</div>
<div class="info-card wide">
<div class="card-icon">💼</div>
<h3>Բիզնեսի համար</h3>
<ul class="compact-list">
<li>Ապրանքների հեշտ տեղադրում</li>
<li>Պատրաստ լսարան</li>
<li>Հարմար գործիքներ</li>
<li>Տեխնիկական աջակցություն</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🛍️</div>
<h3>Գնորդների համար</h3>
<ul class="compact-list">
<li>Ապրանքների լայն ընտրություն</li>
<li>Մրցունակելի գներ</li>
<li>Անվտանգ գնումներ</li>
<li>Արագ առաքում</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🔒</div>
<h3>Մեր արժեքները</h3>
<div class="features-list">
<div class="feature">✓ Թափանցիկություն</div>
<div class="feature">✓ Հուսալիություն</div>
<div class="feature">✓ Նորարարություն</div>
<div class="feature">✓ Հաճախորդային սպասարկում</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📈</div>
<h3>Մեր ճանապարհը</h3>
<div class="timeline">
<div class="timeline-item">
<strong>2024</strong>
<p>Հարթակի գործարկումը Հայաստանում</p>
</div>
<div class="timeline-item">
<strong>2025</strong>
<p>Մուտք ռուսական շուկա</p>
</div>
<div class="timeline-item">
<strong>Այսօր</strong>
<p>Միջազգային ընդլայնում</p>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">🏢</div>
<h3>Ինկերության տվյալները</h3>
<p><strong>Ընկерություն՝</strong> «ԼАВЕРО» ՍՊԸ</p>
<p><strong>Տнօрен՝</strong> ГЕВORG МАТЕВОСЯН (GEVORG MATEVOSYAN)</p>
<p><strong>ՀВHH՝</strong> 03590442</p>
<p><strong>Гранцман h/h՝</strong> 999.110.1583686</p>
<p><strong>Hasцe՝</strong> ՀАЙАСТАН, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, 3-рд ПOЛOC, 28</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Կապվել մեզ հետ</h3>
<a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
<p class="support-note">Մենք միշտ կապի մեջ ենք</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-about-lavero-hy',
templateUrl: './about-hy.component.html',
styleUrls: ['../../../../../../pages/info/about/about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AboutLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,98 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>О нас</h1>
<p class="subtitle">Современный маркетплейс для вашего удобства</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🚀</div>
<h3>Кто мы</h3>
<p>Мы - динамично развивающийся маркетплейс, объединяющий продавцов и покупателей из разных стран. Наша платформа создает удобные условия для безопасной торговли различными товарами и услугами.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🎯</div>
<h3>Наша миссия</h3>
<p>Создавать простую и выгодную экосистему для бизнеса и покупателей, где каждый находит лучшие предложения.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🌍</div>
<h3>География</h3>
<p>Мы работаем в России, Армении, ОАЭ, Турции, Китае, Казахстане, Кыргызстане и других странах.</p>
</div>
<div class="info-card wide">
<div class="card-icon">💼</div>
<h3>Для бизнеса</h3>
<ul class="compact-list">
<li>Простое размещение товаров</li>
<li>Готовая аудитория</li>
<li>Удобные инструменты</li>
<li>Техническая поддержка</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🛍️</div>
<h3>Для покупателей</h3>
<ul class="compact-list">
<li>Широкий выбор товаров</li>
<li>Выгодные цены</li>
<li>Безопасные покупки</li>
<li>Быстрая доставка</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🔒</div>
<h3>Наши ценности</h3>
<div class="features-list">
<div class="feature">✓ Прозрачность</div>
<div class="feature">✓ Надежность</div>
<div class="feature">✓ Инновации</div>
<div class="feature">✓ Клиентский сервис</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📈</div>
<h3>Наш путь</h3>
<div class="timeline">
<div class="timeline-item">
<strong>2024</strong>
<p>Запуск платформы в Армении</p>
</div>
<div class="timeline-item">
<strong>2025</strong>
<p>Выход на российский рынок</p>
</div>
<div class="timeline-item">
<strong>Сегодня</strong>
<p>Международная экспансия</p>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">🏢</div>
<h3>Реквизиты компании</h3>
<p><strong>Компания:</strong> «ЛАВЕРО» ООО</p>
<p><strong>Директор:</strong> ГЕВОРГ МАТЕВОСЯН</p>
<p><strong>ՀВՀՀ (ИНН):</strong> 03590442</p>
<p><strong>Рег. номер:</strong> 999.110.1583686</p>
<p><strong>Адрес:</strong> АРМЕНИЯ, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, ул. 3-я, 28</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Связаться с нами</h3>
<a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
<p class="support-note">Мы всегда на связи</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-about-lavero-ru',
templateUrl: './about-ru.component.html',
styleUrls: ['../../../../../../pages/info/about/about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AboutLaveroRuComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-contacts-lavero-ru /> }
@case ('en') { <app-contacts-lavero-en /> }
@case ('hy') { <app-contacts-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { ContactsLaveroRuComponent } from './ru/contacts-ru.component';
import { ContactsLaveroEnComponent } from './en/contacts-en.component';
import { ContactsLaveroHyComponent } from './hy/contacts-hy.component';
@Component({
selector: 'app-contacts-lavero',
imports: [ContactsLaveroRuComponent, ContactsLaveroEnComponent, ContactsLaveroHyComponent],
templateUrl: './contacts.component.html',
styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContactsLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

View File

@@ -0,0 +1,49 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Contacts</h1>
<p class="subtitle">Get in touch with us</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🏢</div>
<h3>Organization</h3>
<p class="org-name">LLC «LAVERO»</p>
<p><strong>TIN:</strong> 03590442</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Phone</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div>
<div class="info-card wide">
<div class="card-icon">✉️</div>
<h3>Email</h3>
<p><a href="mailto:info@lovero.store">info&#64;lovero.store</a></p>
<p class="note">Response within 24 hours</p>
</div>
<div class="info-card wide">
<div class="card-icon">📍</div>
<h3>Address</h3>
<p>Armenia, Kotayk, Abovyan, Verin Ptghni, 3rd Street, 28</p>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Working Hours</h3>
<p><strong>Support:</strong> 9:00 - 21:00</p>
<p><strong>Days off:</strong> Saturday - Sunday</p>
</div>
<div class="info-card wide">
<div class="card-icon">💬</div>
<h3>Contact Us</h3>
<p>If you experience technical issues with the website or have questions about placing an order, please contact us at <a href="mailto:info@lovero.store">info&#64;lovero.store</a> with a detailed description of the problem.</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-contacts-lavero-en',
templateUrl: './contacts-en.component.html',
styleUrls: ['../../../../../../pages/info/contacts/contacts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContactsLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,49 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Կապ</h1>
<p class="subtitle">Կապվեք մեզ հետ</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🏢</div>
<h3>Կազմակերպություն</h3>
<p class="org-name">ՍՊԸ «ԼԱՎԵՐՈ»</p>
<p><strong>ՀՎՀՀ՝</strong> 03590442</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Հեռախոս</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div>
<div class="info-card wide">
<div class="card-icon">✉️</div>
<h3>Էլ. փոստ</h3>
<p><a href="mailto:info@lovero.store">info&#64;lovero.store</a></p>
<p class="note">Պատասխանը 24 ժամվա ընթացքում</p>
</div>
<div class="info-card wide">
<div class="card-icon">📍</div>
<h3>Հասցե</h3>
<p>Հայաստան, 0501, Արագածոտնի մարզ, ք. Տալին, Գայայի փող. 12</p>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Աշխատանքային ժամեր</h3>
<p><strong>Աջակցություն՝</strong> 9:00 - 21:00</p>
<p><strong>Հանգստյան օրեր՝</strong> Շաբաթ - Կիրակի</p>
</div>
<div class="info-card wide">
<div class="card-icon">💬</div>
<h3>Կապվել մեզ հետ</h3>
<p>Կայքի տեխնիկական խնդիրների դեպքում կամ պատվերի ձևակերպման հարցերի դեպքում դիմեք <a href="mailto:info@lovero.store">info&#64;lovero.store</a> խնդրի մանրամասն նկարագրությամբ։</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-contacts-lavero-hy',
templateUrl: './contacts-hy.component.html',
styleUrls: ['../../../../../../pages/info/contacts/contacts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContactsLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,49 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Контакты</h1>
<p class="subtitle">Свяжитесь с нами</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🏢</div>
<h3>Организация</h3>
<p class="org-name">ООО «ЛАВЕРО»</p>
<p><strong>ИНН:</strong> 03590442</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Телефон</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div>
<div class="info-card wide">
<div class="card-icon">✉️</div>
<h3>Email</h3>
<p><a href="mailto:info@lovero.store">info&#64;lovero.store</a></p>
<p class="note">Ответ в течение 24 часов</p>
</div>
<div class="info-card wide">
<div class="card-icon">📍</div>
<h3>Адрес</h3>
<p>АРМЕНИЯ, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, ул. 3-я, 28</p>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Часы работы</h3>
<p><strong>Поддержка:</strong> 9:00 - 21:00</p>
<p><strong>Выходные:</strong> Суббота - Воскресенье</p>
</div>
<div class="info-card wide">
<div class="card-icon">💬</div>
<h3>Связаться с нами</h3>
<p>При возникновении технических проблем с работой сайта или вопросов по оформлению заказа обращайтесь на <a href="mailto:info@lovero.store">info&#64;lovero.store</a> с подробным описанием проблемы.</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-contacts-lavero-ru',
templateUrl: './contacts-ru.component.html',
styleUrls: ['../../../../../../pages/info/contacts/contacts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContactsLaveroRuComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-delivery-lavero-ru /> }
@case ('en') { <app-delivery-lavero-en /> }
@case ('hy') { <app-delivery-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { DeliveryLaveroRuComponent } from './ru/delivery-ru.component';
import { DeliveryLaveroEnComponent } from './en/delivery-en.component';
import { DeliveryLaveroHyComponent } from './hy/delivery-hy.component';
@Component({
selector: 'app-delivery-lavero',
imports: [DeliveryLaveroRuComponent, DeliveryLaveroEnComponent, DeliveryLaveroHyComponent],
templateUrl: './delivery.component.html',
styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeliveryLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

View File

@@ -0,0 +1,78 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Delivery</h1>
<p class="subtitle">Fast and convenient to your door</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">📧</div>
<h3>Digital Products</h3>
<div class="features-list">
<div class="feature">⚡ Instant delivery</div>
<div class="feature">📨 To your email</div>
<div class="feature">💰 Free</div>
<div class="feature">🔒 Secure</div>
</div>
<p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for digital products. The seller is responsible for quality and functionality.</p>
</div>
<div class="info-card wide">
<div class="card-icon">📦</div>
<h3>Physical Products</h3>
<ul class="compact-list">
<li>СДЭК (2-7 days)</li>
<li>Почта России (5-14 days)</li>
<li>Boxberry (2-5 days)</li>
<li>DPD (1-3 days)</li>
<li>Яндекс.Доставка (same day*)</li>
</ul>
<p class="note">*If available in your city</p>
<p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for the actions of shipping companies. Delivery is handled by СДЭК, Почта России, Boxberry, DPD, and other carriers.</p>
</div>
<div class="info-card wide">
<div class="card-icon">💰</div>
<h3>Delivery Cost</h3>
<div class="delivery-cost">
<div class="cost-item">
<strong>Digital Products</strong>
<span class="free">Free</span>
</div>
<div class="cost-item">
<strong>Physical Products</strong>
<span>Depends on weight and region</span>
</div>
</div>
<p class="note">Exact cost is calculated at checkout</p>
</div>
<div class="info-card wide">
<div class="card-icon">🔍</div>
<h3>Tracking</h3>
<p>After shipping, you will receive a tracking number by email. Track your package on the delivery service website or in your account.</p>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Check upon receipt</h3>
<div class="check-grid">
<div class="check-item">✓ Packaging integrity</div>
<div class="check-item">✓ Product match</div>
<div class="check-item">✓ Completeness</div>
<div class="check-item">✓ No damage</div>
</div>
<p class="note important">If there are issues - file a report with the courier</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Questions about delivery?</h3>
<p>Contact the seller or us:</p>
<a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-delivery-lavero-en',
templateUrl: './delivery-en.component.html',
styleUrls: ['../../../../../../pages/info/delivery/delivery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeliveryLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,78 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Առաքում</h1>
<p class="subtitle">Արագ և հարմար՝ մինչև ձեր դուռը</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">📧</div>
<h3>Թվային ապրանքներ</h3>
<div class="features-list">
<div class="feature">⚡ Ակնթարթային առաքում</div>
<div class="feature">📨 Ձեր email-ին</div>
<div class="feature">💰 Անվճար</div>
<div class="feature">🔒 Անվտանգ</div>
</div>
<p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում թվային ապրանքների համար։ Որակի և գործունակության համար պատասխանատու է վաճառողը։</p>
</div>
<div class="info-card wide">
<div class="card-icon">📦</div>
<h3>Ֆիզիկական ապրանքներ</h3>
<ul class="compact-list">
<li>СДЭК (2-7 օր)</li>
<li>Почта России (5-14 օր)</li>
<li>Boxberry (2-5 օր)</li>
<li>DPD (1-3 օր)</li>
<li>Яндекс.Доставка (պատվերի օրը*)</li>
</ul>
<p class="note">*Եթե հասանելի է ձեր քաղաքում</p>
<p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում տրանսպորտային ընկերությունների գործողությունների համար։ Առաքման համար պատասխանատու են СДЭК, Почта России, Boxberry, DPD և այլ փոխադրողներ։</p>
</div>
<div class="info-card wide">
<div class="card-icon">💰</div>
<h3>Առաքման արժեքը</h3>
<div class="delivery-cost">
<div class="cost-item">
<strong>Թվային ապրանքներ</strong>
<span class="free">Անվճար</span>
</div>
<div class="cost-item">
<strong>Ֆիզիկական ապրանքներ</strong>
<span>Կախված է քաշից և տարածաշրջանից</span>
</div>
</div>
<p class="note">Ճիշտ արժեքը հաշվարկվում է ձևակերպման ժամանակ</p>
</div>
<div class="info-card wide">
<div class="card-icon">🔍</div>
<h3>Հետագծում</h3>
<p>Ուղարկմանից հետո դուք կստանաք թրեք-համար email-ով։ Հետևեք ծանրութը առաքման ծառայության կայքում կամ անձնական էջում։</p>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Ստանալիս ստուգեք</h3>
<div class="check-grid">
<div class="check-item">✓ Փաթեթավորման ամբողջականությունը</div>
<div class="check-item">✓ Ապրանքի համապատասխանությունը</div>
<div class="check-item">✓ Լրիվությունը</div>
<div class="check-item">✓ Վնասվածների բացակայությունը</div>
</div>
<p class="note important">Եթե խնդիրներ կան - կազմեք ակտ սուրհանդեսի հետ</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Առաքման հարցեր՞</h3>
<p>Կապվեք վաճառողի կամ մեզ հետ՝</p>
<a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-delivery-lavero-hy',
templateUrl: './delivery-hy.component.html',
styleUrls: ['../../../../../../pages/info/delivery/delivery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeliveryLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,78 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Доставка</h1>
<p class="subtitle">Быстро и удобно до вашей двери</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">📧</div>
<h3>Цифровые товары</h3>
<div class="features-list">
<div class="feature">⚡ Мгновенная доставка</div>
<div class="feature">📨 На ваш email</div>
<div class="feature">💰 Бесплатно</div>
<div class="feature">🔒 Безопасно</div>
</div>
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за цифровые товары. За качество и работоспособность отвечает продавец.</p>
</div>
<div class="info-card wide">
<div class="card-icon">📦</div>
<h3>Физические товары</h3>
<ul class="compact-list">
<li>СДЭК (2-7 дней)</li>
<li>Почта России (5-14 дней)</li>
<li>Boxberry (2-5 дней)</li>
<li>DPD (1-3 дня)</li>
<li>Яндекс.Доставка (в день заказа*)</li>
</ul>
<p class="note">*При наличии в вашем городе</p>
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за действия транспортных компаний. За доставку отвечают СДЭК, Почта России, Boxberry, DPD и другие перевозчики.</p>
</div>
<div class="info-card wide">
<div class="card-icon">💰</div>
<h3>Стоимость доставки</h3>
<div class="delivery-cost">
<div class="cost-item">
<strong>Цифровые товары</strong>
<span class="free">Бесплатно</span>
</div>
<div class="cost-item">
<strong>Физические товары</strong>
<span>Зависит от веса и региона</span>
</div>
</div>
<p class="note">Точная стоимость рассчитывается при оформлении</p>
</div>
<div class="info-card wide">
<div class="card-icon">🔍</div>
<h3>Отслеживание</h3>
<p>После отправки вы получите трек-номер на email. Отслеживайте посылку на сайте службы доставки или в личном кабинете.</p>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>При получении проверьте</h3>
<div class="check-grid">
<div class="check-item">✓ Целостность упаковки</div>
<div class="check-item">✓ Соответствие товара</div>
<div class="check-item">✓ Комплектность</div>
<div class="check-item">✓ Отсутствие повреждений</div>
</div>
<p class="note important">Если есть проблемы - составьте акт с курьером</p>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Вопросы по доставке?</h3>
<p>Свяжитесь с продавцом или нами:</p>
<a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-delivery-lavero-ru',
templateUrl: './delivery-ru.component.html',
styleUrls: ['../../../../../../pages/info/delivery/delivery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeliveryLaveroRuComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,93 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Frequently Asked Questions</h1>
<p class="subtitle">Quick answers to popular questions</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🛍️</div>
<h3>How to place an order?</h3>
<div class="process-steps-compact">
<p>1. Add the product to your cart</p>
<p>2. Enter your delivery details</p>
<p>3. Choose a payment method</p>
<p>4. Confirm your order</p>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">💳</div>
<h3>Payment Methods</h3>
<ul class="compact-list">
<li>Bank cards</li>
<li>SBP</li>
<li>E-wallets</li>
<li>Cash on delivery*</li>
</ul>
<p class="note">*Depends on the seller</p>
</div>
<div class="info-card wide">
<div class="card-icon">🚚</div>
<h3>Delivery</h3>
<div class="delivery-info">
<div class="delivery-item">
<strong>Digital products</strong>
<p>Instantly via email</p>
</div>
<div class="delivery-item">
<strong>Physical products</strong>
<p>СДЭК, Почта России, DPD</p>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">↩️</div>
<h3>Product Returns</h3>
<p>You can return a product in good condition within 7 days, provided it has not been used and the packaging is intact.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🔒</div>
<h3>Security</h3>
<div class="features-list">
<div class="feature">✓ Secure payments</div>
<div class="feature">✓ Verified sellers</div>
<div class="feature">✓ Return guarantee</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">⏱️</div>
<h3>Order Processing</h3>
<p>Your order is processed immediately after payment. The seller ships the product within 1-3 business days.</p>
</div>
<div class="info-card wide">
<div class="card-icon">💬</div>
<h3>Customer Support</h3>
<div class="contacts-grid">
<div class="contact-item">
<strong>Email</strong>
<a href="mailto:info@lovero.store">info@lovero.store</a>
</div>
<div class="contact-item">
<strong>Phone</strong>
<a [href]="env.phoneTel">{{ env.phones.support }}</a>
</div>
<div class="contact-item">
<strong>Working Hours</strong>
<p>24/7 support</p>
</div>
<div class="contact-item">
<strong>Response Time</strong>
<p>Up to 2 hours during business hours</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-faq-lavero-en',
templateUrl: './faq-en.component.html',
styleUrls: ['../../../../../../pages/info/faq/faq.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FaqLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-faq-lavero-ru /> }
@case ('en') { <app-faq-lavero-en /> }
@case ('hy') { <app-faq-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { FaqLaveroRuComponent } from './ru/faq-ru.component';
import { FaqLaveroEnComponent } from './en/faq-en.component';
import { FaqLaveroHyComponent } from './hy/faq-hy.component';
@Component({
selector: 'app-faq-lavero',
imports: [FaqLaveroRuComponent, FaqLaveroEnComponent, FaqLaveroHyComponent],
templateUrl: './faq.component.html',
styleUrls: ['../../../../../pages/info/faq/faq.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FaqLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

View File

@@ -0,0 +1,93 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Հաճախ տրվող հարցեր</h1>
<p class="subtitle">Արագ պատասխաններ տարածված հարցերին</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🛍️</div>
<h3>Ինչպես կատարել պատվեր՞</h3>
<div class="process-steps-compact">
<p>1. Ավելացրեք ապրանքը զամբյուղին</p>
<p>2. Նշեց առաքման տվյալները</p>
<p>3. Ընտրեք վճարման եղանակը</p>
<p>4. Հաստատեք պատվերը</p>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">💳</div>
<h3>Վճարման եղանակներ</h3>
<ul class="compact-list">
<li>Բանկային քարտեր</li>
<li>ՍԲՊ</li>
<li>Էլեկտրոնային դրամապանակներ</li>
<li>Կանխիկ ստանալիս*</li>
</ul>
<p class="note">*Կախված է վաճառողից</p>
</div>
<div class="info-card wide">
<div class="card-icon">🚚</div>
<h3>Առաքում</h3>
<div class="delivery-info">
<div class="delivery-item">
<strong>Թվային ապրանքներ</strong>
<p>Ակնթարթային email-ին</p>
</div>
<div class="delivery-item">
<strong>Ֆիզիկական ապրանքներ</strong>
<p>СДЭК, Почта России, DPD</p>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">↩️</div>
<h3>Ապրանքի վերադարձ</h3>
<p>Կարելի է վերադարձել որակյալ ապրանքը 7 օրվա ընթացքում, եթե այն չի օգտագործվել և փաթեթավորումը պահպանված է։</p>
</div>
<div class="info-card wide">
<div class="card-icon">🔒</div>
<h3>Անվտանգություն</h3>
<div class="features-list">
<div class="feature">✓ Պաշտպանված վճարումներ</div>
<div class="feature">✓ Ստուգված վաճառողներ</div>
<div class="feature">✓ Վերադարձի երաշխիք</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">⏱️</div>
<h3>Պատվերի մշակում</h3>
<p>Պատվերը մշակվում է վճարումից անմիջապես հետո։ Վաճառողը ապրանքը ուղարկում է 1-3 աշխատանքային օրվա ընթացքում։</p>
</div>
<div class="info-card wide">
<div class="card-icon">💬</div>
<h3>Աջակցության ծառայություն</h3>
<div class="contacts-grid">
<div class="contact-item">
<strong>Email</strong>
<a href="mailto:info@lovero.store">info@lovero.store</a>
</div>
<div class="contact-item">
<strong>Հեռախոս</strong>
<a [href]="env.phoneTel">{{ env.phones.support }}</a>
</div>
<div class="contact-item">
<strong>Աշխատանքային ժամեր</strong>
<p>24/7 տեխնիկական աջակցություն</p>
</div>
<div class="contact-item">
<strong>Պատասխանի ժամանակ</strong>
<p>Մինչև 2 ժամ աշխատանքային ժամերին</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-faq-lavero-hy',
templateUrl: './faq-hy.component.html',
styleUrls: ['../../../../../../pages/info/faq/faq.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FaqLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,93 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Частые вопросы</h1>
<p class="subtitle">Быстрые ответы на популярные вопросы</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🛍️</div>
<h3>Как сделать заказ?</h3>
<div class="process-steps-compact">
<p>1. Добавьте товар в корзину</p>
<p>2. Укажите данные для доставки</p>
<p>3. Выберите способ оплаты</p>
<p>4. Подтвердите заказ</p>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">💳</div>
<h3>Способы оплаты</h3>
<ul class="compact-list">
<li>Банковские карты</li>
<li>СБП</li>
<li>Электронные кошельки</li>
<li>Наличные при получении*</li>
</ul>
<p class="note">*Зависит от продавца</p>
</div>
<div class="info-card wide">
<div class="card-icon">🚚</div>
<h3>Доставка</h3>
<div class="delivery-info">
<div class="delivery-item">
<strong>Цифровые товары</strong>
<p>Мгновенно на email</p>
</div>
<div class="delivery-item">
<strong>Физические товары</strong>
<p>СДЭК, Почта России, DPD</p>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">↩️</div>
<h3>Возврат товара</h3>
<p>Можно вернуть качественный товар в течение 7 дней, если он не использовался и сохранена упаковка.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🔒</div>
<h3>Безопасность</h3>
<div class="features-list">
<div class="feature">✓ Защищенные платежи</div>
<div class="feature">✓ Проверка продавцов</div>
<div class="feature">✓ Гарантия возврата</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">⏱️</div>
<h3>Обработка заказа</h3>
<p>Заказ обрабатывается сразу после оплаты. Продавец отправляет товар в течение 1-3 рабочих дней.</p>
</div>
<div class="info-card wide">
<div class="card-icon">💬</div>
<h3>Служба поддержки</h3>
<div class="contacts-grid">
<div class="contact-item">
<strong>Email</strong>
<a href="mailto:info@lovero.store">info@lovero.store</a>
</div>
<div class="contact-item">
<strong>Телефон</strong>
<a [href]="env.phoneTel">{{ env.phones.support }}</a>
</div>
<div class="contact-item">
<strong>Время работы</strong>
<p>24/7 техподдержка</p>
</div>
<div class="contact-item">
<strong>Время ответа</strong>
<p>До 2 часов в рабочее время</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-faq-lavero-ru',
templateUrl: './faq-ru.component.html',
styleUrls: ['../../../../../../pages/info/faq/faq.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FaqLaveroRuComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,92 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Guarantee</h1>
<p class="subtitle">Protecting your purchases</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🏷️</div>
<h3>Warranty Periods</h3>
<div class="warranty-periods">
<div class="warranty-item">
<strong>Electronics</strong>
<span>12-24 months</span>
</div>
<div class="warranty-item">
<strong>Computer Equipment</strong>
<span>12-36 months</span>
</div>
<div class="warranty-item">
<strong>Clothing and Footwear</strong>
<span>30 days - 6 months</span>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Warranty Conditions</h3>
<ul class="compact-list">
<li>Used as intended</li>
<li>No unauthorized repairs</li>
<li>Seals preserved</li>
<li>No mechanical damage</li>
<li>Warranty card available</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🛠️</div>
<h3>Your Rights for Defects</h3>
<ul class="compact-list">
<li>Free repair</li>
<li>Product replacement</li>
<li>Money refund</li>
<li>Price reduction</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">⏱️</div>
<h3>Repair Timeframe</h3>
<p>Maximum 45 days by law. If the deadline is violated, you can request a replacement or a refund.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🚫</div>
<h3>Warranty Does Not Apply</h3>
<div class="noguar-grid">
<div class="noguar-item">Mechanical damage</div>
<div class="noguar-item">Improper use</div>
<div class="noguar-item">Liquid exposure</div>
<div class="noguar-item">Unauthorized repair</div>
<div class="noguar-item">Force majeure</div>
<div class="noguar-item">Natural wear and tear</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📝</div>
<h3>How to File a Claim</h3>
<div class="process-steps-compact">
<p>1. Contact the seller</p>
<p>2. Describe the issue</p>
<p>3. Get the service address</p>
<p>4. Send the product</p>
<p>5. Receive the acceptance report</p>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Need Help?</h3>
<p>In case of disputes:</p>
<a href="mailto:info@lovero.store" class="contact-email">info&#64;lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
<p class="note">Subject: "Warranty Issue - Order #..."</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-guarantee-lavero-en',
templateUrl: './guarantee-en.component.html',
styleUrls: ['../../../../../../pages/info/guarantee/guarantee.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GuaranteeLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-guarantee-lavero-ru /> }
@case ('en') { <app-guarantee-lavero-en /> }
@case ('hy') { <app-guarantee-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { GuaranteeLaveroRuComponent } from './ru/guarantee-ru.component';
import { GuaranteeLaveroEnComponent } from './en/guarantee-en.component';
import { GuaranteeLaveroHyComponent } from './hy/guarantee-hy.component';
@Component({
selector: 'app-guarantee-lavero',
imports: [GuaranteeLaveroRuComponent, GuaranteeLaveroEnComponent, GuaranteeLaveroHyComponent],
templateUrl: './guarantee.component.html',
styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GuaranteeLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

View File

@@ -0,0 +1,92 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Երաշխիք</h1>
<p class="subtitle">Ձեր գնումների պաշտպանություն</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🏷️</div>
<h3>Երաշխիքի ժամկետներ</h3>
<div class="warranty-periods">
<div class="warranty-item">
<strong>Էլեկտրոնիկա</strong>
<span>12-24 ամիս</span>
</div>
<div class="warranty-item">
<strong>Համակարգչային տեխնիկա</strong>
<span>12-36 ամիս</span>
</div>
<div class="warranty-item">
<strong>Հագուստ և կոշիկ</strong>
<span>30 օր - 6 ամիս</span>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Երաշխիքի պայմաններ</h3>
<ul class="compact-list">
<li>Օգտագործում ըստ նշանակության</li>
<li>Առանց ինքնուրույն նորոգման</li>
<li>Կապարանները պահպանված են</li>
<li>Մեխանիկական վնասներ չկան</li>
<li>Երաշխիքային տոմսը առկա է</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🛠️</div>
<h3>Ձեր իրավունքները թերության դեպքում</h3>
<ul class="compact-list">
<li>Անվճար նորոգում</li>
<li>Ապրանքի փոխարինում</li>
<li>Գումարի վերադարձ</li>
<li>Գնի նվազեցում</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">⏱️</div>
<h3>Նորոգման ժամկետ</h3>
<p>Օրենքով առավելագույնը 45 օր։ Եթե ժամկետը խախտվի ՝ կարող եք պահանջել փոխարինում կամ գումարի վերադարձ։</p>
</div>
<div class="info-card wide">
<div class="card-icon">🚫</div>
<h3>Երաշխիքը չի գործում</h3>
<div class="noguar-grid">
<div class="noguar-item">Մեխանիկական վնասներ</div>
<div class="noguar-item">Սխալ շահագործում</div>
<div class="noguar-item">Հեղուկի ներթափանցում</div>
<div class="noguar-item">Ինքնուրույն նորոգում</div>
<div class="noguar-item">Ֆորս-մաժոր</div>
<div class="noguar-item">Բնական մաշվածություն</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📝</div>
<h3>Ինչպես դիմել հայտ</h3>
<div class="process-steps-compact">
<p>1. Կապվեք վաճառողի հետ</p>
<p>2. Նկարագրեք խնդիրը</p>
<p>3. Ստացեք սերվիսի հասցեը</p>
<p>4. Ուղարկեք ապրանքը</p>
<p>5. Ստացեք ընդունման ակտը</p>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Օգնությու՞ն պետք է՞</h3>
<p>Վեճերի դեպքում՝</p>
<a href="mailto:info@lovero.store" class="contact-email">info&#64;lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
<p class="note">Թեմա՝ “Երաշխիքային հարց - Պատվեր №...”</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-guarantee-lavero-hy',
templateUrl: './guarantee-hy.component.html',
styleUrls: ['../../../../../../pages/info/guarantee/guarantee.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GuaranteeLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,92 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Гарантия</h1>
<p class="subtitle">Защита ваших покупок</p>
</div>
<div class="lavero-cards">
<div class="info-card wide">
<div class="card-icon">🏷️</div>
<h3>Сроки гарантии</h3>
<div class="warranty-periods">
<div class="warranty-item">
<strong>Электроника</strong>
<span>12-24 месяца</span>
</div>
<div class="warranty-item">
<strong>Компьютерная техника</strong>
<span>12-36 месяцев</span>
</div>
<div class="warranty-item">
<strong>Одежда и обувь</strong>
<span>30 дней - 6 месяцев</span>
</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon"></div>
<h3>Условия гарантии</h3>
<ul class="compact-list">
<li>Использование по назначению</li>
<li>Без самостоятельного ремонта</li>
<li>Сохранены пломбы</li>
<li>Нет механических повреждений</li>
<li>Есть гарантийный талон</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">🛠️</div>
<h3>Ваши права при браке</h3>
<ul class="compact-list">
<li>Бесплатный ремонт</li>
<li>Замена товара</li>
<li>Возврат денег</li>
<li>Снижение цены</li>
</ul>
</div>
<div class="info-card wide">
<div class="card-icon">⏱️</div>
<h3>Срок ремонта</h3>
<p>Максимум 45 дней по закону. Если срок нарушен - можно требовать замену или возврат денег.</p>
</div>
<div class="info-card wide">
<div class="card-icon">🚫</div>
<h3>Гарантия не действует</h3>
<div class="noguar-grid">
<div class="noguar-item">Механические повреждения</div>
<div class="noguar-item">Неправильная эксплуатация</div>
<div class="noguar-item">Попадание жидкости</div>
<div class="noguar-item">Самостоятельный ремонт</div>
<div class="noguar-item">Форс-мажор</div>
<div class="noguar-item">Естественный износ</div>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📝</div>
<h3>Как подать заявку</h3>
<div class="process-steps-compact">
<p>1. Свяжитесь с продавцом</p>
<p>2. Опишите проблему</p>
<p>3. Получите адрес сервиса</p>
<p>4. Отправьте товар</p>
<p>5. Получите акт приема</p>
</div>
</div>
<div class="info-card wide">
<div class="card-icon">📞</div>
<h3>Нужна помощь?</h3>
<p>При возникновении споров:</p>
<a href="mailto:info@lovero.store" class="contact-email">info&#64;lovero.store</a>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
<p class="note">Тема: "Гарантийный вопрос - Заказ №..."</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-guarantee-lavero-ru',
templateUrl: './guarantee-ru.component.html',
styleUrls: ['../../../../../../pages/info/guarantee/guarantee.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GuaranteeLaveroRuComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-company-details-lavero-ru /> }
@case ('en') { <app-company-details-lavero-en /> }
@case ('hy') { <app-company-details-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { CompanyDetailsLaveroRuComponent } from './ru/company-details-ru.component';
import { CompanyDetailsLaveroEnComponent } from './en/company-details-en.component';
import { CompanyDetailsLaveroHyComponent } from './hy/company-details-hy.component';
@Component({
selector: 'app-company-details-lavero',
imports: [CompanyDetailsLaveroRuComponent, CompanyDetailsLaveroEnComponent, CompanyDetailsLaveroHyComponent],
templateUrl: './company-details.component.html',
styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CompanyDetailsLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

View File

@@ -0,0 +1,54 @@
<div class="legal-page">
<div class="legal-container">
<h1>Реквизиты организации / Company Details</h1>
<section class="legal-section">
<h2>Полное наименование / Full Name</h2>
<p>«ԼԱՎԵՐՈ» Սահմանափակ պատասխանատվությամբ ընկերություն</p>
<p>«ЛАВЕРО» Общество с Ограниченной Ответственностью</p>
<p>«LAVERO» LIMITED LIABILITY COMPANY</p>
</section>
<section class="legal-section">
<h2>Юридический адрес / Legal Address</h2>
<p>ՀԱՅԱՍՏԱՆ, ԿՈՏԱՅՔ, ԱԲՈՎՅԱՆ, ՎԵՐԻՆ ՊՏՂՆԻ, 3-րդ փողոց, 28, Տ Փ/Դ՝ 2228</p>
</section>
<section class="legal-section">
<h2>Основные реквизиты / Core Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ՀՎՀՀ (ИНН/TIN):</strong>
<span>03590442</span>
</div>
<div class="detail-item">
<strong>Գրանցման համար (Registration Number):</strong>
<span>999.110.1583686</span>
</div>
<div class="detail-item">
<strong>Գրանցման ամսաթիվ (Date of Registration):</strong>
<span>18.05.2026</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Контактная информация / Contact Information</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@lovero.store">info@lovero.store</a></span>
</div>
<div class="detail-item">
<strong>Сайт / Website:</strong>
<span><a href="https://lovero.store" target="_blank">lovero.store</a></span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Руководство / Management</h2>
<p><strong>Տնօրեն (Director):</strong> ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ (GEVORG MATEVOSYAN)</p>
</section>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-company-details-lavero-en',
templateUrl: './company-details-en.component.html',
styleUrls: ['../../../../../../pages/legal/company-details/company-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CompanyDetailsLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,54 @@
<div class="legal-page">
<div class="legal-container">
<h1>Реквизиты организации / Company Details</h1>
<section class="legal-section">
<h2>Полное наименование / Full Name</h2>
<p>«ԼԱՎԵՐՈ» Սահմանափակ պատասխանատվությամբ ընկերություն</p>
<p>«ЛАВЕРО» Общество с Ограниченной Ответственностью</p>
<p>«LAVERO» LIMITED LIABILITY COMPANY</p>
</section>
<section class="legal-section">
<h2>Юридический адрес / Legal Address</h2>
<p>ՀԱՅԱՍՏԱՆ, ԿՈՏԱՅՔ, ԱԲՈՎՅԱՆ, ՎԵՐԻՆ ՊՏՂՆԻ, 3-րդ փողոց, 28, Տ Փ/Դ՝ 2228</p>
</section>
<section class="legal-section">
<h2>Основные реквизиты / Core Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ՀՎՀՀ (ИНН/TIN):</strong>
<span>03590442</span>
</div>
<div class="detail-item">
<strong>Գրանցման համար (Registration Number):</strong>
<span>999.110.1583686</span>
</div>
<div class="detail-item">
<strong>Գրանցման ամսաթիվ (Date of Registration):</strong>
<span>18.05.2026</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Контактная информация / Contact Information</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@lovero.store">info@lovero.store</a></span>
</div>
<div class="detail-item">
<strong>Сайт / Website:</strong>
<span><a href="https://lovero.store" target="_blank">lovero.store</a></span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Руководство / Management</h2>
<p><strong>Տնօրեն (Director):</strong> ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ (GEVORG MATEVOSYAN)</p>
</section>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-company-details-lavero-hy',
templateUrl: './company-details-hy.component.html',
styleUrls: ['../../../../../../pages/legal/company-details/company-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CompanyDetailsLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,54 @@
<div class="legal-page">
<div class="legal-container">
<h1>Реквизиты организации / Company Details</h1>
<section class="legal-section">
<h2>Полное наименование / Full Name</h2>
<p>«ԼԱՎԵՐՈ» Սահմանափակ պատասխանատվությամբ ընկերություն</p>
<p>«ЛАВЕРО» Общество с Ограниченной Ответственностью</p>
<p>«LAVERO» LIMITED LIABILITY COMPANY</p>
</section>
<section class="legal-section">
<h2>Юридический адрес / Legal Address</h2>
<p>ՀԱՅԱՍՏԱՆ, ԿՈՏԱՅՔ, ԱԲՈՎՅԱՆ, ՎԵՐԻՆ ՊՏՂՆԻ, 3-րդ փողոց, 28, Տ Փ/Դ՝ 2228</p>
</section>
<section class="legal-section">
<h2>Основные реквизиты / Core Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ՀՎՀՀ (ИНН/TIN):</strong>
<span>03590442</span>
</div>
<div class="detail-item">
<strong>Գրանցման համար (Registration Number):</strong>
<span>999.110.1583686</span>
</div>
<div class="detail-item">
<strong>Գրանցման ամսաթիվ (Date of Registration):</strong>
<span>18.05.2026</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Контактная информация / Contact Information</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@lovero.store">info@lovero.store</a></span>
</div>
<div class="detail-item">
<strong>Сайт / Website:</strong>
<span><a href="https://lovero.store" target="_blank">lovero.store</a></span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Руководство / Management</h2>
<p><strong>Տնօրեն (Director):</strong> ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ (GEVORG MATEVOSYAN)</p>
</section>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
@Component({
selector: 'app-company-details-lavero-ru',
templateUrl: './company-details-ru.component.html',
styleUrls: ['../../../../../../pages/legal/company-details/company-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CompanyDetailsLaveroRuComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,162 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Payment Terms</h1>
<p class="subtitle">All payment methods and transaction conditions</p>
</div>
<div class="lavero-cards">
<section class="info-card wide">
<div class="card-icon">📋</div>
<h2>1. General Provisions</h2>
<p>1.1. These Terms define the payment procedure for Goods and Services purchased by Buyers through the Lovero Store Marketplace.</p>
<p>1.2. Payment is made for Goods/Services listed by independent Sellers. The Marketplace acts as an information intermediary and provides the technical infrastructure for processing payments.</p>
<p>1.3. Payment for goods and services on the Marketplace is made in Russian rubles (RUB).</p>
<p>1.4. Prices for Goods/Services are set by Sellers independently and are indicated on the corresponding Goods/Services page.</p>
</section>
<section class="info-card wide">
<div class="card-icon">💳</div>
<h2>2. Payment Methods</h2>
<p>2.1. The Marketplace supports the following payment methods:</p>
<div class="payment-methods-grid">
<div class="method-item">
<span class="method-icon">🏦</span>
<div>
<strong>Bank Cards</strong>
<p>Visa, Mastercard, МИР</p>
</div>
</div>
<div class="method-item">
<span class="method-icon"></span>
<div>
<strong>SBP</strong>
<p>Fast Payment System - instant transfer via mobile banking app</p>
</div>
</div>
<div class="method-item">
<span class="method-icon">👛</span>
<div>
<strong>E-Wallets</strong>
<p>ЮMoney, QIWI (if available)</p>
</div>
</div>
<div class="method-item">
<span class="method-icon">🔗</span>
<div>
<strong>Payment by Link</strong>
<p>Generation of a unique payment link for each order</p>
</div>
</div>
</div>
<p>2.2. Available payment methods may vary depending on the Seller and the type of Goods/Services.</p>
<p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p>
</section>
<section class="info-card wide">
<div class="card-icon">⚙️</div>
<h2>3. Payment Process</h2>
<p>3.1. The order payment procedure includes the following steps:</p>
<ol class="compact-list">
<li>Selection of Goods/Services and adding them to the cart</li>
<li>Placing an order with contact details and delivery method</li>
<li>Choosing a payment method from the available options</li>
<li>Redirect to the secure payment system page</li>
<li>Entering payment details and confirming the payment</li>
<li>Receiving a successful payment notification</li>
</ol>
<p>3.2. When paying by bank card, the Buyer may be redirected to the issuing bank's page for additional authentication (3D-Secure).</p>
<p>3.3. The Buyer's payment obligation is considered fulfilled from the moment the funds are received in the payment system account.</p>
</section>
<section class="info-card wide">
<div class="card-icon">🛡️</div>
<h2>4. Payment Security</h2>
<p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p>
<p>4.2. The Marketplace does not store full bank card data of Buyers. Payment data processing is carried out by certified payment aggregators.</p>
<div class="features-list">
<div class="feature">✓ TLS 1.2+ Encryption</div>
<div class="feature">✓ 3D-Secure Technology</div>
<div class="feature">✓ Fraud Protection</div>
<div class="feature">✓ Data Confidentiality</div>
</div>
<p>4.3. To protect against fraud, 3D-Secure technology is used, requiring payment confirmation via SMS code or push notification from the bank.</p>
<p>4.4. In case of suspicious activity, the payment system has the right to request additional identity verification of the Buyer.</p>
</section>
<section class="info-card wide">
<div class="card-icon"></div>
<h2>5. Payment Confirmation</h2>
<p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during checkout.</p>
<p>5.2. The confirmation contains the following information:</p>
<ul class="compact-list">
<li>Order number</li>
<li>Payment date and time</li>
<li>Payment amount</li>
<li>Order contents</li>
<li>Seller contact details</li>
</ul>
<p>5.3. Order information is also displayed in the Buyer's personal account on the Marketplace (if registered).</p>
<p>5.4. A fiscal receipt is sent by the Seller in accordance with the requirements of the legislation of the Russian Federation.</p>
</section>
<section class="info-card wide">
<div class="card-icon">↩️</div>
<h2>6. Refunds</h2>
<p>6.1. The refund procedure is governed by the <a [routerLink]="'/return-policy' | langRoute">Return Policy</a> and depends on the type of purchased Goods/Services.</p>
<p>6.2. Refunds are made to the same payment instrument from which the payment was made.</p>
<p>6.3. The refund processing time is:</p>
<div class="refund-times">
<div class="refund-item">
<strong>Bank Card</strong>
<span>3-30 days</span>
</div>
<div class="refund-item">
<strong>E-Wallet</strong>
<span>1-5 days</span>
</div>
<div class="refund-item">
<strong>SBP</strong>
<span>1-3 days</span>
</div>
</div>
<p class="note">Refunds are made to the same payment instrument used for the original payment</p>
<p>6.4. The Marketplace does not charge a fee for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p>
</section>
<section class="info-card wide">
<div class="card-icon"></div>
<h2>7. Failed Payments</h2>
<p>7.1. A payment may be declined for the following reasons:</p>
<ul class="compact-list">
<li>Insufficient funds in the account</li>
<li>Incorrectly entered payment details</li>
<li>Card is blocked or expired</li>
<li>Transaction limits set by the bank have been exceeded</li>
<li>Transaction rejected by the security system</li>
</ul>
<p>7.2. In case of an unsuccessful payment, the Buyer receives a notification indicating the reason for the decline.</p>
<p>7.3. If you experience payment issues, it is recommended to:</p>
<ul class="compact-list">
<li>Check the accuracy of the entered data</li>
<li>Contact the card issuing bank to clarify the reason for the decline</li>
<li>Try an alternative payment method</li>
<li>Contact support: <a href="mailto:info@lovero.store">info@lovero.store</a></li>
</ul>
</section>
<section class="info-card wide">
<div class="card-icon">📧</div>
<h2>8. Payment Inquiries Contact</h2>
<p>For questions related to order payments, you can contact us:</p>
<ul class="compact-list">
<li><strong>Email:</strong> <a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a></li>
<li><strong>Phone:</strong> <a [href]="env.phoneTel">{{ env.phones.support }}</a></li>
<li><strong>Working hours:</strong> 24/7 (technical support)</li>
<li><strong>Average response time:</strong> Up to 24 hours on business days</li>
</ul>
<p>When contacting us, please include your order number and a brief description of the issue for faster resolution.</p>
</section>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
import { RouterLink } from '@angular/router';
import { LangRoutePipe } from '../../../../../../pipes/lang-route.pipe';
@Component({
selector: 'app-payment-terms-lavero-en',
imports: [RouterLink, LangRoutePipe],
templateUrl: './payment-terms-en.component.html',
styleUrls: ['../../../../../../pages/legal/payment-terms/payment-terms.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PaymentTermsLaveroEnComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,162 @@
<div class="legal-page">
<div class="legal-container">
<div class="lavero-header">
<h1>Վճարման պայմաններ</h1>
<p class="subtitle">Վճարման բոլոր եղանակները և գործարքների իրականացման պայմանները</p>
</div>
<div class="lavero-cards">
<section class="info-card wide">
<div class="card-icon">📋</div>
<h2>1. Ընդհանուր դրույթներ</h2>
<p>1.1. Սույն Կանոնները սահմանում են Lovero Store Մարկեթպլեյսի միջոցով Գնորդների կողմից ձեռք բերվող Ապրանքների և Ծառայությունների վճարման կարգը։</p>
<p>1.2. Վճարումը կատարվում է անկախ Վաճառողների կողմից տեղադրված Ապրանքների/Ծառայությունների համար։ Մարկեթպլեյսը հանդես է գալիս որպես տեղեկատվական միջնորդ և ապահովում է տեխնիկական ենթակառուցվածք վճարումների իրականացման համար։</p>
<p>1.3. Մարկեթպլեյսում ապրանքների և ծառայությունների վճարումը կատարվում է ռուսական ռուբլով (RUB)։</p>
<p>1.4. Ապրանքների/Ծառայությունների գները սահմանվում են Վաճառողների կողմից ինքնուրույն և նշված են համապատասխան Ապրանքի/Ծառայության էջում։</p>
</section>
<section class="info-card wide">
<div class="card-icon">💳</div>
<h2>2. Վճարման եղանակներ</h2>
<p>2.1. Մարկեթպլեյսը աջակցում է վճարման հետևյալ եղանակները՝</p>
<div class="payment-methods-grid">
<div class="method-item">
<span class="method-icon">🏦</span>
<div>
<strong>Բանկային քարտեր</strong>
<p>Visa, Mastercard, ՄИР</p>
</div>
</div>
<div class="method-item">
<span class="method-icon"></span>
<div>
<strong>Արագ վճարման համակարգ (ՍБП)</strong>
<p>Արագ վճարման համակարգ - ակնթարծային փոխանցում բանկի բջջային հավելվածի միջոցով</p>
</div>
</div>
<div class="method-item">
<span class="method-icon">👛</span>
<div>
<strong>Էլեկտրոնային դրամապանակներ</strong>
<p>ЮMoney, QIWI (առկայության դեպքում)</p>
</div>
</div>
<div class="method-item">
<span class="method-icon">🔗</span>
<div>
<strong>Վճարում հղման միջոցով</strong>
<p>Յուրաքանչյուր պատվերի համար յուրահատուկ վճարման հղման ստեղծում</p>
</div>
</div>
</div>
<p>2.2. Հասանելի վճարման եղանակները կարող են տարբերվել կախված Վաճառողից և Ապրանքի/Ծառայության տեսակից։</p>
<p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարման համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p>
</section>
<section class="info-card wide">
<div class="card-icon">⚙️</div>
<h2>3. Վճարման գործընթացը</h2>
<p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p>
<ol class="compact-list">
<li>Ապրանքների/Ծառայությունների ընտրություն և դրանց զամբյուղին ավելացնել</li>
<li>Պատվերի ձևակերպում՝ կոնտակտային տվյալների և առաքման եղանակի նշումով</li>
<li>Վճարման եղանակի ընտրությունը հասանելի տարբերակներից</li>
<li>Վերահղում վճարման համակարգի պաշտպանված էջին</li>
<li>Վճարման տվյալների մուտքագրում և վճարման հաստատում</li>
<li>Հաջող վճարման մասին ծանուցման ստացում</li>
</ol>
<p>3.2. Բանկային քարտով վճարելիս Գնորդը կարող է վերահղվել թողարկող բանկի էջ լրացուցիչ նույնականացման համար (3D-Secure)։</p>
<p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարման համակարգի հաշվին դրամական միջոցների մուտքագրման պահից։</p>
</section>
<section class="info-card wide">
<div class="card-icon">🛡️</div>
<h2>4. Վճարումների անվտանգություն</h2>
<p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապակցով՝ TLS 1.2 և ավելի բարձր արթանագրի օգտագործմամբ։</p>
<p>4.2. Մարկեթպլեյսը չի պահպանում Գնորդների բանկային քարտերի լիարժեք տվյալները։ Վճարման տվյալների մշակումը իրականացվում է սերտիֆիկացված վճարման ագրեգատորների կողմից։</p>
<div class="features-list">
<div class="feature">✓ TLS 1.2+ գաղտնագրում</div>
<div class="feature">✓ 3D-Secure տեխնոլոգիա</div>
<div class="feature">✓ Խաբեությունից պաշտպանություն</div>
<div class="feature">✓ Տվյալների գաղտնիություն</div>
</div>
<p>4.3. Խաբեությունից պաշտպանության համար կիրառվում է 3D-Secure տեխնոլոգիան՝ պահանջելով վճարման հաստատումը SMS կոդի կամ բանկի push ծանուցման միջոցով։</p>
<p>4.4. Կասկածելի գործունեության դեպքում վճարման համակարգը իրավունք ունի պահանջել Գնորդի ինքնության լրացուցիչ ստուգում։</p>
</section>
<section class="info-card wide">
<div class="card-icon"></div>
<h2>5. Վճարման հաստատում</h2>
<p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման թելադրված էլ. փոստի հասցեին։</p>
<p>5.2. Հաստատումը պարունակում է հետևյալ տեղեկությունը՝</p>
<ul class="compact-list">
<li>Պատվերի համար</li>
<li>Վճարման ամսաթիվ և ժամ</li>
<li>Վճարման գումար</li>
<li>Պատվերի կազմը</li>
<li>Վաճառողի կոնտակտային տվյալներ</li>
</ul>
<p>5.3. Պատվերի տեղեկությունը նաև ցուցադրվում է Գնորդի անձնական ընթացում Մարկեթպլեյսում (գրանցման դեպքում)։</p>
<p>5.4. Հարկային կտրոնը ուղարկվում է Վաճառողի կողմից՝ Ռուսաստանի Դաշնության օրենսդրության պահանջներին համապատասխան։</p>
</section>
<section class="info-card wide">
<div class="card-icon">↩️</div>
<h2>6. Միջոցների վերադարձ</h2>
<p>6.1. Դրամական միջոցների վերադարձի կարգը կարգավորվում է <a [routerLink]="'/return-policy' | langRoute">Վերադարձի քաղաքականությամբ</a> և կախված է ձեռք բերված Ապրանքի/Ծառայության տեսակից։</p>
<p>6.2. Միջոցների վերադարձը կատարվում է նույն վճարման գործիքին՝ որից կատարվել էր վճարումը։</p>
<p>6.3. Դրամական միջոցների վերադարձի ժամկետը կազմում է՝</p>
<div class="refund-times">
<div class="refund-item">
<strong>Բանկային քարտ</strong>
<span>3-30 օր</span>
</div>
<div class="refund-item">
<strong>Էլեկտրոնային դրամապանակ</strong>
<span>1-5 օր</span>
</div>
<div class="refund-item">
<strong>Արագ վճարման համակարգ (ՍБП)</strong>
<span>1-3 օր</span>
</div>
</div>
<p class="note">Վերադարձը կատարվում է նույն վճարման գործիքին՝ որը օգտագործվել է վճարման ժամանակ</p>
<p>6.4. Միջոցների վերադարձի մշակման համար Մարկեթպլեյսը միջնորդավճար չի գանձում։ Վճարման համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել դրանց սակագներին համապատասխան։</p>
</section>
<section class="info-card wide">
<div class="card-icon"></div>
<h2>7. Անհաջող վճարումներ</h2>
<p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p>
<ul class="compact-list">
<li>Հաշվին անբավարար միջոցներ</li>
<li>Վճարման տվյալները սխալ են մուտքագրված</li>
<li>Քարտը արգելափակված է կամ ժամկետը լրացել է</li>
<li>Բանկի կողմից սահմանված գործարքների սահմանաչափերը գերազանցված է</li>
<li>Անվտանգության համակարգի կողմից գործարքի մերժում</li>
</ul>
<p>7.2. Անհաջող վճարման դեպքում Գնորդը ստանում է ծանուցում՝ մերժման պատճառի նշումով։</p>
<p>7.3. Վճարման խնդիրների դեպքում խորհուրդ է տրվում՝</p>
<ul class="compact-list">
<li>Ստուգել մուտքագրված տվյալների ճշտությունը</li>
<li>Կապվել քարտ թողարկող բանկի հետ մերժման պատճառը պարզելու համար</li>
<li>Փորձել այլընտրանքային վճարման եղանակ</li>
<li>Դիմել աջակցության ծառայությանը՝ <a href="mailto:info@lovero.store">info@lovero.store</a></li>
</ul>
</section>
<section class="info-card wide">
<div class="card-icon">📧</div>
<h2>8. Վճարման հարցերի կապակցության կոնտակտներ</h2>
<p>Պատվերների վճարման հետ կապված հարցերի համար կարող եք դիմել՝</p>
<ul class="compact-list">
<li><strong>Email:</strong> <a href="mailto:info@lovero.store" class="contact-email">info@lovero.store</a></li>
<li><strong>Հեռախոս՝</strong> <a [href]="env.phoneTel">{{ env.phones.support }}</a></li>
<li><strong>Աշխատանքի ժամերը՝</strong> Հստակետ (տեխնիկական աջակցություն)</li>
<li><strong>Պատասխանի միջին ժամանակը՝</strong> Մինչև 24 ժամ աշխատանքային օրերին</li>
</ul>
<p>Դիմելիս նշեք պատվերի համարը և խնդրի հակիրճ նկարագրությունը՝ հարցի ավելի արագ լուծման համար։</p>
</section>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../../../../../environments/environment';
import { RouterLink } from '@angular/router';
import { LangRoutePipe } from '../../../../../../pipes/lang-route.pipe';
@Component({
selector: 'app-payment-terms-lavero-hy',
imports: [RouterLink, LangRoutePipe],
templateUrl: './payment-terms-hy.component.html',
styleUrls: ['../../../../../../pages/legal/payment-terms/payment-terms.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PaymentTermsLaveroHyComponent {
protected readonly env = environment;
}

View File

@@ -0,0 +1,5 @@
@switch (lang()) {
@case ('ru') { <app-payment-terms-lavero-ru /> }
@case ('en') { <app-payment-terms-lavero-en /> }
@case ('hy') { <app-payment-terms-lavero-hy /> }
}

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../../../services/language.service';
import { PaymentTermsLaveroRuComponent } from './ru/payment-terms-ru.component';
import { PaymentTermsLaveroEnComponent } from './en/payment-terms-en.component';
import { PaymentTermsLaveroHyComponent } from './hy/payment-terms-hy.component';
@Component({
selector: 'app-payment-terms-lavero',
imports: [PaymentTermsLaveroRuComponent, PaymentTermsLaveroEnComponent, PaymentTermsLaveroHyComponent],
templateUrl: './payment-terms.component.html',
styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PaymentTermsLaveroComponent {
lang = inject(LanguageService).currentLanguage;
}

Some files were not shown because too many files have changed in this diff Show More