Remodel/packages/web/lib/api.ts
Manohar Gupta 112c25c26b
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
Add CUF profile viewer with upload capability
2026-05-23 17:22:31 +05:30

421 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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`;
}
// ---------------------------------------------------------------------------
// Profile functions
// ---------------------------------------------------------------------------
export interface ProfileInfo {
id: string;
name: string;
path: string;
}
export interface ProfileStats {
kind: string;
location_id: string;
count: number;
avg: number;
std: number;
min: number;
max: number;
median: number;
p25: number;
p75: number;
p90: number;
p95: number;
cuf_pct: number | null;
}
export interface ProfileData {
location_id: string;
values: number[];
count: number;
}
export async function getAvailableProfiles(): Promise<{ solar: ProfileInfo[]; wind: ProfileInfo[] }> {
return apiFetch<{ solar: ProfileInfo[]; wind: ProfileInfo[] }>("/api/profiles/available");
}
export async function getProfileStats(kind: string, locationId: string): Promise<ProfileStats> {
return apiFetch<ProfileStats>(`/api/profiles/stats/${kind}/${locationId}`);
}
export async function getSolarProfile(locationId: string): Promise<ProfileData> {
return apiFetch<ProfileData>(`/api/profiles/solar/${locationId}`);
}
export async function getWindProfile(locationId: string): Promise<ProfileData> {
return apiFetch<ProfileData>(`/api/profiles/wind/${locationId}`);
}
export function profileCsvUrl(kind: string, locationId: string): string {
return `${API_BASE}/api/profiles/${kind}/${locationId}?as_csv=true`;
}