- Engine: Add ppa_revenue_cr, mcp_revenue_cr, tariff, units to PnLRow - Engine: Split PPA vs MCP revenue in P&L computation - Web: Collapsible rows for PPA/MCP Revenue and Opex - Web: Highlighted rows (Total Revenue, EBITDA, EBIT, PBT, PAT) - Web: Units above Tariff in breakdown, bg-blue-50 highlight - Fix sticky column z-index for horizontal scroll - CLAUDE.md: Add project documentation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
356 lines
8.8 KiB
TypeScript
356 lines
8.8 KiB
TypeScript
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface Scenario {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
kpis_json: string | null;
|
|
created_at: string;
|
|
runtime_s?: number | null;
|
|
}
|
|
|
|
export interface ScenarioDetail extends Scenario {
|
|
inputs_json: string | null;
|
|
statements_json: string | null;
|
|
debt_schedule_json: string | null;
|
|
error_message: string | null;
|
|
}
|
|
|
|
export interface KpiSummary {
|
|
solved_tariff_inr_per_kwh?: number | null;
|
|
equity_irr?: number | null;
|
|
project_irr?: number | null;
|
|
min_dscr?: number | null;
|
|
avg_dscr?: number | null;
|
|
total_capex_cr?: number | null;
|
|
idc_cr?: number | null;
|
|
debt_cr?: number | null;
|
|
solar_y1_cuf?: number | null;
|
|
wind_y1_plf?: number | null;
|
|
lcoe_inr_per_kwh?: number | null;
|
|
payback_years?: number | null;
|
|
rtc_cuf_achieved?: number | null;
|
|
total_shortfall_mwh?: number | null;
|
|
total_curtailed_mwh?: number | null;
|
|
total_mcp_revenue_cr?: number | null;
|
|
}
|
|
|
|
export interface PnLRow {
|
|
year: number;
|
|
revenue_cr: number;
|
|
ppa_revenue_cr: number;
|
|
mcp_revenue_cr: number;
|
|
ppa_tariff_inr_per_kwh: number;
|
|
ppa_units_mwh: number;
|
|
mcp_units_mwh: number;
|
|
opex_total_cr: number;
|
|
om_cr: number;
|
|
insurance_cr: number;
|
|
land_lease_cr: number;
|
|
am_fee_cr: number;
|
|
misc_opex_cr: number;
|
|
ebitda_cr: number;
|
|
depreciation_book_cr: number;
|
|
ebit_cr: number;
|
|
interest_cr: number;
|
|
pbt_cr: number;
|
|
tax_cr: number;
|
|
pat_cr: number;
|
|
}
|
|
|
|
export interface CfsRow {
|
|
year: number;
|
|
pat_cr: number;
|
|
depreciation_cr: number;
|
|
delta_working_capital_cr: number;
|
|
cfo_cr: number;
|
|
capex_cr: number;
|
|
cfi_cr: number;
|
|
debt_drawdown_cr: number;
|
|
debt_repayment_cr: number;
|
|
equity_injection_cr: number;
|
|
cff_cr: number;
|
|
net_cash_flow_cr: number;
|
|
opening_cash_cr: number;
|
|
closing_cash_cr: number;
|
|
}
|
|
|
|
export interface BsRow {
|
|
year: number;
|
|
gross_block_cr: number;
|
|
accumulated_depr_cr: number;
|
|
net_block_cr: number;
|
|
cash_cr: number;
|
|
receivables_cr: number;
|
|
total_assets_cr: number;
|
|
equity_cr: number;
|
|
reserves_cr: number;
|
|
long_term_debt_cr: number;
|
|
payables_cr: number;
|
|
total_liabilities_cr: number;
|
|
}
|
|
|
|
export interface DebtYearRow {
|
|
year: number;
|
|
opening_balance_cr: number;
|
|
interest_cr: number;
|
|
principal_cr: number;
|
|
total_debt_service_cr: number;
|
|
closing_balance_cr: number;
|
|
dscr: number;
|
|
}
|
|
|
|
export interface GenerationRow {
|
|
year: number;
|
|
solar_mwh: number;
|
|
wind_mwh: number;
|
|
gross_mwh: number;
|
|
aux_loss_mwh: number;
|
|
tx_loss_mwh: number;
|
|
dsm_loss_mwh: number;
|
|
net_billable_mwh: number;
|
|
solar_cuf_pct: number | null;
|
|
wind_plf_pct: number | null;
|
|
revenue_cr: number;
|
|
}
|
|
|
|
export interface IdcMonthRow {
|
|
month: number;
|
|
equity_draw_cr: number;
|
|
debt_draw_cr: number;
|
|
idc_accrual_cr: number;
|
|
cum_equity_cr: number;
|
|
cum_debt_cr: number;
|
|
cum_idc_cr: number;
|
|
cum_tpc_cr: number;
|
|
}
|
|
|
|
export interface IdcPhasing {
|
|
construction_months: number;
|
|
base_capex_cr: number;
|
|
idc_cr: number;
|
|
total_capex_cr: number;
|
|
debt_cr: number;
|
|
equity_cr: number;
|
|
monthly: IdcMonthRow[];
|
|
}
|
|
|
|
export interface Statements {
|
|
pnl: PnLRow[];
|
|
cfs: CfsRow[];
|
|
bs: BsRow[];
|
|
generation?: GenerationRow[];
|
|
idc_phasing?: IdcPhasing;
|
|
}
|
|
|
|
export type CostBasis =
|
|
| "PER_WP_DC"
|
|
| "PER_MWP_DC"
|
|
| "PER_MW_AC"
|
|
| "PER_MW_WIND"
|
|
| "PER_MWH_BESS"
|
|
| "PER_ACRE"
|
|
| "PCT_OF_HARDCOST"
|
|
| "ABS_INR_CR";
|
|
|
|
export type DeprClass =
|
|
| "Plant"
|
|
| "BESS"
|
|
| "Building"
|
|
| "Land_NoDepr"
|
|
| "LandLease_Amortized"
|
|
| "Intangible"
|
|
| "Capitalized_NoDepr"
|
|
| "Expensed";
|
|
|
|
export type CostAttribution = "SolarOnly" | "WindOnly" | "BESSOnly" | "Common";
|
|
|
|
export interface CostItem {
|
|
id: string;
|
|
name: string;
|
|
category: "HardCost" | "SoftCost" | "EPCOverhead" | "EPCMargin" | "FinancingCost" | "Contingency";
|
|
basis: CostBasis;
|
|
value: number;
|
|
depr_class: DeprClass;
|
|
tax_pct?: number; // GST/tax rate, default 5% for modules
|
|
attribution: CostAttribution;
|
|
phasing_id?: string;
|
|
escalation_pct?: number;
|
|
}
|
|
|
|
export interface ScenarioInputPayload {
|
|
project?: {
|
|
name?: string;
|
|
state?: string | null;
|
|
capacity_solar_mwp?: number;
|
|
capacity_wind_mw?: number;
|
|
capacity_bess_mwh?: number;
|
|
capacity_bess_mw?: number;
|
|
land_acres?: number;
|
|
cod_year?: number;
|
|
cod_date?: string | null;
|
|
solar_cod_date?: string | null;
|
|
wind_cod_date?: string | null;
|
|
bess_cod_date?: string | null;
|
|
};
|
|
solar?: {
|
|
location_id: string;
|
|
capacity_dc_mwp: number;
|
|
capacity_ac_mw: number;
|
|
dc_ac_ratio?: number;
|
|
availability_fraction?: number;
|
|
dc_loss_fraction?: number;
|
|
soiling_fraction?: number;
|
|
degradation_y1?: number;
|
|
degradation_annual?: number;
|
|
stabilization_days?: number;
|
|
stabilization_energy_loss_frac?: number;
|
|
stabilization_dsm_addon_pct?: number;
|
|
} | null;
|
|
wind?: {
|
|
location_id: string;
|
|
capacity_mw: number;
|
|
hub_height_m?: number;
|
|
availability_fraction?: number;
|
|
wake_loss_fraction?: number;
|
|
stabilization_days?: number;
|
|
stabilization_energy_loss_frac?: number;
|
|
stabilization_dsm_addon_pct?: number;
|
|
} | null;
|
|
bess?: {
|
|
capacity_mwh: number;
|
|
power_mw: number;
|
|
rte?: number;
|
|
dod?: number;
|
|
} | null;
|
|
rtc?: {
|
|
rtc_mw?: number;
|
|
mcp_enabled?: boolean;
|
|
initial_soc_frac?: number;
|
|
} | null;
|
|
commercial?: {
|
|
tariff_inr_per_kwh?: number;
|
|
aux_consumption_pct?: number;
|
|
transmission_loss_pct?: number;
|
|
dsm_loss_pct?: number;
|
|
bad_debt_pct?: number;
|
|
receivable_days?: number;
|
|
payable_days?: number;
|
|
};
|
|
opex?: {
|
|
om_solar_cr_per_mw?: number;
|
|
om_wind_cr_per_mw?: number;
|
|
om_bess_cr_per_mwh?: number;
|
|
insurance_pct_of_capex?: number;
|
|
land_lease_cr?: number;
|
|
om_escalation_pct?: number;
|
|
om_solar_escalation_pct?: number;
|
|
om_solar_escalation_after_year?: number;
|
|
om_wind_escalation_pct?: number;
|
|
om_wind_escalation_after_year?: number;
|
|
om_bess_pct_of_capex?: number | null;
|
|
am_fee_pct_of_revenue?: number;
|
|
misc_cr?: number;
|
|
};
|
|
capex?: {
|
|
cost_items?: CostItem[];
|
|
debt_fraction?: number;
|
|
interest_rate_annual?: number;
|
|
construction_months?: number;
|
|
upfront_fee_pct?: number;
|
|
};
|
|
debt?: {
|
|
interest_rate_annual?: number;
|
|
tenor_years?: number;
|
|
moratorium_years?: number;
|
|
de_ratio?: number;
|
|
min_dscr?: number;
|
|
avg_dscr?: number;
|
|
schedule_shape?: string;
|
|
};
|
|
tax?: {
|
|
rate?: number;
|
|
wdv_plant_rate?: number;
|
|
wdv_bess_rate?: number;
|
|
wdv_building_rate?: number;
|
|
wdv_intangible_rate?: number;
|
|
};
|
|
solver?: {
|
|
mode: "solve_tariff" | "fixed_tariff";
|
|
target_equity_irr?: number;
|
|
fixed_tariff?: number | null;
|
|
};
|
|
}
|
|
|
|
export interface ProgressEvent {
|
|
stage: string;
|
|
pct: number;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${url}`, options);
|
|
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scenario functions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function createScenario(
|
|
name: string,
|
|
inputs?: ScenarioInputPayload,
|
|
): Promise<Scenario> {
|
|
return apiFetch<Scenario>("/api/scenarios", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name, inputs }),
|
|
});
|
|
}
|
|
|
|
export async function getScenario(id: string): Promise<ScenarioDetail> {
|
|
return apiFetch<ScenarioDetail>(`/api/scenarios/${id}`);
|
|
}
|
|
|
|
export async function listScenarios(): Promise<Scenario[]> {
|
|
return apiFetch<Scenario[]>("/api/scenarios");
|
|
}
|
|
|
|
export async function getKpis(id: string): Promise<KpiSummary> {
|
|
return apiFetch<KpiSummary>(`/api/scenarios/${id}/kpis`);
|
|
}
|
|
|
|
export async function getStatements(id: string): Promise<Statements> {
|
|
return apiFetch<Statements>(`/api/scenarios/${id}/statements`);
|
|
}
|
|
|
|
export async function archiveScenario(id: string): Promise<void> {
|
|
await apiFetch(`/api/scenarios/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
export async function updateScenarioInputs(
|
|
id: string,
|
|
inputs: ScenarioInputPayload,
|
|
): Promise<Scenario> {
|
|
return apiFetch<Scenario>(`/api/scenarios/${id}/inputs`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ inputs }),
|
|
});
|
|
}
|
|
|
|
export function scenarioEventsUrl(id: string): string {
|
|
return `${API_BASE}/api/scenarios/${id}/events`;
|
|
}
|
|
|
|
export function scenarioExcelUrl(id: string): string {
|
|
return `${API_BASE}/api/scenarios/${id}/export/excel`;
|
|
}
|