99from typing import Annotated , Any , Dict , List , Optional
1010
1111import numpy as np
12- from fastapi import Depends , FastAPI , HTTPException , Query , Request , WebSocket , WebSocketDisconnect
12+ from fastapi import BackgroundTasks , Depends , FastAPI , HTTPException , Query , Request , WebSocket , WebSocketDisconnect
1313from fastapi .responses import FileResponse , JSONResponse , PlainTextResponse
1414from fastapi .staticfiles import StaticFiles
1515from slowapi .errors import RateLimitExceeded
5757 EventRevenueInput ,
5858 RevenueCalculationResult ,
5959 RevenueShareConfig ,
60+ Stakeholder ,
6061)
6162from src .revenue_sharing_service import revenue_sharing_service
63+ from src import (
64+ calculation_history_store ,
65+ currency_service ,
66+ stakeholder_store ,
67+ )
6268from src .recommender import (
6369 build_item_similarity_matrix ,
6470 get_item_recommendations ,
@@ -182,6 +188,8 @@ def on_startup() -> None:
182188 global model_pipeline , etl_scheduler
183189 settings = get_settings ()
184190 create_generated_reports_table ()
191+ stakeholder_store .create_stakeholders_table ()
192+ calculation_history_store .create_revenue_calculations_table ()
185193 if not settings .SKIP_MODEL_TRAINING :
186194 model_pipeline = train_logistic_regression_pipeline ()
187195
@@ -586,7 +594,10 @@ def recommend_events(payload: RecommendRequest) -> RecommendResponse:
586594# ---------------------------------------------------------------------------
587595
588596@app .post ("/calculate-revenue-share" , response_model = RevenueCalculationResult )
589- def calculate_revenue_share (input_data : EventRevenueInput ) -> RevenueCalculationResult :
597+ def calculate_revenue_share (
598+ input_data : EventRevenueInput ,
599+ background_tasks : BackgroundTasks ,
600+ ) -> RevenueCalculationResult :
590601 """Calculate revenue shares for stakeholders based on event sales."""
591602 log_info ("Revenue share calculation requested" , {
592603 "event_id" : input_data .event_id ,
@@ -599,30 +610,42 @@ def calculate_revenue_share(input_data: EventRevenueInput) -> RevenueCalculation
599610 raise HTTPException (status_code = 400 , detail = {"errors" : errors })
600611 try :
601612 result = revenue_sharing_service .calculate_revenue_shares (input_data )
613+ background_tasks .add_task (calculation_history_store .save_calculation , result )
602614 return result
615+ except HTTPException :
616+ raise
603617 except Exception as exc :
604618 log_error ("Revenue share calculation failed" , {"error" : str (exc )})
605619 raise HTTPException (status_code = 500 , detail = f"Revenue calculation failed: { exc } " )
606620
607621
608622@app .post ("/calculate-revenue-share/batch" , response_model = List [RevenueCalculationResult ])
609623def calculate_revenue_share_batch (
610- inputs : List [EventRevenueInput ],
624+ inputs : List [Dict [str , Any ]],
625+ background_tasks : BackgroundTasks ,
611626) -> List [RevenueCalculationResult ]:
612- """Calculate revenue shares for multiple events."""
627+ """Calculate revenue shares for multiple events, skipping invalid ones ."""
613628 log_info ("Batch revenue share calculation requested" , {"event_count" : len (inputs )})
614629 results : List [RevenueCalculationResult ] = []
615- for input_data in inputs :
630+ for item in inputs :
616631 try :
632+ # Validate model first
633+ input_data = EventRevenueInput .model_validate (item )
634+ # Run service-level validation
617635 is_valid , errors = revenue_sharing_service .validate_input (input_data )
618636 if not is_valid :
637+ log_info ("Skipping invalid event in batch" , {"event_id" : item .get ("event_id" ), "errors" : errors })
619638 continue
620- results .append (revenue_sharing_service .calculate_revenue_shares (input_data ))
639+
640+ result = revenue_sharing_service .calculate_revenue_shares (input_data )
641+ background_tasks .add_task (calculation_history_store .save_calculation , result )
642+ results .append (result )
621643 except Exception as exc :
622- log_error ("Batch revenue calculation failed" , {
623- "event_id" : input_data . event_id ,
644+ log_error ("Batch revenue calculation partially failed" , {
645+ "event_id" : item . get ( " event_id" ) ,
624646 "error" : str (exc ),
625647 })
648+ continue
626649 log_info ("Batch calculation completed" , {
627650 "processed_count" : len (results ),
628651 "requested_count" : len (inputs ),
@@ -637,6 +660,17 @@ def get_revenue_share_config() -> RevenueShareConfig:
637660 return revenue_sharing_service .config
638661
639662
663+ @app .get ("/revenue-share/exchange-rates" )
664+ def get_exchange_rates () -> Dict [str , float ]:
665+ """Return the current currency exchange rates relative to USD."""
666+ log_info ("Currency exchange rates requested" )
667+ try :
668+ return currency_service .get_exchange_rates ()
669+ except Exception as exc :
670+ log_error ("Failed to fetch exchange rates" , {"error" : str (exc )})
671+ raise HTTPException (status_code = 503 , detail = "Exchange rate service unavailable" )
672+
673+
640674@app .get ("/revenue-share/example" , response_model = EventRevenueInput )
641675def get_example_revenue_input () -> EventRevenueInput :
642676 """Return an example revenue calculation input."""
@@ -650,6 +684,73 @@ def get_example_revenue_input() -> EventRevenueInput:
650684 )
651685
652686
687+ @app .post ("/revenue-share/stakeholders/{event_id}" )
688+ def save_stakeholders (
689+ event_id : str ,
690+ stakeholders : List [Stakeholder ],
691+ _ : str = Depends (require_admin_key ),
692+ ) -> Dict [str , Any ]:
693+ """Save custom stakeholder configuration for an event (ADMIN)."""
694+ log_info ("Saving custom stakeholders" , {"event_id" : event_id , "count" : len (stakeholders )})
695+ try :
696+ stakeholder_store .save_stakeholders_for_event (event_id , stakeholders )
697+ return {"success" : True , "message" : f"Saved { len (stakeholders )} stakeholders for event { event_id } " }
698+ except Exception as exc :
699+ log_error ("Failed to save stakeholders" , {"event_id" : event_id , "error" : str (exc )})
700+ raise HTTPException (status_code = 500 , detail = f"Failed to save stakeholders: { exc } " )
701+
702+
703+ @app .get ("/revenue-share/stakeholders/{event_id}" , response_model = List [Stakeholder ])
704+ def get_stakeholders (event_id : str ) -> List [Stakeholder ]:
705+ """Return the stakeholder configuration for an event (custom or default)."""
706+ log_info ("Retrieving stakeholders" , {"event_id" : event_id })
707+ try :
708+ stakeholders = stakeholder_store .get_stakeholders_for_event (event_id )
709+ if not stakeholders :
710+ # Fallback logic duplicated here for the GET endpoint
711+ stakeholders = revenue_sharing_service ._get_default_stakeholders (event_id )
712+ return stakeholders
713+ except Exception as exc :
714+ log_error ("Failed to retrieve stakeholders" , {"event_id" : event_id , "error" : str (exc )})
715+ raise HTTPException (status_code = 500 , detail = f"Failed to retrieve stakeholders: { exc } " )
716+
717+
718+ @app .get ("/revenue-share/history/{event_id}" )
719+ def get_revenue_history (
720+ event_id : str ,
721+ page : int = Query (1 , ge = 1 ),
722+ limit : int = Query (20 , ge = 1 , le = 100 ),
723+ _ : str = Depends (require_admin_key ),
724+ ) -> List [Dict [str , Any ]]:
725+ """Retrieve paginated calculation history for an event (ADMIN)."""
726+ log_info ("Retrieving revenue history" , {"event_id" : event_id , "page" : page , "limit" : limit })
727+ try :
728+ return calculation_history_store .get_history_for_event (event_id , page , limit )
729+ except Exception as exc :
730+ log_error ("Failed to retrieve history" , {"event_id" : event_id , "error" : str (exc )})
731+ raise HTTPException (status_code = 500 , detail = f"Failed to retrieve history: { exc } " )
732+
733+
734+ @app .get ("/revenue-share/history/{event_id}/{calculation_id}" )
735+ def get_calculation_detail (
736+ event_id : str ,
737+ calculation_id : str ,
738+ _ : str = Depends (require_admin_key ),
739+ ) -> Dict [str , Any ]:
740+ """Retrieve a specific stored calculation by ID (ADMIN)."""
741+ log_info ("Retrieving calculation detail" , {"event_id" : event_id , "calculation_id" : calculation_id })
742+ try :
743+ detail = calculation_history_store .get_calculation_by_id (calculation_id )
744+ if not detail or detail ["event_id" ] != event_id :
745+ raise HTTPException (status_code = 404 , detail = "Calculation not found for this event" )
746+ return detail
747+ except HTTPException :
748+ raise
749+ except Exception as exc :
750+ log_error ("Failed to retrieve calculation detail" , {"calculation_id" : calculation_id , "error" : str (exc )})
751+ raise HTTPException (status_code = 500 , detail = f"Failed to retrieve detail: { exc } " )
752+
753+
653754# ---------------------------------------------------------------------------
654755# Daily report
655756# ---------------------------------------------------------------------------
0 commit comments