Entity / Aggregate: PurchaseProgramEntity (+ InstallmentPresetEntity)
Дом полей сущности «Программа покупки» контекста crm. Способ оплаты сделки (Deal.PaymentMethod,
deal.md) и серверный расчёт графика платежей ссылаются сюда.
Назначение
PurchaseProgramEntity — конфигурация способов оплаты покупки у застройщика: 100% оплата (со скидкой
FullPaymentDiscountPercent), рассрочки (список пресетов InstallmentPresetEntity с milestone-платежами)
и ипотека (ставка/срок). Определяет график платежей; расчёт графика — серверный: фронт запрашивает
график у CRM, формула ниже. Один агрегат на застройщика.
Классификация
| Поле | Значение |
|---|---|
| Контекст | crm |
| Kind | AggregateRoot (PurchaseProgramEntity) + child Entity (InstallmentPresetEntity) |
| Source of truth | crm |
| Таблица | crm.purchase_programs + crm.installment_presets |
| Tenant isolation | TenantId = DeveloperCompanyId |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор конфигурации. |
TenantId | Guid | Да | unique ux_purchase_programs_tenant where deleted_at is null | identity (actor) | Один живой агрегат на застройщика. |
FullPaymentDiscountPercent | decimal | Да | numeric(5,2), >= 0 and <= 100, default 0 | app | Скидка при 100% оплате. |
MortgageAnnualRatePercent | decimal | Да | numeric(5,2), >= 0 and <= 100 | app | Годовая ставка ипотеки для калькулятора. |
MortgageTermMonths | int | Да | > 0 and <= 600 | app | Срок ипотеки в месяцах для калькулятора. |
IsMortgageEnabled | bool | Да | default true | app | Доступна ли ипотека у застройщика (выключатель опции). |
IsFullPaymentEnabled | bool | Да | default true | app | Доступна ли 100% оплата. |
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserId | как в шаблоне | — | — | app | Отметки создания/изменения/удаления. |
Дочерняя сущность InstallmentPresetEntity
Таблица crm.installment_presets. Пресет рассрочки — дочерний Entity агрегата PurchaseProgramEntity;
на конкретный выбранный пресет ссылается Deal.InstallmentPresetId.
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор пресета (Deal.InstallmentPresetId ссылается сюда). |
PurchaseProgramId | Guid | Да | FK fk_installment_presets_program → crm.purchase_programs(id) cascade, index | app | Родительская конфигурация. |
TenantId | Guid | Да | index | из программы | Дублируется для tenant-фильтра. |
LegacyId | int? | Нет | unique where not null | app | Старый числовой id для сверки данных. |
Name | string | Да | varchar(128), не пустое; unique ux_installment_presets_program_name on (PurchaseProgramId, lower(Name)) where deleted_at is null | app | Название («Рассрочка 12 месяцев»). |
Code | string? | Нет | varchar(64) | app | Display-код пресета (не доменный ключ). |
InitialPaymentPercent | decimal | Да | numeric(5,2), >= 0 and <= 100 | app | Первоначальный взнос, % от цены. |
MonthlyFixedPayment | decimal | Да | numeric(18,2), >= 0 | app | Фиксированный ежемесячный платёж (валюта — RUB, как цена объекта недвижимости). |
TermMonths | int | Да | > 0 and <= 600 | app | Срок рассрочки, месяцы. |
DiscountPercent | decimal | Да | numeric(5,2), >= 0 and <= 100, default 0 | app | Скидка пресета от цены объекта недвижимости. |
MilestonePayments | MilestonePaymentList (jsonb) | Да (может быть пустым списком) | typed jsonb, модель ниже | app | Промежуточные обязательные платежи. |
SortOrder | int | Да | >= 0, default 0 | app | Порядок показа опций. |
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserId | как в шаблоне | — | — | app | Отметки создания/изменения/удаления. |
Enum PaymentMethod (дом enum-а; используют deal.md и lead.md)
| Значение | Описание |
|---|---|
FullPayment | 100% оплата; скидка FullPaymentDiscountPercent. |
Installment | Рассрочка по выбранному InstallmentPresetEntity. |
Mortgage | Ипотека (MortgageAnnualRatePercent, MortgageTermMonths); банковский процесс — этапы сделки. |
Модели JSONB (versioned)
MilestonePaymentList (jsonb, ContractVersion = 1)
Контейнер: { ContractVersion: int, Items: MilestonePayment[] } (коллекция внутри именованной
container-model).
MilestonePayment
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
Month | int | Да | >= 1 and <= TermMonths пресета | v1 |
Kind | MilestoneKind | Да | enum-строка (percent / fixed / remainder), unknown → ошибка валидации | v1 |
Value | decimal | Да | percent: > 0 and <= 100; fixed: > 0 (сумма RUB); remainder: игнорируется, пишется 0 | v1 |
Enum MilestoneKind
| Значение | Описание |
|---|---|
percent | Платёж = Value% от полной цены объекта недвижимости. |
fixed | Платёж = Value (фиксированная сумма). |
remainder | Платёж = весь непогашенный остаток на месяц Month. |
Серверный расчёт графика платежей (правило, дом формулы)
График платежей рассчитывается на сервере:
basePrice = Deal.Price.Amount(или цена объекта недвижимости при предпросмотре калькулятора).discountedPrice = basePrice × (1 − DiscountPercent/100)(дляFullPayment—FullPaymentDiscountPercent).- Первый платёж (месяц 0) =
discountedPrice × InitialPaymentPercent/100. - Для месяцев
1..max(TermMonths, max(Milestone.Month)): платёж месяца =MonthlyFixedPayment(пока месяц ≤TermMonths) + сумма milestone-платежей этого месяца:percent→ отdiscountedPrice,fixed→Value,remainder→ непогашенный остаток. - Остаток после последнего месяца обязан быть
0— иначе конфигурация невалидна (INV-PPR-4). - Ипотека: аннуитетный платёж от
discountedPrice − первоначальный взноспо ставкеMortgageAnnualRatePercent/12наMortgageTermMonths(детальный контракт запроса/ответа — документ API../api/get-payment-schedule.md, будущий путь по конвенции).
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
InstallmentPresetEntity | owned-child (агрегат) | installment_presets.purchase_program_id | include целиком (единицы строк) | cascade (архив — soft-delete) |
DealEntity | reference (потребитель) | deals.installment_preset_id | загрузка при конвертации лида/смене способа оплаты | restrict (пресет с сделками только архивируется) |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-PPR-1 | Один живой агрегат на застройщика | unique ux_purchase_programs_tenant | CRM_PURCHASE_PROGRAM_EXISTS |
| INV-PPR-2 | Проценты в диапазоне 0–100; сроки > 0 | при создании/изменении (check-констрейнты) | CRM_PURCHASE_PROGRAM_RANGE |
| INV-PPR-3 | В MilestonePayments не более одного remainder, и он последний по Month | при создании/изменении пресета | CRM_INSTALLMENT_REMAINDER_INVALID |
| INV-PPR-4 | График по пресету погашает 100% discountedPrice (проверка расчётом на эталонной цене) | при создании/изменении пресета (шаг 5 формулы) | CRM_INSTALLMENT_SCHEDULE_INCOMPLETE |
| INV-PPR-5 | Архивировать пресет, выбранный в нетерминальных сделках, нельзя (сначала смена способа оплаты в сделках) | при архивировании пресета | CRM_INSTALLMENT_PRESET_IN_USE |
Диаграмма состояний неприменима — статусной машины нет (конфигурация; живая ↔ архив через soft-delete).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
PurchaseProgramChanged | доменное | update конфигурации/пресетов | Id, TenantId |
Конфигурация читается синхронно; сделка хранит выбранные PaymentMethod/InstallmentPresetId.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | purchase_programs: PK, ux_purchase_programs_tenant unique partial; installment_presets: PK, (purchase_program_id), ux_installment_presets_program_name unique partial, unique (legacy_id) where not null |
| Партиционирование / retention | none |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) |
| Permissions / PII | чтение конфигурации — публичное в рамках карточки объекта недвижимости (анонимно с гейтом витрины); crm.purchase-programs.manage — менеджер застройщика и администратор платформы. PII отсутствует. |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete |
Целостность, конкуренция и восстановление
- Один живой агрегат на застройщика. INV-PPR-1 (unique
ux_purchase_programs_tenant): попытка завести второй →CRM_PURCHASE_PROGRAM_EXISTS; конфигурация правится, а не пересоздаётся. - Границы значений. Проценты 0–100, сроки > 0 (INV-PPR-2, check-констрейнты) →
CRM_PURCHASE_PROGRAM_RANGE. - Полнота графика. Пресет валиден, только если график погашает 100%
discountedPriceна эталонной цене (INV-PPR-4, шаг 5 формулы) →CRM_INSTALLMENT_SCHEDULE_INCOMPLETE; не более одногоremainder, и он последний поMonth(INV-PPR-3) →CRM_INSTALLMENT_REMAINDER_INVALID. - Пресет в использовании. Пресет, выбранный в нетерминальных сделках, нельзя архивировать до смены
способа оплаты в этих сделках (INV-PPR-5) →
CRM_INSTALLMENT_PRESET_IN_USE. - Расчёт графика — серверный и синхронный. Фронт запрашивает график у CRM (формула — дом здесь);
предпросмотр калькулятора считается по цене объекта недвижимости, боевой график — по
Deal.Price.Amount(snapshot сделки). Интеграционных событий конфигурация не публикует — сделка хранит выбранныеPaymentMethod/InstallmentPresetId, поэтому правка программы не меняет уже зафиксированный в сделке выбор задним числом. - Конкуренция. Оптимистическая блокировка (правка конфигурации/пресетов не должна затирать чужую).
Acceptance-критерии
- Happy (валидный пресет). Дано: цена,
InitialPaymentPercent,MonthlyFixedPayment,TermMonths, milestone-платежи. Когда пресет сохраняется. Тогда график погашает 100%discountedPrice, пресет доступен в сделке. - Неполный график. Дано: сумма платежей не гасит цену. Когда пресет сохраняется. Тогда отказ
CRM_INSTALLMENT_SCHEDULE_INCOMPLETE. - Несколько remainder. Дано: два
remainderв milestone-платежах. Тогда отказCRM_INSTALLMENT_REMAINDER_INVALID. - Второй агрегат. Дано: у застройщика уже есть конфигурация. Когда создаётся вторая. Тогда отказ
CRM_PURCHASE_PROGRAM_EXISTS. - Архив используемого пресета. Дано: пресет выбран в нетерминальной сделке. Когда его архивируют. Тогда
отказ
CRM_INSTALLMENT_PRESET_IN_USE. - Диапазоны. Дано: ставка > 100% или срок 0. Когда сохраняют. Тогда отказ
CRM_PURCHASE_PROGRAM_RANGE.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: BookingEntity, Entity / Aggregate: DealEntity, Entity / Aggregate: LeadEntity
- API (18): GET /api/agent/crm/deals/{dealId} — Карточка сделки (агент), GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/developer/crm/purchase-program — Программа покупки застройщика, GET /api/investor/crm/deals/{dealId} — Карточка сделки (инвестор), GET /api/pin-manager/crm/deals — Сквозной список всех сделок платформы, GET /api/public/crm/purchase-config — Конфигурация способов оплаты (витрина), POST /api/agent/crm/deals — Создать сделку из лида по объекту, POST /api/agent/crm/deals/{dealId}/engage-developer — Вовлечь застройщика (Draft → Application), POST /api/agent/crm/leads — Создать лид вручную, POST /api/developer/crm/purchase-program/presets — Создать пресет рассрочки, POST /api/developer/crm/purchase-program/presets/{presetId}/archive — Архивировать пресет (+6)
- Use Cases (2): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), US-реестр блока «Сделка» (US-INV / US-AGT / US-DEV / US-MGR)
- События (1): Event Contract: pin.crm.deal-created (DealCreatedEvent)
- scenarios (1): UC-OVR-003. Бронирование объекта у агента (+3a)
- index.md (1): CRM Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- deal.md — потребитель (
PaymentMethod,InstallmentPresetId, влияние на этапы); booking.md — оплата брони (отдельный механизм, не путать). - График платежей — API-документ
../api/get-payment-schedule.md(будущий путь по конвенции).