@uhero/feasim 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/dist/index.cjs +157 -111
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +157 -111
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -169,8 +169,27 @@ var COMMERCIAL_COVERAGE = {
|
|
|
169
169
|
};
|
|
170
170
|
var GRANT = {
|
|
171
171
|
pre_construct: 5e4,
|
|
172
|
-
post_construct: 12e3
|
|
172
|
+
post_construct: 12e3,
|
|
173
|
+
small_unit_sqft: 300,
|
|
174
|
+
small_unit_per_sqft: 40
|
|
173
175
|
};
|
|
176
|
+
var PREVAILING_WAGE = {
|
|
177
|
+
// on-site labor share of hard cost (2022 economic census, DBEDT data book
|
|
178
|
+
// tables 21.05-21.06: 22-27%)
|
|
179
|
+
onsite_labor_share: 0.25,
|
|
180
|
+
// prevailing wage premium on that labor (trey's memo; test 10% and 20%)
|
|
181
|
+
labor_premium: 0.15
|
|
182
|
+
};
|
|
183
|
+
function postGrantPerUnit(unit_sqft) {
|
|
184
|
+
return unit_sqft < GRANT.small_unit_sqft ? unit_sqft * GRANT.small_unit_per_sqft : GRANT.post_construct;
|
|
185
|
+
}
|
|
186
|
+
function mixPostGrant(mix_key) {
|
|
187
|
+
const mix = UNIT_MIX[mix_key] ?? UNIT_MIX["one_bb_100"];
|
|
188
|
+
return mix.reduce(
|
|
189
|
+
(sum, m) => sum + postGrantPerUnit(UNIT_SIZE[m.type] ?? 500) * m.weight,
|
|
190
|
+
0
|
|
191
|
+
);
|
|
192
|
+
}
|
|
174
193
|
var PARKING = {
|
|
175
194
|
// default stalls per dwelling unit
|
|
176
195
|
stalls_per_unit: 0,
|
|
@@ -1501,29 +1520,8 @@ function waikikiOpenSpaceFrac(zone_class, far) {
|
|
|
1501
1520
|
function envelopeCoverage(zone_class, far, coverage) {
|
|
1502
1521
|
return Math.min(coverage, 1 - waikikiOpenSpaceFrac(zone_class, far));
|
|
1503
1522
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
return 18;
|
|
1507
|
-
if (zone_class === "A-2" || zone_class === "AMX-2" || zone_class === "B-2" || zone_class === "mid_rise")
|
|
1508
|
-
return 24;
|
|
1509
|
-
if (zone_class === "A-3" || zone_class === "AMX-3" || zone_class === "BMX-3" || zone_class === "BMX-4" || ZONE_GROUPS.waikiki.includes(zone_class))
|
|
1510
|
-
return 36;
|
|
1511
|
-
return 18;
|
|
1512
|
-
}
|
|
1513
|
-
function resolveConstMonths(zone_class, override) {
|
|
1514
|
-
if (override != null && override > 0) return override;
|
|
1515
|
-
return constructionMonthsByZone(zone_class);
|
|
1516
|
-
}
|
|
1517
|
-
function permittingMonthsByZone(zone_class) {
|
|
1518
|
-
if (zone_class === "A-1" || zone_class === "AMX-1" || zone_class === "low_rise")
|
|
1519
|
-
return Math.round(425 / 30);
|
|
1520
|
-
if (zone_class === "A-2" || zone_class === "AMX-2" || zone_class === "A-3" || zone_class === "AMX-3" || ZONE_GROUPS.waikiki.includes(zone_class) || zone_class === "mid_rise")
|
|
1521
|
-
return Math.round(535 / 30);
|
|
1522
|
-
if (zone_class === "B-2" || zone_class === "BMX-3" || zone_class === "BMX-4")
|
|
1523
|
-
return Math.round(454 / 30);
|
|
1524
|
-
if (zone_class === "R-5") return Math.round(353 / 30);
|
|
1525
|
-
return 14;
|
|
1526
|
-
}
|
|
1523
|
+
var DEFAULT_PERMITTING_MONTHS = 18;
|
|
1524
|
+
var DEFAULT_CONST_MONTHS = 12;
|
|
1527
1525
|
function yardFootprint(lot_sqft, front, rear, side, ratio = 2) {
|
|
1528
1526
|
const width = Math.sqrt(lot_sqft / ratio);
|
|
1529
1527
|
const depth = ratio * width;
|
|
@@ -2011,23 +2009,24 @@ function calcLeaseupCarry(principal, rental_units, sale_units, annual_rate, leas
|
|
|
2011
2009
|
final_principal: running_principal
|
|
2012
2010
|
};
|
|
2013
2011
|
}
|
|
2014
|
-
function calcDevRLV(gdv, dev_cost, total_financing_cost,
|
|
2012
|
+
function calcDevRLV(gdv, dev_cost, total_financing_cost, grant_flows, lot_sqft, total_months, land_rate = DEFAULT_TARGET_RETURN, profit_margin = DEFAULT_PROFIT_MARGIN) {
|
|
2015
2013
|
const selling_costs = gdv * FINANCING.disposition_cost;
|
|
2016
|
-
const post_construction_grant = grant_type === "post_construct" ? affordable_units * grant_amount : 0;
|
|
2017
|
-
const pre_construction_subsidy = grant_type === "pre_construct" ? affordable_units * grant_amount : 0;
|
|
2018
2014
|
const project_cost = dev_cost + total_financing_cost;
|
|
2019
2015
|
const profit = project_cost * profit_margin;
|
|
2020
|
-
const residual = gdv - selling_costs
|
|
2016
|
+
const residual = gdv - selling_costs - project_cost - profit;
|
|
2021
2017
|
const land_carry = (1 + land_rate) ** (total_months / 12);
|
|
2022
|
-
const
|
|
2018
|
+
const grants_pv = grant_flows.reduce(
|
|
2019
|
+
(sum, g, m) => sum + g / (1 + land_rate) ** (m / 12),
|
|
2020
|
+
0
|
|
2021
|
+
);
|
|
2022
|
+
const rlv = residual / land_carry + grants_pv;
|
|
2023
2023
|
const rlv_sqft = rlv / lot_sqft;
|
|
2024
2024
|
return {
|
|
2025
2025
|
selling_costs: Math.round(selling_costs),
|
|
2026
|
-
|
|
2027
|
-
pre_construction_subsidy,
|
|
2026
|
+
grants_pv: Math.round(grants_pv),
|
|
2028
2027
|
profit: Math.round(profit),
|
|
2029
2028
|
project_cost: Math.round(project_cost),
|
|
2030
|
-
land_carry
|
|
2029
|
+
land_carry,
|
|
2031
2030
|
rlv: Math.round(rlv),
|
|
2032
2031
|
rlv_sqft: Math.round(rlv_sqft * 100) / 100
|
|
2033
2032
|
};
|
|
@@ -2053,7 +2052,7 @@ function calcDscrMaxLoan(noi, permanent_rate, amort_years, min_dscr) {
|
|
|
2053
2052
|
const annual_constant = calcAnnualLoanConstant(permanent_rate, amort_years);
|
|
2054
2053
|
return noi > 0 && annual_constant > 0 ? noi / (min_dscr * annual_constant) : 0;
|
|
2055
2054
|
}
|
|
2056
|
-
function sizeConstructionLoan(amount_to_finance, equity_floor, ltc_stabilized_balance, take_out
|
|
2055
|
+
function sizeConstructionLoan(amount_to_finance, equity_floor, ltc_stabilized_balance, take_out) {
|
|
2057
2056
|
const ltc_principal = amount_to_finance * (1 - equity_floor);
|
|
2058
2057
|
const balance_per_dollar = ltc_principal > 0 ? ltc_stabilized_balance / ltc_principal : 0;
|
|
2059
2058
|
const take_out_principal = take_out == null || balance_per_dollar <= 0 ? Infinity : take_out / balance_per_dollar;
|
|
@@ -2062,10 +2061,7 @@ function sizeConstructionLoan(amount_to_finance, equity_floor, ltc_stabilized_ba
|
|
|
2062
2061
|
0,
|
|
2063
2062
|
Math.min(ltc_principal, take_out_principal)
|
|
2064
2063
|
);
|
|
2065
|
-
const equity_amount = Math.max(
|
|
2066
|
-
0,
|
|
2067
|
-
amount_to_finance - loan_principal - closing_grant
|
|
2068
|
-
);
|
|
2064
|
+
const equity_amount = Math.max(0, amount_to_finance - loan_principal);
|
|
2069
2065
|
return {
|
|
2070
2066
|
loan_principal,
|
|
2071
2067
|
equity_amount,
|
|
@@ -2185,7 +2181,8 @@ function zeroOutput(tmk, zone_class, lot_sqft, height_limit = 0) {
|
|
|
2185
2181
|
total_predev_cost: 0,
|
|
2186
2182
|
construction_equity: 0,
|
|
2187
2183
|
rlv_selling_costs: 0,
|
|
2188
|
-
|
|
2184
|
+
rlv_grants_pv: 0,
|
|
2185
|
+
rlv_land_carry: 1,
|
|
2189
2186
|
rlv_profit: 0,
|
|
2190
2187
|
rlv_project_cost: 0,
|
|
2191
2188
|
rlv: 0,
|
|
@@ -2373,7 +2370,7 @@ function bill6DevFlip(equity_amount, loan_balance, gdv, noi, dev_cost, total_fin
|
|
|
2373
2370
|
dev_equity_invested: total_equity_invested
|
|
2374
2371
|
};
|
|
2375
2372
|
}
|
|
2376
|
-
function bill6Hold(equity_amount, loan_balance, gdv, noi, egi, operating_expenses, dev_cost, total_financing_cost, total_predev_cost, hard_cost, total_months, land_cost_sqft, lot_sqft, permanent_rate, hold_years, amort_years, min_dscr, cap_rate, gross_sales) {
|
|
2373
|
+
function bill6Hold(equity_amount, loan_balance, gdv, noi, egi, operating_expenses, dev_cost, total_financing_cost, total_predev_cost, hard_cost, total_months, permitting_months, monthly_predev_spend, land_cost_sqft, lot_sqft, permanent_rate, hold_years, amort_years, min_dscr, cap_rate, gross_sales) {
|
|
2377
2374
|
const land_cost = land_cost_sqft * lot_sqft;
|
|
2378
2375
|
const land_value = land_cost;
|
|
2379
2376
|
const improvement_value = hard_cost;
|
|
@@ -2400,7 +2397,14 @@ function bill6Hold(equity_amount, loan_balance, gdv, noi, egi, operating_expense
|
|
|
2400
2397
|
const hold_months = hold_years * 12;
|
|
2401
2398
|
const total_with_hold = total_months + hold_months;
|
|
2402
2399
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
2403
|
-
|
|
2400
|
+
equityFirstDraws(
|
|
2401
|
+
equity_amount,
|
|
2402
|
+
land_cost,
|
|
2403
|
+
monthly_predev_spend,
|
|
2404
|
+
permitting_months
|
|
2405
|
+
).forEach((d, m) => {
|
|
2406
|
+
cf[m] += d;
|
|
2407
|
+
});
|
|
2404
2408
|
cf[total_months] += excess_sale_cash;
|
|
2405
2409
|
let current_egi = egi, current_opex = operating_expenses;
|
|
2406
2410
|
let assessed_value = land_value + improvement_value;
|
|
@@ -2488,7 +2492,7 @@ function calcbill6Single(sliders) {
|
|
|
2488
2492
|
const impact_fee_per_unit = sliders.impact_fee ?? FEES.impact.per_dwelling;
|
|
2489
2493
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
2490
2494
|
const floor_height = sliders.floor_height ?? BILL6_DEFAULT_FLOOR_HEIGHT;
|
|
2491
|
-
const permitting_months_val = sliders.permitting_months ??
|
|
2495
|
+
const permitting_months_val = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
2492
2496
|
const setback = sliders.setback ?? 10;
|
|
2493
2497
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
2494
2498
|
const parking_ratio = sliders.parking_ratio ?? (sliders.parking_exempt ? 0 : legalParkingRatio(unit_type));
|
|
@@ -2513,7 +2517,7 @@ function calcbill6Single(sliders) {
|
|
|
2513
2517
|
);
|
|
2514
2518
|
if (hard.max_units <= 0)
|
|
2515
2519
|
return zeroOutput("hypothetical", zone_class, lot_sqft, height_limit);
|
|
2516
|
-
const const_months =
|
|
2520
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
2517
2521
|
const cost_inflation_multiplier = FINANCING.cost_inflation ** ((permitting_months_val + const_months / 2) / 12);
|
|
2518
2522
|
const hard_cost = hard.hard_cost * cost_inflation_multiplier;
|
|
2519
2523
|
const soft = calcSoftCosts({
|
|
@@ -2617,10 +2621,8 @@ function calcbill6Single(sliders) {
|
|
|
2617
2621
|
noi_result.gdv,
|
|
2618
2622
|
loan.total_dev_cost,
|
|
2619
2623
|
rlv_financing_cost,
|
|
2620
|
-
|
|
2621
|
-
// no
|
|
2622
|
-
"none",
|
|
2623
|
-
// no grant_type
|
|
2624
|
+
[],
|
|
2625
|
+
// no grants in bill6
|
|
2624
2626
|
lot_sqft,
|
|
2625
2627
|
total_months,
|
|
2626
2628
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -2652,6 +2654,8 @@ function calcbill6Single(sliders) {
|
|
|
2652
2654
|
predev.total_predev_cost,
|
|
2653
2655
|
hard_cost,
|
|
2654
2656
|
total_months,
|
|
2657
|
+
permitting_months_val,
|
|
2658
|
+
predev.monthly_predev_spend,
|
|
2655
2659
|
land_cost,
|
|
2656
2660
|
lot_sqft,
|
|
2657
2661
|
permanent_rate,
|
|
@@ -2728,7 +2732,8 @@ function calcbill6Single(sliders) {
|
|
|
2728
2732
|
cap_rate: noi_result.cap_rate,
|
|
2729
2733
|
gdv: noi_result.gdv,
|
|
2730
2734
|
rlv_selling_costs: rlv.selling_costs,
|
|
2731
|
-
|
|
2735
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
2736
|
+
rlv_land_carry: rlv.land_carry,
|
|
2732
2737
|
rlv_profit: rlv.profit,
|
|
2733
2738
|
rlv_project_cost: rlv.project_cost,
|
|
2734
2739
|
rlv: rlv.rlv,
|
|
@@ -2763,7 +2768,7 @@ function calcBill7Single(sliders) {
|
|
|
2763
2768
|
const min_dscr = sliders.min_dscr ?? 1.15;
|
|
2764
2769
|
const opex_ratio = sliders.opex_ratio ?? 0.19;
|
|
2765
2770
|
const grant_type = sliders.grant_type ?? DEFAULT_GRANT_TYPE;
|
|
2766
|
-
const grant_amount = sliders.grant_amount ?? (grant_type === "pre_construct" ? GRANT.pre_construct :
|
|
2771
|
+
const grant_amount = sliders.grant_amount ?? (grant_type === "pre_construct" ? GRANT.pre_construct : mixPostGrant(unit_type));
|
|
2767
2772
|
const include_retail = sliders.include_retail ?? false;
|
|
2768
2773
|
const retail_rent = sliders.retail_rent ?? 3;
|
|
2769
2774
|
const affordable_ami = sliders.affordable_ami ?? DEFAULT_AFFORDABLE_AMI;
|
|
@@ -2771,7 +2776,7 @@ function calcBill7Single(sliders) {
|
|
|
2771
2776
|
const construction_rate = sliders.construction_rate ?? FINANCING.construction_rate;
|
|
2772
2777
|
const has_zone = sliders.zone_class != null;
|
|
2773
2778
|
const zone_class = sliders.zone_class ?? (height_limit <= 30 ? "low_rise" : "mid_rise");
|
|
2774
|
-
const permitting_months_val = sliders.permitting_months ??
|
|
2779
|
+
const permitting_months_val = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
2775
2780
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
2776
2781
|
const parking_ratio = sliders.parking_ratio ?? 0;
|
|
2777
2782
|
const retail_share = sliders.retail_share ?? DEFAULT_RETAIL_SHARE;
|
|
@@ -2827,9 +2832,22 @@ function calcBill7Single(sliders) {
|
|
|
2827
2832
|
const affordable_units = afd_rent_units;
|
|
2828
2833
|
const parking_cost = env.parking_cost;
|
|
2829
2834
|
const hard_cost_base = building_cost + parking_cost;
|
|
2830
|
-
const const_months_val =
|
|
2835
|
+
const const_months_val = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
2831
2836
|
const cost_inflation_multiplier = FINANCING.cost_inflation ** ((permitting_months_val + const_months_val / 2) / 12);
|
|
2832
|
-
const
|
|
2837
|
+
const open_shop_hard_cost = hard_cost_base * cost_inflation_multiplier;
|
|
2838
|
+
const post_construction_grant = grant_type === "post_construct" ? affordable_units * grant_amount : 0;
|
|
2839
|
+
const is_pre = grant_type === "pre_construct";
|
|
2840
|
+
const pw_premium = is_pre ? sliders.prevailing_wage_premium ?? PREVAILING_WAGE.labor_premium : 0;
|
|
2841
|
+
const onsite_labor = (sliders.onsite_labor_share ?? PREVAILING_WAGE.onsite_labor_share) * open_shop_hard_cost;
|
|
2842
|
+
const pw_premium_cost = onsite_labor * pw_premium;
|
|
2843
|
+
const hard_cost = open_shop_hard_cost + pw_premium_cost;
|
|
2844
|
+
const subsidy_labor_cap = onsite_labor + pw_premium_cost;
|
|
2845
|
+
const subsidy_units = sliders.grant_units === "affordable" ? affordable_units : max_units;
|
|
2846
|
+
const subsidy_requested = is_pre ? subsidy_units * grant_amount : 0;
|
|
2847
|
+
const pre_construction_subsidy = Math.min(
|
|
2848
|
+
subsidy_requested,
|
|
2849
|
+
subsidy_labor_cap
|
|
2850
|
+
);
|
|
2833
2851
|
const living_stories = env.living_stories;
|
|
2834
2852
|
const soft = calcSoftCosts({
|
|
2835
2853
|
hard_cost,
|
|
@@ -2848,20 +2866,15 @@ function calcBill7Single(sliders) {
|
|
|
2848
2866
|
fu_per_unit: mixFixtureUnits(unit_type)
|
|
2849
2867
|
});
|
|
2850
2868
|
const soft_cost = soft.total;
|
|
2851
|
-
const pre_construction_subsidy = grant_type === "pre_construct" ? affordable_units * grant_amount : 0;
|
|
2852
2869
|
const construct_cost = hard_cost + soft_cost;
|
|
2853
2870
|
const hard_cost_contingency = hard_cost * FINANCING.contingency;
|
|
2854
2871
|
const total_dev_cost = hard_cost_contingency + soft_cost;
|
|
2855
2872
|
const amount_to_finance = total_dev_cost;
|
|
2856
2873
|
const land_cost_total = land_cost * lot_sqft;
|
|
2857
|
-
const
|
|
2858
|
-
const financeAt = (land_in_base, loan_principal2, grant = 0) => {
|
|
2874
|
+
const financeAt = (land_in_base, loan_principal2) => {
|
|
2859
2875
|
const base = amount_to_finance + land_in_base;
|
|
2860
2876
|
const loan_fee2 = loan_principal2 * FINANCING.loan_fee;
|
|
2861
|
-
const equity_amount2 = Math.max(
|
|
2862
|
-
0,
|
|
2863
|
-
base + loan_fee2 - loan_principal2 - grant
|
|
2864
|
-
);
|
|
2877
|
+
const equity_amount2 = Math.max(0, base + loan_fee2 - loan_principal2);
|
|
2865
2878
|
const predev2 = calcPredevEquity(
|
|
2866
2879
|
construct_cost,
|
|
2867
2880
|
permitting_months_val,
|
|
@@ -2893,6 +2906,12 @@ function calcBill7Single(sliders) {
|
|
|
2893
2906
|
const ltc_pass = financeAt(0, amount_to_finance * (1 - equity));
|
|
2894
2907
|
const total_carry_months = ltc_pass.leaseup.total_carry_months;
|
|
2895
2908
|
const total_months = permitting_months_val + const_months_val + total_carry_months;
|
|
2909
|
+
const construction_end = permitting_months_val + const_months_val;
|
|
2910
|
+
const grant_flows = new Array(total_months + 1).fill(0);
|
|
2911
|
+
for (let m = permitting_months_val + 1; m <= construction_end; m++)
|
|
2912
|
+
grant_flows[m] += pre_construction_subsidy / const_months_val;
|
|
2913
|
+
grant_flows[construction_end] += post_construction_grant;
|
|
2914
|
+
const total_grants = pre_construction_subsidy + post_construction_grant;
|
|
2896
2915
|
const afd_rent_multiplier = FINANCING.ami_escalation ** ((const_months_val + total_carry_months) / 12);
|
|
2897
2916
|
const mkt_rent_multiplier = FINANCING.ami_escalation ** ((permitting_months_val + const_months_val + total_carry_months) / 12);
|
|
2898
2917
|
const affordable_rent = mixRentByAmi(affordable_ami, unit_type) * afd_rent_multiplier;
|
|
@@ -2917,7 +2936,7 @@ function calcBill7Single(sliders) {
|
|
|
2917
2936
|
const tax_exempt = Number(String(affordable_ami).replace("ami_", "")) <= 80;
|
|
2918
2937
|
const property_tax = calcPropertyTax(
|
|
2919
2938
|
land_cost * lot_sqft,
|
|
2920
|
-
|
|
2939
|
+
open_shop_hard_cost,
|
|
2921
2940
|
tax_exempt
|
|
2922
2941
|
);
|
|
2923
2942
|
const tax = allocatePropertyTax(property_tax, residential_egi, retail_egi);
|
|
@@ -2941,17 +2960,12 @@ function calcBill7Single(sliders) {
|
|
|
2941
2960
|
);
|
|
2942
2961
|
const rlv_financing_cost = (rlv_sizing.dscr_binds ? financeAt(0, rlv_sizing.loan_principal) : ltc_pass).total_financing_cost;
|
|
2943
2962
|
const deal_base = amount_to_finance + land_cost_total;
|
|
2944
|
-
const deal_ltc = financeAt(
|
|
2945
|
-
land_cost_total,
|
|
2946
|
-
deal_base * (1 - equity),
|
|
2947
|
-
closing_grant
|
|
2948
|
-
);
|
|
2963
|
+
const deal_ltc = financeAt(land_cost_total, deal_base * (1 - equity));
|
|
2949
2964
|
const sizing = sizeConstructionLoan(
|
|
2950
2965
|
deal_base,
|
|
2951
2966
|
equity,
|
|
2952
2967
|
deal_ltc.leaseup.final_principal,
|
|
2953
|
-
take_out
|
|
2954
|
-
closing_grant
|
|
2968
|
+
take_out
|
|
2955
2969
|
);
|
|
2956
2970
|
const {
|
|
2957
2971
|
equity_amount,
|
|
@@ -2961,28 +2975,24 @@ function calcBill7Single(sliders) {
|
|
|
2961
2975
|
const_int,
|
|
2962
2976
|
leaseup,
|
|
2963
2977
|
total_financing_cost
|
|
2964
|
-
} = sizing.dscr_binds ? financeAt(land_cost_total, sizing.loan_principal
|
|
2978
|
+
} = sizing.dscr_binds ? financeAt(land_cost_total, sizing.loan_principal) : deal_ltc;
|
|
2965
2979
|
const target_return = sliders.target_return ?? DEFAULT_TARGET_RETURN;
|
|
2966
2980
|
const rlv = calcDevRLV(
|
|
2967
2981
|
gdv,
|
|
2968
2982
|
total_dev_cost,
|
|
2969
2983
|
rlv_financing_cost,
|
|
2970
|
-
|
|
2971
|
-
grant_type,
|
|
2984
|
+
grant_flows,
|
|
2972
2985
|
lot_sqft,
|
|
2973
2986
|
total_months,
|
|
2974
2987
|
target_return,
|
|
2975
|
-
sliders.profit_margin ?? DEFAULT_PROFIT_MARGIN
|
|
2976
|
-
grant_amount
|
|
2988
|
+
sliders.profit_margin ?? DEFAULT_PROFIT_MARGIN
|
|
2977
2989
|
);
|
|
2978
2990
|
const total_equity_invested = equity_amount;
|
|
2979
2991
|
let dev;
|
|
2980
2992
|
if (hold_years === 0) {
|
|
2981
2993
|
const selling_costs = gdv * FINANCING.disposition_cost;
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
post_grant = affordable_units * grant_amount;
|
|
2985
|
-
const equity_out = gdv - selling_costs - leaseup.final_principal + post_grant;
|
|
2994
|
+
const post_grant = post_construction_grant;
|
|
2995
|
+
const equity_out = gdv - selling_costs - leaseup.final_principal;
|
|
2986
2996
|
const cashflows = new Array(total_months + 1).fill(0);
|
|
2987
2997
|
equityFirstDraws(
|
|
2988
2998
|
equity_amount,
|
|
@@ -2992,10 +3002,14 @@ function calcBill7Single(sliders) {
|
|
|
2992
3002
|
).forEach((d, m) => {
|
|
2993
3003
|
cashflows[m] += d;
|
|
2994
3004
|
});
|
|
2995
|
-
|
|
3005
|
+
grant_flows.forEach((g, m) => {
|
|
3006
|
+
cashflows[m] += g;
|
|
3007
|
+
});
|
|
3008
|
+
cashflows[total_months] += equity_out;
|
|
2996
3009
|
const levered_irr = solveIRR(cashflows, true);
|
|
2997
|
-
const
|
|
2998
|
-
const
|
|
3010
|
+
const cash_in = equity_out + total_grants;
|
|
3011
|
+
const equity_multiple = cash_in / total_equity_invested;
|
|
3012
|
+
const roi = (cash_in - total_equity_invested) / total_equity_invested;
|
|
2999
3013
|
const total_dev_cost_full = total_dev_cost + land_cost_total + total_financing_cost;
|
|
3000
3014
|
const profit = gdv - selling_costs - total_dev_cost_full + post_grant + pre_construction_subsidy;
|
|
3001
3015
|
const capital_subsidy = post_grant + pre_construction_subsidy;
|
|
@@ -3016,7 +3030,7 @@ function calcBill7Single(sliders) {
|
|
|
3016
3030
|
dev_roc: Math.round(roc * 1e4) / 1e4,
|
|
3017
3031
|
dev_gdv: gdv,
|
|
3018
3032
|
dev_selling_costs: Math.round(selling_costs),
|
|
3019
|
-
dev_equity_out: Math.round(
|
|
3033
|
+
dev_equity_out: Math.round(cash_in),
|
|
3020
3034
|
dev_loan_payoff: Math.round(leaseup.final_principal),
|
|
3021
3035
|
dev_perm_principal: null,
|
|
3022
3036
|
dev_perm_rate: null,
|
|
@@ -3030,11 +3044,9 @@ function calcBill7Single(sliders) {
|
|
|
3030
3044
|
dev_equity_invested: total_equity_invested
|
|
3031
3045
|
};
|
|
3032
3046
|
} else {
|
|
3033
|
-
|
|
3034
|
-
if (grant_type === "post_construct")
|
|
3035
|
-
post_grant = affordable_units * grant_amount;
|
|
3047
|
+
const post_grant = post_construction_grant;
|
|
3036
3048
|
const land_value = land_cost * lot_sqft;
|
|
3037
|
-
const improvement_value =
|
|
3049
|
+
const improvement_value = open_shop_hard_cost;
|
|
3038
3050
|
const tax_rate = FINANCING.rpt_residential;
|
|
3039
3051
|
const yr1_prop_tax = tax_exempt ? land_value * tax_rate : (land_value + improvement_value) * tax_rate;
|
|
3040
3052
|
const yr1_noi_after_tax = egi - operating_expenses - yr1_prop_tax;
|
|
@@ -3051,7 +3063,17 @@ function calcBill7Single(sliders) {
|
|
|
3051
3063
|
const hold_months = hold_years * 12;
|
|
3052
3064
|
const total_with_hold = total_months + hold_months;
|
|
3053
3065
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
3054
|
-
|
|
3066
|
+
equityFirstDraws(
|
|
3067
|
+
equity_amount,
|
|
3068
|
+
land_cost_total,
|
|
3069
|
+
predev.monthly_predev_spend,
|
|
3070
|
+
permitting_months_val
|
|
3071
|
+
).forEach((d, m) => {
|
|
3072
|
+
cf[m] += d;
|
|
3073
|
+
});
|
|
3074
|
+
grant_flows.forEach((g, m) => {
|
|
3075
|
+
cf[m] += g;
|
|
3076
|
+
});
|
|
3055
3077
|
let current_egi = egi;
|
|
3056
3078
|
let current_opex = operating_expenses;
|
|
3057
3079
|
let assessed_value = land_value + improvement_value;
|
|
@@ -3064,8 +3086,7 @@ function calcBill7Single(sliders) {
|
|
|
3064
3086
|
}
|
|
3065
3087
|
const prop_tax = tax_exempt && yr <= HOLD.tax_exemption_years ? land_value * tax_rate : assessed_value * tax_rate;
|
|
3066
3088
|
const current_noi = current_egi - current_opex - prop_tax;
|
|
3067
|
-
|
|
3068
|
-
if (yr === 1 && post_grant > 0) levered_cf += post_grant;
|
|
3089
|
+
const levered_cf = current_noi - perm.annual_debt_svc;
|
|
3069
3090
|
const monthly_cf = levered_cf / 12;
|
|
3070
3091
|
for (let mo = 1; mo <= 12; mo++) {
|
|
3071
3092
|
cf[total_months + (yr - 1) * 12 + mo] = monthly_cf;
|
|
@@ -3148,6 +3169,9 @@ function calcBill7Single(sliders) {
|
|
|
3148
3169
|
total_financing_cost,
|
|
3149
3170
|
rlv_financing_cost,
|
|
3150
3171
|
pre_construction_subsidy,
|
|
3172
|
+
pw_premium_cost,
|
|
3173
|
+
subsidy_labor_cap: is_pre ? subsidy_labor_cap : 0,
|
|
3174
|
+
subsidy_cap_binds: subsidy_requested > subsidy_labor_cap,
|
|
3151
3175
|
equity_amount,
|
|
3152
3176
|
dev_equity_pct_realized: sizing.equity_pct_realized,
|
|
3153
3177
|
dev_dscr_binds: sizing.dscr_binds,
|
|
@@ -3179,7 +3203,8 @@ function calcBill7Single(sliders) {
|
|
|
3179
3203
|
gdv,
|
|
3180
3204
|
// RLV
|
|
3181
3205
|
rlv_selling_costs: rlv.selling_costs,
|
|
3182
|
-
|
|
3206
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
3207
|
+
rlv_land_carry: rlv.land_carry,
|
|
3183
3208
|
rlv_profit: rlv.profit,
|
|
3184
3209
|
rlv_project_cost: rlv.project_cost,
|
|
3185
3210
|
rlv: rlv.rlv,
|
|
@@ -3230,7 +3255,7 @@ function calcH201Single(sliders) {
|
|
|
3230
3255
|
const permitting_months = sliders.permitting_months ?? H201_DEFAULT_PERMITTING_MO;
|
|
3231
3256
|
const impact_fee_per_unit = sliders.impact_fee ?? 0;
|
|
3232
3257
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
3233
|
-
const const_months =
|
|
3258
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
3234
3259
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
3235
3260
|
const parking_ratio = sliders.parking_ratio ?? (sliders.parking_exempt ? 0 : legalParkingRatio(unit_type));
|
|
3236
3261
|
const retail_share = sliders.retail_share ?? DEFAULT_RETAIL_SHARE;
|
|
@@ -3409,10 +3434,8 @@ function calcH201Single(sliders) {
|
|
|
3409
3434
|
gdv,
|
|
3410
3435
|
dev_cost,
|
|
3411
3436
|
rlv_financing_cost,
|
|
3412
|
-
|
|
3413
|
-
// no
|
|
3414
|
-
"none",
|
|
3415
|
-
// no grant type
|
|
3437
|
+
[],
|
|
3438
|
+
// no grants in 201H
|
|
3416
3439
|
lot_sqft,
|
|
3417
3440
|
total_months,
|
|
3418
3441
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -3420,7 +3443,6 @@ function calcH201Single(sliders) {
|
|
|
3420
3443
|
);
|
|
3421
3444
|
const rlv = {
|
|
3422
3445
|
...rlv_base,
|
|
3423
|
-
post_construction_grant: 0,
|
|
3424
3446
|
rlv: rlv_base.rlv,
|
|
3425
3447
|
rlv_sqft: Math.round(rlv_base.rlv / lot_sqft * 100) / 100
|
|
3426
3448
|
};
|
|
@@ -3504,7 +3526,14 @@ function calcH201Single(sliders) {
|
|
|
3504
3526
|
const hold_months = hold_years * 12;
|
|
3505
3527
|
const total_with_hold = total_months + hold_months;
|
|
3506
3528
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
3507
|
-
|
|
3529
|
+
equityFirstDraws(
|
|
3530
|
+
equity_amount,
|
|
3531
|
+
land_cost_total,
|
|
3532
|
+
predev.monthly_predev_spend,
|
|
3533
|
+
permitting_months
|
|
3534
|
+
).forEach((d, m) => {
|
|
3535
|
+
cf[m] += d;
|
|
3536
|
+
});
|
|
3508
3537
|
cf[total_months] += excess_sale_cash;
|
|
3509
3538
|
let current_egi = egi, current_opex = operating_expenses;
|
|
3510
3539
|
let assessed_value = land_value + improvement_value;
|
|
@@ -3634,7 +3663,8 @@ function calcH201Single(sliders) {
|
|
|
3634
3663
|
cap_rate,
|
|
3635
3664
|
gdv: Math.round(gdv),
|
|
3636
3665
|
rlv_selling_costs: rlv.selling_costs,
|
|
3637
|
-
|
|
3666
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
3667
|
+
rlv_land_carry: rlv.land_carry,
|
|
3638
3668
|
rlv_profit: rlv.profit,
|
|
3639
3669
|
rlv_project_cost: rlv.project_cost,
|
|
3640
3670
|
rlv: rlv.rlv,
|
|
@@ -3825,7 +3855,7 @@ function standardDevFlip(equity_amount, loan_balance, gdv, noi, dev_cost, total_
|
|
|
3825
3855
|
dev_equity_invested: total_equity_invested
|
|
3826
3856
|
};
|
|
3827
3857
|
}
|
|
3828
|
-
function standardHold(equity_amount, loan_balance, gdv, noi, egi, operating_expenses, dev_cost, total_financing_cost, total_predev_cost, hard_cost, total_months, land_cost_sqft, lot_sqft, permanent_rate, hold_years, amort_years, min_dscr, cap_rate, gross_sales) {
|
|
3858
|
+
function standardHold(equity_amount, loan_balance, gdv, noi, egi, operating_expenses, dev_cost, total_financing_cost, total_predev_cost, hard_cost, total_months, permitting_months, monthly_predev_spend, land_cost_sqft, lot_sqft, permanent_rate, hold_years, amort_years, min_dscr, cap_rate, gross_sales) {
|
|
3829
3859
|
const land_cost = land_cost_sqft * lot_sqft;
|
|
3830
3860
|
const land_value = land_cost;
|
|
3831
3861
|
const improvement_value = hard_cost;
|
|
@@ -3852,7 +3882,14 @@ function standardHold(equity_amount, loan_balance, gdv, noi, egi, operating_expe
|
|
|
3852
3882
|
const hold_months = hold_years * 12;
|
|
3853
3883
|
const total_with_hold = total_months + hold_months;
|
|
3854
3884
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
3855
|
-
|
|
3885
|
+
equityFirstDraws(
|
|
3886
|
+
equity_amount,
|
|
3887
|
+
land_cost,
|
|
3888
|
+
monthly_predev_spend,
|
|
3889
|
+
permitting_months
|
|
3890
|
+
).forEach((d, m) => {
|
|
3891
|
+
cf[m] += d;
|
|
3892
|
+
});
|
|
3856
3893
|
cf[total_months] += excess_sale_cash;
|
|
3857
3894
|
let current_egi = egi, current_opex = operating_expenses;
|
|
3858
3895
|
let assessed_value = land_value + improvement_value;
|
|
@@ -3940,7 +3977,7 @@ function calcStandardSingle(sliders) {
|
|
|
3940
3977
|
const impact_fee_per_unit = sliders.impact_fee ?? FEES.impact.per_dwelling;
|
|
3941
3978
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
3942
3979
|
const floor_height = sliders.floor_height ?? STD_DEFAULT_FLOOR_HEIGHT;
|
|
3943
|
-
const permitting_months_val = sliders.permitting_months ??
|
|
3980
|
+
const permitting_months_val = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
3944
3981
|
const setback = sliders.setback ?? 10;
|
|
3945
3982
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
3946
3983
|
const parking_ratio = sliders.parking_ratio ?? (sliders.parking_exempt ? 0 : legalParkingRatio(unit_type));
|
|
@@ -3965,7 +4002,7 @@ function calcStandardSingle(sliders) {
|
|
|
3965
4002
|
);
|
|
3966
4003
|
if (hard.max_units <= 0)
|
|
3967
4004
|
return zeroOutput("hypothetical", zone_class, lot_sqft, height_limit);
|
|
3968
|
-
const const_months =
|
|
4005
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
3969
4006
|
const cost_inflation_multiplier = FINANCING.cost_inflation ** ((permitting_months_val + const_months / 2) / 12);
|
|
3970
4007
|
const hard_cost = hard.hard_cost * cost_inflation_multiplier;
|
|
3971
4008
|
const soft = calcSoftCosts({
|
|
@@ -4069,10 +4106,8 @@ function calcStandardSingle(sliders) {
|
|
|
4069
4106
|
noi_result.gdv,
|
|
4070
4107
|
loan.total_dev_cost,
|
|
4071
4108
|
rlv_financing_cost,
|
|
4072
|
-
|
|
4073
|
-
// no
|
|
4074
|
-
"none",
|
|
4075
|
-
// no grant_type
|
|
4109
|
+
[],
|
|
4110
|
+
// no grants in Standard
|
|
4076
4111
|
lot_sqft,
|
|
4077
4112
|
total_months,
|
|
4078
4113
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -4104,6 +4139,8 @@ function calcStandardSingle(sliders) {
|
|
|
4104
4139
|
predev.total_predev_cost,
|
|
4105
4140
|
hard_cost,
|
|
4106
4141
|
total_months,
|
|
4142
|
+
permitting_months_val,
|
|
4143
|
+
predev.monthly_predev_spend,
|
|
4107
4144
|
land_cost,
|
|
4108
4145
|
lot_sqft,
|
|
4109
4146
|
permanent_rate,
|
|
@@ -4180,7 +4217,8 @@ function calcStandardSingle(sliders) {
|
|
|
4180
4217
|
cap_rate: noi_result.cap_rate,
|
|
4181
4218
|
gdv: noi_result.gdv,
|
|
4182
4219
|
rlv_selling_costs: rlv.selling_costs,
|
|
4183
|
-
|
|
4220
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
4221
|
+
rlv_land_carry: rlv.land_carry,
|
|
4184
4222
|
rlv_profit: rlv.profit,
|
|
4185
4223
|
rlv_project_cost: rlv.project_cost,
|
|
4186
4224
|
rlv: rlv.rlv,
|
|
@@ -4223,11 +4261,11 @@ function calcTodSingle(sliders) {
|
|
|
4223
4261
|
const affordable_ami = sliders.affordable_ami ?? TOD_DEFAULT_AFFORDABLE_AMI;
|
|
4224
4262
|
const market_ami = sliders.market_ami ?? TOD_DEFAULT_MARKET_AMI;
|
|
4225
4263
|
const construction_rate = sliders.construction_rate ?? FINANCING.construction_rate;
|
|
4226
|
-
const permitting_months = sliders.permitting_months ??
|
|
4264
|
+
const permitting_months = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
4227
4265
|
const impact_fee_per_unit = sliders.impact_fee ?? FEES.impact.per_dwelling;
|
|
4228
4266
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
4229
4267
|
const park = sliders.park ?? "in_lieu";
|
|
4230
|
-
const const_months =
|
|
4268
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
4231
4269
|
const opex_ratio = sliders.opex_ratio ?? 0.3;
|
|
4232
4270
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
4233
4271
|
const parking_ratio = sliders.parking_ratio ?? PARKING.stalls_per_unit;
|
|
@@ -4415,8 +4453,7 @@ function calcTodSingle(sliders) {
|
|
|
4415
4453
|
gdv,
|
|
4416
4454
|
dev_cost,
|
|
4417
4455
|
rlv_financing_cost,
|
|
4418
|
-
|
|
4419
|
-
"none",
|
|
4456
|
+
[],
|
|
4420
4457
|
lot_sqft,
|
|
4421
4458
|
total_months,
|
|
4422
4459
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -4502,7 +4539,14 @@ function calcTodSingle(sliders) {
|
|
|
4502
4539
|
const hold_months = hold_years * 12;
|
|
4503
4540
|
const total_with_hold = total_months + hold_months;
|
|
4504
4541
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
4505
|
-
|
|
4542
|
+
equityFirstDraws(
|
|
4543
|
+
equity_amount,
|
|
4544
|
+
land_cost_total,
|
|
4545
|
+
predev.monthly_predev_spend,
|
|
4546
|
+
permitting_months
|
|
4547
|
+
).forEach((d, m) => {
|
|
4548
|
+
cf[m] += d;
|
|
4549
|
+
});
|
|
4506
4550
|
cf[total_months] += excess_sale_cash;
|
|
4507
4551
|
let current_egi = egi, current_opex = operating_expenses;
|
|
4508
4552
|
let assessed_value = land_value + improvement_value;
|
|
@@ -4632,7 +4676,8 @@ function calcTodSingle(sliders) {
|
|
|
4632
4676
|
cap_rate,
|
|
4633
4677
|
gdv: Math.round(gdv),
|
|
4634
4678
|
rlv_selling_costs: rlv.selling_costs,
|
|
4635
|
-
|
|
4679
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
4680
|
+
rlv_land_carry: rlv.land_carry,
|
|
4636
4681
|
rlv_profit: rlv.profit,
|
|
4637
4682
|
rlv_project_cost: rlv.project_cost,
|
|
4638
4683
|
rlv: rlv.rlv,
|
|
@@ -4904,8 +4949,9 @@ var DEFAULT_INPUTS = (() => {
|
|
|
4904
4949
|
retail_rent: 3,
|
|
4905
4950
|
retail_share: 0.1,
|
|
4906
4951
|
building_area_per_unit: 800,
|
|
4952
|
+
// no grant_amount: the model takes it from grant_type (GRANT), so a
|
|
4953
|
+
// pre-construction run gets $50,000 rather than the post-construction $12,000
|
|
4907
4954
|
grant_type: "post_construct",
|
|
4908
|
-
grant_amount: 12e3,
|
|
4909
4955
|
hard_cost_per_sqft: 450,
|
|
4910
4956
|
land_cost: 130,
|
|
4911
4957
|
opex_ratio: 0.19,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uhero/feasim",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "UHERO's development feasibility model for Oahu housing policy pathways",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
|
-
"repository": "github:UHERO/
|
|
27
|
+
"repository": "github:UHERO/feasim",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
30
30
|
"prepare": "npm run build",
|