POST /api/public/crm/payment-schedule — Серверный график платежей (витрина)
Назначение
Публичный серверный расчёт графика платежей по «Программам покупки» застройщика для карточки объекта
недвижимости витрины. Формула считается на сервере по единому правилу — фронт запрашивает готовый график
у CRM. По объекту и выбранному способу оплаты возвращает полный график: первый платёж, помесячные суммы,
milestone-платежи, аннуитет ипотеки, остаток. Метод только читает: агрегаты
PurchaseProgramEntity/InstallmentPresetEntity не изменяются, события не публикуются. Дом полей и дом
формулы — purchase-program.md (§ «Серверный расчёт графика платежей»).
POST (а не GET) — чтобы нести структурированное тело калькуляции; семантика при этом чистая read-only
(безопасно ретраить).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | public (гейт витрины: JWT инвестора или реферальная cookie) |
| Feature group | purchase-program |
| Статус | draft |
| Документ | docs/06-services/crm/api/public/get-payment-schedule.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Anonymous (JWT инвестора/реф-cookie опциональны, на расчёт не влияют) |
| Auth policy / Permissions | none (публичное чтение конфигурации в рамках карточки объекта); rate limit по IP |
| Scope (RBAC) | не применяется (актор — вне tenant) |
| Record-level | не применяется |
| Tenant isolation | TenantId = DeveloperCompanyId, вычисляется по объекту (chessboard-lookup, шаг 2), не из claims актора |
| Audit | none (read; PII отсутствует — только цена и конфигурация оплаты) |
Вызвать может любой аноним с витрины — JWT инвестора и реферальная cookie опциональны и на расчёт не влияют;
permission не требуется, доступ ограничен только rate limit по IP. TenantId = DeveloperCompanyId
вычисляется не из claims актора, а по объекту через chessboard-lookup (шаг 2 алгоритма).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Success status | 200 |
| Route | /api/public/crm/payment-schedule |
| Idempotency header | не применяется (чистое чтение без побочных эффектов) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
PropertyObjectId | body | Guid | Да | != Guid.Empty; существующий объект chessboard (шаг 2) — из него DeveloperCompanyId (tenant программы) и базовая цена | — |
BasePrice | body | decimal? | Нет | если задан — > 0 (numeric(18,2), RUB); override цены объекта для предпросмотра калькулятора | — |
PaymentMethod | body | PaymentMethod | Да | enum-строка (FullPayment/Installment/Mortgage); unknown → VALIDATION_FAILED | — |
InstallmentPresetId | body | Guid? | Условно | обязателен и != Guid.Empty при PaymentMethod = Installment; иначе игнорируется | — |
Программа застройщика анонимно резолвится только через объект (иначе — утечка конфигурации чужого tenant).
BasePrice — лишь override суммы для «что если» на карточке.
Модель ответа PaymentScheduleModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PaymentMethod | PaymentMethod | Да | echo запроса | — | Способ оплаты (enum-строка). |
BasePrice | decimal | Да | BasePrice ?? снапшот цены объекта | — | Базовая цена (RUB). |
DiscountPercent | decimal | Да | вычисление (программа/пресет) | — | Применённая скидка, %. |
DiscountedPrice | decimal | Да | вычисление | — | Цена со скидкой. |
InitialPayment | decimal | Да | вычисление | — | Первый платёж (месяц 0). |
TermMonths | int | Да | вычисление | — | Горизонт графика, месяцев (0 для FullPayment). |
TotalAmount | decimal | Да | вычисление | — | Сумма всех платежей (FullPayment/Installment = DiscountedPrice; Mortgage = сумма аннуитетов). |
OverpaymentAmount | decimal | Да | вычисление | — | Mortgage: TotalAmount − DiscountedPrice; иначе 0. |
Items | PaymentScheduleItemList | Да | вычисление | — | Помесячный график (container-model ниже). |
PaymentScheduleItemList (container-model, ContractVersion = 1)
Коллекция внутри именованной обёртки: { ContractVersion: int, Items: PaymentScheduleItem[] }. Это
вычисляемая ограниченная (≤ TermMonths + 1) последовательность одного ответа, а не страничная выборка БД.
PaymentScheduleItem
| Поле | Тип | Обязательность | Описание |
|---|---|---|---|
Month | int | Да | 0 — первый платёж; 1..TermMonths — помесячные. |
Amount | decimal | Да | Платёж месяца (RUB). |
MonthlyPart | decimal? | Нет | Часть MonthlyFixedPayment (Installment). |
MilestonePart | decimal? | Нет | Сумма milestone-платежей месяца (Installment). |
InterestPart | decimal? | Нет | Процентная часть аннуитета (Mortgage). |
PrincipalPart | decimal? | Нет | Погашение тела (Mortgage). |
RemainingBalance | decimal | Да | Непогашенный остаток после платежа; на последнем месяце = 0 (INV-PPR-4). |
Алгоритм
- Контекст и доступ. Публичный метод; актор
Anonymous(JWT/реф-cookie опциональны и на расчёт не влияют). Tenant в контексте отсутствует — вычисляется на шаге 2 из объекта. - Проверка входа и предусловий.
- формат:
PropertyObjectId != Guid.Empty;PaymentMethod— известное enum-значение;PaymentMethod = Installment ⇒ InstallmentPresetIdзадан и!= Guid.Empty;BasePrice, если задан,> 0→ иначеVALIDATION_FAILED; - существование объекта: lookup в
chessboardпоPropertyObjectId→ нет →CRM_PROPERTY_OBJECT_NOT_FOUND; из ответа берёмDeveloperCompanyIdи базовую цену объекта; - допустимость состояния: способ включён у застройщика —
FullPayment ⇒ IsFullPaymentEnabled,Mortgage ⇒ IsMortgageEnabled(шаг 3); иначеCRM_PAYMENT_METHOD_DISABLED; TenantId = DeveloperCompanyId(публичный метод — tenant из объекта, не из claims).
- формат:
- Что читаем. Живую программу покупки застройщика (единственную, unique, INV-PPR-1) с её пресетами
рассрочки → нет →
CRM_PURCHASE_PROGRAM_NOT_FOUND. ДляInstallment— конкретный пресет поInstallmentPresetId(живой) → нет →CRM_INSTALLMENT_PRESET_NOT_FOUND. - Бизнес-правила и расчёт (дом формулы — purchase-program.md §
«Серверный расчёт»):
basePrice = BasePrice ?? снапшот цены объекта. По способу:FullPayment:DiscountPercent = FullPaymentDiscountPercent;discountedPrice = basePrice × (1 − DiscountPercent/100);Items = [{Month 0, Amount = discountedPrice, RemainingBalance = 0}];InitialPayment = discountedPrice,TermMonths = 0.Installment:DiscountPercent = preset.DiscountPercent;discountedPrice = basePrice × (1 − DiscountPercent/100);Month 0 = discountedPrice × InitialPaymentPercent/100; дляMonth 1..max(preset.TermMonths, max(Milestone.Month)):MonthlyPart = MonthlyFixedPayment(покаMonth ≤ TermMonths) +MilestonePart= сумма milestone месяца (percent→ % отdiscountedPrice;fixed→Value;remainder→ непогашенный остаток), с пересчётомRemainingBalance;TotalAmount = discountedPrice. Остаток последнего месяца обязан быть0(INV-PPR-4) — иначе конфигурация битая →CRM_INSTALLMENT_SCHEDULE_INCOMPLETE.Mortgage:DiscountPercent = 0;discountedPrice = basePrice(первоначальный взнос — банковский процесс этапов сделки, в витринную оценку не входит); аннуитет поMortgageAnnualRatePercent/12наMortgageTermMonthsс разбиениемInterestPart/PrincipalPart;TotalAmount = Σ Amount,OverpaymentAmount = TotalAmount − discountedPrice. Округление до копеек (numeric(18,2)), расхождение округления добирается последним платежом (остаток = 0).
- Результат.
PaymentScheduleModel(поля из шага 4) → HTTP200; при отказе — соответствующий код.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Внешние вызовы | chessboard lookup объекта по PropertyObjectId (DeveloperCompanyId + цена; timeout 2 c, retry 1) |
| Кэш | допустим edge-кэш ответа по ключу propertyObjectId+method+preset+basePrice (расчёт детерминирован по входу) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | неизвестный PaymentMethod, отсутствует InstallmentPresetId для рассрочки, BasePrice ≤ 0 | «Проверьте параметры расчёта» | no retry |
CRM_PROPERTY_OBJECT_NOT_FOUND | 404 | объект не найден в chessboard | «Объект не найден» | no retry |
CRM_PURCHASE_PROGRAM_NOT_FOUND | 404 | у застройщика объекта нет живой программы покупки | «Условия оплаты недоступны» | no retry |
CRM_INSTALLMENT_PRESET_NOT_FOUND | 404 | пресет не найден/архивирован в программе | «Выбранная рассрочка недоступна» | no retry |
CRM_PAYMENT_METHOD_DISABLED | 409 | способ выключен (IsFullPaymentEnabled/IsMortgageEnabled = false) | «Способ оплаты недоступен» | no retry |
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE | 422 | пресет не погашает 100% discountedPrice (битая конфигурация, INV-PPR-4) | «Ошибка конфигурации рассрочки» | no retry |
CHESSBOARD_UNAVAILABLE | 503 | chessboard недоступен на шаге 2 | «Сервис временно недоступен» | retry |
Совместимость и наблюдаемость
- Формула — по дому purchase-program.md. Новые optional-поля ответа
(
OverpaymentAmount, разбиение*Part) — additive; удаление/переименование/смена типа полей или неизвестное значениеPaymentMethod— breaking (migration + rollout).PaymentScheduleItemList.ContractVersionверсионирует форму графика. - Logs:
payment-schedule computed/failed(PropertyObjectId,PaymentMethod,PresetId,TenantId; PII нет); metricscrm_payment_schedule_total{method,result}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboard Grafanapin-crm.
Acceptance Criteria
- AC-1 (100% оплата). Дано: у застройщика объекта
IsFullPaymentEnabled = true. Когда:PaymentMethod = FullPayment. Тогда:DiscountPercent = FullPaymentDiscountPercent, единственныйItems[Month=0]сAmount = DiscountedPriceиRemainingBalance = 0,TermMonths = 0,200. - AC-2 (рассрочка). Дано: живой пресет по
InstallmentPresetId, погашающий 100% (INV-PPR-4). Когда:PaymentMethod = Installment. Тогда:Items[0]= первоначальный взнос, помесячныеMonthlyPart+MilestonePart, остаток последнего месяца =0,TotalAmount = DiscountedPrice,200. - AC-3 (ипотека). Дано:
IsMortgageEnabled = true. Когда:PaymentMethod = Mortgage. Тогда: аннуитет поMortgageAnnualRatePercent/MortgageTermMonthsс разбиениемInterestPart/PrincipalPart,OverpaymentAmount = TotalAmount − DiscountedPrice > 0,RemainingBalanceпоследнего месяца =0,200. - AC-4 (рассрочка без пресета). Дано:
PaymentMethod = Installment,InstallmentPresetIdне задан. Когда: запрос. Тогда: отказVALIDATION_FAILED(400). - AC-5 (способ выключен). Дано: выбранный способ выключен у застройщика. Когда: запрос. Тогда: отказ
CRM_PAYMENT_METHOD_DISABLED(409). - AC-6 (битая конфигурация рассрочки). Дано: пресет не погашает 100%
DiscountedPrice. Когда: запрос. Тогда: отказCRM_INSTALLMENT_SCHEDULE_INCOMPLETE(422). - AC-7 (объект/программа не найдены). Дано:
PropertyObjectIdотсутствует вchessboard(или у застройщика нет живой программы). Когда: запрос. Тогда: отказCRM_PROPERTY_OBJECT_NOT_FOUND(404) /CRM_PURCHASE_PROGRAM_NOT_FOUND(404). - AC-8 (chessboard недоступен). Дано:
chessboardне отвечает на резолве объекта. Когда: запрос. Тогда: отказCHESSBOARD_UNAVAILABLE(503, retriable). Расчёт не изменяет состояние ни при каком исходе (метод read-only, безопасно ретраить).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): GET /api/public/crm/purchase-config — Конфигурация способов оплаты (витрина)
- Use Cases (1): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика)
- registries (1): Реестр API-методов PIN
Связанные документы
- Дом формулы и полей: purchase-program.md (формула, enum
PaymentMethod/MilestoneKind,MilestonePaymentList, инварианты INV-PPR-1/3/4); потребитель способа оплаты — deal.md (PaymentMethod,InstallmentPresetId). - Карта областей — README контекста; глоссарий — glossary.md.
- Смежные публичные входы витрины: create-booking-request.md, create-consultation-request.md.
- ADR: 0052 tenancy, 0056 simplicity-first.