gl29/kervent-projections
0
1# test_entity_simulation.py2import json3from pathlib import Path4 5import numpy as np6 7from core import GlobalConfig, SimContext, run_entity_simulation8from ledger import (9 BaseLedgerConfig,10 CapExItem,11 CapExLedger,12 DebtServiceModel,13 OpCoEntity,14 OpExLedger,15 PersonEntity,16 Phase,17 RevenueModel,18 RevenueStream,19 SCIEntity,20 TaxModel,21 TaxTables,22)23 24 25def _ctx(max_t: int = 12, vat_franchise_active: bool = True) -> SimContext:26 return SimContext(27 max_t=max_t,28 calendar_months=np.arange(max_t) % 12,29 global_cfg=GlobalConfig(30 inflation_rate=0.0,31 tax_tables=TaxTables(),32 vat_franchise_active=vat_franchise_active,33 ),34 )35 36 37def test_defaults_profile_runs_smoke() -> None:38 defaults_path = Path(__file__).resolve().parents[1] / "defaults.json"39 defaults = json.loads(defaults_path.read_text())40 ctx = SimContext(41 max_t=24,42 calendar_months=np.arange(24) % 12,43 global_cfg=GlobalConfig(44 inflation_rate=defaults["global"].get("inflation_rate", 0.0),45 tax_tables=TaxTables.from_dict(defaults["global"].get("tax_tables", {})),46 vat_franchise_active=defaults["global"].get("vat_franchise_active", True),47 ),48 )49 base_configs = {50 mid: BaseLedgerConfig.from_dict(model["config"])51 for mid, model in defaults["models"].items()52 }53 phases = [Phase.from_dict(data) for data in defaults["phases"]]54 result = run_entity_simulation(55 ctx,56 phases,57 base_configs,58 SCIEntity.from_dict(defaults["entities"]["sci"]),59 OpCoEntity.from_dict(defaults["entities"]["opco"]),60 [PersonEntity.from_dict(data) for data in defaults["entities"]["persons"]],61 )62 63 assert len(result.consolidated_treasury) == 2464 assert len(result.sci_treasury) == 2465 assert len(result.opco_treasury) == 2466 assert result.person_treasuries67 68 69def test_sci_ir_tax_is_allocated_to_people_not_sci_cash() -> None:70 ctx = _ctx()71 model = BaseLedgerConfig(72 revenue=RevenueModel(73 streams=[74 RevenueStream(75 name="rentable",76 price=10000.0,77 base_occ=1.0,78 peak_occ=1.0,79 occ_curve_type="flat",80 units=1,81 )82 ]83 ),84 capex=CapExLedger(),85 opex=OpExLedger(),86 tax=TaxModel(),87 )88 phase = Phase(89 id="phase_1",90 name="Launch",91 trigger={"type": "immediate"},92 new_loan=DebtServiceModel(),93 active_models=["model"],94 model_configs={},95 )96 sci = SCIEntity(97 tax_regime="ir",98 monthly_rent=5000.0,99 dividend_reserve=1_000_000_000.0,100 initial_capital=100_000.0,101 )102 person = PersonEntity(103 name="Family",104 ownership_share=1.0,105 living_expenses=OpExLedger(),106 )107 108 result = run_entity_simulation(109 ctx,110 [phase],111 {"model": model},112 sci,113 OpCoEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),114 [person],115 )116 117 assert result.annual_sci_tax == 0.0118 assert result.annual_person_tax["Family"] > 0.0119 # sci ends with initial + 12 * 5000 = 100000 + 60000120 assert result.sci_treasury[-1] == 160000.0121 122 123def test_loan_entity_sci_moves_capex_and_depreciation_to_sci() -> None:124 ctx = _ctx()125 model = BaseLedgerConfig(126 revenue=RevenueModel(),127 capex=CapExLedger(128 items=[CapExItem(name="equip", amount=10000.0, useful_life_years=5)]129 ),130 opex=OpExLedger(),131 tax=TaxModel(),132 )133 134 sci_result = run_entity_simulation(135 ctx,136 [137 Phase(138 id="phase_1",139 name="Launch",140 trigger={"type": "immediate"},141 new_loan=DebtServiceModel(bank_pct=0.8, interest=0.05, loan_years=5),142 active_models=["model"],143 model_configs={},144 loan_entity="SCI",145 )146 ],147 {"model": model},148 SCIEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),149 OpCoEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),150 [],151 )152 opco_result = run_entity_simulation(153 ctx,154 [155 Phase(156 id="phase_1",157 name="Launch",158 trigger={"type": "immediate"},159 new_loan=DebtServiceModel(bank_pct=0.8, interest=0.05, loan_years=5),160 active_models=["model"],161 model_configs={},162 loan_entity="OpCo",163 )164 ],165 {"model": model},166 SCIEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),167 OpCoEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),168 [],169 )170 171 assert sci_result.opco_capex[0] == 0.0172 assert sci_result.sci_treasury[0] < 100_000.0173 assert float(np.sum(sci_result.sci_debt[:12])) > 0.0174 assert float(np.sum(sci_result.sci_depreciation[:12])) > 0.0175 assert float(np.sum(sci_result.opco_loan_payments[:12])) == 0.0176 177 assert opco_result.opco_capex[0] > 0.0178 assert float(np.sum(opco_result.opco_loan_payments[:12])) > 0.0179 assert float(np.sum(opco_result.sci_depreciation[:12])) == 0.0180 181 182def test_dscr_counts_opco_debt_service() -> None:183 ctx = _ctx()184 model = BaseLedgerConfig(185 revenue=RevenueModel(186 streams=[187 RevenueStream(188 name="rev",189 price=2000.0,190 base_occ=1.0,191 peak_occ=1.0,192 occ_curve_type="flat",193 units=1,194 )195 ]196 ),197 capex=CapExLedger(198 items=[CapExItem(name="equip", amount=12000.0, useful_life_years=5)]199 ),200 opex=OpExLedger(),201 tax=TaxModel(),202 )203 result = run_entity_simulation(204 ctx,205 [206 Phase(207 id="phase_1",208 name="Launch",209 trigger={"type": "immediate"},210 new_loan=DebtServiceModel(bank_pct=1.0, interest=0.12, loan_years=1),211 active_models=["model"],212 model_configs={},213 loan_entity="OpCo",214 )215 ],216 {"model": model},217 SCIEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),218 OpCoEntity(dividend_reserve=1_000_000_000.0, initial_capital=100_000.0),219 [],220 )221 222 assert float(np.sum(result.opco_loan_payments[:12])) > 0.0223 assert result.dscr < 99.9224 225 226def test_delayed_baseline_capex_creates_real_sci_cash_outflow() -> None:227 ctx = _ctx(vat_franchise_active=True)228 baseline_capex = CapExLedger(229 items=[230 CapExItem(231 name="delayed", amount=10000.0, start_delay=6, useful_life_years=5232 )233 ]234 )235 sci = SCIEntity(dividend_reserve=1_000_000_000.0)236 sci.capex = baseline_capex237 result = run_entity_simulation(238 ctx,239 [240 Phase(241 id="phase_1",242 name="Launch",243 trigger={"type": "immediate"},244 new_loan=DebtServiceModel(bank_pct=0.8, interest=0.05, loan_years=5),245 active_models=[],246 model_configs={},247 )248 ],249 {},250 sci,251 OpCoEntity(dividend_reserve=1_000_000_000.0),252 [],253 )254 255 assert result.sci_treasury[5] == 0.0256 assert result.sci_treasury[6] < 0.0257 assert result.sci_debt[6] > 0.0258 259 260def test_external_income_increases_person_cash_before_tax() -> None:261 ctx = _ctx()262 person = PersonEntity(263 name="Family",264 external_monthly_income=1000.0,265 living_expenses=OpExLedger(),266 )267 result = run_entity_simulation(268 ctx,269 [270 Phase(271 id="phase_1",272 name="Launch",273 trigger={"type": "immediate"},274 new_loan=DebtServiceModel(),275 active_models=[],276 model_configs={},277 )278 ],279 {},280 SCIEntity(dividend_reserve=1_000_000_000.0),281 OpCoEntity(dividend_reserve=1_000_000_000.0),282 [person],283 )284 285 assert result.person_treasuries["Family"][-1] > 11000.0286 assert result.annual_person_tax["Family"] > 0.0287 