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

Entity / Aggregate: PurchaseProgramEntity (+ InstallmentPresetEntity)

Дом полей сущности «Программа покупки» контекста crm. Способ оплаты сделки (Deal.PaymentMethod, deal.md) и серверный расчёт графика платежей ссылаются сюда.

Назначение

PurchaseProgramEntity — конфигурация способов оплаты покупки у застройщика: 100% оплата (со скидкой FullPaymentDiscountPercent), рассрочки (список пресетов InstallmentPresetEntity с milestone-платежами) и ипотека (ставка/срок). Определяет график платежей; расчёт графика — серверный: фронт запрашивает график у CRM, формула ниже. Один агрегат на застройщика.

Классификация

ПолеЗначение
Контекстcrm
KindAggregateRoot (PurchaseProgramEntity) + child Entity (InstallmentPresetEntity)
Source of truthcrm
Таблицаcrm.purchase_programs + crm.installment_presets
Tenant isolationTenantId = DeveloperCompanyId

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор конфигурации.
TenantIdGuidДаunique ux_purchase_programs_tenant where deleted_at is nullidentity (actor)Один живой агрегат на застройщика.
FullPaymentDiscountPercentdecimalДаnumeric(5,2), >= 0 and <= 100, default 0appСкидка при 100% оплате.
MortgageAnnualRatePercentdecimalДаnumeric(5,2), >= 0 and <= 100appГодовая ставка ипотеки для калькулятора.
MortgageTermMonthsintДа> 0 and <= 600appСрок ипотеки в месяцах для калькулятора.
IsMortgageEnabledboolДаdefault trueappДоступна ли ипотека у застройщика (выключатель опции).
IsFullPaymentEnabledboolДаdefault trueappДоступна ли 100% оплата.
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserIdкак в шаблонеappОтметки создания/изменения/удаления.

Дочерняя сущность InstallmentPresetEntity

Таблица crm.installment_presets. Пресет рассрочки — дочерний Entity агрегата PurchaseProgramEntity; на конкретный выбранный пресет ссылается Deal.InstallmentPresetId.

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор пресета (Deal.InstallmentPresetId ссылается сюда).
PurchaseProgramIdGuidДаFK fk_installment_presets_program → crm.purchase_programs(id) cascade, indexappРодительская конфигурация.
TenantIdGuidДаindexиз программыДублируется для tenant-фильтра.
LegacyIdint?Нетunique where not nullappСтарый числовой id для сверки данных.
NamestringДаvarchar(128), не пустое; unique ux_installment_presets_program_name on (PurchaseProgramId, lower(Name)) where deleted_at is nullappНазвание («Рассрочка 12 месяцев»).
Codestring?Нетvarchar(64)appDisplay-код пресета (не доменный ключ).
InitialPaymentPercentdecimalДаnumeric(5,2), >= 0 and <= 100appПервоначальный взнос, % от цены.
MonthlyFixedPaymentdecimalДаnumeric(18,2), >= 0appФиксированный ежемесячный платёж (валюта — RUB, как цена объекта недвижимости).
TermMonthsintДа> 0 and <= 600appСрок рассрочки, месяцы.
DiscountPercentdecimalДаnumeric(5,2), >= 0 and <= 100, default 0appСкидка пресета от цены объекта недвижимости.
MilestonePaymentsMilestonePaymentList (jsonb)Да (может быть пустым списком)typed jsonb, модель нижеappПромежуточные обязательные платежи.
SortOrderintДа>= 0, default 0appПорядок показа опций.
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserIdкак в шаблонеappОтметки создания/изменения/удаления.

Enum PaymentMethod (дом enum-а; используют deal.md и lead.md)

ЗначениеОписание
FullPayment100% оплата; скидка FullPaymentDiscountPercent.
InstallmentРассрочка по выбранному InstallmentPresetEntity.
MortgageИпотека (MortgageAnnualRatePercent, MortgageTermMonths); банковский процесс — этапы сделки.

Модели JSONB (versioned)

MilestonePaymentList (jsonb, ContractVersion = 1)

Контейнер: { ContractVersion: int, Items: MilestonePayment[] } (коллекция внутри именованной container-model).

MilestonePayment

ПолеТипОбязательностьОграниченияSince version
MonthintДа>= 1 and <= TermMonths пресетаv1
KindMilestoneKindДаenum-строка (percent / fixed / remainder), unknown → ошибка валидацииv1
ValuedecimalДаpercent: > 0 and <= 100; fixed: > 0 (сумма RUB); remainder: игнорируется, пишется 0v1

Enum MilestoneKind

ЗначениеОписание
percentПлатёж = Value% от полной цены объекта недвижимости.
fixedПлатёж = Value (фиксированная сумма).
remainderПлатёж = весь непогашенный остаток на месяц Month.

Серверный расчёт графика платежей (правило, дом формулы)

График платежей рассчитывается на сервере:

  1. basePrice = Deal.Price.Amount (или цена объекта недвижимости при предпросмотре калькулятора).
  2. discountedPrice = basePrice × (1 − DiscountPercent/100) (для FullPaymentFullPaymentDiscountPercent).
  3. Первый платёж (месяц 0) = discountedPrice × InitialPaymentPercent/100.
  4. Для месяцев 1..max(TermMonths, max(Milestone.Month)): платёж месяца = MonthlyFixedPayment (пока месяц ≤ TermMonths) + сумма milestone-платежей этого месяца: percent → от discountedPrice, fixedValue, remainder → непогашенный остаток.
  5. Остаток после последнего месяца обязан быть 0 — иначе конфигурация невалидна (INV-PPR-4).
  6. Ипотека: аннуитетный платёж от discountedPrice − первоначальный взнос по ставке MortgageAnnualRatePercent/12 на MortgageTermMonths (детальный контракт запроса/ответа — документ API ../api/get-payment-schedule.md, будущий путь по конвенции).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
InstallmentPresetEntityowned-child (агрегат)installment_presets.purchase_program_idinclude целиком (единицы строк)cascade (архив — soft-delete)
DealEntityreference (потребитель)deals.installment_preset_idзагрузка при конвертации лида/смене способа оплатыrestrict (пресет с сделками только архивируется)

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-PPR-1Один живой агрегат на застройщикаunique ux_purchase_programs_tenantCRM_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
Партиционирование / retentionnone
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.

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

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

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

  • deal.md — потребитель (PaymentMethod, InstallmentPresetId, влияние на этапы); booking.md — оплата брони (отдельный механизм, не путать).
  • График платежей — API-документ ../api/get-payment-schedule.md (будущий путь по конвенции).