feat(finmodel): financing overlay (кредит 15%) поверх unlevered DCF (epic #1881 PR-5)
Финансирование как ДОПОЛНИТЕЛЬНЫЙ overlay поверх готового unlevered cashflow: кредитная линия покрывает накопительный кассовый разрыв, проценты капитализируются (compound), ставка _CREDIT_RATE_ANNUAL=15% (норматив, ОТДЕЛЬНО от ставки дисконтирования — разная семантика: стоимость долга vs требуемая доходность). Backend (financial.py): - _compute_financing(operating_cf, annual_rate) — чистая (READ-ONLY вход, не мутирует): процент на остаток на начало месяца ДО движений → капитализация; draw=-cf при оттоке, repay=min(cf,debt) при притоке (долг не уходит в минус). Возвращает peak_debt_rub / total_interest_rub / ending_debt_rub. - compute_financial: вызов после DCF, net_profit_after_financing = net − interest. - _build_monthly_cashflow и unlevered NPV/IRR/PBP НЕ ТРОНУТЫ → инвариант Σ unlevered cashflow == net_profit сохранён (регресс-тест). - FinancialModel +6 полей (financing_enabled/annual_rate_used/peak_debt_rub/ total_interest_rub/net_profit_after_financing_rub/financing_is_simplified). Frontend: ParcelFinancialEstimate +6 полей; adaptFinanceDrawer + concept-каскад показывают пиковый долг/проценты/чистую прибыль после финанс. (когда enabled). MVP: только агрегаты, БЕЗ levered IRR (хрупко на хардкод-100%-долге — отдельный PR). HEAVY caveat: проценты — реальная стоимость (net после < net до); 100% покрытие разрыва (нет equity/LTC); compound; эскроу не моделируется; unlevered headline не затронут. financing_is_simplified=True флаг. mypy strict clean, +9 backend + 3 frontend тестов (compound точное значение, no-mutation, инвариант-регресс, монотонность, net_after<net), api-types регенерён авторитетно, deps не тронуты (hand-roll). Refs #1881
This commit is contained in:
parent
9495c2d157
commit
d350d27325
10 changed files with 364 additions and 3 deletions
|
|
@ -91,6 +91,17 @@ class FinancialModel(BaseModel):
|
|||
# проекта). True → UI/PDF показывают caveat «график — типовое допущение».
|
||||
schedule_is_default: bool = True
|
||||
|
||||
# ── Financing overlay (PR-5, эпик #1881) ───────────────────────────────────
|
||||
# OVERLAY поверх unlevered cashflow: кредит покрывает кассовый разрыв, проценты
|
||||
# капитализируются (compound). Unlevered NPV/IRR (headline) НЕ затронуты.
|
||||
financing_enabled: bool = False # True когда overlay посчитан
|
||||
annual_rate_used: float = 0.0 # _CREDIT_RATE_ANNUAL (норматив, отдельно от дисконта)
|
||||
peak_debt_rub: float = 0.0 # «задолженность по кредиту» Excel — пик долга
|
||||
total_interest_rub: float = 0.0 # «поток по финдеятельности» Excel — Σ процентов
|
||||
net_profit_after_financing_rub: float = 0.0 # net_profit − total_interest
|
||||
# honest-флаг: 100% покрытие разрыва, compound %, эскроу не моделируется точно.
|
||||
financing_is_simplified: bool = True
|
||||
|
||||
# ── Price calibration (PR-2, эпик #1881) ───────────────────────────────────
|
||||
# Цена продажи жилья, руб/кв.м, фактически использованная в выручке. Либо
|
||||
# калиброванная по рынку (Objective / district reference), либо норматив класса.
|
||||
|
|
|
|||
|
|
@ -89,6 +89,34 @@ flag the source honestly (``price_source="class_norm"``) so the UI/PDF never
|
|||
passes a class norm off as a market figure. Construction cost (СМР) is *not*
|
||||
calibrated in PR-2 — it stays a per-class norm. Parking price/cost also stay norms.
|
||||
|
||||
FINANCING — OVERLAY (PR-5)
|
||||
--------------------------
|
||||
Unlevered (project) DCF — NPV / IRR / PBP — остаётся HEADLINE и НЕ меняется:
|
||||
это интринсивная доходность проекта, инвариант Σ cashflow == net_profit
|
||||
сохраняется. Финансирование (кредит/проектное финансирование) — это
|
||||
ДОПОЛНИТЕЛЬНЫЙ overlay ПОВЕРХ готового unlevered cashflow; он его читает
|
||||
(READ-ONLY) и НЕ пересобирает.
|
||||
|
||||
* **Проценты — реальная стоимость долга**, поэтому ``net_profit_after_financing
|
||||
< net_profit`` для любого проекта с кассовым разрывом.
|
||||
* **Ставка ``annual_rate_used`` — норматив** (``_CREDIT_RATE_ANNUAL``), отдельный
|
||||
от ставки дисконтирования (см. константы), а не выведенная из проекта величина.
|
||||
* **100% покрытие разрыва** — кредитная линия покрывает весь накопительный
|
||||
кассовый разрыв; equity / LTC / лимиты банка НЕ моделируются (нет доли
|
||||
собственных средств).
|
||||
* **Проценты капитализируются (compound)** — начисляются на остаток на начало
|
||||
месяца ДО движений и добавляются в тело долга.
|
||||
* **Эскроу-специфику не моделируем точно** — раскрытие средств с эскроу-счетов
|
||||
по этапам стройки, лимиты раскрытия, ставка по бридж/целевой части — всё это
|
||||
упрощено до единой капитализируемой ставки на кассовый разрыв.
|
||||
* **Levered IRR НЕ считается** в этом PR (хрупко на хардкод-100%-долге — отдельный
|
||||
PR). MVP раскрывает только агрегаты: ``peak_debt_rub`` / ``total_interest_rub``
|
||||
/ ``net_profit_after_financing_rub``; debt_schedule в схему НЕ выносится.
|
||||
* **Unlevered NPV / IRR / PBP НЕ затронуты** overlay'ем — это всё та же
|
||||
интринсивная доходность. График наследует неточность ``schedule_is_default``:
|
||||
финансирование считается по тому же дефолт-графику фаз, поэтому
|
||||
``financing_is_simplified=True`` (honest-флаг для UI/PDF).
|
||||
|
||||
This function stays **pure**: it does no DB lookup itself (the API layer does the
|
||||
lookup and passes the result in). Детерминированно, без LLM / внешних API / БД.
|
||||
"""
|
||||
|
|
@ -160,6 +188,11 @@ _PROJECT_YEARS: float = 3.0
|
|||
|
||||
# Годовая ставка дисконтирования (Excel-эталон девелоперской модели).
|
||||
_DISCOUNT_RATE_ANNUAL: float = 0.15
|
||||
# Ставка по кредиту/проектному финансированию (Excel-эталон). ОТДЕЛЬНА от
|
||||
# _DISCOUNT_RATE_ANNUAL: дисконт = требуемая доходность инвестора (NPV), кредитная
|
||||
# ставка = стоимость долга (проценты). Совпадение значения (0.15) — калибровка,
|
||||
# НЕ тождество; не сливать в одну константу.
|
||||
_CREDIT_RATE_ANNUAL: float = 0.15
|
||||
# Длительность ПИР (проектирование/экспертиза), мес.
|
||||
_PIR_DURATION_MONTHS: int = 6
|
||||
# Длительность СМР по типу застройки, мес (точечная быстрее высотной).
|
||||
|
|
@ -258,6 +291,42 @@ def _payback_months(cf: list[float]) -> float | None:
|
|||
return None
|
||||
|
||||
|
||||
def _compute_financing(operating_cf: list[float], annual_rate: float) -> dict[str, float]:
|
||||
"""OVERLAY финансирования поверх unlevered cashflow (PR-5). Чистая функция.
|
||||
|
||||
``operating_cf`` — готовый unlevered cashflow (READ-ONLY, НЕ мутируется).
|
||||
Кредитная линия покрывает накопительный кассовый разрыв на 100%; проценты
|
||||
КАПИТАЛИЗИРУЮТСЯ в долг (compound) — упрощение под эскроу-логику РФ. Порядок
|
||||
внутри месяца: процент НАЧИСЛЯЕМ на остаток на начало месяца ДО draw/repay,
|
||||
затем выборка (cf<0) либо погашение притоком (cf>0, не уходя ниже 0).
|
||||
|
||||
Если поток не уходит в минус → долг не выбирается → debt=0, interest=0
|
||||
(корректно: финансирование не требуется).
|
||||
|
||||
Возвращает агрегаты (без debt_schedule — только итоговые числа для MVP):
|
||||
``peak_debt_rub`` — пиковая задолженность, ``total_interest_rub`` — суммарные
|
||||
капитализированные проценты, ``ending_debt_rub`` — остаток долга на конец.
|
||||
"""
|
||||
monthly_rate = (1.0 + annual_rate) ** (1.0 / 12.0) - 1.0
|
||||
debt = 0.0
|
||||
peak_debt = 0.0
|
||||
total_interest = 0.0
|
||||
for cf in operating_cf:
|
||||
interest = debt * monthly_rate # процент на остаток на начало месяца (до движений)
|
||||
debt += interest
|
||||
total_interest += interest
|
||||
if cf < 0:
|
||||
debt += -cf # выборка на покрытие оттока
|
||||
else:
|
||||
debt -= min(cf, debt) # гасим долг притоком (не уходим в минус)
|
||||
peak_debt = max(peak_debt, debt)
|
||||
return {
|
||||
"peak_debt_rub": round(peak_debt, 2),
|
||||
"total_interest_rub": round(total_interest, 2),
|
||||
"ending_debt_rub": round(debt, 2),
|
||||
}
|
||||
|
||||
|
||||
def _spread_linear(total: float, start: int, duration: int, horizon: int) -> list[float]:
|
||||
"""Разложить ``total`` равными долями по месяцам ``[start .. start+duration)``.
|
||||
|
||||
|
|
@ -457,6 +526,13 @@ def compute_financial(
|
|||
irr = max(-1.0, min(1.0, roi / _PROJECT_YEARS))
|
||||
irr_is_proxy = True
|
||||
|
||||
# ── Финансирование: OVERLAY поверх unlevered cashflow (PR-5) ───────────────
|
||||
# Тот же массив cashflow, что построен выше (НЕ пересобираем — защита инварианта:
|
||||
# overlay читает unlevered поток и НЕ мутирует его). Проценты — реальная стоимость
|
||||
# долга, режут прибыль: net_after < net. Levered IRR НЕ считаем (отдельный PR).
|
||||
financing = _compute_financing(cashflow, _CREDIT_RATE_ANNUAL)
|
||||
net_profit_after_financing = net_profit - financing["total_interest_rub"]
|
||||
|
||||
model = FinancialModel(
|
||||
# legacy summary
|
||||
revenue_rub=round(revenue, 2),
|
||||
|
|
@ -488,6 +564,13 @@ def compute_financial(
|
|||
payback_months=(round(payback, 1) if payback is not None else None),
|
||||
discount_rate_used=_DISCOUNT_RATE_ANNUAL,
|
||||
schedule_is_default=True,
|
||||
# financing overlay (PR-5)
|
||||
financing_enabled=True,
|
||||
annual_rate_used=_CREDIT_RATE_ANNUAL,
|
||||
peak_debt_rub=financing["peak_debt_rub"],
|
||||
total_interest_rub=financing["total_interest_rub"],
|
||||
net_profit_after_financing_rub=round(net_profit_after_financing, 2),
|
||||
financing_is_simplified=True,
|
||||
# price calibration (PR-2)
|
||||
price_per_sqm_used=round(sale_price, 2),
|
||||
price_is_calibrated=price_is_calibrated,
|
||||
|
|
@ -495,7 +578,8 @@ def compute_financial(
|
|||
)
|
||||
logger.info(
|
||||
"financial: revenue=%.0f cost=%.0f net=%.0f roi=%.3f npv=%.0f "
|
||||
"irr=%.3f%s pbp=%s price=%.0f/m2 source=%s",
|
||||
"irr=%.3f%s pbp=%s price=%.0f/m2 source=%s "
|
||||
"peak_debt=%.0f total_interest=%.0f net_after_fin=%.0f",
|
||||
model.revenue_rub,
|
||||
model.cost_rub,
|
||||
model.net_profit_rub,
|
||||
|
|
@ -506,6 +590,9 @@ def compute_financial(
|
|||
model.payback_months,
|
||||
model.price_per_sqm_used,
|
||||
model.price_source,
|
||||
model.peak_debt_rub,
|
||||
model.total_interest_rub,
|
||||
model.net_profit_after_financing_rub,
|
||||
)
|
||||
return model
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,13 @@ def test_concepts_response_matches_contract_keys() -> None:
|
|||
"payback_months",
|
||||
"discount_rate_used",
|
||||
"schedule_is_default",
|
||||
# financing overlay (PR-5)
|
||||
"financing_enabled",
|
||||
"annual_rate_used",
|
||||
"peak_debt_rub",
|
||||
"total_interest_rub",
|
||||
"net_profit_after_financing_rub",
|
||||
"financing_is_simplified",
|
||||
# price calibration (PR-2)
|
||||
"price_per_sqm_used",
|
||||
"price_is_calibrated",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from app.schemas.concept import TEAP
|
|||
from app.services.generative import financial
|
||||
from app.services.generative.financial import (
|
||||
_build_monthly_cashflow,
|
||||
_compute_financing,
|
||||
_irr_monthly,
|
||||
_npv,
|
||||
_payback_months,
|
||||
|
|
@ -255,3 +256,96 @@ def test_unknown_development_type_defaults_to_mid_rise() -> None:
|
|||
)
|
||||
assert none_type.npv_rub == explicit_mid.npv_rub
|
||||
assert none_type.irr == explicit_mid.irr
|
||||
|
||||
|
||||
# ── FINANCING — OVERLAY (PR-5) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_financing_does_not_mutate_operating_cashflow() -> None:
|
||||
# OVERLAY: _compute_financing читает unlevered поток READ-ONLY, НЕ мутирует.
|
||||
cf = [-1000.0, -500.0, 0.0, 800.0, 900.0]
|
||||
snapshot = list(cf)
|
||||
_compute_financing(cf, 0.15)
|
||||
assert cf == snapshot
|
||||
|
||||
|
||||
def test_financing_draws_debt_on_cash_gap() -> None:
|
||||
# Ранний отток / поздняя выручка → нужна кредитная линия → peak_debt>0, %>0.
|
||||
cf = [-1000.0, -1000.0, 0.0, 1500.0, 1500.0]
|
||||
res = _compute_financing(cf, 0.15)
|
||||
assert res["peak_debt_rub"] > 0.0
|
||||
assert res["total_interest_rub"] > 0.0
|
||||
|
||||
|
||||
def test_financing_zero_when_no_cash_gap() -> None:
|
||||
# Поток без минуса → финансирование не требуется → debt=0, interest=0.
|
||||
cf = [100.0, 200.0, 300.0]
|
||||
res = _compute_financing(cf, 0.15)
|
||||
assert res["peak_debt_rub"] == 0.0
|
||||
assert res["total_interest_rub"] == 0.0
|
||||
assert res["ending_debt_rub"] == 0.0
|
||||
|
||||
|
||||
def test_financing_interest_capitalises_compound() -> None:
|
||||
# Долг 1000 в мес 0, дальше ноль → проценты КАПИТАЛИЗИРУЮТСЯ (начисляются на
|
||||
# растущий остаток). За мес 1 и 2 суммарный % > 2× месячного % от 1000 (compound).
|
||||
cf = [-1000.0, 0.0, 0.0]
|
||||
res = _compute_financing(cf, 0.15)
|
||||
monthly_rate = (1.0 + 0.15) ** (1.0 / 12.0) - 1.0
|
||||
# Мес 0: % с нуля = 0, выборка 1000. Мес 1: % = 1000·r. Мес 2: % = (1000+1000·r)·r.
|
||||
expected = 1000.0 * monthly_rate + (1000.0 + 1000.0 * monthly_rate) * monthly_rate
|
||||
assert abs(res["total_interest_rub"] - round(expected, 2)) < 0.5
|
||||
# Compound строго больше простого 2× месячного % от исходного тела долга.
|
||||
assert res["total_interest_rub"] > 2.0 * 1000.0 * monthly_rate
|
||||
|
||||
|
||||
def test_financing_interest_monotonic_in_rate() -> None:
|
||||
# Монотонность: выше ставка → выше суммарные проценты при том же потоке.
|
||||
cf = [-1000.0, -1000.0, 0.0, 1500.0, 1500.0]
|
||||
low = _compute_financing(cf, 0.10)
|
||||
high = _compute_financing(cf, 0.20)
|
||||
assert high["total_interest_rub"] > low["total_interest_rub"]
|
||||
assert high["peak_debt_rub"] >= low["peak_debt_rub"]
|
||||
|
||||
|
||||
def test_net_profit_after_financing_equals_net_minus_interest() -> None:
|
||||
# Тождество модели: net_after == net_profit − total_interest (float-tol).
|
||||
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
|
||||
model = financial.compute_financial(
|
||||
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
|
||||
)
|
||||
expected = model.net_profit_rub - model.total_interest_rub
|
||||
assert abs(model.net_profit_after_financing_rub - expected) < 0.01
|
||||
|
||||
|
||||
def test_net_profit_after_financing_below_net_for_project_with_gap() -> None:
|
||||
# Проект с кассовым разрывом: проценты режут прибыль → net_after < net.
|
||||
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
|
||||
model = financial.compute_financial(
|
||||
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
|
||||
)
|
||||
assert model.total_interest_rub > 0.0
|
||||
assert model.net_profit_after_financing_rub < model.net_profit_rub
|
||||
|
||||
|
||||
def test_financing_overlay_does_not_touch_unlevered_invariant() -> None:
|
||||
# РЕГРЕСС: overlay не трогает unlevered cashflow → инвариант Σ cf == net_profit
|
||||
# держится как до PR-5 (financing — поверх готового потока, не пересборка).
|
||||
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
|
||||
model = financial.compute_financial(
|
||||
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
|
||||
)
|
||||
cf = _rebuild_cashflow(model, construction_months=36)
|
||||
assert abs(sum(cf) - model.net_profit_rub) < 1.0
|
||||
|
||||
|
||||
def test_financing_enabled_on_realistic_parcel() -> None:
|
||||
# Интеграция compute_financial: реалистичный участок → overlay посчитан.
|
||||
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
|
||||
model = financial.compute_financial(
|
||||
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
|
||||
)
|
||||
assert model.financing_enabled is True
|
||||
assert model.financing_is_simplified is True
|
||||
assert model.annual_rate_used == financial._CREDIT_RATE_ANNUAL
|
||||
assert model.peak_debt_rub > 0.0
|
||||
|
|
|
|||
|
|
@ -307,6 +307,12 @@ function VariantPanel({ parcel, variant }: PanelProps) {
|
|||
: "норматив класса (нет рыночных данных по участку)"}
|
||||
; себестоимость СМР и цена паркинга — нормативные ориентиры.
|
||||
Коммерческие и офисные площади не учитываются.
|
||||
{financial.financing_enabled && financial.financing_is_simplified
|
||||
? " Финансирование упрощено: весь кассовый разрыв покрыт " +
|
||||
"кредитом по ставке-нормативу, проценты капитализируются, " +
|
||||
"эскроу не моделируется точно. Проценты — реальная стоимость " +
|
||||
"(чистая прибыль после финансирования ниже прибыли без него)."
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
|
@ -436,6 +442,29 @@ function FinancialCascadeTable({ financial }: { financial: FinancialModel }) {
|
|||
label="Окупаемость (PBP)"
|
||||
value={formatPayback(financial.payback_months)}
|
||||
/>
|
||||
{/* #1881 PR-5 — финансирование (кредит). Показываем только когда движок
|
||||
смоделировал финансирование (financing_enabled). */}
|
||||
{financial.financing_enabled ? (
|
||||
<>
|
||||
<CascadeRow
|
||||
label="Пиковый долг"
|
||||
value={formatMoneyCompact(financial.peak_debt_rub)}
|
||||
/>
|
||||
<CascadeRow
|
||||
label={`Проценты по кредиту (ставка ${formatPct(
|
||||
financial.annual_rate_used,
|
||||
)} годовых)`}
|
||||
value={formatMoneyCompact(financial.total_interest_rub)}
|
||||
/>
|
||||
<CascadeRow
|
||||
label="Чистая прибыль после финансирования"
|
||||
value={formatMoneyCompact(
|
||||
financial.net_profit_after_financing_rub,
|
||||
)}
|
||||
emphasis
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -129,6 +129,13 @@ function financialFixture(): ParcelFinancialEstimate {
|
|||
apartments_count: 520,
|
||||
},
|
||||
caveat: "Расчёт по максимальной застройке НСПД, а не реальной концепции.",
|
||||
// #1881 PR-5 — финансирование (кредит).
|
||||
financing_enabled: true,
|
||||
annual_rate_used: 0.18,
|
||||
peak_debt_rub: 1_800_000_000,
|
||||
total_interest_rub: 320_000_000,
|
||||
net_profit_after_financing_rub: 780_000_000,
|
||||
financing_is_simplified: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -182,9 +189,39 @@ describe("ptica-adapt finance (#1881 PR-4)", () => {
|
|||
const fin = financialFixture();
|
||||
const d = adaptFinanceDrawer(fin);
|
||||
expect(d.rows.every((r) => r.field.isReal === true)).toBe(true);
|
||||
expect(d.summary).toBe(fin.caveat);
|
||||
const keys = d.rows.map((r) => r.k);
|
||||
expect(keys).toContain("IRR");
|
||||
expect(keys).toContain("ТЭП · квартир");
|
||||
});
|
||||
|
||||
// #1881 PR-5 — финансирование (кредит)
|
||||
it("adaptFinanceDrawer: financing_enabled → debt/interest/net-after rows + caption", () => {
|
||||
const fin = financialFixture(); // financing_enabled: true
|
||||
const d = adaptFinanceDrawer(fin);
|
||||
const keys = d.rows.map((r) => r.k);
|
||||
expect(keys).toContain("Пиковый долг");
|
||||
expect(keys).toContain("Проценты по кредиту");
|
||||
expect(keys).toContain("Чистая прибыль после финанс.");
|
||||
const interest = d.rows.find((r) => r.k === "Проценты по кредиту");
|
||||
expect(interest?.field.isReal).toBe(true);
|
||||
expect(interest?.field.caption).toContain("18");
|
||||
expect(interest?.field.caption).toContain("годовых");
|
||||
});
|
||||
|
||||
it("adaptFinanceDrawer: financing_is_simplified → caveat appended to summary", () => {
|
||||
const d = adaptFinanceDrawer(financialFixture());
|
||||
expect(d.summary).toContain("упрощен");
|
||||
expect(d.summary).toContain("эскроу");
|
||||
});
|
||||
|
||||
it("adaptFinanceDrawer: financing_enabled=false → no financing rows", () => {
|
||||
const d = adaptFinanceDrawer({
|
||||
...financialFixture(),
|
||||
financing_enabled: false,
|
||||
});
|
||||
const keys = d.rows.map((r) => r.k);
|
||||
expect(keys).not.toContain("Пиковый долг");
|
||||
expect(keys).not.toContain("Проценты по кредиту");
|
||||
expect(keys).not.toContain("Чистая прибыль после финанс.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1725,9 +1725,46 @@ export function adaptFinanceDrawer(
|
|||
)} мес)`
|
||||
: "не окупается в горизонте модели";
|
||||
|
||||
// #1881 PR-5 — финансирование (кредит). Показываем строки только когда
|
||||
// financing_enabled; проценты — реальная стоимость (чистая прибыль после
|
||||
// финансирования < чистой прибыли до). financing_is_simplified → честный
|
||||
// caveat в summary, чтобы финдиректор не принял упрощение за точную модель.
|
||||
const financingRows: DrawerKvRow[] = financial.financing_enabled
|
||||
? [
|
||||
{
|
||||
k: "Пиковый долг",
|
||||
field: real(`${formatBln(financial.peak_debt_rub)} млрд ₽`),
|
||||
},
|
||||
{
|
||||
k: "Проценты по кредиту",
|
||||
field: {
|
||||
value: `${formatBln(financial.total_interest_rub)} млрд ₽`,
|
||||
isReal: true,
|
||||
caption: `ставка ${formatPctFromFraction(
|
||||
financial.annual_rate_used,
|
||||
)} % годовых`,
|
||||
},
|
||||
},
|
||||
{
|
||||
k: "Чистая прибыль после финанс.",
|
||||
field: real(
|
||||
`${formatBln(financial.net_profit_after_financing_rub)} млрд ₽`,
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const summary =
|
||||
financial.financing_enabled && financial.financing_is_simplified
|
||||
? `${financial.caveat} Финансирование упрощено: весь кассовый разрыв ` +
|
||||
"покрыт кредитом по ставке-нормативу, проценты капитализируются, эскроу " +
|
||||
"не моделируется точно."
|
||||
: financial.caveat;
|
||||
|
||||
return {
|
||||
summary: financial.caveat,
|
||||
summary,
|
||||
rows: [
|
||||
...financingRows,
|
||||
{ k: "GDV (выручка)", field: real(`${formatBln(financial.revenue_rub)} млрд ₽`) },
|
||||
{
|
||||
k: "Совокупные затраты",
|
||||
|
|
|
|||
|
|
@ -3569,6 +3569,36 @@ export interface components {
|
|||
* @default true
|
||||
*/
|
||||
schedule_is_default: boolean;
|
||||
/**
|
||||
* Financing Enabled
|
||||
* @default false
|
||||
*/
|
||||
financing_enabled: boolean;
|
||||
/**
|
||||
* Annual Rate Used
|
||||
* @default 0
|
||||
*/
|
||||
annual_rate_used: number;
|
||||
/**
|
||||
* Peak Debt Rub
|
||||
* @default 0
|
||||
*/
|
||||
peak_debt_rub: number;
|
||||
/**
|
||||
* Total Interest Rub
|
||||
* @default 0
|
||||
*/
|
||||
total_interest_rub: number;
|
||||
/**
|
||||
* Net Profit After Financing Rub
|
||||
* @default 0
|
||||
*/
|
||||
net_profit_after_financing_rub: number;
|
||||
/**
|
||||
* Financing Is Simplified
|
||||
* @default true
|
||||
*/
|
||||
financing_is_simplified: boolean;
|
||||
/** Price Per Sqm Used */
|
||||
price_per_sqm_used: number;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -91,6 +91,22 @@ export interface FinancialModel {
|
|||
price_is_calibrated: boolean;
|
||||
/** "objective_district_median" | "district_reference" | "class_norm". */
|
||||
price_source: PriceSource;
|
||||
// Financing / кредит (PR-5, эпик #1881)
|
||||
/** false → финансирование не моделировалось (строки кредита не показываем). */
|
||||
financing_enabled: boolean;
|
||||
/** Годовая ставка кредита, доля (0.15 = 15% годовых). */
|
||||
annual_rate_used: number;
|
||||
/** Пиковый долг по телу кредита, ₽. */
|
||||
peak_debt_rub: number;
|
||||
/** Суммарные проценты по кредиту за проект, ₽. */
|
||||
total_interest_rub: number;
|
||||
/** Чистая прибыль за вычетом процентов по кредиту, ₽. */
|
||||
net_profit_after_financing_rub: number;
|
||||
/**
|
||||
* true — упрощённая модель: весь кассовый разрыв покрыт кредитом по
|
||||
* ставке-нормативу, проценты капитализируются, эскроу не моделируется точно.
|
||||
*/
|
||||
financing_is_simplified: boolean;
|
||||
}
|
||||
|
||||
export type PriceSource =
|
||||
|
|
|
|||
|
|
@ -525,6 +525,19 @@ export interface ParcelFinancialEstimate {
|
|||
apartments_count: number;
|
||||
};
|
||||
caveat: string;
|
||||
// #1881 PR-5 — финансирование (кредит) в финмодели. financing_enabled=false →
|
||||
// финансирование не моделировалось (UI не показывает строки кредита).
|
||||
// annual_rate_used — годовая ставка кредита (доля 0..1). peak_debt_rub —
|
||||
// пиковый долг по телу кредита, ₽. total_interest_rub — суммарные проценты, ₽.
|
||||
// net_profit_after_financing_rub — чистая прибыль за вычетом процентов, ₽.
|
||||
// financing_is_simplified=true → упрощённая модель (100% покрытие кассового
|
||||
// разрыва, проценты капитализируются, эскроу не моделируется точно) — caveat.
|
||||
financing_enabled: boolean;
|
||||
annual_rate_used: number;
|
||||
peak_debt_rub: number;
|
||||
total_interest_rub: number;
|
||||
net_profit_after_financing_rub: number;
|
||||
financing_is_simplified: boolean;
|
||||
}
|
||||
|
||||
export type PoiCategory =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue