11from sqlalchemy .engine .row import Row
22
33from policyengine_api .data import database
4- from policyengine_api .constants import COUNTRY_PACKAGE_VERSIONS
4+ from policyengine_api .constants import get_report_output_cache_version
55
66
77class ReportOutputService :
8+ def _get_report_output_row (self , report_output_id : int ) -> dict | None :
9+ row : Row | None = database .query (
10+ "SELECT * FROM report_outputs WHERE id = ?" ,
11+ (report_output_id ,),
12+ ).fetchone ()
13+ return dict (row ) if row is not None else None
14+
15+ def get_stored_report_output (self , report_output_id : int ) -> dict | None :
16+ """
17+ Get the raw stored report output row by ID without aliasing to the
18+ current runtime lineage. This is useful for mutation paths, which must
19+ update the originally addressed row rather than a resolved alias.
20+ """
21+ return self ._get_report_output_row (report_output_id )
22+
23+ def _is_current_report_output (self , report_output : dict ) -> bool :
24+ return report_output .get ("api_version" ) == get_report_output_cache_version (
25+ report_output ["country_id" ]
26+ )
27+
28+ def _get_or_create_current_report_output (self , report_output : dict ) -> dict :
29+ current_report = self .find_existing_report_output (
30+ country_id = report_output ["country_id" ],
31+ simulation_1_id = report_output ["simulation_1_id" ],
32+ simulation_2_id = report_output ["simulation_2_id" ],
33+ year = report_output ["year" ],
34+ )
35+ if current_report is not None :
36+ return current_report
37+
38+ return self .create_report_output (
39+ country_id = report_output ["country_id" ],
40+ simulation_1_id = report_output ["simulation_1_id" ],
41+ simulation_2_id = report_output ["simulation_2_id" ],
42+ year = report_output ["year" ],
43+ )
44+
45+ def _alias_report_output (self , report_output_id : int , report_output : dict ) -> dict :
46+ aliased_report = dict (report_output )
47+ aliased_report ["id" ] = report_output_id
48+ return aliased_report
49+
850 def find_existing_report_output (
951 self ,
1052 country_id : str ,
@@ -25,18 +67,20 @@ def find_existing_report_output(
2567 dict | None: The existing report output data or None if not found.
2668 """
2769 print ("Checking for existing report output" )
70+ api_version = get_report_output_cache_version (country_id )
2871
2972 try :
30- # Check for existing record with the same simulation IDs and year (excluding api_version)
31- query = "SELECT * FROM report_outputs WHERE country_id = ? AND simulation_1_id = ? AND year = ?"
32- params = [country_id , simulation_1_id , year ]
73+ query = "SELECT * FROM report_outputs WHERE country_id = ? AND simulation_1_id = ? AND year = ? AND api_version = ?"
74+ params = [country_id , simulation_1_id , year , api_version ]
3375
3476 if simulation_2_id is not None :
3577 query += " AND simulation_2_id = ?"
3678 params .append (simulation_2_id )
3779 else :
3880 query += " AND simulation_2_id IS NULL"
3981
82+ query += " ORDER BY id DESC"
83+
4084 row = database .query (query , tuple (params )).fetchone ()
4185
4286 existing_report = None
@@ -71,9 +115,18 @@ def create_report_output(
71115 dict: The created report output record.
72116 """
73117 print ("Creating new report output" )
74- api_version : str = COUNTRY_PACKAGE_VERSIONS . get (country_id )
118+ api_version = get_report_output_cache_version (country_id )
75119
76120 try :
121+ existing_report = self .find_existing_report_output (
122+ country_id , simulation_1_id , simulation_2_id , year
123+ )
124+ if existing_report is not None :
125+ print (
126+ f"Reusing existing report output with ID: { existing_report ['id' ]} "
127+ )
128+ return existing_report
129+
77130 # Insert with default status 'pending'
78131 if simulation_2_id is not None :
79132 database .query (
@@ -132,18 +185,15 @@ def get_report_output(self, report_output_id: int) -> dict | None:
132185 f"Invalid report output ID: { report_output_id } . Must be a positive integer."
133186 )
134187
135- row : Row | None = database .query (
136- "SELECT * FROM report_outputs WHERE id = ?" ,
137- (report_output_id ,),
138- ).fetchone ()
188+ report_output = self ._get_report_output_row (report_output_id )
189+ if report_output is None :
190+ return None
139191
140- report_output = None
141- if row is not None :
142- report_output = dict (row )
143- # Keep output as JSON string - frontend expects string format
144- # Frontend will parse it using JSON.parse()
192+ if self ._is_current_report_output (report_output ):
193+ return report_output
145194
146- return report_output
195+ current_report = self ._get_or_create_current_report_output (report_output )
196+ return self ._alias_report_output (report_output_id , current_report )
147197
148198 except Exception as e :
149199 print (
@@ -172,10 +222,12 @@ def update_report_output(
172222 bool: True if update was successful.
173223 """
174224 print (f"Updating report output { report_id } " )
175- # Automatically update api_version on every update to latest
176- api_version : str = COUNTRY_PACKAGE_VERSIONS .get (country_id )
177225
178226 try :
227+ requested_report = self ._get_report_output_row (report_id )
228+ if requested_report is None :
229+ raise Exception (f"Report output #{ report_id } not found" )
230+
179231 # Build the update query dynamically based on provided fields
180232 update_fields = []
181233 update_values = []
@@ -193,16 +245,12 @@ def update_report_output(
193245 update_fields .append ("error_message = ?" )
194246 update_values .append (error_message )
195247
196- # Always update API version
197- update_fields .append ("api_version = ?" )
198- update_values .append (api_version )
199-
200248 if not update_fields :
201249 print ("No fields to update" )
202250 return False
203251
204252 # Add report_id to the end of values for WHERE clause
205- update_values .append (report_id )
253+ update_values .append (requested_report [ "id" ] )
206254
207255 query = f"UPDATE report_outputs SET { ', ' .join (update_fields )} WHERE id = ?"
208256
0 commit comments