@uhero/feasim 0.1.1 → 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/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
- function constructionMonthsByZone(zone_class) {
1505
- if (zone_class === "R-5" || zone_class === "A-1" || zone_class === "AMX-1" || zone_class === "low_rise")
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, affordable_units, grant_type, lot_sqft, total_months, land_rate = DEFAULT_TARGET_RETURN, profit_margin = DEFAULT_PROFIT_MARGIN, grant_amount = GRANT.post_construct) {
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 + post_construction_grant - project_cost - profit;
2016
+ const residual = gdv - selling_costs - project_cost - profit;
2021
2017
  const land_carry = (1 + land_rate) ** (total_months / 12);
2022
- const rlv = residual / land_carry + pre_construction_subsidy;
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
- post_construction_grant,
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: Math.round(land_carry * 1e4) / 1e4,
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, closing_grant = 0) {
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
- rlv_post_grant: 0,
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
- cf[0] = -equity_amount;
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 ?? permittingMonthsByZone(zone_class);
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 = resolveConstMonths(zone_class, sliders.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
- 0,
2621
- // no affordable_units for grants in bill6
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
- rlv_post_grant: rlv.post_construction_grant,
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 : GRANT.post_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 ?? (has_zone ? permittingMonthsByZone(zone_class) : height_limit <= 30 ? Math.round(425 / 30) : Math.round(535 / 30));
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 = has_zone ? resolveConstMonths(zone_class, sliders.const_months) : sliders.const_months ?? 12;
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 hard_cost = hard_cost_base * cost_inflation_multiplier;
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 closing_grant = grant_type === "pre_construct" ? pre_construction_subsidy : 0;
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
- hard_cost,
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, closing_grant) : deal_ltc;
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
- affordable_units,
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
- let post_grant = 0;
2983
- if (grant_type === "post_construct")
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
- cashflows[total_months] = equity_out;
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 equity_multiple = equity_out / total_equity_invested;
2998
- const roi = (equity_out - total_equity_invested) / total_equity_invested;
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(equity_out),
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
- let post_grant = 0;
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 = hard_cost;
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
- cf[0] = -equity_amount;
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
- let levered_cf = current_noi - perm.annual_debt_svc;
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
- rlv_post_grant: rlv.post_construction_grant,
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 = resolveConstMonths(zone_class, sliders.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
- 0,
3413
- // no per-unit grant
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
- cf[0] = -equity_amount;
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
- rlv_post_grant: 0,
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
- cf[0] = -equity_amount;
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 ?? permittingMonthsByZone(zone_class);
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 = resolveConstMonths(zone_class, sliders.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
- 0,
4073
- // no affordable_units for grants in Standard
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
- rlv_post_grant: rlv.post_construction_grant,
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 ?? 18;
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 = resolveConstMonths(zone_class, sliders.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
- 0,
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
- cf[0] = -equity_amount;
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
- rlv_post_grant: rlv.post_construction_grant,
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.1.1",
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",