POST /api/developer/crm/purchase-program/presets — Создать пресет рассрочки
Менеджер застройщика добавляет пресет рассрочки («Рассрочка 12 месяцев») в единственную программу покупки своего застройщика: первоначальный взнос, фиксированный ежемесячный платёж, срок, скидку и milestone-платежи (typed jsonb). Дом полей, инвариантов и формулы графика — purchase-program.md.
Назначение
Заводит новый пресет рассрочки внутри программы покупки застройщика (TenantId = DeveloperCompanyId): Name,
InitialPaymentPercent, MonthlyFixedPayment, TermMonths, DiscountPercent, MilestonePayments
(MilestonePaymentList, ContractVersion = 1). Вызывает менеджер застройщика; на выходе — карточка
созданного пресета. График должен быть консистентен: один хвостовой remainder (INV-PPR-3) и полное погашение
discountedPrice (INV-PPR-4).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | purchase-program |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/create-installment-preset.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId; администратор платформы — тот же permission) |
| Auth policy / Permissions | crm.purchase-programs.manage |
| Scope (RBAC) | организация (застройщик): менеджер настраивает программу покупки своего застройщика |
| Record-level | пресет создаётся в программе застройщика PurchaseProgramEntity.TenantId == actor.DeveloperCompanyId |
| Tenant isolation | TenantId = DeveloperCompanyId в каждом запросе (ADR-0052) |
| Audit | create (CreatedByUserId = actor, инкремент Version); программа-родитель — version bump |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/purchase-program/presets (id программы не в маршруте — у застройщика ровно один живой агрегат, INV-PPR-1) |
| Success status | 201 (пресет создан; тело — карточка пресета InstallmentPresetModel) |
| Idempotency header | не применяется — создание не идемпотентно (повтор даёт дубль по Name(ci) → 409, INV уникальности) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера; record-level + CreatedByUserId | — |
Name | body | string | Да | 1..128, не пустое после trim; уникально по (PurchaseProgramId, lower(Name)) среди живых | — |
Code | body | string? | Нет | 0..64; display-код, не доменный ключ | — |
InitialPaymentPercent | body | decimal | Да | numeric(5,2), >= 0 and <= 100 (INV-PPR-2) | — |
MonthlyFixedPayment | body | decimal | Да | numeric(18,2), >= 0 (RUB, как цена объекта недвижимости) | — |
TermMonths | body | int | Да | > 0 and <= 600 (INV-PPR-2) | — |
DiscountPercent | body | decimal | Да | numeric(5,2), >= 0 and <= 100, default 0 | — |
MilestonePayments | body | MilestonePaymentList (typed jsonb, ContractVersion = 1) | Да (допустим пустой Items) | каждый Item: Month ∈ [1, TermMonths]; Kind — enum-строкой (percent/fixed/remainder, Enum.IsDefined); Value — по Kind; INV-PPR-3 (см. шаг 2) | — |
SortOrder | body | int | Нет | >= 0, default 0 (порядок показа опции) | — |
Модель MilestonePaymentList и enum MilestoneKind — дом
purchase-program.md («Модели JSONB»); здесь не переописываются.
Модель ответа InstallmentPresetModel
Проекция дочерней сущности — дом полей purchase-program.md; ниже только
состав карточки, возвращаемой create/list. TenantId/PurchaseProgramId наружу не отдаются.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | InstallmentPresetEntity.Id | — | Идентификатор пресета (Deal.InstallmentPresetId ссылается сюда). |
Name | string | Да | InstallmentPresetEntity.Name | — | Название («Рассрочка 12 месяцев»). |
Code | string? | Нет | InstallmentPresetEntity.Code | — | Display-код пресета. |
InitialPaymentPercent | decimal | Да | InstallmentPresetEntity.InitialPaymentPercent | — | Первоначальный взнос, % от цены. |
MonthlyFixedPayment | decimal | Да | InstallmentPresetEntity.MonthlyFixedPayment | — | Фиксированный ежемесячный платёж (RUB). |
TermMonths | int | Да | InstallmentPresetEntity.TermMonths | — | Срок рассрочки, месяцы. |
DiscountPercent | decimal | Да | InstallmentPresetEntity.DiscountPercent | — | Скидка пресета от цены объекта недвижимости. |
MilestonePayments | MilestonePaymentList | Да | InstallmentPresetEntity.MilestonePayments | — | Промежуточные обязательные платежи (typed jsonb). |
SortOrder | int | Да | InstallmentPresetEntity.SortOrder | — | Порядок показа опции. |
Version | long | Да | InstallmentPresetEntity.Version | — | Оптимистическая версия (== 1 после создания). |
Алгоритм
-
Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.purchase-programs.manage); tenant —TenantId = DeveloperCompanyId. Создание не идемпотентно. -
Проверка входа и предусловий.
Nameне пусто после trim (1..128);Code≤ 64;InitialPaymentPercent/DiscountPercent ∈ [0,100];MonthlyFixedPayment >= 0;TermMonths ∈ (0,600](INV-PPR-2);SortOrder >= 0; для каждогоMilestonePayments.Items[i]:Month ∈ [1, TermMonths],Kind ∈ {percent, fixed, remainder}(unknown → ошибка),ValueпоKind(percent → (0,100],fixed → > 0,remainder → игнорируется) — иначеVALIDATION_FAILED;- INV-PPR-3: среди
Itemsне более одногоremainder, и он — последний поMonth; иначеCRM_INSTALLMENT_REMAINDER_INVALID; - существование родителя: у застройщика есть живой агрегат программы покупки (единственный, INV-PPR-1) —
иначе
CRM_PURCHASE_PROGRAM_NOT_FOUND; - уникальность имени (pre-check): живой пресет с таким
Name(ci) в программе — если есть →CRM_INSTALLMENT_PRESET_NAME_DUPLICATE; - scope: программа принадлежит tenant актора (кросс-tenant недостижим).
-
Что читаем. Программу покупки застройщика под блокировкой строки (с дочерними пресетами — единицы строк) — блокировка сериализует параллельные вставки пресетов и защищает version bump агрегата. Для проверки INV-PPR-4 используется эталонная цена калькулятора (константа проверки, дом формулы — purchase-program.md, «Серверный расчёт графика платежей»); расчёт от конкретного объекта недвижимости не зависит.
-
Бизнес-правила и переходы. Собирается
MilestonePaymentList { ContractVersion = 1, Items }и в программу добавляется пресет. Закрепляется:- INV-PPR-3: единственный
remainder— хвостовой (дубль guard шага 2, инвариант в модели); - INV-PPR-4 (формула purchase-program.md):
discountedPrice = referencePrice × (1 − DiscountPercent/100); месяц 0 =discountedPrice × InitialPaymentPercent/100; месяцы1..max(TermMonths, maxMilestoneMonth)=MonthlyFixedPayment(пока месяц ≤TermMonths) + milestone месяца (percent→ отdiscountedPrice,fixed→Value,remainder→ непогашенный остаток); остаток по графику не уходит в минус (нет переплаты) и в конце равен0(в пределах эпсилон округления) — иначеCRM_INSTALLMENT_SCHEDULE_INCOMPLETE.
Маппинг поле-в-поле:
Id— новый идентификатор;PurchaseProgramId = Id программы;TenantId = DeveloperCompanyId;Name = trim(Name);Code;InitialPaymentPercent/MonthlyFixedPayment/TermMonths/DiscountPercent/SortOrder— из входа;MilestonePayments— собранный список;CreatedByUserId = ActorUserId;Version = 1. Фиксируется доменный фактPurchaseProgramChanged. - INV-PPR-3: единственный
-
Что меняется. Появляется новый пресет в графе программы; программа-родитель получает version bump. Гонку параллельного create того же имени ловит partial-unique
ux_installment_presets_program_name→CRM_INSTALLMENT_PRESET_NAME_DUPLICATE(pre-check шага 2 — для дружелюбного ответа на неконкурентном пути). -
События. Интеграционных Kafka-событий нет: конфигурация покупки читается синхронно, топика
pin.crm.purchase-programне существует (наружуPaymentMethodвиден только через deal-stage-changed.md). Внутренний доменный фактPurchaseProgramChangedинвалидирует кэш конфигурации/калькулятора графика платежей. -
Согласованность. Вставка пресета и version bump программы фиксируются в одном согласованном изменении (атомарно). Нарушение unique-индекса откатывает изменение → duplicate-код (шаг 5).
-
Результат. Карточка
InstallmentPresetModelсозданного пресета (Id,Name,Code,InitialPaymentPercent,MonthlyFixedPayment,TermMonths,DiscountPercent,MilestonePayments,SortOrder,Version = 1) → HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (программа покупки — внутренний справочник; kafka-топика нет, конфиг читается синхронно) |
| External calls | none |
| Cache / projections / search | доменное PurchaseProgramChanged → инвалидация кэша конфигурации и серверного калькулятора графика платежей |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Name пусто / проценты вне [0,100] / TermMonths вне (0,600] / Month вне [1,TermMonths] / неизвестный Kind / Value не по Kind | «Проверьте корректность полей пресета» | no retry |
CRM_INSTALLMENT_REMAINDER_INVALID | 422 | более одного remainder либо remainder не последний по Month (INV-PPR-3) | «Остаток (remainder) должен быть один и последним в графике» | no retry |
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE | 422 | график не погашает 100% discountedPrice на эталонной цене (INV-PPR-4) | «График платежей не покрывает полную стоимость» | no retry |
CRM_PURCHASE_PROGRAM_NOT_FOUND | 404 | у застройщика нет живого агрегата PurchaseProgramEntity в TenantId | «Программа покупки не настроена» | no retry |
CRM_INSTALLMENT_PRESET_NAME_DUPLICATE | 409 | живой пресет с таким Name(ci) уже есть в программе (pre-check либо ux_installment_presets_program_name) | «Пресет с таким названием уже существует» | no retry (сменить название) |
Инвариант архива CRM_INSTALLMENT_PRESET_IN_USE (INV-PPR-5) к create не относится — см.
purchase-program.md и команду архива пресета.
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика (или администратор платформы) с permission
crm.purchase-programs.manage; - у застройщика есть единственный живой агрегат программы покупки (INV-PPR-1) в
TenantId = DeveloperCompanyId; - нет живого пресета с тем же именем (ci) в этой программе;
- график консистентен: не более одного хвостового
remainder(INV-PPR-3) и полное погашениеdiscountedPriceна эталонной цене (INV-PPR-4).
Постусловия (гарантированы после успеха):
- в программу добавлен новый живой пресет (
Version = 1,CreatedByUserId); программа-родитель получает version bump; - график платежей пресета проверен как полный (INV-PPR-4) и с единственным хвостовым
remainder(INV-PPR-3); - интеграционных Kafka-событий нет; внутренний факт
PurchaseProgramChangedинвалидирует кэш конфигурации и серверного калькулятора графика — пресет предлагается при конвертации лида в сделку при следующем чтении (синхронный источник, окна eventual-consistency наружу нет); - существующие сделки со ссылками на другие пресеты не затронуты (держат snapshot).
Acceptance Criteria
- Happy path. Дано: живая программа покупки, уникальное имя, консистентный график. Когда: создание. Тогда:
201, пресет создан (Version = 1), программа получает version bump. - Несколько
remainder/ не последний. Дано: дваremainderилиremainderне последний поMonth. Когда: создание. Тогда:422 CRM_INSTALLMENT_REMAINDER_INVALID(INV-PPR-3). - Неполный график. Дано: график не погашает 100%
discountedPriceна эталонной цене. Когда: создание. Тогда:422 CRM_INSTALLMENT_SCHEDULE_INCOMPLETE(INV-PPR-4). - Нет программы. Дано: у застройщика нет живого агрегата программы покупки. Когда: создание. Тогда:
404 CRM_PURCHASE_PROGRAM_NOT_FOUND. - Дубль имени. Дано: живой пресет с таким именем (ci) уже есть. Когда: создание (в т.ч. гонка). Тогда:
409 CRM_INSTALLMENT_PRESET_NAME_DUPLICATE. - Некорректный milestone. Дано:
Monthвне[1, TermMonths], неизвестныйKindилиValueне поKind. Когда: создание. Тогда:400 VALIDATION_FAILED.
Совместимость и наблюдаемость
- Route/тело стабильны; добавление optional-полей в
InstallmentPresetModel/MilestonePayment— additive (без breaking); удаление/переименование/смена типа — breaking (migration + rollout).Kindсериализуется строкой; на входе принимаются только определённые значения (Enum.IsDefined) — unknown →VALIDATION_FAILED.MilestonePaymentList.ContractVersion = 1— рост версии при смене схемы jsonb. - Logs:
installment-preset create requested/created/duplicate/schedule-invalid(TenantId,PurchaseProgramId,PresetId,TermMonths; PII нет); metrics:crm_installment_preset_create_total{result}; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Уникальность Name(ci) проверяется дважды: pre-check на шаге 2 (для дружелюбного 409 на неконкурентном
пути) и partial-unique-индекс ux_installment_presets_program_name на сохранении (шаг 5) — он ловит гонку
параллельных create с одним именем и тоже отдаёт CRM_INSTALLMENT_PRESET_NAME_DUPLICATE.
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность, инварианты, формула графика: purchase-program.md (INV-PPR-1
единственный агрегат, INV-PPR-2 диапазоны, INV-PPR-3 хвостовой
remainder, INV-PPR-4 полнота графика, моделиMilestonePaymentList/MilestoneKind, индексux_installment_presets_program_name); потребитель пресета: deal.md (PaymentMethod,InstallmentPresetId); карта событий и область developer — ../../README.md. - События агрегата: домен
PurchaseProgramChanged(IDomainEvent, внутренний; kafka-канала нет — см. карту событий ../../README.md); наружуPaymentMethodвиден только через deal-stage-changed.md. - ADR: 0052 tenancy, 0056 simplicity-first.