diff --git a/src/plant_fs_transaction_data/router.py b/src/plant_fs_transaction_data/router.py index 65115c3..ec735d8 100644 --- a/src/plant_fs_transaction_data/router.py +++ b/src/plant_fs_transaction_data/router.py @@ -1,4 +1,5 @@ -from typing import Optional +from typing import List, Optional +from uuid import UUID from fastapi import APIRouter, HTTPException, Query, status @@ -11,10 +12,12 @@ from .schema import ( PlantFSTransactionDataCreate, PlantFSTransactionDataImport, PlantFSTransactionDataPagination, + PlantFSTransactionDataRead, PlantFSTransactionDataUpdate, + PlantFSChartData, ) -from .service import create, delete, get, get_all, update, update_fs_charts_from_matrix +from .service import create, delete, get, get_all, update, update_fs_charts_from_matrix, get_charts from typing import List @@ -43,15 +46,57 @@ async def list_fs_transactions( ) +@router.post( + "/import/charts", + response_model=StandardResponse[List[PlantFSTransactionDataRead]], +) +async def import_fs_charts( + db_session: DbSession, + payload: PlantFSTransactionDataImport, + current_user: CurrentUser, +): + updated, missing = await update_fs_charts_from_matrix( + db_session=db_session, + payload=payload, + updated_by=getattr(current_user, "user_id", None) if current_user else None, + ) + + msg = "Data imported successfully." + if missing: + msg += f" Note: Years {missing} were not found." + + return StandardResponse(data=updated, message=msg) + + +@router.get("/charts", response_model=StandardResponse[PlantFSChartData]) +async def get_chart_data(db_session: DbSession, common: CommonParameters): + chart_data, bep_year, bep_total_lcc = await get_charts( + db_session=db_session, common=common + ) + if not chart_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No chart data found.", + ) + return StandardResponse( + data={ + "items": chart_data, + "bep_year": bep_year, + "bep_total_lcc": bep_total_lcc, + }, + message="Data retrieved successfully", + ) + + @router.get( "/{fs_transaction_id}", response_model=StandardResponse[PlantFSTransactionDataRead], ) async def retrieve_fs_transaction( db_session: DbSession, - fs_transaction_id: str, + fs_transaction_id: UUID, ): - record = await get(db_session=db_session, fs_transaction_id=fs_transaction_id) + record = await get(db_session=db_session, fs_transaction_id=str(fs_transaction_id)) if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -82,11 +127,11 @@ async def create_fs_transaction( ) async def update_fs_transaction( db_session: DbSession, - fs_transaction_id: str, + fs_transaction_id: UUID, payload: PlantFSTransactionDataUpdate, current_user: CurrentUser, ): - record = await get(db_session=db_session, fs_transaction_id=fs_transaction_id) + record = await get(db_session=db_session, fs_transaction_id=str(fs_transaction_id)) if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -109,39 +154,20 @@ async def update_fs_transaction( ) async def delete_fs_transaction( db_session: DbSession, - fs_transaction_id: str, + fs_transaction_id: UUID, ): - record = await get(db_session=db_session, fs_transaction_id=fs_transaction_id) + record = await get(db_session=db_session, fs_transaction_id=str(fs_transaction_id)) if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=[{"msg": "A data with this id does not exist."}], ) - await delete(db_session=db_session, fs_transaction_id=fs_transaction_id) + await delete(db_session=db_session, fs_transaction_id=str(fs_transaction_id)) return StandardResponse(data=record, message="Data deleted successfully") -@router.post( - "/import/charts", - response_model=StandardResponse[List[PlantFSTransactionDataRead]], -) -async def import_fs_charts( - db_session: DbSession, - payload: PlantFSTransactionDataImport, - current_user: CurrentUser, -): - updated, missing = await update_fs_charts_from_matrix( - db_session=db_session, - payload=payload, - updated_by=getattr(current_user, "user_id", None) if current_user else None, - ) - msg = "Data imported successfully." - if missing: - msg += f" Note: Years {missing} were not found." - - return StandardResponse(data=updated, message=msg) diff --git a/src/plant_fs_transaction_data/schema.py b/src/plant_fs_transaction_data/schema.py index b85cc3b..7315262 100644 --- a/src/plant_fs_transaction_data/schema.py +++ b/src/plant_fs_transaction_data/schema.py @@ -89,3 +89,14 @@ class PlantFSTransactionDataPagination(Pagination): class PlantFSTransactionDataImport(DefaultBase): data: List[List[Any]] seq: Optional[int] = None + + +class PlantFSTransactionChart(PlantFSTransactionDataBase): + pass + + +class PlantFSChartData(DefaultBase): + items: List[PlantFSTransactionChart] + bep_year: Optional[int] = Field(None, nullable=True, ge=0, le=9999) + bep_total_lcc: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) + diff --git a/src/plant_fs_transaction_data/service.py b/src/plant_fs_transaction_data/service.py index 96431be..16ce785 100644 --- a/src/plant_fs_transaction_data/service.py +++ b/src/plant_fs_transaction_data/service.py @@ -244,3 +244,66 @@ async def update_fs_charts_from_matrix( await db_session.commit() return updated_records, missing_years + + +async def get_charts( + *, + db_session: DbSession, + common, +): + """Returns all documents.""" + query = Select(PlantFSTransactionData).order_by(PlantFSTransactionData.tahun.asc()) + results = await db_session.execute(query) + + chart_data = results.scalars().all() + bep_year = None + previous_year = None + previous_total_cost = None + previous_revenue = None + bep_total_lcc = 0 + + for idx, item in enumerate(chart_data): + total_cost = ( + _safe_float(item.fs_chart_capex_annualized) + + _safe_float(item.fs_chart_oem_annualized) + + _safe_float(item.fs_chart_fuel_cost_annualized) + ) + revenue = _safe_float(item.fs_chart_revenue_annualized) + + if previous_total_cost is not None and previous_revenue is not None: + prev_diff = previous_total_cost - previous_revenue + curr_diff = total_cost - revenue + + # If signs differ there's a crossing between previous and current point + if prev_diff == 0: + bep_year = previous_year + bep_total_lcc = previous_total_cost + break + + if prev_diff * curr_diff < 0: + # Interpolate linearly between the two years to estimate BEP year + denom = ( (total_cost - previous_total_cost) - (revenue - previous_revenue) ) + if denom != 0: + t = (previous_revenue - previous_total_cost) / denom + # clamp t to [0,1] + t = max(0.0, min(1.0, t)) + try: + bep_year = previous_year + t * (item.tahun - previous_year) + except Exception: + bep_year = previous_year + bep_total_lcc = previous_total_cost + t * (total_cost - previous_total_cost) + else: + # fallback if interpolation is not possible + if total_cost < revenue: + bep_total_lcc = previous_total_cost + bep_year = previous_year + else: + bep_total_lcc = total_cost + bep_year = item.tahun + break + + previous_total_cost = total_cost + previous_revenue = revenue + previous_year = item.tahun + + return chart_data, int(bep_year) if bep_year is not None else None, bep_total_lcc