# База знаний — продукт и доменная модель Qadam

## Паспорт документа

- Статус документа: working reference
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении инженерного backlog, локального workflow или платформенного статуса
- Область применения: внутренний инженерный knowledge/rules/backlog слой проекта
- Связанные документы:
  - [Индекс документации](../README.md)
  - [Текущее состояние](../project/current-state.md)
  - [Roadmap](../project/roadmap.md)

Этот файл содержит доменную и продуктовую базу знаний по Qadam. Для правил разработки и operational-практик используй [`rules/`](rules/), а для текущего статуса платформы и production truth сверяйся с `../current-state.md`, `../roadmap.md` и `../api-routes.md`.

## Product Overview

**Qadam** (Қадам, "Step") is an EdTech aggregator for supplementary education in Uzbekistan. It connects learners (primarily **parents looking for education for their children**) with educational providers across three categories:

1. **Offline Learning Centers** (`school_offline`) — physical institutions: language schools, IT academies, exam prep centers, art studios, sports clubs
2. **Online Schools** (`online_school`) — digital courses and mini-group classes sold on the platform
3. **Individual Tutors** (`individual_contributor`) — private tutors, coaches, mentors

The core user journey: **Parent searches → Finds a course → Submits a lead (inquiry) → Provider contacts them → Enrollment**.

### Product Name & Branding

- **Product name**: Qadam (Қадам)
- **Primary color**: `#1DB57A` (green)
- **Font**: "Outfit" (Google Fonts) + system sans-serif fallback
- **Container**: max-width `1280px`
- **Responsive**: mobile-first with `md`/`lg` breakpoints
- **Dark theme**: not implemented

## User Roles & Subtypes

### Current Roles

| Role | Code | Subtypes | Description |
|------|------|----------|-------------|
| **Buyer** | `buyer` | `parent`, `student` | Searches courses, submits leads, writes reviews |
| **Seller** | `seller` | `school_offline`, `online_school`, `individual_contributor` | Manages courses, handles leads, manages staff |
| **Seller Staff** | `seller_staff` | — | Employee bound to a seller organization |
| **Admin** | `admin` | — | Moderation, reference data, analytics |

### Route Access

| Role | Routes | Key Actions |
|------|--------|-------------|
| Guest | `/`, `/item/*`, `/sellers/*` | Browse catalog, view items, submit leads |
| Buyer | + `/me/*` | Manage leads, write reviews, edit profile |
| Seller | + `/seller/*` | Manage items, view leads, manage staff |
| Seller Staff | отдельного стабильного кабинета пока нет | В текущем production редиректится в `staff-unavailable` |
| Admin | + `/admin/*` | Moderation, reference data management |

### Planned CRM Roles (Seller Dashboard)

| Role | Code | Access Level |
|------|------|-------------|
| Owner | `owner` | Full access including billing and org deletion |
| Admin | `admin` | Schedule, staff, clients, analytics management |
| Manager | `manager` | Leads, clients, bookings |
| Teacher | `teacher` | Own schedule, own groups, attendance marking |

**CRM Permissions Matrix:**

| Function | Owner | Admin | Manager | Teacher |
|----------|-------|-------|---------|---------|
| Org settings | + | + | - | - |
| Role/staff management | + | + | - | - |
| Service management | + | + | + | - |
| Schedule (all staff) | + | + | + | - |
| Schedule (own) | + | + | + | + |
| Client base | + | + | + | Own only |
| Bookings (all) | + | + | + | - |
| Bookings (own) | + | + | + | + |
| Groups (all) | + | + | + | - |
| Groups (own) | + | + | + | + |
| Leads | + | + | + | - |
| Analytics (full) | + | + | - | - |
| Analytics (own) | + | + | + | + |
| Special offers | + | + | + | - |
| Finance/reports | + | + | - | - |
| Attendance marking | + | + | + | + |

## Core Domain Entities

### Item (Course/Service) — Central Entity

The `Item` is the universal entity representing any educational offering. It is the **most important entity** in the system.

**Key attributes:**

| Field | Type | Description |
|-------|------|-------------|
| `item_id` | UUID | Primary key |
| `seller_id` | UUID FK | Owner seller |
| `school_id` | UUID FK (nullable) | Linked offline school |
| `online_school_id` | UUID FK (nullable) | Linked online school |
| `item_name` | string | Course title |
| `item_desc` | text | Full description |
| `item_shortdesc` | text | Short description (for cards) |
| `item_price_from` | decimal | Min price |
| `item_price_to` | decimal | Max price |
| `item_age_group` | string | Target age group label |
| `item_age_from` / `item_age_to` | int | Age range |
| `item_classes_from` / `item_classes_to` | int | School grade range |
| `subject_id` | UUID FK | Subject (e.g., Mathematics) |
| `subject_group_id` | UUID FK | Subject group (e.g., School subjects) |
| `item_studytype` | enum | `group`, `mini_group`, `one_on_one` |
| `item_studyformat` | enum | `online`, `offline`, `hybrid` |
| `item_language` | enum | `ru`, `uz_latin`, `uz_cyrillic`, `en`, `kk`, `tg`, `any` |
| `item_studyduration_from` / `to` | int | Duration range |
| `item_isvisible` | boolean | Visibility flag |
| `item_image_url` | string | Cover image |
| `location_id` | UUID FK | Location reference |
| `schedule_id` | UUID FK | Schedule reference |
| `specialoffer_group_id` | UUID | Special offers group |
| `item_perfomer_group_id` | UUID FK | Assigned teachers/performers |
| `moderation_status_id` | UUID FK | Moderation status |

### Buyer

A technical entity linking an account to either a Parent or Student profile:

- `buyer_type`: `parent` or `student`
- Parents can add students (children) to their profile
- Students can add parents to their profile
- This bidirectional linking is stored in the respective profile

### Seller

A technical entity linking an account to one of three subtypes:

- `school_offline` → `SchoolProfile` (name, phone, email, desc, location)
- `online_school` → `OnlineSchoolProfile` (name, phone, email, desc)
- `individual_contributor` → `IndividualContributorProfile` (name, phone, email, desc, location)

### Lead (Inquiry)

A user inquiry about an item:

| Field | Description |
|-------|-------------|
| `lead_type` | `trial` (trial lesson) or `buy` (enrollment) |
| `lead_name` | Contact name |
| `lead_phone` | Contact phone |
| `lead_email` | Contact email |
| `lead_comment` | Free-text comment |
| `lead_source` | Where the lead came from |
| `lead_status` | `created` → `contacted` → `enrolled` or `rejected` |
| `is_contacts_confirmed` | Phone/email verified |

### Review

- Rating: 1-5 stars (integer)
- Text comment
- Linked to item, buyer, and seller
- Aggregated stats pre-calculated in SAL layer (`rating_avg`, `reviews_count`)

### Schedule & Performers

- `ScheduleProfile`: datetime range, status, timezone
- `PerformerProfile`: active flag, description, specialization, experience, linked to seller_staff
- `PerformerGroupProfile`: groups performers for an item

### Moderation

- Items go through moderation before publishing
- Status codes: `active`, `rejected`, `pending`
- Admin can add reason and comment
- `is_visible_to_seller` controls whether rejection details are shown

### Reference Data (Registries)

| Registry | Fields | Notes |
|----------|--------|-------|
| **Subjects** | name, desc, group_id, group_name | Two-level hierarchy: group → subject |
| **Locations** | name, longitude, latitude | Cities/districts with coordinates |
| **Tags** | type, raw_value, name_ru, name_uz, is_visible | Bilingual tags |
| **Events** | name, type | User action types for analytics |
| **Moderation Statuses** | code, name | `active`, `rejected`, `pending` |

## Canonical Enums & Status Machines

> **Source of truth.** All specs MUST use these exact values. If a spec disagrees with this section, the spec is wrong — fix the spec.

### ItemStatus (moderation_status)

```prisma
enum ItemStatus {
  draft             // создан, не отправлен на модерацию
  pending           // на модерации у Admin
  active            // одобрен, публично виден (НЕ approved — это устаревшее)
  rejected          // отклонён, продавец видит причину
  revision_required // требует доработки от продавца
}
```

> Видимость в каталоге: `item_isvisible = true AND moderation_status = active`

### LeadStatus

```prisma
enum LeadStatus {
  new               // только что создан (НЕ pending, НЕ created)
  contacted         // продавец связался с байером (НЕ processing)
  enrolled          // клиент записан на занятие
  attended          // пришёл на пробное занятие
  no_show           // не пришёл на пробное занятие
  purchased         // оплатил / оформил покупку
  not_purchased     // решил не покупать
  rejected          // продавец отклонил лид
}
```

### ReviewStatus

```prisma
enum ReviewStatus {
  pending             // ожидает первичной модерации
  published           // одобрен и виден публично (НЕ active)
  rejected            // отклонён модератором
  pending_moderation  // был published, получил жалобу → скрыт, ждёт повторной проверки
}
```

> `rating_avg` считается только по `status = published`.

### PriceType

```prisma
enum PriceType {
  per_lesson   // цена за одно занятие
  per_month    // цена в месяц
  per_package  // цена за пакет N занятий  (НЕ просто package)
  one_time     // разовая оплата за весь курс (НЕ subscription)
}
```

### DiscountType

```prisma
enum DiscountType {
  percent       // скидка в процентах
  fixed_amount  // скидка фиксированной суммой
  gift          // подарок / бонусное занятие
}
```

### ItemView (аналитика просмотров)

`ItemView` — **входит в MVP** (Spec 11). Трекинг уникальных просмотров айтемов для seller dashboard. Spec 06 НЕ исключает ItemView — она исключает лишь `view_count` на публичной карточке айтема (не отображаем счётчик байеру). Трекинг пишется в БД, данные видны только продавцу в дашборде.

### CPL Deduplication Rule

**30-дневное окно:** если за последние 30 дней уже существует лид с тем же `buyer_account_id + item_id` (или тем же `lead_phone + item_id` для незалогиненных) — CPL НЕ начисляется. Лид создаётся, но `CplTransaction` не генерируется. Это правило обязательно учитывается в Spec 16 (CPL Billing) при биллинге.

---

## Category Tree

The platform has 11 top-level categories with subcategories:

| # | Category | Slug | Subcategories |
|---|----------|------|---------------|
| 1 | School & University subjects | `school` | Math, Physics, Chemistry, Biology, History, Informatics, Russian, Literature, Social Studies, Uzbek |
| 2 | Foreign languages | `languages` | English, German, Chinese, French, Russian as foreign, Uzbek as foreign |
| 3 | IT & Business | `it` | Programming, Robotics, 3D Modeling, Circuit Design |
| 4 | Creative arts | `creative` | Drawing, Music, Dance, Theater, Design |
| 5 | Beauty & Health | `beauty-health` | — |
| 6 | Sports | `sports` | Football, Swimming, General fitness |
| 7 | Driving | `driving` | — |
| 8 | Soft skills | `soft-skills` | Critical thinking, Public speaking |
| 9 | Financial literacy | `finance` | — |
| 10 | Chess | `chess` | — |
| 11 | Other | `other` | — |

Categories use a slug-based system. The `getCategoriesInfo(slug)` function resolves a slug to its display labels (parent + child).

## Пользовательские сценарии

### Реально работающие сейчас

#### 1. Catalog → Item → Lead

```text
Buyer открывает каталог →
Открывает карточку айтема →
Создаёт лид через buyer-авторизацию →
Лид появляется в seller-кабинете
```

#### 2. Seller registration → Profile → Moderation

```text
Seller проходит регистрацию →
Создаёт и редактирует профиль →
Создаёт айтем →
Отправляет его на модерацию →
После approve айтем попадает в публичный каталог
```

#### 3. Buyer registration / profile backend flow

```text
Buyer регистрируется через registration API →
Создаёт или обновляет профиль →
Parent управляет детьми →
Student управляет interests
```

### Частично реализовано

- Публичный seller profile
- Reviews flow
- Admin moderation
- Buyer cabinet

### Осознанно не считать реализованным

- Quiz flow
- Telegram lead notifications
- CRM booking / schedule / groups
- Owner analytics и promo tools

### Journey 10 (CRM): Teacher Role
```
Login → Limited menu: My Schedule, My Groups, My Students →
Calendar → Only own bookings → Group → Mark attendance →
Own stats: lessons count, workload percentage
```

## Pages & UI Architecture

### Public Pages

| Page | URL | Key Elements |
|------|-----|-------------|
| Home / Catalog | `/` | Search bar, category nav (pill buttons), filter sidebar (desktop) / drawer (mobile), course feed с infinite scroll, quiz modal, lead modal |
| Course Detail | `/item/[slug]` | SSR page, course details, similar courses, reviews block, sidebar with price + "Enroll" button, lead modal |
| Seller Profile | `/sellers/[id]` | Seller info, course list, rating & reviews |
| Login | `/login` | Role selection (Buyer/Seller), phone/email login, multi-account support |
| Registration | `/register` | 4-step flow: role → account → subtype → onboarding |

### Buyer Dashboard (`/me/*`)

Navigation: Overview | Profile | My Leads | My Reviews

| Page | URL | Content |
|------|-----|---------|
| Overview | `/me` | Dashboard cards with quick links |
| Profile | `/me/profile` | Edit name, phone, email |
| My Leads | `/me/leads` | Lead list + status badges (New, Contacted, Enrolled, Rejected) |
| My Reviews | `/me/reviews` | Review list with ratings |

### Seller Dashboard (`/seller/*`)

Navigation: Overview | About School | My Courses | Leads | Staff

| Page | URL | Content |
|------|-----|---------|
| Overview | `/seller` | Stats: course count, leads, rating |
| About School | `/seller/profile` | Edit organization profile |
| My Courses | `/seller/items` | Course list + moderation status badges |
| Create Course | `/seller/items/new` | Multi-step creation form |
| Edit Course | `/seller/items/[id]/edit` | Pre-filled edit form |
| Leads | `/seller/leads` | Incoming leads + status change |
| Staff | `/seller/staff` | CRUD staff (name, phone, email, role) |

### Admin Panel (`/admin/*`)

Navigation: Overview | Moderation | All Leads | Subjects | Locations

| Page | URL | Content |
|------|-----|---------|
| Overview | `/admin` | Stats: pending, total courses, leads, sellers |
| Moderation | `/admin/moderation/items` | Items pending review + Approve/Reject |
| All Leads | `/admin/leads` | Table view of all system leads |
| Subjects | `/admin/reference/subjects` | CRUD subject groups and subjects |
| Locations | `/admin/reference/locations` | CRUD locations (City, District, Region) |

### UI Components Inventory

| Component | Purpose |
|-----------|---------|
| `Header` | Global nav: logo, city selector, language switcher (RU/UZ), auth buttons |
| `CategoryNav` | Category/subcategory navigation (pill-style buttons) |
| `SearchBar` | Search input + location selector + mobile filter toggle |
| `FilterSidebar` | Desktop filter panel (checkboxes, sliders for price/age) |
| `FilterDrawer` | Mobile filter modal (same filters as sidebar) |
| `HeroBanner` | Hero banner on home page |
| `CourseFeed` | Course card grid + infinite scroll через sentinel |
| `CourseCard` | Card: photo, title, seller name, rating, price, format badge, location |
| `CourseCardSkeleton` | Loading skeleton for course cards |
| `SimilarItems` | "Similar courses" block on item detail page |
| `ReviewsBlock` | Reviews list with ratings on item detail page |
| `ReviewForm` | Star rating + text input for writing reviews |
| `LeadModal` | Inquiry modal: name, phone, type (trial/buy), comment |
| `QuizModal` | Quiz dialog for personalized recommendations |
| `OfferSidebar` | Course detail sidebar: price, schedule summary, "Enroll" button |
| `QadamMap` | OpenStreetMap map with markers |
| `NominatimAutocomplete` | Address autocomplete with debounce |
| `ItemBadge` | Status/tag badge |

### Catalog Filter Fields

The catalog supports filtering by:

| Filter | Type | Values |
|--------|------|--------|
| Format | checkbox | `online`, `offline`, `hybrid` |
| Language | checkbox | `ru`, `uz_latin`, `uz_cyrillic`, `en`, `kk`, `tg`, `any` |
| Study type | checkbox | `group`, `mini_group`, `one_on_one` |
| Time slot | checkbox | `morning`, `afternoon`, `evening` |
| Price range | slider | `priceFrom` — `priceTo` |
| Age range | slider | `ageMin` — `ageMax` |
| Category | pill buttons | From `CATEGORY_TREE` |
| Location | selector | From `LocationRegistry` |

Filters applied via URL query params with AND logic. UI-driven pagination works through infinite scroll; `page` и `limit` используются как внутренние transport-параметры запросов, а не как пользовательский URL state.

## When Working with the Catalog

The catalog is the main public-facing feature:
- Displays items with filters (subject, location, type, price range)
- Uses server-side rendering for SEO
- Catalog data is cached in Redis with 5-minute TTL
- Search is powered by PostgreSQL full-text search (with plans for Elasticsearch later)
- Categories are displayed in a horizontal navigation bar (pill buttons)

### Filtering Logic

Filters are applied via URL query parameters:
- `?subject=mathematics&location=tashkent&studyFormat=OFFLINE`
- Filters are combined with AND logic
- Pagination: infinite scroll + transport-level `page/limit` под капотом

## When Working with Locations and Maps

Locations are a critical part of the platform — learning centers, tutors, and group classes have physical addresses. Detailed rules in [rules/data-locations-and-maps.md](rules/data-locations-and-maps.md).

### Location Types

| Type | Has Address | Has Coordinates | Example |
|------|------------|-----------------|---------|
| `OFFLINE_ADDRESS` | Required | Required | Language school at a fixed address |
| `ONLINE` | No | No | Online-only course |
| `HYBRID` | Required | Required | School with online option |
| `MOBILE_TUTOR` | Coverage area only | No | Tutor visits student's home |
| `MULTIPLE_BRANCHES` | Multiple via ItemBranch | Yes, per branch | Large school chain |

### Geocoding — Nominatim (OpenStreetMap)

We use Nominatim for address autocomplete and geocoding:
- **Component**: `NominatimAutocomplete` — debounced autocomplete with 300ms delay
- **Map**: `QadamMap` — renders OpenStreetMap tiles with markers
- **API**: `https://nominatim.openstreetmap.org/search` with `countrycodes=uz`
- **User-Agent**: Required by Nominatim policy — set `Qadam/1.0 (contact@qadam.uz)`
- **Rate limit**: Max 1 request per second (Nominatim policy)
- **Response language**: `accept-language: uz,ru`

### Uzbekistan Coordinate Bounds

All coordinates must fall within Uzbekistan's bounding box:
- Latitude: 37.0 — 45.6
- Longitude: 55.9 — 73.2

Any coordinates outside these bounds are rejected. This prevents wrong map rendering (e.g., pin in the Atlantic Ocean at `[0, 0]`).

### Key Cities and Their Approximate Coordinates

| City | Latitude | Longitude |
|------|----------|-----------|
| Tashkent (Тошкент) | 41.2995 | 69.2401 |
| Samarkand (Самарқанд) | 39.6542 | 66.9597 |
| Bukhara (Бухоро) | 39.7747 | 64.4286 |
| Namangan (Наманган) | 41.0011 | 71.6722 |
| Andijan (Андижон) | 40.7830 | 72.3442 |
| Fergana (Фарғона) | 40.3842 | 71.7890 |
| Nukus (Нукус) | 42.4628 | 59.6035 |
| Karshi (Қарши) | 38.8610 | 65.7986 |

### Common Location Bugs to Watch For

1. **Map at `[0, 0]`**: Coordinates are `null`/`undefined`, map library defaults to zero. Always check before rendering.
2. **Swapped lat/lon**: Nominatim returns `lat,lon` but GeoJSON uses `[lon, lat]`. Always use typed `Coordinates` object.
3. **Results from wrong country**: Missing `countrycodes=uz` in Nominatim query.
4. **SSR crash on map**: Map libraries use `window` object. Always lazy-load with `ssr: false`.
5. **Rate limited**: No debounce on autocomplete. Always debounce 300ms minimum.
6. **Private address exposed**: Missing `displayPublicly` check in DTO. Always filter.

### Privacy Rules for Locations

- **City name**: Always public (needed for search/filtering)
- **Full address**: Only if `displayPublicly === true`
- **Coordinates**: Only if `displayPublicly === true`
- **Map rendering**: Only on item detail page and only if address is public

## When Working with Authentication

- JWT-based authentication via NestJS
- Tokens stored in httpOnly cookies (not localStorage — this is a fix from the old project which used localStorage)
- Access token (short-lived) + Refresh token (long-lived)
- Auth flow: Register → Verify Phone → Login
- Phone verification via SMS (Uzbekistan phone numbers: +998)
- Multi-account support: users can be both buyer and seller
- Login supports both phone and email
- Registration is a 4-step flow: role → account data → subtype → onboarding
- Global JWT guard with `@Public()` decorator for public endpoints

## When Working with Internationalization

Detailed rules in [rules/patterns-i18n.md](rules/patterns-i18n.md).

The platform supports three languages:
- **uz** (Uzbek) — primary/source language, always complete
- **ru** (Russian) — secondary language (majority of current users)
- **en** (English) — tertiary language

### Configuration Architecture

| Layer | File | Purpose |
|-------|------|---------|
| Config source of truth | `/i18n.json` | Defines source (`uz`) and target (`ru`, `en`) locales |
| Next.js i18n config | `apps/web/i18n.config.ts` | Reads from `i18n.json`, configures next-intl |
| Translation files | `apps/web/messages/{uz,ru,en}.json` | One JSON file per locale |
| Server-side loading | `apps/web/lib/i18n-server.ts` | `loadTranslations()` with caching and Uzbek fallback |
| Locale detection | `apps/web/lib/get-locale.ts` | Cookie → Accept-Language → default (`uz`) |
| Server Components | `getTranslations()` from `next-intl/server` | No JS bundle impact |
| Client Components | `useTranslations()` from `next-intl` | For interactive elements only |

### Two Types of Text

1. **UI strings** (buttons, labels, error messages) → Translation JSON files, always translated to all 3 languages
2. **Content** (item titles, descriptions, reviews) → Database, in seller's/user's chosen language, NOT translated via i18n

### Fallback Strategy

Missing translation keys fall back to Uzbek text (source language). This is implemented by merging locale translations on top of the complete Uzbek file:

```typescript
const merged = { ...uzTranslations, ...localeTranslations };
```

This ensures users never see raw translation keys like `catalog.title` — they see Uzbek text at minimum.

### Pluralization

Uzbek and Russian have different plural rules. Always use ICU message format:

```json
{
  "resultsCount": "{count, plural, one {# kurs topildi} other {# ta kurs topildi}}"
}
```

### When Adding New UI Text

1. Add the key to `messages/uz.json` first (source language)
2. Add translations to `messages/ru.json` and `messages/en.json`
3. Use `t('namespace.key')` in the component
4. Prefer Server Component `getTranslations()` over Client Component `useTranslations()`

## Analytical Database Architecture

The database follows a **three-layer analytical architecture** designed for event sourcing and data warehousing. This architecture is critical for analytics and must be preserved.

### Layer Overview

```
SAA (Staging Area Applied) → SAL (Staging Area Load) → DDS (Data Vault)
```

| Layer | Purpose | Mutability |
|-------|---------|-----------|
| **SAA** | Enriched operational data. Append-only event streams and profile snapshots | **Immutable** — new values = new row |
| **SAL** | Pre-calculated aggregates and transformations | **Immutable** — recalculated, not updated |
| **DDS** | Core data warehouse using Data Vault (Hub/Link/Satellite) | **Immutable** — deduplicated before insert |

### Key Principles

1. **Append-only**: Rows in SAA, SAL, and DDS are **never modified**. If a value changes, a new row is written with a new `event_time`.
2. **Every row has `event_time`**: This is the timestamp of when the data was captured, enabling full history reconstruction.
3. **Deduplication**: Before writing to DDS, values are checked against existing data. If identical, no new row is created.
4. **Current state via `DISTINCT ON`**: The "current" profile is derived by selecting the most recent row per entity ID.

### SAA Table Types

| Suffix | Purpose | Example |
|--------|---------|---------|
| `*_stream` | User action event streams (high volume) | `buyer_stream`, `cookie_stream`, `seller_stream` |
| `*_profile` | Entity characteristics (change-tracked) | `buyer_profile`, `seller_profile`, `item_profile` |
| `*_registry` | Admin-managed reference data | `subject_registry`, `location_registry`, `tag_registry` |

**Stream tables** always have `session_id` (user actions happen in sessions).
**Profile tables** store real-world characteristics that change over time.
**Registry tables** contain artificial data created by employees.

### SAL Aggregates (Pre-calculated)

| Table | Fields | Purpose |
|-------|--------|---------|
| `current_item_review_stats` | `item_id`, `rating_avg`, `reviews_count` | Per-item review statistics |
| `current_seller_review_stats` | `seller_id`, `rating_avg`, `reviews_count`, `items_count` | Per-seller review statistics |

### DDS — Data Vault Pattern

The DDS layer uses **Hub/Link/Satellite** modeling:

| Type | Prefix | Purpose | Example |
|------|--------|---------|---------|
| **Hub** | `H_` | Business key registration | `h_buyer`, `h_seller`, `h_item`, `h_review` |
| **Link** | `L_` | Relationships between two entities | `l_seller_item`, `l_buyer_review`, `l_item_location` |
| **Satellite** | `S_` | Entity attributes/characteristics | `s_item_name`, `s_school_phone`, `s_review_rating` |

**Key DDS entities (Hubs):** `h_buyer`, `h_parent`, `h_student`, `h_seller`, `h_school`, `h_item`, `h_review`, `h_serp`, `h_cookie`, `h_photo`, `h_video`, `h_location`, `h_session`, `h_event`

**Rules for DDS:**
- Hub tables contain only the business key + `event_time`
- Link tables connect exactly two Hub entities + `event_time`
- Satellite tables store one attribute per table (6NF) + `event_time`
- Only `*_profile` and `*_registry` SAA tables feed into Satellite tables
- `*_stream` tables feed into Hub and Link tables only (no attributes)
- No column in DDS can be null

### When Building Features — Database Implications

When adding a new feature, consider:

1. **Operational tables** (Prisma schema): These are the tables your NestJS code reads/writes directly
2. **Analytical shadow**: Each operational write should eventually flow into the SAA layer for analytics
3. **The Prisma schema is NOT the analytical schema**: Prisma models are for the operational database. The SAA/SAL/DDS architecture runs alongside, populated via ETL/CDC processes
4. **Design Prisma models for operational use**, but ensure they capture enough data to feed the analytical pipeline later

## Текущее расположение репозиториев

```text
qadam-core/
  apps/api/         # NestJS + Fastify backend
  packages/shared/  # Zod schemas, constants, backend contracts
  packages/prisma/  # Prisma schema, migrations, seed
  docs/             # каноническая документация
  specs/            # платформенные спеки

qadam-web/
  apps/web/         # Next.js frontend
  openapi/          # mirror OpenAPI artifact
  docs/             # frontend handoff / fallback docs
```

### Backend Module Structure (NestJS)

Each feature module follows the pattern:
```
src/modules/{feature}/
  {feature}.controller.ts    # HTTP layer (thin)
  {feature}.service.ts       # Business logic
  {feature}.module.ts        # NestJS module definition
  repositories/
    {feature}.repository.ts  # Data access (Prisma)
  dto/
    create-{feature}.dto.ts  # Input validation schemas
    {feature}.response.ts    # Response DTOs
```

### Frontend Page Structure (Next.js)

Pages use App Router conventions:
```
app/
  page.tsx                   # Home / Catalog (Server Component)
  item/[slug]/page.tsx       # Item detail (Server Component)
  sellers/[id]/page.tsx      # Seller profile (Server Component)
  login/page.tsx             # Login
  register/page.tsx          # Registration
  me/
    page.tsx                 # Buyer cabinet entry
    profile/page.tsx         # Buyer profile edit
    leads/page.tsx           # Buyer leads list
    reviews/page.tsx         # Buyer reviews
  seller/
    layout.tsx               # Seller dashboard layout
    page.tsx                 # Seller overview
    profile/page.tsx         # Seller profile edit
    items/page.tsx           # Seller items list
    items/new/page.tsx       # Create new item
    items/[id]/edit/page.tsx # Edit item
    leads/page.tsx           # Seller leads
    staff/page.tsx           # Staff management
  admin/
    layout.tsx               # Admin layout
    page.tsx                 # Admin overview
    moderation/items/page.tsx # Item moderation
    leads/page.tsx           # All leads
    reference/
      subjects/page.tsx      # Subject CRUD
      locations/page.tsx     # Location CRUD
```

## Database Connection Strategy

### Read/Write Splitting

- **Write operations** (INSERT, UPDATE, DELETE): Use primary PostgreSQL connection
- **Read operations** (SELECT): Use read replica via `prisma.$replica()`
- This reduces load on the primary database and improves read performance

### Redis Usage

| Use Case | Key Pattern | TTL |
|----------|-------------|-----|
| Catalog cache | `catalog:{filters_hash}` | 5 min |
| Reference data | `ref:subjects`, `ref:locations` | 30 min |
| Session | `session:{token}` | 24 hours |
| Rate limiting | `rate:{ip}:{endpoint}` | 1 min |
| Popular items | `popular:items` | 15 min |
| Review stats | `stats:item:{id}`, `stats:seller:{id}` | 10 min |

## SEO Requirements

- All public pages must be server-rendered (SSR or SSG)
- Each item page must have proper `<title>`, `<meta description>`, Open Graph tags
- Slugs must be transliterated from item titles (Cyrillic → Latin)
- Sitemap generation for all published items
- Structured data (JSON-LD) for course/service pages

## API Documentation

Текущий base prefix API: `/api/v1/`.

Источники истины:

- обзорная карта маршрутов: `../api-routes.md`
- машиночитаемый контракт: `../../apps/api/openapi/openapi.json`
- runtime Swagger UI: `/api/docs`
- runtime OpenAPI JSON: `/api/openapi.json`

Правило:

- примеры маршрутов внутри старых specs могут содержать исторические имена endpoint-ов;
- перед реализацией всегда сверяйся с `api-routes.md` и OpenAPI artifact.

## Infrastructure

### Текущий production contour

- product domain: `https://qadam.2fab.app`
- roadmap portal: `https://qadam-roadmap.2fab.app`
- runtime: `systemd` + host-level `nginx`
- API: `127.0.0.1:5001`
- Web: `127.0.0.1:3000`

### Local development ports

| Service | Port |
|---------|------|
| API (NestJS) | `localhost:5001` |
| Web (Next.js) | `localhost:5002` |
| PostgreSQL | обычно `localhost:5432` |
| Redis | обычно `localhost:6379` |

### Rate Limiting

- Global: 100 requests per 60 seconds per IP

### Notifications

- Telegram verification endpoints для seller уже есть
- доставка lead notifications в Telegram и Email пока не завершена
