@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/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 +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ A pathway is a set of rules a project can be built under. Each pathway sets what
|
|
|
47
47
|
| --- | --- | --- |
|
|
48
48
|
| `"bill6"` | Standard (current zoning) | The zone's FAR (as set by Ordinance 26-7), lot coverage and height under the Land Use Ordinance, with the Article 6 parking minimums |
|
|
49
49
|
| `"standard"` | Before Ord. 26-7 | The FAR and lot coverage rules as they stood before Ordinance 26-7, for comparison |
|
|
50
|
-
| `"bill7"` | Bill 7 | An affordable housing pathway in the A, AMX, BMX and Waikiki (Apart, ApartMix) districts, with FAR 4.0 and a 60 ft height cap, construction grants, waived impact fees, and no required parking.
|
|
50
|
+
| `"bill7"` | Bill 7 | An affordable housing pathway in the A, AMX, BMX and Waikiki (Apart, ApartMix) districts, with FAR 4.0 and a 60 ft height cap, construction grants, waived impact fees, and no required parking. Excavation and stepbacks follow the zone |
|
|
51
51
|
| `"tod"` | Transit-oriented development | PD-T and IPD-T permits near rail stations (ROH § 21-9.100): FAR and height bonuses by tier, station area plan limits, no required parking |
|
|
52
52
|
| `"h201"` | HRS § 201H | Exemptions for affordable housing projects, on current (Ord. 26-7) zoning FAR and lot coverage. The defaults make 51% of units affordable at 80% of area median income (AMI), with market units at 120% AMI; set `affordability` and `affordable_ami` to change that. |
|
|
53
53
|
|
|
@@ -91,6 +91,8 @@ calcParcel("bill6", parcel, {
|
|
|
91
91
|
|
|
92
92
|
**Note:** `height_limit` and `max_far` override the pathway's envelope. Bill 7 still caps height at 60 ft. See the `SliderInputs` type for the full list, with units.
|
|
93
93
|
|
|
94
|
+
**Note:** every pathway takes 18 months of permitting and 12 of construction, whatever the zone. Set `permitting_months` and `const_months` to change them.
|
|
95
|
+
|
|
94
96
|
### Input values
|
|
95
97
|
|
|
96
98
|
The model doesn't check its inputs. A value it doesn't recognize falls back to a default or gives an empty result without an error, so use these exactly:
|
|
@@ -98,11 +100,15 @@ The model doesn't check its inputs. A value it doesn't recognize falls back to a
|
|
|
98
100
|
| Input | Use | If it's wrong |
|
|
99
101
|
| --- | --- | --- |
|
|
100
102
|
| `program` | `"bill6"`, `"standard"`, `"bill7"`, `"tod"` or `"h201"`, lowercase | Anything else runs `"bill6"` |
|
|
101
|
-
| `zone_class` | The district code as the Land Use Ordinance writes it, uppercase: `"A-2"`, `"AMX-3"`, `"BMX-4"`, `"B-2"`, `"I-2"`; Waikiki is `"Apart"` or `"ApartMix"` | `"a-2"` or an unknown code has no FAR, so `calcParcel` builds nothing; with your own `max_far` it builds, but without the zone's coverage
|
|
103
|
+
| `zone_class` | The district code as the Land Use Ordinance writes it, uppercase: `"A-2"`, `"AMX-3"`, `"BMX-4"`, `"B-2"`, `"I-2"`; Waikiki is `"Apart"` or `"ApartMix"` | `"a-2"` or an unknown code has no FAR, so `calcParcel` builds nothing; with your own `max_far` it builds, but without the zone's coverage and retail rules |
|
|
102
104
|
| `affordable_ami`, `market_ami` | `"ami_"` plus a bracket from 10 to 140 in steps of 10, such as `"ami_80"` | Other numbers (`"ami_85"`) are scaled from the 50% row and won't match the published HHFDC limits |
|
|
103
105
|
| `unit_type` | `"studio_100"`, `"studio75_1br25"`, `"studio50_1br50"`, `"studio25_1br75"`, `"one_bb_100"`, `"one_br50_2br50"`, `"two_bb_100"` or `"third_each"` | Anything else is treated as all one-bedroom |
|
|
104
106
|
| `affordability` (201H) | `0.51` or more | Below 50% + 1 of units, `calcSingle` throws |
|
|
105
107
|
| `permanent_rate`, `amort_years` | Above `0` | `0` makes the IRR, equity multiple, profit and ROI `NaN` |
|
|
108
|
+
| `grant_type` (Bill 7) | `"post_construct"` (default), `"pre_construct"` or `"none"` | Anything else pays no grant |
|
|
109
|
+
| `grant_amount` (Bill 7) | Dollars per unit. Leave it out for the ordinance amount: $50,000 pre-construction; $12,000 post-construction, or $40/sqft for units under 300 sqft | A value you pass applies to every unit, overriding the small-unit rule |
|
|
110
|
+
| `grant_units` (Bill 7) | `"all"` (default) or `"affordable"`: the units the pre-construction subsidy pays on. Post-construction always pays on affordable units | Anything else pays on all units |
|
|
111
|
+
| `prevailing_wage_premium`, `onsite_labor_share` (Bill 7) | Fractions: the extra cost of on-site labor at prevailing wages (default `0.15`), and on-site labor as a share of hard cost (default `0.25`). Pre-construction runs only | `onsite_labor_share` also caps the subsidy, so setting it too low cuts the subsidy |
|
|
106
112
|
|
|
107
113
|
With `calcSingle` and `"tod"`, pass a `max_far`, or the model can't run the project without the open-space bonus to compare (see Model notes). `calcParcel` always passes one.
|
|
108
114
|
|
|
@@ -116,9 +122,10 @@ Every function returns a `ParcelOutput`. Some of its fields:
|
|
|
116
122
|
| Units | `affordable_units`, `mkt_rent_units`, `afd_rent_units`, `mkt_sale_units`, `afd_sale_units` |
|
|
117
123
|
| Parking | `parking_stalls`, split into `surface_stalls`, `ground_floor_stalls` and `podium_stalls`; `podium_levels`, `loading_spaces`, `bicycle_spaces` |
|
|
118
124
|
| Costs | `hard_cost`, `soft_cost` (itemized in `soft_cost_breakdown`: permits, fees, water charges, construction management), `dev_cost`, `total_financing_cost` |
|
|
125
|
+
| Bill 7 grants | `pre_construction_subsidy`, `dev_post_grant`; pre-construction runs also report `pw_premium_cost` (prevailing wage cost, included in `hard_cost`), `subsidy_labor_cap` and `subsidy_cap_binds` |
|
|
119
126
|
| Income | `egi` (effective gross income), `noi` (net operating income), `gdv` (gross development value) |
|
|
120
|
-
| Returns | `dev_roc` (return on cost), `dev_levered_irr`, `dev_equity_multiple`, `dev_profit`, `dev_dscr` |
|
|
121
|
-
| Land | `rlv` and `rlv_sqft`: the residual land value, or the most a developer could pay for the land and still hit their target return |
|
|
127
|
+
| Returns | `dev_roc` (return on cost: stabilized NOI over development cost net of grants, at face value), `dev_levered_irr`, `dev_equity_multiple`, `dev_profit`, `dev_dscr` |
|
|
128
|
+
| Land | `rlv` and `rlv_sqft`: the residual land value, or the most a developer could pay for the land and still hit their target return. `rlv_grants_pv` (grants discounted to the land purchase) and `rlv_land_carry` (the discount factor to stabilization) split it: `rlv = (gdv - rlv_selling_costs - rlv_project_cost - rlv_profit) / rlv_land_carry + rlv_grants_pv` |
|
|
122
129
|
|
|
123
130
|
**Note:** Rates are fractions, so `0.058` means 5.8% and money is in dollars.
|
|
124
131
|
|
|
@@ -219,6 +226,8 @@ The package also exports the rules behind the model, such as the zoning FAR tabl
|
|
|
219
226
|
- The model estimates whether a typical project could pencil, it does not design a specific building.
|
|
220
227
|
- The open-space FAR bonus is optional
|
|
221
228
|
- Where a zone offers one (B-2, BMX-3, BMX-4), `calcSingle` runs the project with and without it and keeps the one with the higher residual land value, or the bigger building when neither pencils. The bonus costs 1 sqft of public open space per 5 sqft of extra floor area, so it often isn't worth it on lower buildings. `open_space_bonus` on the result is the bonus taken.
|
|
229
|
+
- Bill 7's pre-construction subsidy comes with prevailing wages
|
|
230
|
+
- ROH § 32-5.2(c)(3) lets the subsidy pay only prevailing wages to every on-site worker under HRS Chapter 104. The model counts 25% of hard cost as on-site labor, charges a 15% premium on it (3.75% of hard cost), and caps the subsidy at that labor at prevailing wages. The 25% comes from the 2022 Economic Census for Hawaiʻi (DBEDT State Data Book, Tables 21.05–21.06). The post-construction grant (§ 32-4.2) has no wage condition.
|
|
222
231
|
|
|
223
232
|
The model's logic and data sources are documented here:
|
|
224
233
|
|
package/dist/index.cjs
CHANGED
|
@@ -309,8 +309,27 @@ var COMMERCIAL_COVERAGE = {
|
|
|
309
309
|
};
|
|
310
310
|
var GRANT = {
|
|
311
311
|
pre_construct: 5e4,
|
|
312
|
-
post_construct: 12e3
|
|
312
|
+
post_construct: 12e3,
|
|
313
|
+
small_unit_sqft: 300,
|
|
314
|
+
small_unit_per_sqft: 40
|
|
313
315
|
};
|
|
316
|
+
var PREVAILING_WAGE = {
|
|
317
|
+
// on-site labor share of hard cost (2022 economic census, DBEDT data book
|
|
318
|
+
// tables 21.05-21.06: 22-27%)
|
|
319
|
+
onsite_labor_share: 0.25,
|
|
320
|
+
// prevailing wage premium on that labor (trey's memo; test 10% and 20%)
|
|
321
|
+
labor_premium: 0.15
|
|
322
|
+
};
|
|
323
|
+
function postGrantPerUnit(unit_sqft) {
|
|
324
|
+
return unit_sqft < GRANT.small_unit_sqft ? unit_sqft * GRANT.small_unit_per_sqft : GRANT.post_construct;
|
|
325
|
+
}
|
|
326
|
+
function mixPostGrant(mix_key) {
|
|
327
|
+
const mix = UNIT_MIX[mix_key] ?? UNIT_MIX["one_bb_100"];
|
|
328
|
+
return mix.reduce(
|
|
329
|
+
(sum, m) => sum + postGrantPerUnit(UNIT_SIZE[m.type] ?? 500) * m.weight,
|
|
330
|
+
0
|
|
331
|
+
);
|
|
332
|
+
}
|
|
314
333
|
var PARKING = {
|
|
315
334
|
// default stalls per dwelling unit
|
|
316
335
|
stalls_per_unit: 0,
|
|
@@ -1641,29 +1660,8 @@ function waikikiOpenSpaceFrac(zone_class, far) {
|
|
|
1641
1660
|
function envelopeCoverage(zone_class, far, coverage) {
|
|
1642
1661
|
return Math.min(coverage, 1 - waikikiOpenSpaceFrac(zone_class, far));
|
|
1643
1662
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
return 18;
|
|
1647
|
-
if (zone_class === "A-2" || zone_class === "AMX-2" || zone_class === "B-2" || zone_class === "mid_rise")
|
|
1648
|
-
return 24;
|
|
1649
|
-
if (zone_class === "A-3" || zone_class === "AMX-3" || zone_class === "BMX-3" || zone_class === "BMX-4" || ZONE_GROUPS.waikiki.includes(zone_class))
|
|
1650
|
-
return 36;
|
|
1651
|
-
return 18;
|
|
1652
|
-
}
|
|
1653
|
-
function resolveConstMonths(zone_class, override) {
|
|
1654
|
-
if (override != null && override > 0) return override;
|
|
1655
|
-
return constructionMonthsByZone(zone_class);
|
|
1656
|
-
}
|
|
1657
|
-
function permittingMonthsByZone(zone_class) {
|
|
1658
|
-
if (zone_class === "A-1" || zone_class === "AMX-1" || zone_class === "low_rise")
|
|
1659
|
-
return Math.round(425 / 30);
|
|
1660
|
-
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")
|
|
1661
|
-
return Math.round(535 / 30);
|
|
1662
|
-
if (zone_class === "B-2" || zone_class === "BMX-3" || zone_class === "BMX-4")
|
|
1663
|
-
return Math.round(454 / 30);
|
|
1664
|
-
if (zone_class === "R-5") return Math.round(353 / 30);
|
|
1665
|
-
return 14;
|
|
1666
|
-
}
|
|
1663
|
+
var DEFAULT_PERMITTING_MONTHS = 18;
|
|
1664
|
+
var DEFAULT_CONST_MONTHS = 12;
|
|
1667
1665
|
function yardFootprint(lot_sqft, front, rear, side, ratio = 2) {
|
|
1668
1666
|
const width = Math.sqrt(lot_sqft / ratio);
|
|
1669
1667
|
const depth = ratio * width;
|
|
@@ -2151,23 +2149,24 @@ function calcLeaseupCarry(principal, rental_units, sale_units, annual_rate, leas
|
|
|
2151
2149
|
final_principal: running_principal
|
|
2152
2150
|
};
|
|
2153
2151
|
}
|
|
2154
|
-
function calcDevRLV(gdv, dev_cost, total_financing_cost,
|
|
2152
|
+
function calcDevRLV(gdv, dev_cost, total_financing_cost, grant_flows, lot_sqft, total_months, land_rate = DEFAULT_TARGET_RETURN, profit_margin = DEFAULT_PROFIT_MARGIN) {
|
|
2155
2153
|
const selling_costs = gdv * FINANCING.disposition_cost;
|
|
2156
|
-
const post_construction_grant = grant_type === "post_construct" ? affordable_units * grant_amount : 0;
|
|
2157
|
-
const pre_construction_subsidy = grant_type === "pre_construct" ? affordable_units * grant_amount : 0;
|
|
2158
2154
|
const project_cost = dev_cost + total_financing_cost;
|
|
2159
2155
|
const profit = project_cost * profit_margin;
|
|
2160
|
-
const residual = gdv - selling_costs
|
|
2156
|
+
const residual = gdv - selling_costs - project_cost - profit;
|
|
2161
2157
|
const land_carry = (1 + land_rate) ** (total_months / 12);
|
|
2162
|
-
const
|
|
2158
|
+
const grants_pv = grant_flows.reduce(
|
|
2159
|
+
(sum, g, m) => sum + g / (1 + land_rate) ** (m / 12),
|
|
2160
|
+
0
|
|
2161
|
+
);
|
|
2162
|
+
const rlv = residual / land_carry + grants_pv;
|
|
2163
2163
|
const rlv_sqft = rlv / lot_sqft;
|
|
2164
2164
|
return {
|
|
2165
2165
|
selling_costs: Math.round(selling_costs),
|
|
2166
|
-
|
|
2167
|
-
pre_construction_subsidy,
|
|
2166
|
+
grants_pv: Math.round(grants_pv),
|
|
2168
2167
|
profit: Math.round(profit),
|
|
2169
2168
|
project_cost: Math.round(project_cost),
|
|
2170
|
-
land_carry
|
|
2169
|
+
land_carry,
|
|
2171
2170
|
rlv: Math.round(rlv),
|
|
2172
2171
|
rlv_sqft: Math.round(rlv_sqft * 100) / 100
|
|
2173
2172
|
};
|
|
@@ -2193,7 +2192,7 @@ function calcDscrMaxLoan(noi, permanent_rate, amort_years, min_dscr) {
|
|
|
2193
2192
|
const annual_constant = calcAnnualLoanConstant(permanent_rate, amort_years);
|
|
2194
2193
|
return noi > 0 && annual_constant > 0 ? noi / (min_dscr * annual_constant) : 0;
|
|
2195
2194
|
}
|
|
2196
|
-
function sizeConstructionLoan(amount_to_finance, equity_floor, ltc_stabilized_balance, take_out
|
|
2195
|
+
function sizeConstructionLoan(amount_to_finance, equity_floor, ltc_stabilized_balance, take_out) {
|
|
2197
2196
|
const ltc_principal = amount_to_finance * (1 - equity_floor);
|
|
2198
2197
|
const balance_per_dollar = ltc_principal > 0 ? ltc_stabilized_balance / ltc_principal : 0;
|
|
2199
2198
|
const take_out_principal = take_out == null || balance_per_dollar <= 0 ? Infinity : take_out / balance_per_dollar;
|
|
@@ -2202,10 +2201,7 @@ function sizeConstructionLoan(amount_to_finance, equity_floor, ltc_stabilized_ba
|
|
|
2202
2201
|
0,
|
|
2203
2202
|
Math.min(ltc_principal, take_out_principal)
|
|
2204
2203
|
);
|
|
2205
|
-
const equity_amount = Math.max(
|
|
2206
|
-
0,
|
|
2207
|
-
amount_to_finance - loan_principal - closing_grant
|
|
2208
|
-
);
|
|
2204
|
+
const equity_amount = Math.max(0, amount_to_finance - loan_principal);
|
|
2209
2205
|
return {
|
|
2210
2206
|
loan_principal,
|
|
2211
2207
|
equity_amount,
|
|
@@ -2325,7 +2321,8 @@ function zeroOutput(tmk, zone_class, lot_sqft, height_limit = 0) {
|
|
|
2325
2321
|
total_predev_cost: 0,
|
|
2326
2322
|
construction_equity: 0,
|
|
2327
2323
|
rlv_selling_costs: 0,
|
|
2328
|
-
|
|
2324
|
+
rlv_grants_pv: 0,
|
|
2325
|
+
rlv_land_carry: 1,
|
|
2329
2326
|
rlv_profit: 0,
|
|
2330
2327
|
rlv_project_cost: 0,
|
|
2331
2328
|
rlv: 0,
|
|
@@ -2513,7 +2510,7 @@ function bill6DevFlip(equity_amount, loan_balance, gdv, noi, dev_cost, total_fin
|
|
|
2513
2510
|
dev_equity_invested: total_equity_invested
|
|
2514
2511
|
};
|
|
2515
2512
|
}
|
|
2516
|
-
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) {
|
|
2513
|
+
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) {
|
|
2517
2514
|
const land_cost = land_cost_sqft * lot_sqft;
|
|
2518
2515
|
const land_value = land_cost;
|
|
2519
2516
|
const improvement_value = hard_cost;
|
|
@@ -2540,7 +2537,14 @@ function bill6Hold(equity_amount, loan_balance, gdv, noi, egi, operating_expense
|
|
|
2540
2537
|
const hold_months = hold_years * 12;
|
|
2541
2538
|
const total_with_hold = total_months + hold_months;
|
|
2542
2539
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
2543
|
-
|
|
2540
|
+
equityFirstDraws(
|
|
2541
|
+
equity_amount,
|
|
2542
|
+
land_cost,
|
|
2543
|
+
monthly_predev_spend,
|
|
2544
|
+
permitting_months
|
|
2545
|
+
).forEach((d, m) => {
|
|
2546
|
+
cf[m] += d;
|
|
2547
|
+
});
|
|
2544
2548
|
cf[total_months] += excess_sale_cash;
|
|
2545
2549
|
let current_egi = egi, current_opex = operating_expenses;
|
|
2546
2550
|
let assessed_value = land_value + improvement_value;
|
|
@@ -2628,7 +2632,7 @@ function calcbill6Single(sliders) {
|
|
|
2628
2632
|
const impact_fee_per_unit = sliders.impact_fee ?? FEES.impact.per_dwelling;
|
|
2629
2633
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
2630
2634
|
const floor_height = sliders.floor_height ?? BILL6_DEFAULT_FLOOR_HEIGHT;
|
|
2631
|
-
const permitting_months_val = sliders.permitting_months ??
|
|
2635
|
+
const permitting_months_val = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
2632
2636
|
const setback = sliders.setback ?? 10;
|
|
2633
2637
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
2634
2638
|
const parking_ratio = sliders.parking_ratio ?? (sliders.parking_exempt ? 0 : legalParkingRatio(unit_type));
|
|
@@ -2653,7 +2657,7 @@ function calcbill6Single(sliders) {
|
|
|
2653
2657
|
);
|
|
2654
2658
|
if (hard.max_units <= 0)
|
|
2655
2659
|
return zeroOutput("hypothetical", zone_class, lot_sqft, height_limit);
|
|
2656
|
-
const const_months =
|
|
2660
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
2657
2661
|
const cost_inflation_multiplier = FINANCING.cost_inflation ** ((permitting_months_val + const_months / 2) / 12);
|
|
2658
2662
|
const hard_cost = hard.hard_cost * cost_inflation_multiplier;
|
|
2659
2663
|
const soft = calcSoftCosts({
|
|
@@ -2757,10 +2761,8 @@ function calcbill6Single(sliders) {
|
|
|
2757
2761
|
noi_result.gdv,
|
|
2758
2762
|
loan.total_dev_cost,
|
|
2759
2763
|
rlv_financing_cost,
|
|
2760
|
-
|
|
2761
|
-
// no
|
|
2762
|
-
"none",
|
|
2763
|
-
// no grant_type
|
|
2764
|
+
[],
|
|
2765
|
+
// no grants in bill6
|
|
2764
2766
|
lot_sqft,
|
|
2765
2767
|
total_months,
|
|
2766
2768
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -2792,6 +2794,8 @@ function calcbill6Single(sliders) {
|
|
|
2792
2794
|
predev.total_predev_cost,
|
|
2793
2795
|
hard_cost,
|
|
2794
2796
|
total_months,
|
|
2797
|
+
permitting_months_val,
|
|
2798
|
+
predev.monthly_predev_spend,
|
|
2795
2799
|
land_cost,
|
|
2796
2800
|
lot_sqft,
|
|
2797
2801
|
permanent_rate,
|
|
@@ -2868,7 +2872,8 @@ function calcbill6Single(sliders) {
|
|
|
2868
2872
|
cap_rate: noi_result.cap_rate,
|
|
2869
2873
|
gdv: noi_result.gdv,
|
|
2870
2874
|
rlv_selling_costs: rlv.selling_costs,
|
|
2871
|
-
|
|
2875
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
2876
|
+
rlv_land_carry: rlv.land_carry,
|
|
2872
2877
|
rlv_profit: rlv.profit,
|
|
2873
2878
|
rlv_project_cost: rlv.project_cost,
|
|
2874
2879
|
rlv: rlv.rlv,
|
|
@@ -2903,7 +2908,7 @@ function calcBill7Single(sliders) {
|
|
|
2903
2908
|
const min_dscr = sliders.min_dscr ?? 1.15;
|
|
2904
2909
|
const opex_ratio = sliders.opex_ratio ?? 0.19;
|
|
2905
2910
|
const grant_type = sliders.grant_type ?? DEFAULT_GRANT_TYPE;
|
|
2906
|
-
const grant_amount = sliders.grant_amount ?? (grant_type === "pre_construct" ? GRANT.pre_construct :
|
|
2911
|
+
const grant_amount = sliders.grant_amount ?? (grant_type === "pre_construct" ? GRANT.pre_construct : mixPostGrant(unit_type));
|
|
2907
2912
|
const include_retail = sliders.include_retail ?? false;
|
|
2908
2913
|
const retail_rent = sliders.retail_rent ?? 3;
|
|
2909
2914
|
const affordable_ami = sliders.affordable_ami ?? DEFAULT_AFFORDABLE_AMI;
|
|
@@ -2911,7 +2916,7 @@ function calcBill7Single(sliders) {
|
|
|
2911
2916
|
const construction_rate = sliders.construction_rate ?? FINANCING.construction_rate;
|
|
2912
2917
|
const has_zone = sliders.zone_class != null;
|
|
2913
2918
|
const zone_class = sliders.zone_class ?? (height_limit <= 30 ? "low_rise" : "mid_rise");
|
|
2914
|
-
const permitting_months_val = sliders.permitting_months ??
|
|
2919
|
+
const permitting_months_val = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
2915
2920
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
2916
2921
|
const parking_ratio = sliders.parking_ratio ?? 0;
|
|
2917
2922
|
const retail_share = sliders.retail_share ?? DEFAULT_RETAIL_SHARE;
|
|
@@ -2967,9 +2972,22 @@ function calcBill7Single(sliders) {
|
|
|
2967
2972
|
const affordable_units = afd_rent_units;
|
|
2968
2973
|
const parking_cost = env.parking_cost;
|
|
2969
2974
|
const hard_cost_base = building_cost + parking_cost;
|
|
2970
|
-
const const_months_val =
|
|
2975
|
+
const const_months_val = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
2971
2976
|
const cost_inflation_multiplier = FINANCING.cost_inflation ** ((permitting_months_val + const_months_val / 2) / 12);
|
|
2972
|
-
const
|
|
2977
|
+
const open_shop_hard_cost = hard_cost_base * cost_inflation_multiplier;
|
|
2978
|
+
const post_construction_grant = grant_type === "post_construct" ? affordable_units * grant_amount : 0;
|
|
2979
|
+
const is_pre = grant_type === "pre_construct";
|
|
2980
|
+
const pw_premium = is_pre ? sliders.prevailing_wage_premium ?? PREVAILING_WAGE.labor_premium : 0;
|
|
2981
|
+
const onsite_labor = (sliders.onsite_labor_share ?? PREVAILING_WAGE.onsite_labor_share) * open_shop_hard_cost;
|
|
2982
|
+
const pw_premium_cost = onsite_labor * pw_premium;
|
|
2983
|
+
const hard_cost = open_shop_hard_cost + pw_premium_cost;
|
|
2984
|
+
const subsidy_labor_cap = onsite_labor + pw_premium_cost;
|
|
2985
|
+
const subsidy_units = sliders.grant_units === "affordable" ? affordable_units : max_units;
|
|
2986
|
+
const subsidy_requested = is_pre ? subsidy_units * grant_amount : 0;
|
|
2987
|
+
const pre_construction_subsidy = Math.min(
|
|
2988
|
+
subsidy_requested,
|
|
2989
|
+
subsidy_labor_cap
|
|
2990
|
+
);
|
|
2973
2991
|
const living_stories = env.living_stories;
|
|
2974
2992
|
const soft = calcSoftCosts({
|
|
2975
2993
|
hard_cost,
|
|
@@ -2988,20 +3006,15 @@ function calcBill7Single(sliders) {
|
|
|
2988
3006
|
fu_per_unit: mixFixtureUnits(unit_type)
|
|
2989
3007
|
});
|
|
2990
3008
|
const soft_cost = soft.total;
|
|
2991
|
-
const pre_construction_subsidy = grant_type === "pre_construct" ? affordable_units * grant_amount : 0;
|
|
2992
3009
|
const construct_cost = hard_cost + soft_cost;
|
|
2993
3010
|
const hard_cost_contingency = hard_cost * FINANCING.contingency;
|
|
2994
3011
|
const total_dev_cost = hard_cost_contingency + soft_cost;
|
|
2995
3012
|
const amount_to_finance = total_dev_cost;
|
|
2996
3013
|
const land_cost_total = land_cost * lot_sqft;
|
|
2997
|
-
const
|
|
2998
|
-
const financeAt = (land_in_base, loan_principal2, grant = 0) => {
|
|
3014
|
+
const financeAt = (land_in_base, loan_principal2) => {
|
|
2999
3015
|
const base = amount_to_finance + land_in_base;
|
|
3000
3016
|
const loan_fee2 = loan_principal2 * FINANCING.loan_fee;
|
|
3001
|
-
const equity_amount2 = Math.max(
|
|
3002
|
-
0,
|
|
3003
|
-
base + loan_fee2 - loan_principal2 - grant
|
|
3004
|
-
);
|
|
3017
|
+
const equity_amount2 = Math.max(0, base + loan_fee2 - loan_principal2);
|
|
3005
3018
|
const predev2 = calcPredevEquity(
|
|
3006
3019
|
construct_cost,
|
|
3007
3020
|
permitting_months_val,
|
|
@@ -3033,6 +3046,12 @@ function calcBill7Single(sliders) {
|
|
|
3033
3046
|
const ltc_pass = financeAt(0, amount_to_finance * (1 - equity));
|
|
3034
3047
|
const total_carry_months = ltc_pass.leaseup.total_carry_months;
|
|
3035
3048
|
const total_months = permitting_months_val + const_months_val + total_carry_months;
|
|
3049
|
+
const construction_end = permitting_months_val + const_months_val;
|
|
3050
|
+
const grant_flows = new Array(total_months + 1).fill(0);
|
|
3051
|
+
for (let m = permitting_months_val + 1; m <= construction_end; m++)
|
|
3052
|
+
grant_flows[m] += pre_construction_subsidy / const_months_val;
|
|
3053
|
+
grant_flows[construction_end] += post_construction_grant;
|
|
3054
|
+
const total_grants = pre_construction_subsidy + post_construction_grant;
|
|
3036
3055
|
const afd_rent_multiplier = FINANCING.ami_escalation ** ((const_months_val + total_carry_months) / 12);
|
|
3037
3056
|
const mkt_rent_multiplier = FINANCING.ami_escalation ** ((permitting_months_val + const_months_val + total_carry_months) / 12);
|
|
3038
3057
|
const affordable_rent = mixRentByAmi(affordable_ami, unit_type) * afd_rent_multiplier;
|
|
@@ -3057,7 +3076,7 @@ function calcBill7Single(sliders) {
|
|
|
3057
3076
|
const tax_exempt = Number(String(affordable_ami).replace("ami_", "")) <= 80;
|
|
3058
3077
|
const property_tax = calcPropertyTax(
|
|
3059
3078
|
land_cost * lot_sqft,
|
|
3060
|
-
|
|
3079
|
+
open_shop_hard_cost,
|
|
3061
3080
|
tax_exempt
|
|
3062
3081
|
);
|
|
3063
3082
|
const tax = allocatePropertyTax(property_tax, residential_egi, retail_egi);
|
|
@@ -3081,17 +3100,12 @@ function calcBill7Single(sliders) {
|
|
|
3081
3100
|
);
|
|
3082
3101
|
const rlv_financing_cost = (rlv_sizing.dscr_binds ? financeAt(0, rlv_sizing.loan_principal) : ltc_pass).total_financing_cost;
|
|
3083
3102
|
const deal_base = amount_to_finance + land_cost_total;
|
|
3084
|
-
const deal_ltc = financeAt(
|
|
3085
|
-
land_cost_total,
|
|
3086
|
-
deal_base * (1 - equity),
|
|
3087
|
-
closing_grant
|
|
3088
|
-
);
|
|
3103
|
+
const deal_ltc = financeAt(land_cost_total, deal_base * (1 - equity));
|
|
3089
3104
|
const sizing = sizeConstructionLoan(
|
|
3090
3105
|
deal_base,
|
|
3091
3106
|
equity,
|
|
3092
3107
|
deal_ltc.leaseup.final_principal,
|
|
3093
|
-
take_out
|
|
3094
|
-
closing_grant
|
|
3108
|
+
take_out
|
|
3095
3109
|
);
|
|
3096
3110
|
const {
|
|
3097
3111
|
equity_amount,
|
|
@@ -3101,28 +3115,24 @@ function calcBill7Single(sliders) {
|
|
|
3101
3115
|
const_int,
|
|
3102
3116
|
leaseup,
|
|
3103
3117
|
total_financing_cost
|
|
3104
|
-
} = sizing.dscr_binds ? financeAt(land_cost_total, sizing.loan_principal
|
|
3118
|
+
} = sizing.dscr_binds ? financeAt(land_cost_total, sizing.loan_principal) : deal_ltc;
|
|
3105
3119
|
const target_return = sliders.target_return ?? DEFAULT_TARGET_RETURN;
|
|
3106
3120
|
const rlv = calcDevRLV(
|
|
3107
3121
|
gdv,
|
|
3108
3122
|
total_dev_cost,
|
|
3109
3123
|
rlv_financing_cost,
|
|
3110
|
-
|
|
3111
|
-
grant_type,
|
|
3124
|
+
grant_flows,
|
|
3112
3125
|
lot_sqft,
|
|
3113
3126
|
total_months,
|
|
3114
3127
|
target_return,
|
|
3115
|
-
sliders.profit_margin ?? DEFAULT_PROFIT_MARGIN
|
|
3116
|
-
grant_amount
|
|
3128
|
+
sliders.profit_margin ?? DEFAULT_PROFIT_MARGIN
|
|
3117
3129
|
);
|
|
3118
3130
|
const total_equity_invested = equity_amount;
|
|
3119
3131
|
let dev;
|
|
3120
3132
|
if (hold_years === 0) {
|
|
3121
3133
|
const selling_costs = gdv * FINANCING.disposition_cost;
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
post_grant = affordable_units * grant_amount;
|
|
3125
|
-
const equity_out = gdv - selling_costs - leaseup.final_principal + post_grant;
|
|
3134
|
+
const post_grant = post_construction_grant;
|
|
3135
|
+
const equity_out = gdv - selling_costs - leaseup.final_principal;
|
|
3126
3136
|
const cashflows = new Array(total_months + 1).fill(0);
|
|
3127
3137
|
equityFirstDraws(
|
|
3128
3138
|
equity_amount,
|
|
@@ -3132,10 +3142,14 @@ function calcBill7Single(sliders) {
|
|
|
3132
3142
|
).forEach((d, m) => {
|
|
3133
3143
|
cashflows[m] += d;
|
|
3134
3144
|
});
|
|
3135
|
-
|
|
3145
|
+
grant_flows.forEach((g, m) => {
|
|
3146
|
+
cashflows[m] += g;
|
|
3147
|
+
});
|
|
3148
|
+
cashflows[total_months] += equity_out;
|
|
3136
3149
|
const levered_irr = solveIRR(cashflows, true);
|
|
3137
|
-
const
|
|
3138
|
-
const
|
|
3150
|
+
const cash_in = equity_out + total_grants;
|
|
3151
|
+
const equity_multiple = cash_in / total_equity_invested;
|
|
3152
|
+
const roi = (cash_in - total_equity_invested) / total_equity_invested;
|
|
3139
3153
|
const total_dev_cost_full = total_dev_cost + land_cost_total + total_financing_cost;
|
|
3140
3154
|
const profit = gdv - selling_costs - total_dev_cost_full + post_grant + pre_construction_subsidy;
|
|
3141
3155
|
const capital_subsidy = post_grant + pre_construction_subsidy;
|
|
@@ -3156,7 +3170,7 @@ function calcBill7Single(sliders) {
|
|
|
3156
3170
|
dev_roc: Math.round(roc * 1e4) / 1e4,
|
|
3157
3171
|
dev_gdv: gdv,
|
|
3158
3172
|
dev_selling_costs: Math.round(selling_costs),
|
|
3159
|
-
dev_equity_out: Math.round(
|
|
3173
|
+
dev_equity_out: Math.round(cash_in),
|
|
3160
3174
|
dev_loan_payoff: Math.round(leaseup.final_principal),
|
|
3161
3175
|
dev_perm_principal: null,
|
|
3162
3176
|
dev_perm_rate: null,
|
|
@@ -3170,11 +3184,9 @@ function calcBill7Single(sliders) {
|
|
|
3170
3184
|
dev_equity_invested: total_equity_invested
|
|
3171
3185
|
};
|
|
3172
3186
|
} else {
|
|
3173
|
-
|
|
3174
|
-
if (grant_type === "post_construct")
|
|
3175
|
-
post_grant = affordable_units * grant_amount;
|
|
3187
|
+
const post_grant = post_construction_grant;
|
|
3176
3188
|
const land_value = land_cost * lot_sqft;
|
|
3177
|
-
const improvement_value =
|
|
3189
|
+
const improvement_value = open_shop_hard_cost;
|
|
3178
3190
|
const tax_rate = FINANCING.rpt_residential;
|
|
3179
3191
|
const yr1_prop_tax = tax_exempt ? land_value * tax_rate : (land_value + improvement_value) * tax_rate;
|
|
3180
3192
|
const yr1_noi_after_tax = egi - operating_expenses - yr1_prop_tax;
|
|
@@ -3191,7 +3203,17 @@ function calcBill7Single(sliders) {
|
|
|
3191
3203
|
const hold_months = hold_years * 12;
|
|
3192
3204
|
const total_with_hold = total_months + hold_months;
|
|
3193
3205
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
3194
|
-
|
|
3206
|
+
equityFirstDraws(
|
|
3207
|
+
equity_amount,
|
|
3208
|
+
land_cost_total,
|
|
3209
|
+
predev.monthly_predev_spend,
|
|
3210
|
+
permitting_months_val
|
|
3211
|
+
).forEach((d, m) => {
|
|
3212
|
+
cf[m] += d;
|
|
3213
|
+
});
|
|
3214
|
+
grant_flows.forEach((g, m) => {
|
|
3215
|
+
cf[m] += g;
|
|
3216
|
+
});
|
|
3195
3217
|
let current_egi = egi;
|
|
3196
3218
|
let current_opex = operating_expenses;
|
|
3197
3219
|
let assessed_value = land_value + improvement_value;
|
|
@@ -3204,8 +3226,7 @@ function calcBill7Single(sliders) {
|
|
|
3204
3226
|
}
|
|
3205
3227
|
const prop_tax = tax_exempt && yr <= HOLD.tax_exemption_years ? land_value * tax_rate : assessed_value * tax_rate;
|
|
3206
3228
|
const current_noi = current_egi - current_opex - prop_tax;
|
|
3207
|
-
|
|
3208
|
-
if (yr === 1 && post_grant > 0) levered_cf += post_grant;
|
|
3229
|
+
const levered_cf = current_noi - perm.annual_debt_svc;
|
|
3209
3230
|
const monthly_cf = levered_cf / 12;
|
|
3210
3231
|
for (let mo = 1; mo <= 12; mo++) {
|
|
3211
3232
|
cf[total_months + (yr - 1) * 12 + mo] = monthly_cf;
|
|
@@ -3288,6 +3309,9 @@ function calcBill7Single(sliders) {
|
|
|
3288
3309
|
total_financing_cost,
|
|
3289
3310
|
rlv_financing_cost,
|
|
3290
3311
|
pre_construction_subsidy,
|
|
3312
|
+
pw_premium_cost,
|
|
3313
|
+
subsidy_labor_cap: is_pre ? subsidy_labor_cap : 0,
|
|
3314
|
+
subsidy_cap_binds: subsidy_requested > subsidy_labor_cap,
|
|
3291
3315
|
equity_amount,
|
|
3292
3316
|
dev_equity_pct_realized: sizing.equity_pct_realized,
|
|
3293
3317
|
dev_dscr_binds: sizing.dscr_binds,
|
|
@@ -3319,7 +3343,8 @@ function calcBill7Single(sliders) {
|
|
|
3319
3343
|
gdv,
|
|
3320
3344
|
// RLV
|
|
3321
3345
|
rlv_selling_costs: rlv.selling_costs,
|
|
3322
|
-
|
|
3346
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
3347
|
+
rlv_land_carry: rlv.land_carry,
|
|
3323
3348
|
rlv_profit: rlv.profit,
|
|
3324
3349
|
rlv_project_cost: rlv.project_cost,
|
|
3325
3350
|
rlv: rlv.rlv,
|
|
@@ -3370,7 +3395,7 @@ function calcH201Single(sliders) {
|
|
|
3370
3395
|
const permitting_months = sliders.permitting_months ?? H201_DEFAULT_PERMITTING_MO;
|
|
3371
3396
|
const impact_fee_per_unit = sliders.impact_fee ?? 0;
|
|
3372
3397
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
3373
|
-
const const_months =
|
|
3398
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
3374
3399
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
3375
3400
|
const parking_ratio = sliders.parking_ratio ?? (sliders.parking_exempt ? 0 : legalParkingRatio(unit_type));
|
|
3376
3401
|
const retail_share = sliders.retail_share ?? DEFAULT_RETAIL_SHARE;
|
|
@@ -3549,10 +3574,8 @@ function calcH201Single(sliders) {
|
|
|
3549
3574
|
gdv,
|
|
3550
3575
|
dev_cost,
|
|
3551
3576
|
rlv_financing_cost,
|
|
3552
|
-
|
|
3553
|
-
// no
|
|
3554
|
-
"none",
|
|
3555
|
-
// no grant type
|
|
3577
|
+
[],
|
|
3578
|
+
// no grants in 201H
|
|
3556
3579
|
lot_sqft,
|
|
3557
3580
|
total_months,
|
|
3558
3581
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -3560,7 +3583,6 @@ function calcH201Single(sliders) {
|
|
|
3560
3583
|
);
|
|
3561
3584
|
const rlv = {
|
|
3562
3585
|
...rlv_base,
|
|
3563
|
-
post_construction_grant: 0,
|
|
3564
3586
|
rlv: rlv_base.rlv,
|
|
3565
3587
|
rlv_sqft: Math.round(rlv_base.rlv / lot_sqft * 100) / 100
|
|
3566
3588
|
};
|
|
@@ -3644,7 +3666,14 @@ function calcH201Single(sliders) {
|
|
|
3644
3666
|
const hold_months = hold_years * 12;
|
|
3645
3667
|
const total_with_hold = total_months + hold_months;
|
|
3646
3668
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
3647
|
-
|
|
3669
|
+
equityFirstDraws(
|
|
3670
|
+
equity_amount,
|
|
3671
|
+
land_cost_total,
|
|
3672
|
+
predev.monthly_predev_spend,
|
|
3673
|
+
permitting_months
|
|
3674
|
+
).forEach((d, m) => {
|
|
3675
|
+
cf[m] += d;
|
|
3676
|
+
});
|
|
3648
3677
|
cf[total_months] += excess_sale_cash;
|
|
3649
3678
|
let current_egi = egi, current_opex = operating_expenses;
|
|
3650
3679
|
let assessed_value = land_value + improvement_value;
|
|
@@ -3774,7 +3803,8 @@ function calcH201Single(sliders) {
|
|
|
3774
3803
|
cap_rate,
|
|
3775
3804
|
gdv: Math.round(gdv),
|
|
3776
3805
|
rlv_selling_costs: rlv.selling_costs,
|
|
3777
|
-
|
|
3806
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
3807
|
+
rlv_land_carry: rlv.land_carry,
|
|
3778
3808
|
rlv_profit: rlv.profit,
|
|
3779
3809
|
rlv_project_cost: rlv.project_cost,
|
|
3780
3810
|
rlv: rlv.rlv,
|
|
@@ -3965,7 +3995,7 @@ function standardDevFlip(equity_amount, loan_balance, gdv, noi, dev_cost, total_
|
|
|
3965
3995
|
dev_equity_invested: total_equity_invested
|
|
3966
3996
|
};
|
|
3967
3997
|
}
|
|
3968
|
-
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) {
|
|
3998
|
+
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) {
|
|
3969
3999
|
const land_cost = land_cost_sqft * lot_sqft;
|
|
3970
4000
|
const land_value = land_cost;
|
|
3971
4001
|
const improvement_value = hard_cost;
|
|
@@ -3992,7 +4022,14 @@ function standardHold(equity_amount, loan_balance, gdv, noi, egi, operating_expe
|
|
|
3992
4022
|
const hold_months = hold_years * 12;
|
|
3993
4023
|
const total_with_hold = total_months + hold_months;
|
|
3994
4024
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
3995
|
-
|
|
4025
|
+
equityFirstDraws(
|
|
4026
|
+
equity_amount,
|
|
4027
|
+
land_cost,
|
|
4028
|
+
monthly_predev_spend,
|
|
4029
|
+
permitting_months
|
|
4030
|
+
).forEach((d, m) => {
|
|
4031
|
+
cf[m] += d;
|
|
4032
|
+
});
|
|
3996
4033
|
cf[total_months] += excess_sale_cash;
|
|
3997
4034
|
let current_egi = egi, current_opex = operating_expenses;
|
|
3998
4035
|
let assessed_value = land_value + improvement_value;
|
|
@@ -4080,7 +4117,7 @@ function calcStandardSingle(sliders) {
|
|
|
4080
4117
|
const impact_fee_per_unit = sliders.impact_fee ?? FEES.impact.per_dwelling;
|
|
4081
4118
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
4082
4119
|
const floor_height = sliders.floor_height ?? STD_DEFAULT_FLOOR_HEIGHT;
|
|
4083
|
-
const permitting_months_val = sliders.permitting_months ??
|
|
4120
|
+
const permitting_months_val = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
4084
4121
|
const setback = sliders.setback ?? 10;
|
|
4085
4122
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
4086
4123
|
const parking_ratio = sliders.parking_ratio ?? (sliders.parking_exempt ? 0 : legalParkingRatio(unit_type));
|
|
@@ -4105,7 +4142,7 @@ function calcStandardSingle(sliders) {
|
|
|
4105
4142
|
);
|
|
4106
4143
|
if (hard.max_units <= 0)
|
|
4107
4144
|
return zeroOutput("hypothetical", zone_class, lot_sqft, height_limit);
|
|
4108
|
-
const const_months =
|
|
4145
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
4109
4146
|
const cost_inflation_multiplier = FINANCING.cost_inflation ** ((permitting_months_val + const_months / 2) / 12);
|
|
4110
4147
|
const hard_cost = hard.hard_cost * cost_inflation_multiplier;
|
|
4111
4148
|
const soft = calcSoftCosts({
|
|
@@ -4209,10 +4246,8 @@ function calcStandardSingle(sliders) {
|
|
|
4209
4246
|
noi_result.gdv,
|
|
4210
4247
|
loan.total_dev_cost,
|
|
4211
4248
|
rlv_financing_cost,
|
|
4212
|
-
|
|
4213
|
-
// no
|
|
4214
|
-
"none",
|
|
4215
|
-
// no grant_type
|
|
4249
|
+
[],
|
|
4250
|
+
// no grants in Standard
|
|
4216
4251
|
lot_sqft,
|
|
4217
4252
|
total_months,
|
|
4218
4253
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -4244,6 +4279,8 @@ function calcStandardSingle(sliders) {
|
|
|
4244
4279
|
predev.total_predev_cost,
|
|
4245
4280
|
hard_cost,
|
|
4246
4281
|
total_months,
|
|
4282
|
+
permitting_months_val,
|
|
4283
|
+
predev.monthly_predev_spend,
|
|
4247
4284
|
land_cost,
|
|
4248
4285
|
lot_sqft,
|
|
4249
4286
|
permanent_rate,
|
|
@@ -4320,7 +4357,8 @@ function calcStandardSingle(sliders) {
|
|
|
4320
4357
|
cap_rate: noi_result.cap_rate,
|
|
4321
4358
|
gdv: noi_result.gdv,
|
|
4322
4359
|
rlv_selling_costs: rlv.selling_costs,
|
|
4323
|
-
|
|
4360
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
4361
|
+
rlv_land_carry: rlv.land_carry,
|
|
4324
4362
|
rlv_profit: rlv.profit,
|
|
4325
4363
|
rlv_project_cost: rlv.project_cost,
|
|
4326
4364
|
rlv: rlv.rlv,
|
|
@@ -4363,11 +4401,11 @@ function calcTodSingle(sliders) {
|
|
|
4363
4401
|
const affordable_ami = sliders.affordable_ami ?? TOD_DEFAULT_AFFORDABLE_AMI;
|
|
4364
4402
|
const market_ami = sliders.market_ami ?? TOD_DEFAULT_MARKET_AMI;
|
|
4365
4403
|
const construction_rate = sliders.construction_rate ?? FINANCING.construction_rate;
|
|
4366
|
-
const permitting_months = sliders.permitting_months ??
|
|
4404
|
+
const permitting_months = sliders.permitting_months ?? DEFAULT_PERMITTING_MONTHS;
|
|
4367
4405
|
const impact_fee_per_unit = sliders.impact_fee ?? FEES.impact.per_dwelling;
|
|
4368
4406
|
const waivable_fee_basis = sliders.waivable_fee_basis ?? "market_units";
|
|
4369
4407
|
const park = sliders.park ?? "in_lieu";
|
|
4370
|
-
const const_months =
|
|
4408
|
+
const const_months = sliders.const_months || DEFAULT_CONST_MONTHS;
|
|
4371
4409
|
const opex_ratio = sliders.opex_ratio ?? 0.3;
|
|
4372
4410
|
const hard_cost_per_sqft = sliders.hard_cost_per_sqft ?? 450;
|
|
4373
4411
|
const parking_ratio = sliders.parking_ratio ?? PARKING.stalls_per_unit;
|
|
@@ -4555,8 +4593,7 @@ function calcTodSingle(sliders) {
|
|
|
4555
4593
|
gdv,
|
|
4556
4594
|
dev_cost,
|
|
4557
4595
|
rlv_financing_cost,
|
|
4558
|
-
|
|
4559
|
-
"none",
|
|
4596
|
+
[],
|
|
4560
4597
|
lot_sqft,
|
|
4561
4598
|
total_months,
|
|
4562
4599
|
sliders.target_return ?? DEFAULT_TARGET_RETURN,
|
|
@@ -4642,7 +4679,14 @@ function calcTodSingle(sliders) {
|
|
|
4642
4679
|
const hold_months = hold_years * 12;
|
|
4643
4680
|
const total_with_hold = total_months + hold_months;
|
|
4644
4681
|
const cf = new Array(total_with_hold + 1).fill(0);
|
|
4645
|
-
|
|
4682
|
+
equityFirstDraws(
|
|
4683
|
+
equity_amount,
|
|
4684
|
+
land_cost_total,
|
|
4685
|
+
predev.monthly_predev_spend,
|
|
4686
|
+
permitting_months
|
|
4687
|
+
).forEach((d, m) => {
|
|
4688
|
+
cf[m] += d;
|
|
4689
|
+
});
|
|
4646
4690
|
cf[total_months] += excess_sale_cash;
|
|
4647
4691
|
let current_egi = egi, current_opex = operating_expenses;
|
|
4648
4692
|
let assessed_value = land_value + improvement_value;
|
|
@@ -4772,7 +4816,8 @@ function calcTodSingle(sliders) {
|
|
|
4772
4816
|
cap_rate,
|
|
4773
4817
|
gdv: Math.round(gdv),
|
|
4774
4818
|
rlv_selling_costs: rlv.selling_costs,
|
|
4775
|
-
|
|
4819
|
+
rlv_grants_pv: rlv.grants_pv,
|
|
4820
|
+
rlv_land_carry: rlv.land_carry,
|
|
4776
4821
|
rlv_profit: rlv.profit,
|
|
4777
4822
|
rlv_project_cost: rlv.project_cost,
|
|
4778
4823
|
rlv: rlv.rlv,
|
|
@@ -5044,8 +5089,9 @@ var DEFAULT_INPUTS = (() => {
|
|
|
5044
5089
|
retail_rent: 3,
|
|
5045
5090
|
retail_share: 0.1,
|
|
5046
5091
|
building_area_per_unit: 800,
|
|
5092
|
+
// no grant_amount: the model takes it from grant_type (GRANT), so a
|
|
5093
|
+
// pre-construction run gets $50,000 rather than the post-construction $12,000
|
|
5047
5094
|
grant_type: "post_construct",
|
|
5048
|
-
grant_amount: 12e3,
|
|
5049
5095
|
hard_cost_per_sqft: 450,
|
|
5050
5096
|
land_cost: 130,
|
|
5051
5097
|
opex_ratio: 0.19,
|