Qadam Roadmap
проектdocs/frontend/frontend-package-discounts-handoff-2026-04-15.md

Handoff для frontend по Spec 02a — Package Discounts & Promo Text

Обновлён 15 апр. 2026 г., 10:05 · 0 комментариев

Handoff для frontend по Spec 02a — Package Discounts & Promo Text

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

  • Статус документа: working reference

  • Актуально на: 15 апреля 2026 года

  • Владелец: backend/platform-команда, совместно с frontend-командой

  • Пересмотр: при изменении pricing-контрактов ItemPriceVariant, catalog price aggregation или seller price-variant CRUD

  • Область применения: handoff backend-контракта скидок и promo text для frontend-команды qadam-web

  • Связанные документы:

  • Статус: ready for frontend

  • Актуально на: 15 апреля 2026

  • Репозиторий: qadam-core

  • Ветка: dev

  • Источник истины: openapi.json

Что реализовано на backend

В qadam-core добавлен pricing-слой на уровне пакета занятий:

  • новый Prisma model: ItemPriceVariant
  • скидка хранится как discountAmount
  • промо-текст хранится как promoText
  • backend сам вычисляет и возвращает:
    • totalPrice
    • discountPercent
    • discountedPrice
    • priceLabel
  • каталог теперь возвращает агрегированную минимальную цену по пакетам
  • детальная страница айтема и seller detail возвращают полную матрицу пакетов

Важно:

  • backend-контракт использует camelCase, не snake_case
  • все пути идут через существующую схему /api/v1/...
  • старая логика special offers не используется для price rendering

1. Модель пакета занятий

Каждый пакет содержит:

  • studyType: ONE_ON_ONE | MINI_GROUP | GROUP
  • lessonsCount
  • amount
  • currency
  • description
  • isHighlighted
  • sortOrder
  • discountAmount
  • promoText

В response backend дополнительно вычисляет:

  • totalPrice
  • discountPercent
  • discountedPrice
  • priceLabel

Форматы studyType

Backend уже возвращает готовый priceLabel, но фактический mapping такой:

  • ONE_ON_ONEПерсональные занятия
  • MINI_GROUPЗанятия в мини-группах
  • GROUPГрупповые занятия

2. Seller endpoints

Создать пакет

POST /api/v1/seller/items/:id/price-variants

Body:

{
  "studyType": "ONE_ON_ONE",
  "lessonsCount": 5,
  "amount": 500000,
  "currency": "UZS",
  "description": "Индивидуальные занятия",
  "isHighlighted": false,
  "sortOrder": 0,
  "discountAmount": 136000,
  "promoText": "Скидка до сентября"
}

Обновить пакет

PATCH /api/v1/seller/items/:id/price-variants/:variantId

Body partial:

{
  "discountAmount": 136000,
  "promoText": "Скидка до сентября"
}

Удалить пакет

DELETE /api/v1/seller/items/:id/price-variants/:variantId

Response пакета

И POST, и PATCH, и DELETE возвращают один и тот же contract пакета:

{
  "id": "uuid",
  "studyType": "ONE_ON_ONE",
  "lessonsCount": 5,
  "amount": 500000,
  "currency": "UZS",
  "description": "Индивидуальные занятия",
  "isHighlighted": false,
  "sortOrder": 0,
  "totalPrice": 2500000,
  "discountAmount": 136000,
  "discountPercent": 5,
  "discountedPrice": 2364000,
  "promoText": "Скидка до сентября",
  "priceLabel": "Персональные занятия",
  "createdAt": "2026-04-15T09:00:00.000Z",
  "updatedAt": "2026-04-15T09:00:00.000Z"
}

Снятие скидки и промо

  • снять скидку: PATCH с { "discountAmount": null }
  • снять промо: PATCH с { "promoText": null }

3. Валидации backend

Backend уже валидирует:

  • amount > 0
  • lessonsCount >= 1
  • discountAmount > 0, если передан
  • discountAmount < totalPrice
  • максимальная скидка — 99%
  • promoText максимум 30 символов
  • на один studyType можно завести не более 3 пакетов

Тексты ошибок:

  • Скидка не может быть больше стоимости пакета
  • Максимальная скидка — 99%
  • Укажите сумму скидки или отключите скидку
  • Максимум 30 символов
  • Для одного формата можно добавить не более 3 пакетов

4. Поведение item после изменения пакетов

После create / update / delete пакета backend автоматически:

  • пересчитывает агрегированные priceFrom и priceTo у Item
  • переводит item обратно в DRAFT
  • ставит isVisible = false

Это важно для seller UI: после изменения pricing айтем нужно снова довести до актуального состояния и повторно отправить на модерацию.

5. Seller item detail

GET /api/v1/seller/items/:id

В seller item detail теперь есть:

  • legacy поля priceFrom / priceTo
  • новый массив priceVariants

Для нового шага "Настройка занятий и скидок" primary source должен быть priceVariants.

6. Catalog response

GET /api/v1/catalog/items

В каждом элементе каталога backend теперь возвращает дополнительные поля:

  • priceFrom это минимальная итоговая цена среди всех пакетов
  • priceOriginal это оригинальная цена минимального пакета до скидки, либо null
  • priceCurrency
  • promoText промо-текст пакета с минимальной ценой
  • priceLabel label пакета с минимальной ценой

Пример:

{
  "id": "uuid",
  "name": "Подготовка к ЕГЭ",
  "slug": "ege-course",
  "priceFrom": 1200000,
  "priceOriginal": 1350000,
  "priceCurrency": "UZS",
  "promoText": "Скидка до сентября",
  "priceLabel": "Персональные занятия"
}

Правило рендера:

  • если priceOriginal !== null, показывать зачёркнутую старую цену
  • если promoText !== null, показывать под ценой
  • если priceLabel !== null, показывать подпись формата

Каталог не возвращает полный массив пакетов. Для карточки каталога нужно использовать только агрегированные поля выше.

7. Public item detail

GET /api/v1/catalog/items/:slug

В item теперь есть:

  • priceVariants: PriceVariantDto[]

То есть фронт получает полную матрицу пакетов со всеми вычисленными полями и может строить детальную сетку тарифов на своей стороне.

Пример:

{
  "item": {
    "id": "uuid",
    "name": "Подготовка к ЕГЭ",
    "priceVariants": [
      {
        "id": "uuid",
        "studyType": "ONE_ON_ONE",
        "lessonsCount": 5,
        "amount": 500000,
        "currency": "UZS",
        "description": "Индивидуальные занятия",
        "isHighlighted": false,
        "sortOrder": 0,
        "totalPrice": 2500000,
        "discountAmount": 136000,
        "discountPercent": 5,
        "discountedPrice": 2364000,
        "promoText": "Скидка до сентября",
        "priceLabel": "Персональные занятия"
      }
    ]
  },
  "similarItems": []
}

8. Что изменилось относительно старой модели

Backend больше не использует special offer как источник цены на карточке каталога и детальной страницы.

Теперь:

  • скидка живёт внутри ItemPriceVariant
  • каталог выбирает самый дешёвый пакет по итоговой цене
  • item detail возвращает полную матрицу пакетов

Legacy priceFrom / priceTo у Item пока сохранены как агрегированные поля для:

  • сортировки
  • фильтрации
  • совместимости текущего API

Они автоматически синхронизируются из пакетов.

9. Что фронту нужно сделать

  1. Переключить seller pricing UI на priceVariants.
  2. Использовать новые seller endpoints:
    • POST /api/v1/seller/items/:id/price-variants
    • PATCH /api/v1/seller/items/:id/price-variants/:variantId
    • DELETE /api/v1/seller/items/:id/price-variants/:variantId
  3. В каталоге использовать только агрегированные поля:
    • priceFrom
    • priceOriginal
    • priceCurrency
    • promoText
    • priceLabel
  4. На детальной странице строить тарифную матрицу из item.priceVariants.
  5. Не ждать от backend полей в snake_case и не ожидать отдельного сущностного special offer API.

10. Где смотреть контракт