feat: Expand Project Cost Breakdown to show all cost items
Some checks are pending
CI / Engine — lint / typecheck / test (push) Waiting to run
CI / API — lint / typecheck / test (push) Waiting to run
CI / Web — typecheck / lint / build (push) Waiting to run

- Split Solar Land and Wind Land into separate lines
- Add DSRA (Debt Service Reserve Account)
- Split EPC overheads into Solar and Wind
- Add EPC Margin categories (WTG Tower+BOP, Solar BOS)
- Split Contingency into Solar and Wind
- Add Total Hard Cost subtotal
- Add financing costs (upfront fee, IDC, bank guarantee)
This commit is contained in:
Manohar Gupta 2026-05-29 17:37:51 +05:30
parent 581eafbf36
commit dfa5ae36d8

View file

@ -791,10 +791,16 @@ export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) {
const solarBOS = solarItems.filter(it => ["solar_inverter", "solar_dc_bos", "solar_ac_bos", "solar_hr"].includes(it.id)).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarBOS = solarItems.filter(it => ["solar_inverter", "solar_dc_bos", "solar_ac_bos", "solar_hr"].includes(it.id)).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const solarModule = solarItems.filter(it => it.id === "solar_module").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarModule = solarItems.filter(it => it.id === "solar_module").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const windWTG = windItems.filter(it => ["wind_wtg", "wind_tower", "wind_bop", "wind_e_and_c"].includes(it.id)).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const windWTG = windItems.filter(it => ["wind_wtg", "wind_tower", "wind_bop", "wind_e_and_c"].includes(it.id)).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const allLand = [...solarItems, ...windItems].filter(it => it.id.includes("land")).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarLandCost = solarItems.filter(it => it.id.includes("land") && it.attribution === "SolarOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const windLandCost = windItems.filter(it => it.id.includes("land") && it.attribution === "WindOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const bessAll = bessItems.reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const bessAll = bessItems.reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const epcAll = [...solarItems, ...windItems, ...bessItems].filter(it => it.category === "EPCOverhead").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarEPC = solarItems.filter(it => it.category === "EPCOverhead").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const contAll = [...solarItems, ...windItems].filter(it => it.category === "Contingency").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const windEPC = windItems.filter(it => it.category === "EPCOverhead").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const solarCont = solarItems.filter(it => it.category === "Contingency" && it.attribution === "SolarOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const windCont = windItems.filter(it => it.category === "Contingency" && it.attribution === "WindOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
// EPC Margins
const epcMarginWTG = [...windItems, ...bessItems].filter(it => it.category === "EPCMargin" && it.attribution === "WindOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
const epcMarginSolarBOS = solarItems.filter(it => it.category === "EPCMargin" && it.attribution === "SolarOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
// Financing // Financing
const idcRate = gv(inputs, "capex", "interest_rate_annual", 0.09); const idcRate = gv(inputs, "capex", "interest_rate_annual", 0.09);
const constrMonths = gv(inputs, "capex", "construction_months", 24); const constrMonths = gv(inputs, "capex", "construction_months", 24);
@ -802,6 +808,15 @@ export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) {
const idcCost = totalCostCr * debtFrac * idcRate * constrMonths / 12; const idcCost = totalCostCr * debtFrac * idcRate * constrMonths / 12;
const upfrontPct = gv(inputs, "capex", "upfront_fee_pct", 0.01); const upfrontPct = gv(inputs, "capex", "upfront_fee_pct", 0.01);
const upfrontCost = totalCostCr * debtFrac * upfrontPct; const upfrontCost = totalCostCr * debtFrac * upfrontPct;
const bankGuaranteePct = gv(inputs, "capex", "bank_guarantee_pct", 0.001);
const bankGuaranteeCost = totalCostCr * bankGuaranteePct;
// DSRA (Debt Service Reserve Account) - typically 1-2 months of debt service
const dsraMonths = gv(inputs, "capex", "dsra_months", 2);
const annualDebtService = totalCostCr * debtFrac * 0.12; // approximate annual debt service
const dsraCost = annualDebtService * dsraMonths / 12;
// Compute Total Hard Cost (sum of all component costs before financing)
const totalHardCost = solarModule + solarBOS + windWTG + solarLandCost + windLandCost + bessAll + dsraCost + solarEPC + windEPC + epcMarginWTG + epcMarginSolarBOS + solarCont + windCont;
return ( return (
<div className="border border-border rounded-lg overflow-hidden"> <div className="border border-border rounded-lg overflow-hidden">
@ -813,13 +828,32 @@ export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) {
{solarModule > 0 && <tr><td className="px-3 py-1.5">Solar Module</td><td className="px-3 py-1.5 text-right tabular-nums">{solarModule.toFixed(2)}</td></tr>} {solarModule > 0 && <tr><td className="px-3 py-1.5">Solar Module</td><td className="px-3 py-1.5 text-right tabular-nums">{solarModule.toFixed(2)}</td></tr>}
{solarBOS > 0 && <tr><td className="px-3 py-1.5">Solar BoS</td><td className="px-3 py-1.5 text-right tabular-nums">{solarBOS.toFixed(2)}</td></tr>} {solarBOS > 0 && <tr><td className="px-3 py-1.5">Solar BoS</td><td className="px-3 py-1.5 text-right tabular-nums">{solarBOS.toFixed(2)}</td></tr>}
{windEnabled && windWTG > 0 && <tr><td className="px-3 py-1.5">Wind WTG</td><td className="px-3 py-1.5 text-right tabular-nums">{windWTG.toFixed(2)}</td></tr>} {windEnabled && windWTG > 0 && <tr><td className="px-3 py-1.5">Wind WTG</td><td className="px-3 py-1.5 text-right tabular-nums">{windWTG.toFixed(2)}</td></tr>}
{allLand > 0 && <tr><td className="px-3 py-1.5">Land Cost</td><td className="px-3 py-1.5 text-right tabular-nums">{allLand.toFixed(2)}</td></tr>} {solarLandCost > 0 && <tr><td className="px-3 py-1.5">Solar Land Cost</td><td className="px-3 py-1.5 text-right tabular-nums">{solarLandCost.toFixed(2)}</td></tr>}
{bessEnabled && bessAll > 0 && <tr><td className="px-3 py-1.5">Storage (BESS)</td><td className="px-3 py-1.5 text-right tabular-nums">{bessAll.toFixed(2)}</td></tr>} {windEnabled && windLandCost > 0 && <tr><td className="px-3 py-1.5">Wind Land Cost</td><td className="px-3 py-1.5 text-right tabular-nums">{windLandCost.toFixed(2)}</td></tr>}
{epcAll > 0 && <tr><td className="px-3 py-1.5">EPC Overheads</td><td className="px-3 py-1.5 text-right tabular-nums">{epcAll.toFixed(2)}</td></tr>} {bessEnabled && bessAll > 0 && <tr><td className="px-3 py-1.5">Storage</td><td className="px-3 py-1.5 text-right tabular-nums">{bessAll.toFixed(2)}</td></tr>}
{contAll > 0 && <tr><td className="px-3 py-1.5">Contingency</td><td className="px-3 py-1.5 text-right tabular-nums">{contAll.toFixed(2)}</td></tr>} {dsraCost > 0 && <tr><td className="px-3 py-1.5">DSRA</td><td className="px-3 py-1.5 text-right tabular-nums">{dsraCost.toFixed(2)}</td></tr>}
{solarEPC > 0 && <tr><td className="px-3 py-1.5">Solar EPC Overheads</td><td className="px-3 py-1.5 text-right tabular-nums">{solarEPC.toFixed(2)}</td></tr>}
{windEnabled && windEPC > 0 && <tr><td className="px-3 py-1.5">Wind EPC Overheads</td><td className="px-3 py-1.5 text-right tabular-nums">{windEPC.toFixed(2)}</td></tr>}
{epcMarginWTG > 0 && <tr><td className="px-3 py-1.5">EPC Margin - WTG Tower+BOP</td><td className="px-3 py-1.5 text-right tabular-nums">{epcMarginWTG.toFixed(2)}</td></tr>}
{epcMarginSolarBOS > 0 && <tr><td className="px-3 py-1.5">EPC Margin - Solar BOS</td><td className="px-3 py-1.5 text-right tabular-nums">{epcMarginSolarBOS.toFixed(2)}</td></tr>}
{solarCont > 0 && <tr><td className="px-3 py-1.5">Contingency - Solar</td><td className="px-3 py-1.5 text-right tabular-nums">{solarCont.toFixed(2)}</td></tr>}
{windEnabled && windCont > 0 && <tr><td className="px-3 py-1.5">Contingency - Wind (in WTG cost)</td><td className="px-3 py-1.5 text-right tabular-nums">{windCont.toFixed(2)}</td></tr>}
<tr className="border-t-2 border-border bg-muted/30">
<td className="px-3 py-2 font-semibold">Total Hard Cost</td>
<td className="px-3 py-2 text-right tabular-nums font-semibold">{totalHardCost.toFixed(2)}</td>
</tr>
<tr className="border-t border-dashed border-border/50">
<td className="px-3 py-1.5 text-muted-foreground">Upfront Financing Fee</td><td className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">{upfrontCost.toFixed(2)}</td>
</tr>
<tr>
<td className="px-3 py-1.5 text-muted-foreground">Interest During Construction</td><td className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">{idcCost.toFixed(2)}</td>
</tr>
<tr>
<td className="px-3 py-1.5 text-muted-foreground">Bank Guarantee Cost</td><td className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">{bankGuaranteeCost.toFixed(2)}</td>
</tr>
<tr className="border-t-2 border-border bg-muted/30"> <tr className="border-t-2 border-border bg-muted/30">
<td className="px-3 py-2 font-semibold">Total Project Cost</td> <td className="px-3 py-2 font-semibold">Total Project Cost</td>
<td className="px-3 py-2 text-right tabular-nums font-bold text-primary">{(totalCostCr + idcCost + upfrontCost).toFixed(2)}</td> <td className="px-3 py-2 text-right tabular-nums font-bold text-primary">{(totalHardCost + upfrontCost + idcCost + bankGuaranteeCost).toFixed(2)}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>