Files
marketplaces/docs/API_CHANGES_REQUIRED.md

727 lines
23 KiB
Markdown
Raw Normal View History

2026-02-28 17:42:36 +04:00
# Complete Backend API Documentation
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
> **Last updated:** February 2026
> **Frontend:** Angular 21 · Dual-brand (Dexar + Novo)
> **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
---
## 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`
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
Simple health check.
**Response `200`:**
```json
{ "message": "pong" }
```
2026-02-28 17:18:24 +04:00
---
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
## 2. Catalog — Categories
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
### `GET /category`
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
Returns all top-level categories. Respects `X-Region` and `X-Language` headers.
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
**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`:**
2026-01-18 18:57:06 +04:00
```json
[
{
2026-02-28 17:18:24 +04:00
"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"
2026-01-18 18:57:06 +04:00
}
]
```
2026-02-28 17:18:24 +04:00
**Region object:**
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
| 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 |
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
> **Fallback:** If this endpoint is down, the frontend uses 6 hardcoded defaults: Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi.
2026-02-28 17:18:24 +04:00
---
2026-02-28 17:42:36 +04:00
## 8. Authentication (Telegram Login)
2026-02-28 17:18:24 +04:00
Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None).
2026-02-28 17:42:36 +04:00
All auth endpoints must include `withCredentials: true` CORS support.
### Auth flow
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
```
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
```
---
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
### `GET /auth/session` — Check current session
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
**Request:** Cookies only (session cookie set by backend).
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
**Response `200`** (authenticated):
2026-01-18 18:57:06 +04:00
```json
{
2026-02-28 17:18:24 +04:00
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": true,
"expiresAt": "2026-03-01T12:00:00Z"
2026-01-18 18:57:06 +04:00
}
```
2026-02-28 17:42:36 +04:00
**Response `200`** (expired):
2026-01-18 18:57:06 +04:00
```json
{
2026-02-28 17:18:24 +04:00
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": false,
"expiresAt": "2026-02-27T12:00:00Z"
2026-01-18 18:57:06 +04:00
}
```
2026-02-28 17:42:36 +04:00
**Response `401`** (no session):
2026-02-28 17:18:24 +04:00
```json
2026-02-28 17:42:36 +04:00
{ "error": "No active session" }
2026-02-28 17:18:24 +04:00
```
2026-01-18 18:57:06 +04:00
2026-02-28 17:18:24 +04:00
**AuthSession object:**
2026-02-28 17:42:36 +04:00
| 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 |
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
---
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
### `GET /auth/telegram/callback` — Telegram bot auth callback
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
Called by the Telegram bot after user authenticates.
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
**Request body (from bot):**
2026-01-18 18:57:06 +04:00
```json
{
2026-02-28 17:18:24 +04:00
"id": 123456789,
"first_name": "John",
"last_name": "Doe",
"username": "john_doe",
"photo_url": "https://t.me/i/userpic/...",
"auth_date": 1709100000,
"hash": "abc123def456..."
2026-01-18 18:57:06 +04:00
}
```
2026-02-28 17:42:36 +04:00
**Response:** Must set a session cookie and return:
2026-01-18 18:57:06 +04:00
```json
{
2026-02-28 17:18:24 +04:00
"sessionId": "sess_abc123",
"message": "Authenticated successfully"
2026-01-18 18:57:06 +04:00
}
```
2026-02-28 17:18:24 +04:00
**Cookie requirements:**
2026-02-28 17:42:36 +04:00
| 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 |
2026-01-18 18:57:06 +04:00
2026-02-28 17:18:24 +04:00
---
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
### `POST /auth/logout` — End session
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
**Request:** Cookies only, empty body `{}`
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
**Response `200`:**
2026-01-18 18:57:06 +04:00
```json
2026-02-28 17:42:36 +04:00
{ "message": "Logged out" }
2026-01-18 18:57:06 +04:00
```
2026-02-28 17:42:36 +04:00
Must clear/invalidate the session cookie.
2026-01-18 18:57:06 +04:00
2026-02-28 17:18:24 +04:00
---
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
### Session refresh
2026-01-18 18:57:06 +04:00
2026-02-28 17:42:36 +04:00
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`.
2026-02-28 17:18:24 +04:00
---
2026-02-28 17:42:36 +04:00
## 9. i18n / Translations
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
The frontend supports 3 languages: **Russian (ru)**, **English (en)**, **Armenian (hy)**.
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
The active language is sent via the `X-Language` HTTP header on every request.
### What the backend should do with `X-Language`
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
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.
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
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"
}
}
}
```
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
3. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default.
2026-02-28 17:18:24 +04:00
---
2026-02-28 17:42:36 +04:00
## 10. CORS Configuration
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
For auth cookies and custom headers to work, the backend CORS config must include:
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
```
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.
2026-02-28 17:18:24 +04:00
2026-02-28 17:42:36 +04:00
**Allowed origins:**
- `https://dexarmarket.ru`
- `https://novo.market`
- `http://localhost:4200` (dev)
- `http://localhost:4201` (dev, Novo)
2026-01-18 18:57:06 +04:00
---
2026-02-28 17:42:36 +04:00
## 11. Telegram Bot Setup
2026-01-18 18:57:06 +04:00
2026-02-28 17:18:24 +04:00
Each brand needs its own bot:
- **Dexar:** `@dexarmarket_bot`
- **Novo:** `@novomarket_bot`
2026-01-18 18:57:06 +04:00
2026-02-28 17:18:24 +04:00
The bot should:
1. Listen for `/start auth_{callbackUrl}` command
2. Extract the callback URL
2026-02-28 17:42:36 +04:00
3. Send the user's Telegram data (`id`, `first_name`, `username`, etc.) to that callback URL
2026-02-28 17:18:24 +04:00
4. The callback URL is `{apiUrl}/auth/telegram/callback`
2026-02-28 17:42:36 +04:00
---
## 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.