Перейти к основному содержимому

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 подсветил ошибку до сохранения.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature grouppurchase-programs
Статусdraft
Документdocs/06-services/crm/api/developer/preview-payment-schedule.md

Актор и доступ

ПроверкаЗначение
Actor typeUser — Менеджер застройщика (редактор «Программ покупки»); администратор платформы (ADR-0053)
Auth policy / Permissionscrm.purchase-programs.manage (JWT identity) — как редактирование конфигурации
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelне применяется (черновик не привязан к строке БД; проверок владельца нет)
Tenant isolationTenantId в каждом запросе; БД не затрагивается — tenant несёт лишь контекст actor/логов
Auditread-only compute; PII отсутствует (числовой конфиг + цена)
Доступ

Вызывать может только менеджер застройщика (permission crm.purchase-programs.manage) в своём tenant (TenantId = DeveloperCompanyId) либо администратор платформы (ADR-0053). Record-level не применяется — черновик пресета не привязан к строке БД, проверок владельца нет.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/purchase-program/preview-schedule
Success status200 (график рассчитан)
Idempotency headerне применяется (без side-effects; повтор даёт тот же результат)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

Черновой конфиг + эталонная цена целиком в теле; семантика полей — дом purchase-program.md (здесь только источник и роль в расчёте).

Важно

Пресет, программные условия и эталонная цена приходят целиком в теле запроса — метод ничего не читает из БД по Id пресета/программы. Это позволяет считать график ещё до сохранения черновика в UI.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired, != Guid.Empty (DeveloperCompanyId)Tenant isolation
ActorUserIdidentity claimsGuidДаJWT менеджера застройщикаaudit/логи
ReferencePricebodyMoneyInputДаAmount > 0; CurrencyId short? — ссылка на справочник валют handbook, не задан → валюта RUB (RUB-only, ADR-0048) — эталонная цена объекта недвижимости; проекция VO Money (дом — deal.md)
PresetbodyDraftInstallmentPresetModelДамодель ниже
ProgramTermsbodyDraftProgramTermsModelДамодель ниже (условия 100%/ипотеки)

DraftInstallmentPresetModel

Черновик пресета рассрочки — одноимённые поля InstallmentPresetEntity (дом — purchase-program.md); дублируется только MilestonePayments-контейнер.

ПолеТипОбязательностьВалидацияОписание
InitialPaymentPercentdecimalДа>= 0 and <= 100Первый взнос, % от discountedPrice (месяц 0).
MonthlyFixedPaymentdecimalДа>= 0Фиксированный ежемесячный платёж (RUB).
TermMonthsintДа> 0 and <= 600Срок рассрочки, месяцы.
DiscountPercentdecimalДа>= 0 and <= 100Скидка пресета от ReferencePrice.
MilestonePaymentsMilestonePaymentListДа (может быть пустым)typed jsonb-модель (дом — purchase-program.md); Kind строкой percent/fixed/remainder, INV-PPR-3Промежуточные обязательные платежи.

DraftProgramTermsModel

Программные условия (для 100%-цены и аннуитета) — поля PurchaseProgramEntity (дом — там же).

ПолеТипОбязательностьВалидацияОписание
FullPaymentDiscountPercentdecimalДа>= 0 and <= 100Скидка при 100% оплате.
IsFullPaymentEnabledboolДаСчитать ли FullPaymentPrice (иначе null).
MortgageAnnualRatePercentdecimalДа>= 0 and <= 100Годовая ставка ипотеки.
MortgageTermMonthsintДа> 0 and <= 600Срок ипотеки, месяцы.
IsMortgageEnabledboolДаСчитать ли Mortgage (иначе null).

Модель ответа PaymentScheduleModel

Все суммы — MoneyModel (проекция VO Money, дом — deal.md: Amount: decimal, CurrencyId: short — справочник валют handbook), валюта = ReferencePrice.CurrencyId; enum сериализуются строкой.

ПолеТипОбязательностьИсточникОписание
ReferencePriceMoneyModelДаecho входаЭталонная цена расчёта.
FullPaymentPriceMoneyModel?НетcomputedReferencePrice × (1 − FullPaymentDiscountPercent/100); null при IsFullPaymentEnabled = false.
DiscountedPriceMoneyModelДаcomputedЦена рассрочки со скидкой пресета: ReferencePrice × (1 − DiscountPercent/100).
InitialPaymentMoneyModelДаcomputedПервый взнос (месяц 0): DiscountedPrice × InitialPaymentPercent/100.
InstallmentInstallmentScheduleModelДаcomputedПомесячный график рассрочки.
MortgageMortgageScheduleModel?НетcomputedАннуитет ипотеки; null при IsMortgageEnabled = false.

InstallmentScheduleModel

ПолеТипОбязательностьИсточникОписание
MonthsInstallmentScheduleItemModel[]ДаcomputedСтроки месяцев 0..DurationMonths (месяц 0 — первый взнос).
DurationMonthsintДаcomputedmax(TermMonths, max(MilestonePayments.Month)).
TotalPaidMoneyModelДаcomputedСумма всех платежей; по INV-PPR-4 равна DiscountedPrice.

InstallmentScheduleItemModel

ПолеТипОбязательностьИсточникОписание
MonthintДаcomputedНомер месяца (0 — первый взнос).
MonthlyBaseMoneyModelДаcomputedMonthlyFixedPayment при 1 <= Month <= TermMonths, иначе 0.
MilestoneAmountMoneyModelДаcomputedСумма milestone-платежей месяца (percent/fixed/remainder).
PaymentMoneyModelДаcomputedПлатёж месяца = MonthlyBase + MilestoneAmount (месяц 0 = InitialPayment).
RemainingBalanceMoneyModelДаcomputedНепогашенный остаток DiscountedPrice после месяца (INV-PPR-4: 0 в конце).

MortgageScheduleModel

ПолеТипОбязательностьИсточникОписание
PrincipalMoneyModelДаcomputedТело кредита: DiscountedPrice − InitialPayment.
AnnualRatePercentdecimalДаechoГодовая ставка.
TermMonthsintДаechoСрок ипотеки.
MonthlyAnnuityPaymentMoneyModelДаcomputedАннуитет: P·r·(1+r)^n / ((1+r)^n − 1), r = AnnualRatePercent/100/12, n = TermMonths; при r = 0P/n.
TotalPaidMoneyModelДаcomputedMonthlyAnnuityPayment × TermMonths.
OverpaymentMoneyModelДаcomputedTotalPaid − Principal.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.purchase-programs.manage, иначе CRM_PURCHASE_PROGRAMS_FORBIDDEN; администратор платформы — ADR-0053); tenant — TenantId = DeveloperCompanyId (несёт лишь контекст actor/логов, БД не затрагивается). Без side-effects, идемпотентность не требуется.
  2. Проверка входа и предусловий.
    • 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 не применяется.
  3. Бизнес-правила и расчёт (формула — дом 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 месяца (percentdiscountedPrice × Value/100; fixedValue; 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.
  4. Результат. PaymentScheduleModel → HTTP 200 + тело. Невалидный график (остаток ≠ 0) → CRM_INSTALLMENT_SCHEDULE_INCOMPLETE (422); нарушения диапазонов/milestone → 400. Ничего не читается и не пишется — чистая функция входа; сохранение черновика — отдельная команда upsert-пресета.

Диаграмма

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400пустой/некорректный формат тела или ReferencePrice.Amount <= 0«Проверьте параметры расчёта»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_PURCHASE_PROGRAMS_FORBIDDEN403нет permission crm.purchase-programs.manage«Недостаточно прав»no retry
CRM_PURCHASE_PROGRAM_RANGE400процент вне 0..100 или срок вне 1..600 (INV-PPR-2)«Проверьте проценты и сроки»no retry
CRM_INSTALLMENT_REMAINDER_INVALID400больше одного remainder или он не последний (INV-PPR-3)«Некорректный итоговый платёж рассрочки»no retry
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE422график не погашает 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 = falseMortgage = 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.manage403 CRM_PURCHASE_PROGRAMS_FORBIDDEN.

Обратные ссылки

Автоссылки: где используется этот документ.

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

  • Дом формулы и полей: purchase-program.md (§«Серверный расчёт», InstallmentPresetEntity/PurchaseProgramEntity, MilestonePaymentList, INV-PPR-2/3/4); Money/MoneyModeldeal.md; чтение конфигурации — get-purchase-program.md; карта — README.
  • tenancy — ADR-0052; simplicity/округление — ADR-0056.