Update pricing model to include half-year pricing and enhance validation
CI / Backend (build + test) (push) Successful in 1m24s
CI / Frontend (lint + typecheck + build) (push) Failing after 15s

- Introduced a new pricing field, `PricePerConfigPerHalfYear`, to the `PricingSettings` model, allowing for more flexible pricing options.
- Updated the `UpdatePricingSettingsCommand` and its validator to include the new half-year pricing, ensuring proper validation against the quarterly and yearly rates.
- Modified the `PricingSettingsDto` and related frontend components to accommodate the new half-year pricing field, including validation logic to prevent pricing discrepancies.
- Enhanced API documentation and frontend forms to reflect the updated pricing structure and validation rules.
This commit is contained in:
Leonid Pershin
2026-07-18 20:17:32 +03:00
parent 6dfd51ae7c
commit 32221af503
16 changed files with 1133 additions and 39 deletions
+1 -1
View File
@@ -249,7 +249,7 @@ reject/approve владением тикета не ограничены. Еди
| DELETE | `/api/admin/roles/{id}` | admin | — | `204 No Content` (системные `admin`/`user` удалить нельзя) |
| PATCH | `/api/admin/users/{id}/role` | admin | `{ roleId }` | `204 No Content` (`409 Roles.CannotRemoveLastAdmin`, если у цели сейчас `admin`, новая роль другая, и это единственный админ) |
| GET | `/api/admin/pricing` | admin | — | `PricingSettingsDto` (глобальная справочная цена за конфиг **в месяц**, одна на весь сервис — не per-роль) |
| PUT | `/api/admin/pricing` | admin | `{ pricePerConfigPerQuarter?, pricePerConfigPerYear? }` | `PricingSettingsDto` (`409`/`400`, если годовая ставка×12 дешевле квартальной×3) |
| PUT | `/api/admin/pricing` | admin | `{ pricePerConfigPerQuarter?, pricePerConfigPerHalfYear?, pricePerConfigPerYear? }` | `PricingSettingsDto` (`400`, если итог более длинного тарифа дешевле итога более короткого) |
Нет отдельного эндпоинта «активировать напрямую без запроса» — активация только через
approve/reject над `ActivationRequest`.
+20 -17
View File
@@ -302,28 +302,31 @@ UI **настойчиво напоминает** привязать его (ед
к роли: одна цена на весь сервис. Не биллинг — без статусов оплаты, дат окончания, интеграций с
платёжными системами.
| Поле | Тип | Заметки |
| --------------------------- | ----------------- | --------------------------------------------------- |
| `Id` | `Guid` | PK |
| `PricePerConfigPerQuarter` | `int?` | Цена за конфиг **в месяц** при оплате раз в 3 месяца (минимальный период), руб. |
| `PricePerConfigPerYear` | `int?` | Цена за конфиг **в месяц** при оплате раз в год, руб. Может быть ниже квартальной (скидка за годовую оплату) |
| `UpdatedAt` | `DateTimeOffset` | |
| Поле | Тип | Заметки |
| ---------------------------- | ----------------- | --------------------------------------------------- |
| `Id` | `Guid` | PK |
| `PricePerConfigPerQuarter` | `int?` | Цена за конфиг **в месяц** при оплате раз в 3 месяца (минимальный период), руб. |
| `PricePerConfigPerHalfYear` | `int?` | Цена за конфиг **в месяц** при оплате раз в полгода, руб. Может быть ниже квартальной (скидка за оплату на полгода вперёд) |
| `PricePerConfigPerYear` | `int?` | Цена за конфиг **в месяц** при оплате раз в год, руб. Может быть ниже полугодовой (скидка за годовую оплату) |
| `UpdatedAt` | `DateTimeOffset` | |
Оба поля — ставка **за месяц**, не за весь период целиком. Итог за период = `ставка × число_месяцев ×
AppRole.MaxConfigs`, считается на фронте (таблица ролей в админке), нигде не хранится:
Все три поля — ставка **за месяц**, не за весь период целиком. Итог за период = `ставка ×
число_месяцев × AppRole.MaxConfigs`, считается на фронте (таблица ролей в админке), нигде не
хранится:
- 3 месяца = `PricePerConfigPerQuarter × 3 × MaxConfigs`
- полгода = `PricePerConfigPerQuarter × 6 × MaxConfigs` (используется квартальная ставка — отдельного
полугодового тарифа нет, это просто два квартальных периода подряд)
- полгода = `PricePerConfigPerHalfYear × 6 × MaxConfigs`
- год = `PricePerConfigPerYear × 12 × MaxConfigs`
Например, `user` с `MaxConfigs=3`, ставка 200₽/мес по квартальному тарифу → 600₽/3мес, 1200₽/полгода;
при равной годовой ставке (200₽/мес) → 2400₽/год (линейный рост, скидки нет). Для ролей с
`MaxConfigs = -1` (unlimited, в т.ч. `admin`) итог не считается — отображается как «не задано».
Например, `user` с `MaxConfigs=3` и одинаковой ставкой 200₽/мес на всех трёх тарифах → 600₽/3мес,
1200₽/полгода, 2400₽/год (линейный рост, скидки нет). Для ролей с `MaxConfigs = -1` (unlimited, в
т.ч. `admin`) итог не считается — отображается как «не задано».
**Инвариант**: `UpdatePricingSettingsCommandValidator` не даёт сохранить годовую ставку настолько
низкой, что итог за год (`×12`) окажется дешевле итога за квартал (`×3`) — иначе выгоднее купить «год»
и не продлевать, чем платить за квартал. Формально: `PricePerConfigPerYear × 12
PricePerConfigPerQuarter × 3`.
**Инвариант**: `UpdatePricingSettingsCommandValidator` не даёт сохранить более длинный тариф настолько
дешёвым, что его итог окажется дешевле итога более короткого — иначе выгоднее купить длинный тариф и
не продлевать, чем платить за короткий. Формально: `PricePerConfigPerHalfYear × 6
PricePerConfigPerQuarter × 3` и `PricePerConfigPerYear × 12 ≥ PricePerConfigPerHalfYear × 6` (если
полугодовая ставка не задана — год сверяется напрямую с кварталом: `PricePerConfigPerYear × 12 ≥
PricePerConfigPerQuarter × 3`).
`GET/PUT /api/admin/pricing` — только `admin` (в отличие от `RoleDto`, цена никогда не попадает в
`GET /api/support/roles`, доступный любому активированному пользователю, — это два независимых DTO).