33from datetime import date , datetime , timezone
44from typing import List , Literal , Optional
55
6- from pydantic import BaseModel , Field , model_validator
6+ from pydantic import BaseModel , ConfigDict , Field , model_validator
77
88from .firefly_models import (
99 AccountRead ,
@@ -171,6 +171,8 @@ def to_transaction_split_store(self) -> TransactionSplitStore:
171171class ListAccountRequest (BaseModel ):
172172 """Request model for listing accounts."""
173173
174+ model_config = ConfigDict (extra = 'forbid' )
175+
174176 type : AccountTypeFilter = Field (
175177 ...,
176178 description = (
@@ -183,6 +185,8 @@ class ListAccountRequest(BaseModel):
183185class SearchAccountRequest (BaseModel ):
184186 """Request model for searching accounts."""
185187
188+ model_config = ConfigDict (extra = 'forbid' )
189+
186190 query : str = Field (
187191 ...,
188192 description = 'Text to search for in account names (supports partial matching)' ,
@@ -199,6 +203,8 @@ class SearchAccountRequest(BaseModel):
199203class GetAccountRequest (BaseModel ):
200204 """Request model for getting a single account."""
201205
206+ model_config = ConfigDict (extra = 'forbid' )
207+
202208 id : str = Field (
203209 ..., description = 'Unique identifier of the account (from list_accounts or search_accounts)'
204210 )
@@ -207,6 +213,8 @@ class GetAccountRequest(BaseModel):
207213class CreateWithdrawalRequest (BaseModel ):
208214 """Request model for creating a withdrawal transaction."""
209215
216+ model_config = ConfigDict (extra = 'forbid' )
217+
210218 amount : float = Field (
211219 ..., description = 'Amount to withdraw as positive number (e.g., 25.50 for $25.50 expense)'
212220 )
@@ -224,11 +232,20 @@ class CreateWithdrawalRequest(BaseModel):
224232 'Must be an asset account you own.'
225233 ),
226234 )
235+ destination_id : Optional [str ] = Field (
236+ default = None ,
237+ description = (
238+ 'ID of the expense account receiving the money (from list_accounts type=expense). '
239+ 'Use destination_id OR destination_name, not both. '
240+ 'If neither provided, defaults to Cash.'
241+ ),
242+ )
227243 destination_name : Optional [str ] = Field (
228244 default = None ,
229245 description = (
230246 'Where the money went ("Groceries", "Gas Station", "ATM"). '
231- 'Creates expense account if new. Leave blank for cash withdrawals.'
247+ 'Creates expense account if new. Use destination_id OR destination_name, not both. '
248+ 'If neither provided, defaults to Cash.'
232249 ),
233250 )
234251 budget_id : Optional [str ] = Field (
@@ -239,10 +256,21 @@ class CreateWithdrawalRequest(BaseModel):
239256 description = 'Name of budget if ID is unknown. Will use ID if both provided.' ,
240257 )
241258
259+ @model_validator (mode = 'after' )
260+ def validate_destination_mutual_exclusivity (self ):
261+ """Ensure destination_id and destination_name are not both provided."""
262+ if self .destination_id is not None and self .destination_name is not None :
263+ raise ValueError (
264+ 'Cannot specify both destination_id and destination_name. Use one or the other.'
265+ )
266+ return self
267+
242268
243269class CreateDepositRequest (BaseModel ):
244270 """Request model for creating a deposit transaction."""
245271
272+ model_config = ConfigDict (extra = 'forbid' )
273+
246274 amount : float = Field (
247275 ..., description = 'Amount received as positive number (e.g., 2500.00 for $2500 salary)'
248276 )
@@ -253,11 +281,20 @@ class CreateDepositRequest(BaseModel):
253281 default_factory = utc_now ,
254282 description = 'When the income was received (defaults to current time if not specified)' ,
255283 )
284+ source_id : Optional [str ] = Field (
285+ default = None ,
286+ description = (
287+ 'ID of the revenue account the money comes from (from list_accounts type=revenue). '
288+ 'Use source_id OR source_name, not both. '
289+ 'If neither provided, defaults to Cash.'
290+ ),
291+ )
256292 source_name : Optional [str ] = Field (
257293 default = None ,
258294 description = (
259295 'Where the money came from ("Employer", "Client Name", "Gift"). '
260- 'Creates revenue account if new.'
296+ 'Creates revenue account if new. Use source_id OR source_name, not both. '
297+ 'If neither provided, defaults to Cash.'
261298 ),
262299 )
263300 destination_id : str = Field (
@@ -268,10 +305,19 @@ class CreateDepositRequest(BaseModel):
268305 ),
269306 )
270307
308+ @model_validator (mode = 'after' )
309+ def validate_source_mutual_exclusivity (self ):
310+ """Ensure source_id and source_name are not both provided."""
311+ if self .source_id is not None and self .source_name is not None :
312+ raise ValueError ('Cannot specify both source_id and source_name. Use one or the other.' )
313+ return self
314+
271315
272316class CreateTransferRequest (BaseModel ):
273317 """Request model for creating a transfer transaction."""
274318
319+ model_config = ConfigDict (extra = 'forbid' )
320+
275321 amount : float = Field (
276322 ..., description = 'Amount to move as positive number (e.g., 500.00 to move $500)'
277323 )
@@ -296,6 +342,8 @@ class CreateTransferRequest(BaseModel):
296342class GetTransactionsRequest (BaseModel ):
297343 """Request model for retrieving transactions."""
298344
345+ model_config = ConfigDict (extra = 'forbid' )
346+
299347 account_id : Optional [str ] = Field (
300348 None ,
301349 description = (
@@ -332,6 +380,8 @@ class GetTransactionsRequest(BaseModel):
332380class SearchTransactionsRequest (BaseModel ):
333381 """Request model for searching transactions."""
334382
383+ model_config = ConfigDict (extra = 'forbid' )
384+
335385 query : str | None = Field (
336386 None ,
337387 description = (
@@ -453,12 +503,16 @@ def validate_search_criteria(self):
453503class DeleteTransactionRequest (BaseModel ):
454504 """Request model for deleting a transaction."""
455505
506+ model_config = ConfigDict (extra = 'forbid' )
507+
456508 id : str = Field (..., description = 'Unique identifier of the transaction to permanently remove' )
457509
458510
459511class GetTransactionRequest (BaseModel ):
460512 """Request model for getting a single transaction."""
461513
514+ model_config = ConfigDict (extra = 'forbid' )
515+
462516 id : str = Field (..., description = 'Unique identifier of the transaction to get details for' )
463517
464518
@@ -536,6 +590,8 @@ def from_transaction_array(
536590class ListBudgetsRequest (BaseModel ):
537591 """Request for listing budgets."""
538592
593+ model_config = ConfigDict (extra = 'forbid' )
594+
539595 active : Optional [bool ] = Field (
540596 None ,
541597 description = 'Show only active budgets (true), inactive (false), or all budgets '
@@ -546,6 +602,8 @@ class ListBudgetsRequest(BaseModel):
546602class GetBudgetRequest (BaseModel ):
547603 """Request for getting a single budget by ID."""
548604
605+ model_config = ConfigDict (extra = 'forbid' )
606+
549607 id : str = Field (..., description = 'Unique identifier of the budget to get details for' )
550608
551609
@@ -572,6 +630,8 @@ class BudgetSpending(BaseModel):
572630class GetBudgetSpendingRequest (BaseModel ):
573631 """Request for getting budget spending data."""
574632
633+ model_config = ConfigDict (extra = 'forbid' )
634+
575635 budget_id : str = Field (
576636 ..., description = 'Unique identifier of the budget to analyze spending for'
577637 )
@@ -601,6 +661,8 @@ class BudgetSummary(BaseModel):
601661class GetBudgetSummaryRequest (BaseModel ):
602662 """Request for getting budget summary."""
603663
664+ model_config = ConfigDict (extra = 'forbid' )
665+
604666 start_date : Optional [date ] = Field (
605667 None , description = 'Start date for summary period (YYYY-MM-DD), inclusive'
606668 )
@@ -621,6 +683,8 @@ class AvailableBudget(BaseModel):
621683class GetAvailableBudgetRequest (BaseModel ):
622684 """Request for getting available budget."""
623685
686+ model_config = ConfigDict (extra = 'forbid' )
687+
624688 start_date : Optional [date ] = Field (
625689 None ,
626690 description = 'Start date for budget analysis (YYYY-MM-DD). Defaults to '
@@ -637,6 +701,8 @@ class GetAvailableBudgetRequest(BaseModel):
637701class CreateBulkTransactionsRequest (BaseModel ):
638702 """Create multiple transactions in one operation."""
639703
704+ model_config = ConfigDict (extra = 'forbid' )
705+
640706 transactions : List [Transaction ] = Field (
641707 ...,
642708 description = (
@@ -672,6 +738,8 @@ def validate_transactions(self):
672738class UpdateTransactionRequest (BaseModel ):
673739 """Update an existing transaction."""
674740
741+ model_config = ConfigDict (extra = 'forbid' )
742+
675743 transaction_id : str = Field (..., description = 'Unique identifier of the transaction to modify' )
676744 amount : Optional [float ] = Field (None , description = 'New transaction amount (positive number)' )
677745 description : Optional [str ] = Field (
@@ -697,6 +765,8 @@ class UpdateTransactionRequest(BaseModel):
697765class BulkUpdateTransactionsRequest (BaseModel ):
698766 """Update multiple transactions in one operation."""
699767
768+ model_config = ConfigDict (extra = 'forbid' )
769+
700770 updates : List [UpdateTransactionRequest ] = Field (
701771 ...,
702772 description = 'Array of transaction modifications to apply in a single operation' ,
0 commit comments