POST /api/developer/crm/purchase-program/preview-schedule — Предпросмотр графика платежей
Менеджер застройщика получает серверный расчёт помесячного графика платежей для несохранённого/
редактируемого пресета «Программы покупки» прямо во время его настройки, до сохранения. Единственный дом
формулы — purchase-program.md §«Серверный расчёт графика платежей».
Метод ничего не читает и не пишет (черновой пресет, программные условия и эталонная цена — в теле;
результат — чистая функция входа); POST выбран лишь из-за тела с черновиком, семантически это Query без
побочных эффектов.
Назначение
Даёт редактору «Программ покупки» предпросмотр помесячного графика рассрочки и аннуитета ипотеки по ещё не
сохранённому пресету на эталонной цене, до его сохранения (дом команды/пресета —
purchase-program.md). Одновременно — серверная проверка INV-PPR-4 (график
погашает 100% discountedPrice) и INV-PPR-2/3 на черновике, чтобы UI подсветил ошибку до сохранения.
| Поле | Значение |
|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | purchase-programs |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/preview-payment-schedule.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User — Менеджер застройщика (редактор «Программ покупки»); администратор платформы (ADR-0053) |
| Auth policy / Permissions | crm.purchase-programs.manage (JWT identity) — как редактирование конфигурации |
| Scope (RBAC) | организация (застройщик) — TenantId = DeveloperCompanyId |
| Record-level | не применяется (черновик не привязан к строке БД; проверок владельца нет) |
| Tenant isolation | TenantId в каждом запросе; БД не затрагивается — tenant несёт лишь контекст actor/логов |
| Audit | read-only compute; PII отсутствует (числовой конфиг + цена) |
Вызывать может только менеджер застройщика (permission crm.purchase-programs.manage) в своём tenant
(TenantId = DeveloperCompanyId) либо администратор платформы (ADR-0053). Record-level не применяется —
черновик пресета не привязан к строке БД, проверок владельца нет.
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/developer/crm/purchase-program/preview-schedule |
| Success status | 200 (график рассчитан) |
| Idempotency header | не применяется (без side-effects; повтор даёт тот же результат) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Черновой конфиг + эталонная цена целиком в теле; семантика полей — дом purchase-program.md (здесь только источник и роль в расчёте).
Пресет, программные условия и эталонная цена приходят целиком в теле запроса — метод ничего не читает из БД
по Id пресета/программы. Это позволяет считать график ещё до сохранения черновика в UI.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims | Guid | Да | Required, != Guid.Empty (DeveloperCompanyId) | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика | audit/логи |
ReferencePrice | body | MoneyInput | Да | Amount > 0; CurrencyId short? — ссылка на справочник валют handbook, не задан → валюта RUB (RUB-only, ADR-0048) — эталонная цена объекта недвижимости; проекция VO Money (дом — deal.md) | — |
Preset | body | DraftInstallmentPresetModel | Да | модель ниже | — |
ProgramTerms | body | DraftProgramTermsModel | Да | модель ниже (условия 100%/ипотеки) | — |
DraftInstallmentPresetModel
Черновик пресета рассрочки — одноимённые поля InstallmentPresetEntity (дом — purchase-program.md); дублируется только MilestonePayments-контейнер.
| Поле | Тип | Обязательность | Валидация | Описание |
|---|
InitialPaymentPercent | decimal | Да | >= 0 and <= 100 | Первый взнос, % от discountedPrice (месяц 0). |
MonthlyFixedPayment | decimal | Да | >= 0 | Фиксированный ежемесячный платёж (RUB). |
TermMonths | int | Да | > 0 and <= 600 | Срок рассрочки, месяцы. |
DiscountPercent | decimal | Да | >= 0 and <= 100 | Скидка пресета от ReferencePrice. |
MilestonePayments | MilestonePaymentList | Да (может быть пустым) | typed jsonb-модель (дом — purchase-program.md); Kind строкой percent/fixed/remainder, INV-PPR-3 | Промежуточные обязательные платежи. |
DraftProgramTermsModel
Программные условия (для 100%-цены и аннуитета) — поля PurchaseProgramEntity (дом — там же).
| Поле | Тип | Обязательность | Валидация | Описание |
|---|
FullPaymentDiscountPercent | decimal | Да | >= 0 and <= 100 | Скидка при 100% оплате. |
IsFullPaymentEnabled | bool | Да | — | Считать ли FullPaymentPrice (иначе null). |
MortgageAnnualRatePercent | decimal | Да | >= 0 and <= 100 | Годовая ставка ипотеки. |
MortgageTermMonths | int | Да | > 0 and <= 600 | Срок ипотеки, месяцы. |
IsMortgageEnabled | bool | Да | — | Считать ли Mortgage (иначе null). |
Модель ответа PaymentScheduleModel
Все суммы — MoneyModel (проекция VO Money, дом — deal.md: Amount: decimal,
CurrencyId: short — справочник валют handbook), валюта = ReferencePrice.CurrencyId; enum сериализуются строкой.
| Поле | Тип | Обязательность | Источник | Описание |
|---|
ReferencePrice | MoneyModel | Да | echo входа | Эталонная цена расчёта. |
FullPaymentPrice | MoneyModel? | Нет | computed | ReferencePrice × (1 − FullPaymentDiscountPercent/100); null при IsFullPaymentEnabled = false. |
DiscountedPrice | MoneyModel | Да | computed | Цена рассрочки со скидкой пресета: ReferencePrice × (1 − DiscountPercent/100). |
InitialPayment | MoneyModel | Да | computed | Первый взнос (месяц 0): DiscountedPrice × InitialPaymentPercent/100. |
Installment | InstallmentScheduleModel | Да | computed | Помесячный график рассрочки. |
Mortgage | MortgageScheduleModel? | Нет | computed | Аннуитет ипотеки; null при IsMortgageEnabled = false. |
InstallmentScheduleModel
| Поле | Тип | Обязательность | Источник | Описание |
|---|
Months | InstallmentScheduleItemModel[] | Да | computed | Строки месяцев 0..DurationMonths (месяц 0 — первый взнос). |
DurationMonths | int | Да | computed | max(TermMonths, max(MilestonePayments.Month)). |
TotalPaid | MoneyModel | Да | computed | Сумма всех платежей; по INV-PPR-4 равна DiscountedPrice. |
InstallmentScheduleItemModel
| Поле | Тип | Обязательность | Источник | Описание |
|---|
Month | int | Да | computed | Номер месяца (0 — первый взнос). |
MonthlyBase | MoneyModel | Да | computed | MonthlyFixedPayment при 1 <= Month <= TermMonths, иначе 0. |
MilestoneAmount | MoneyModel | Да | computed | Сумма milestone-платежей месяца (percent/fixed/remainder). |
Payment | MoneyModel | Да | computed | Платёж месяца = MonthlyBase + MilestoneAmount (месяц 0 = InitialPayment). |
RemainingBalance | MoneyModel | Да | computed | Непогашенный остаток DiscountedPrice после месяца (INV-PPR-4: 0 в конце). |
MortgageScheduleModel
| Поле | Тип | Обязательность | Источник | Описание |
|---|
Principal | MoneyModel | Да | computed | Тело кредита: DiscountedPrice − InitialPayment. |
AnnualRatePercent | decimal | Да | echo | Годовая ставка. |
TermMonths | int | Да | echo | Срок ипотеки. |
MonthlyAnnuityPayment | MoneyModel | Да | computed | Аннуитет: P·r·(1+r)^n / ((1+r)^n − 1), r = AnnualRatePercent/100/12, n = TermMonths; при r = 0 → P/n. |
TotalPaid | MoneyModel | Да | computed | MonthlyAnnuityPayment × TermMonths. |
Overpayment | MoneyModel | Да | computed | TotalPaid − Principal. |
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permission crm.purchase-programs.manage,
иначе CRM_PURCHASE_PROGRAMS_FORBIDDEN; администратор платформы — ADR-0053); tenant — TenantId = DeveloperCompanyId (несёт лишь контекст actor/логов, БД не затрагивается). Без side-effects, идемпотентность
не требуется.
- Проверка входа и предусловий.
ReferencePrice.Amount > 0; проценты InitialPaymentPercent/DiscountPercent/
FullPaymentDiscountPercent/MortgageAnnualRatePercent ∈ 0..100; MonthlyFixedPayment >= 0;
TermMonths/MortgageTermMonths ∈ 1..600 (INV-PPR-2 → CRM_PURCHASE_PROGRAM_RANGE);
MilestonePayments[*]: Month ∈ 1..TermMonths; Kind ∈ {percent, fixed, remainder} (unknown →
VALIDATION_FAILED); percent: Value ∈ (0,100], fixed: Value > 0; не более одного remainder, он
последний по Month (INV-PPR-3 → CRM_INSTALLMENT_REMAINDER_INVALID);
- существование сущностей и состояние — не применяется (черновик, из БД ничего не читается); scope — только
TenantId запроса, record-level не применяется.
- Бизнес-правила и расчёт (формула — дом purchase-program.md
§«Серверный расчёт»; здесь порядок и маппинг, без переописания формулы):
basePrice = ReferencePrice.Amount; discountedPrice = basePrice × (1 − DiscountPercent/100);
fullPaymentPrice = basePrice × (1 − FullPaymentDiscountPercent/100) (если IsFullPaymentEnabled);
- месяц 0:
InitialPayment = discountedPrice × InitialPaymentPercent/100; remaining = discountedPrice − InitialPayment;
- для
m в 1..DurationMonths (DurationMonths = max(TermMonths, max(Milestone.Month))):
MonthlyBase = m ≤ TermMonths ? MonthlyFixedPayment : 0; MilestoneAmount = Σ milestone месяца
(percent → discountedPrice × Value/100; fixed → Value; remainder → текущий remaining);
Payment = MonthlyBase + MilestoneAmount; remaining −= Payment;
- округление до 2 знаков (RUB, banker's rounding, ADR-0056); копеечная дельта относится на последний платёж,
чтобы
remaining сходился в ноль;
- INV-PPR-4:
remaining после последнего месяца обязан быть 0; иначе
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE с остатком в сообщении (невалидный черновик);
- ипотека (если
IsMortgageEnabled): Principal = discountedPrice − InitialPayment, аннуитет по
MortgageScheduleModel.
- Результат.
PaymentScheduleModel → HTTP 200 + тело. Невалидный график (остаток ≠ 0) →
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE (422); нарушения диапазонов/milestone → 400. Ничего не читается и не
пишется — чистая функция входа; сохранение черновика — отдельная команда upsert-пресета.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | пустой/некорректный формат тела или ReferencePrice.Amount <= 0 | «Проверьте параметры расчёта» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_PURCHASE_PROGRAMS_FORBIDDEN | 403 | нет permission crm.purchase-programs.manage | «Недостаточно прав» | no retry |
CRM_PURCHASE_PROGRAM_RANGE | 400 | процент вне 0..100 или срок вне 1..600 (INV-PPR-2) | «Проверьте проценты и сроки» | no retry |
CRM_INSTALLMENT_REMAINDER_INVALID | 400 | больше одного remainder или он не последний (INV-PPR-3) | «Некорректный итоговый платёж рассрочки» | no retry |
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE | 422 | график не погашает 100% discountedPrice (INV-PPR-4) | «График не покрывает стоимость: остаток {amount}» | no retry |
Совместимость и наблюдаемость
- Новые optional поля ответа — additive (без breaking); удаление/переименование/смена типа — breaking (migration +
rollout). Формула — единственный дом purchase-program.md;
MilestoneKind —
строкой, unknown толерантно.
- Logs:
purchase-program preview computed (TenantId, ActorUserId, DurationMonths; суммы не пишутся); metrics:
crm_purchase_program_preview_total{result}, latency p95 (< 500 мс, SLO команд README); traces — сквозной trace
через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).
Acceptance criteria
- Given валидный черновик рассрочки, чей график погашает
discountedPrice, When редактор запрашивает
предпросмотр, Then 200 с PaymentScheduleModel: Installment.TotalPaid == DiscountedPrice,
RemainingBalance последнего месяца = 0 (INV-PPR-4), месяц 0 = InitialPayment.
- Given
IsMortgageEnabled = true, When расчёт, Then Mortgage рассчитан (аннуитет; при ставке
r = 0 платёж = P/n); при IsMortgageEnabled = false — Mortgage = null.
- Given
IsFullPaymentEnabled = false, When расчёт, Then FullPaymentPrice = null.
- Given процент вне
0..100 или срок вне 1..600, When расчёт, Then 400 CRM_PURCHASE_PROGRAM_RANGE (INV-PPR-2).
- Given более одного milestone
remainder или remainder не последний по Month, When расчёт,
Then 400 CRM_INSTALLMENT_REMAINDER_INVALID (INV-PPR-3).
- Given график не сходится в ноль (остаток ≠ 0), When расчёт, Then
422 CRM_INSTALLMENT_SCHEDULE_INCOMPLETE с величиной остатка в сообщении.
- Given один и тот же вход отправлен дважды, When расчёт, Then идентичный результат — чистая
функция входа, БД не читается и не пишется, черновик не сохраняется (сохранение — отдельная upsert-команда).
- Given
ReferencePrice.Amount <= 0 или битое тело, When расчёт, Then 400 VALIDATION_FAILED;
без crm.purchase-programs.manage — 403 CRM_PURCHASE_PROGRAMS_FORBIDDEN.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы