1717 ExpenseValidationError ,
1818 CategoryConflictError ,
1919 CapacityExceededError ,
20+ CapacityResult ,
2021)
2122
2223expenses_bp = Blueprint ('expenses' , __name__ )
@@ -101,7 +102,7 @@ def create_expense():
101102 }
102103
103104 # Create expense
104- expense_id = service .create_expense (user_id , expense_data )
105+ expense_id , capacity_result = service .create_expense (user_id , expense_data )
105106
106107 # Handle file uploads
107108 if 'receipts' in request .files :
@@ -127,7 +128,21 @@ def create_expense():
127128
128129 # Fetch the created expense to return full data for optimistic UI
129130 created_expense = service .get_expense (expense_id , user_id )
130- return jsonify ({'success' : True , 'expense_id' : expense_id , 'expense' : created_expense })
131+
132+ # Build response
133+ response = {'success' : True , 'expense_id' : expense_id , 'expense' : created_expense }
134+
135+ # Add capping info if reimbursement was capped
136+ if capacity_result and capacity_result .was_capped :
137+ response ['reimbursement_capped' ] = True
138+ response ['capping_info' ] = {
139+ 'original_amount' : capacity_result .original_amount ,
140+ 'capped_amount' : capacity_result .capped_amount ,
141+ 'remaining_capacity' : capacity_result .remaining_capacity ,
142+ 'message' : f'Reimbursement was capped at ${ capacity_result .capped_amount :.2f} (remaining { expense_data ["category" ]} election capacity)'
143+ }
144+
145+ return jsonify (response )
131146
132147 except CategoryConflictError as e :
133148 conn .rollback ()
@@ -155,19 +170,30 @@ def update_expense(expense_id: int):
155170
156171 try :
157172 service = ExpenseService (conn )
158- service .update_expense (expense_id , user_id , data )
173+ _ , capacity_result = service .update_expense (expense_id , user_id , data )
159174 conn .commit ()
160175
161176 # Fetch the updated expense to return full data for optimistic UI
162177 updated_expense = service .get_expense (expense_id , user_id )
163- return jsonify ({'success' : True , 'expense' : updated_expense })
178+
179+ # Build response
180+ response = {'success' : True , 'expense' : updated_expense }
181+
182+ # Add capping info if reimbursement was capped
183+ if capacity_result and capacity_result .was_capped :
184+ response ['reimbursement_capped' ] = True
185+ response ['capping_info' ] = {
186+ 'original_amount' : capacity_result .original_amount ,
187+ 'capped_amount' : capacity_result .capped_amount ,
188+ 'remaining_capacity' : capacity_result .remaining_capacity ,
189+ 'message' : f'Reimbursement was capped at ${ capacity_result .capped_amount :.2f} (remaining election capacity)'
190+ }
191+
192+ return jsonify (response )
164193
165194 except CategoryConflictError as e :
166195 conn .rollback ()
167196 return jsonify ({'error' : e .message }), 400
168- except CapacityExceededError as e :
169- conn .rollback ()
170- return jsonify ({'error' : e .message }), 400
171197 except ExpenseValidationError as e :
172198 conn .rollback ()
173199 if 'not found' in e .message .lower ():
@@ -183,17 +209,19 @@ def update_expense(expense_id: int):
183209@expenses_bp .route ('/api/expenses/<int:expense_id>' , methods = ['PATCH' ])
184210@login_required
185211def patch_expense (expense_id : int ):
186- """API endpoint to partially update an expense (status, provider, amount_inflow)."""
212+ """API endpoint to partially update an expense (status, provider, amount_inflow).
213+
214+ Uses the ExpenseService to ensure FSA/DCFSA capacity validation and HSA balance tracking.
215+ """
187216 data = request .json
188217 user_id = session ['user_id' ]
189218 conn = get_db_connection ()
190219
191220 try :
221+ service = ExpenseService (conn )
222+
192223 # Get current expense for smart reimbursement logic
193- expense = conn .execute (
194- 'SELECT id, amount_outflow FROM expenses WHERE id = ? AND user_id = ?' ,
195- (expense_id , user_id )
196- ).fetchone ()
224+ expense = service .get_expense (expense_id , user_id )
197225
198226 if not expense :
199227 return jsonify ({'error' : 'Expense not found' }), 404
@@ -213,19 +241,36 @@ def patch_expense(expense_id: int):
213241 if not filtered_data :
214242 return jsonify ({'error' : 'No valid fields to update' }), 400
215243
216- # Build update query
217- fields = [f"{ field } = ?" for field in filtered_data .keys ()]
218- values = list (filtered_data .values ())
219- fields .append ("updated_at = ?" )
220- values .append (datetime .now ().isoformat ())
221- values .extend ([expense_id , user_id ])
222-
223- query = f"UPDATE expenses SET { ', ' .join (fields )} WHERE id = ? AND user_id = ?"
224- conn .execute (query , values )
244+ # Use the service to update (handles FSA/DCFSA capacity capping and HSA balance)
245+ _ , capacity_result = service .update_expense (expense_id , user_id , filtered_data )
225246 conn .commit ()
226247
227- return jsonify ({'success' : True })
248+ # Fetch updated expense
249+ updated_expense = service .get_expense (expense_id , user_id )
250+
251+ # Build response
252+ response = {'success' : True , 'expense' : updated_expense }
228253
254+ # Add capping info if reimbursement was capped
255+ if capacity_result and capacity_result .was_capped :
256+ response ['reimbursement_capped' ] = True
257+ response ['capping_info' ] = {
258+ 'original_amount' : capacity_result .original_amount ,
259+ 'capped_amount' : capacity_result .capped_amount ,
260+ 'remaining_capacity' : capacity_result .remaining_capacity ,
261+ 'message' : f'Reimbursement was capped at ${ capacity_result .capped_amount :.2f} (remaining { expense ["category" ]} election capacity)'
262+ }
263+
264+ return jsonify (response )
265+
266+ except CategoryConflictError as e :
267+ conn .rollback ()
268+ return jsonify ({'error' : e .message }), 400
269+ except ExpenseValidationError as e :
270+ conn .rollback ()
271+ if 'not found' in e .message .lower ():
272+ return jsonify ({'error' : e .message }), 404
273+ return jsonify ({'error' : e .message }), 400
229274 except Exception as e :
230275 conn .rollback ()
231276 return jsonify ({'error' : str (e )}), 500
0 commit comments