- Add hourly_solar_profile and hourly_wind_profile to store per-hour output values - Add solar_dc_mwp, solar_ac_mw, wind_mw to PipelineResult for capacity display - Add columns: Solar 8760, DC MW, Solar MW / Wind 8760, MW, Wind MW - Updated UI to show detailed breakdown at each level (Year/Month/Day/Hour) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
369 lines
9.2 KiB
TypeScript
369 lines
9.2 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;
|
||
// Hourly generation: 25 years × 8760 hours = 219,000 values per technology
|
||
solar_hourly?: number[];
|
||
wind_hourly?: number[];
|
||
// Hourly timeseries
|
||
hourly_timestamps?: string[];
|
||
hourly_fy?: string[];
|
||
hourly_proj_year?: number[];
|
||
hourly_total_re?: number[];
|
||
hourly_client_end?: number[];
|
||
hourly_load?: number[];
|
||
// Profile data for detailed view
|
||
hourly_solar_profile?: number[];
|
||
hourly_wind_profile?: number[];
|
||
}
|
||
|
||
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`;
|
||
}
|