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 сам вычисляет и возвращает:
totalPricediscountPercentdiscountedPricepriceLabel
- каталог теперь возвращает агрегированную минимальную цену по пакетам
- детальная страница айтема и seller detail возвращают полную матрицу пакетов
Важно:
- backend-контракт использует
camelCase, неsnake_case - все пути идут через существующую схему
/api/v1/... - старая логика
special offersне используется для price rendering
1. Модель пакета занятий
Каждый пакет содержит:
studyType:ONE_ON_ONE | MINI_GROUP | GROUPlessonsCountamountcurrencydescriptionisHighlightedsortOrderdiscountAmountpromoText
В response backend дополнительно вычисляет:
totalPricediscountPercentdiscountedPricepriceLabel
Форматы 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 > 0lessonsCount >= 1discountAmount > 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это оригинальная цена минимального пакета до скидки, либоnullpriceCurrencypromoTextпромо-текст пакета с минимальной ценойpriceLabellabel пакета с минимальной ценой
Пример:
{
"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. Что фронту нужно сделать
- Переключить seller pricing UI на
priceVariants. - Использовать новые seller endpoints:
POST /api/v1/seller/items/:id/price-variantsPATCH /api/v1/seller/items/:id/price-variants/:variantIdDELETE /api/v1/seller/items/:id/price-variants/:variantId
- В каталоге использовать только агрегированные поля:
priceFrompriceOriginalpriceCurrencypromoTextpriceLabel
- На детальной странице строить тарифную матрицу из
item.priceVariants. - Не ждать от backend полей в
snake_caseи не ожидать отдельного сущностногоspecial offerAPI.
10. Где смотреть контракт
- OpenAPI: openapi.json
- Backend handoff: frontend-package-discounts-handoff-2026-04-15.md