@marcos_feitoza/personal-finance-backen-trades-assets 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.1.0](https://github.com/MarcosOps/personal-finance-backend-trades-assets/compare/v1.0.4...v1.1.0) (2025-11-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * add Schema for updating a Trade ([315b157](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/315b157d5de5d5db82570fc941fb117a80f4cbfe))
7
+
8
+ ## [1.0.4](https://github.com/MarcosOps/personal-finance-backend-trades-assets/compare/v1.0.3...v1.0.4) (2025-11-21)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * update trades and assets to support current user ([7a976a5](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/7a976a576eea4ab9758e5fa20d5b1d314ae72912))
14
+
1
15
  ## [1.0.3](https://github.com/MarcosOps/personal-finance-backend-trades-assets/compare/v1.0.2...v1.0.3) (2025-11-11)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-backen-trades-assets",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -4,6 +4,8 @@ from sqlalchemy import distinct
4
4
  from personal_finance_shared import models, schemas
5
5
  from personal_finance_shared.database import get_db
6
6
  import logging
7
+ from personal_finance_shared.dependencies import get_current_user
8
+
7
9
 
8
10
  logging.basicConfig(level=logging.INFO)
9
11
  logger = logging.getLogger(__name__)
@@ -14,16 +16,23 @@ router = APIRouter(
14
16
  )
15
17
 
16
18
  @router.post("/", response_model=schemas.AssetResponse)
17
- def create_asset(asset: schemas.AssetCreate, db: Session = Depends(get_db)):
19
+ def create_asset(
20
+ asset: schemas.AssetCreate,
21
+ db: Session = Depends(get_db),
22
+ current_user: models.User = Depends(get_current_user) # Keep for authorization if needed
23
+ ):
24
+ # Assets are global now, check for symbol existence globally
18
25
  db_asset = db.query(models.Asset).filter(models.Asset.symbol == asset.symbol).first()
19
26
  if db_asset:
20
27
  raise HTTPException(status_code=400, detail="Asset with this symbol already exists")
28
+
29
+ # user_id is removed from Asset model
21
30
  db_asset = models.Asset(
22
31
  symbol=asset.symbol,
23
32
  name=asset.name,
24
33
  asset_type=asset.asset_type,
25
34
  industry=asset.industry,
26
- id_crypto=asset.id_crypto
35
+ id_crypto=asset.id_crypto,
27
36
  )
28
37
  db.add(db_asset)
29
38
  db.commit()
@@ -31,21 +40,26 @@ def create_asset(asset: schemas.AssetCreate, db: Session = Depends(get_db)):
31
40
  return db_asset
32
41
 
33
42
  @router.get("/", response_model=list[schemas.AssetResponse])
34
- def read_assets(investment_account: str = None, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
35
- logger.info(f"Reading assets for investment account: {investment_account}")
36
- query = db.query(models.Asset)
43
+ def read_assets(
44
+ investment_account: str = None,
45
+ skip: int = 0,
46
+ limit: int = 100,
47
+ db: Session = Depends(get_db),
48
+ current_user: models.User = Depends(get_current_user)
49
+ ):
50
+ logger.info(f"Reading assets for user {current_user.id}, investment account: {investment_account}")
51
+
52
+ # The query should join Trades and Assets to find assets owned by the user.
53
+ query = db.query(models.Asset).join(models.Trade).filter(
54
+ models.Trade.user_id == current_user.id
55
+ )
37
56
 
38
57
  if investment_account:
39
- # Get asset_ids from trades for the given investment_account
40
- asset_ids_in_account = db.query(distinct(models.Trade.asset_id)).filter(
41
- models.Trade.investment_account == investment_account
42
- ).all()
43
- # Extract just the IDs from the list of tuples
44
- asset_ids = [id for (id,) in asset_ids_in_account]
45
-
46
- # Filter assets by these IDs
47
- query = query.filter(models.Asset.id.in_(asset_ids))
58
+ query = query.filter(models.Trade.investment_account == investment_account)
59
+
60
+ # Use distinct to avoid duplicate assets if a user traded the same asset multiple times
61
+ query = query.distinct()
48
62
 
49
63
  assets = query.offset(skip).limit(limit).all()
50
- logger.info(f"Found {len(assets)} assets")
64
+ logger.info(f"Found {len(assets)} assets for user {current_user.id}")
51
65
  return assets
@@ -1,11 +1,14 @@
1
1
  from fastapi import APIRouter, Depends, HTTPException
2
2
  from sqlalchemy.orm import Session
3
+ from datetime import datetime
3
4
  from personal_finance_shared import models, schemas, crud
4
5
  from personal_finance_shared.database import get_db
5
6
  import logging
6
7
  import requests
7
8
  import os
8
9
  import httpx
10
+ from personal_finance_shared.dependencies import get_current_user, get_current_token
11
+
9
12
 
10
13
  logging.basicConfig(level=logging.INFO)
11
14
  logger = logging.getLogger(__name__)
@@ -31,56 +34,66 @@ def _get_asset_details_from_service(symbol: str) -> dict:
31
34
  return {}
32
35
 
33
36
  @router.post("/", response_model=schemas.TradeResponse)
34
- async def create_trade(trade: schemas.TradeCreate, db: Session = Depends(get_db)):
37
+ async def create_trade(
38
+ trade: schemas.TradeCreate,
39
+ db: Session = Depends(get_db),
40
+ current_user: models.User = Depends(get_current_user),
41
+ token: str = Depends(get_current_token)
42
+ ):
35
43
  trade_symbol = trade.symbol.upper()
36
- logger.info(f"Processing trade for symbol: {trade_symbol}")
44
+ logger.info(f"Processing trade for user {current_user.id}, symbol: {trade_symbol}")
37
45
 
38
- db_asset = db.query(models.Asset).filter(models.Asset.symbol == trade_symbol).first()
46
+ # Search for the asset globally, not by user_id
47
+ db_asset = db.query(models.Asset).filter(
48
+ models.Asset.symbol == trade_symbol
49
+ ).first()
39
50
 
40
51
  if not db_asset:
41
- logger.info(f"Asset with symbol {trade_symbol} not found. Creating new asset...")
52
+ logger.info(f"Asset with symbol {trade_symbol} not found globally. Creating new asset...")
42
53
  db_asset = models.Asset(
43
54
  symbol=trade_symbol,
44
55
  name=trade.name,
45
56
  asset_type=trade.asset_type,
46
57
  industry=trade.industry,
47
- id_crypto=trade.id_crypto if hasattr(trade, 'id_crypto') else None
58
+ id_crypto=trade.id_crypto if hasattr(trade, 'id_crypto') else None,
59
+ # user_id is no longer part of Asset model
48
60
  )
49
61
  db.add(db_asset)
50
62
  db.commit()
51
63
  db.refresh(db_asset)
52
- logger.info(f"Created new asset with ID: {db_asset.id}")
64
+ logger.info(f"Created new global asset with ID: {db_asset.id}")
53
65
  else:
54
- logger.info(f"Found existing asset with ID: {db_asset.id}")
66
+ logger.info(f"Found existing global asset with ID: {db_asset.id}")
55
67
 
56
68
  if trade.trade_type == 'buy':
57
69
  investment_account_name = trade.investment_account
58
70
  trade_cost = trade.shares * trade.price
59
71
  current_balance = 0
72
+ headers = {"Authorization": f"Bearer {token}"}
60
73
  try:
61
74
  async with httpx.AsyncClient() as client:
62
- response = await client.get(f"{BALANCE_SERVICE_URL}/api/balance/{investment_account_name}")
75
+ response = await client.get(f"{BALANCE_SERVICE_URL}/api/balance/{investment_account_name}", headers=headers)
63
76
  response.raise_for_status()
64
77
  data = response.json()
65
78
  current_balance = data['balance']
66
79
  logger.info(f"Calculated cash balance for '{investment_account_name}' is {current_balance}")
67
80
  except Exception as e:
68
- logger.error(f"Could not retrieve balance for validation: {e}")
81
+ logger.error(f"Could not retrieve balance for validation for user {current_user.id}: {e}")
69
82
 
70
83
  if float(current_balance) < float(trade_cost):
71
- logger.warning(f"Insufficient funds in '{investment_account_name}'. Balance: {current_balance}, Required: {trade_cost}")
84
+ logger.warning(f"Insufficient funds in '{investment_account_name}' for user {current_user.id}. Balance: {current_balance}, Required: {trade_cost}")
72
85
  raise HTTPException(
73
86
  status_code=400,
74
87
  detail=f"Insufficient cash balance in '{investment_account_name}' to complete trade."
75
88
  )
76
89
 
77
90
  trade_data = trade.dict(exclude={'symbol', 'name', 'asset_type', 'industry', 'id_crypto'})
78
- db_trade = models.Trade(**trade_data, asset_id=db_asset.id)
91
+ db_trade = models.Trade(**trade_data, asset_id=db_asset.id, user_id=current_user.id)
79
92
 
80
93
  if db_trade.trade_type == 'buy':
81
- logger.info(f"STOCK PURCHASE: Bought {db_trade.shares} shares of {trade_symbol} at ${db_trade.price:.2f} each.")
94
+ logger.info(f"STOCK PURCHASE for user {current_user.id}: Bought {db_trade.shares} shares of {trade_symbol} at ${db_trade.price:.2f} each.")
82
95
  elif db_trade.trade_type == 'sell':
83
- logger.info(f"STOCK SALE: Sold {db_trade.shares} shares of {trade_symbol} at ${db_trade.price:.2f} each.")
96
+ logger.info(f"STOCK SALE for user {current_user.id}: Sold {db_trade.shares} shares of {trade_symbol} at ${db_trade.price:.2f} each.")
84
97
 
85
98
  db.add(db_trade)
86
99
  db.commit()
@@ -92,13 +105,41 @@ def read_trades(
92
105
  investment_account: str = None,
93
106
  skip: int = 0,
94
107
  limit: int = 100,
95
- db: Session = Depends(get_db)
108
+ db: Session = Depends(get_db),
109
+ current_user: models.User = Depends(get_current_user)
96
110
  ):
97
- logger.info(f"Reading trades for account: {investment_account}")
98
- query = db.query(models.Trade)
111
+ logger.info(f"Reading trades for user {current_user.id}, account: {investment_account}")
112
+ query = db.query(models.Trade).filter(
113
+ models.Trade.user_id == current_user.id,
114
+ models.Trade.deleted_at.is_(None) # Filter out soft-deleted trades
115
+ )
99
116
  if investment_account:
100
117
  query = query.filter(models.Trade.investment_account == investment_account)
101
118
 
102
119
  trades = query.order_by(models.Trade.date.desc()).offset(skip).limit(limit).all()
103
- logger.info(f"Found {len(trades)} trades")
104
- return trades
120
+ logger.info(f"Found {len(trades)} trades for user {current_user.id}")
121
+ return trades
122
+
123
+ @router.delete("/{trade_id}", response_model=schemas.TradeResponse)
124
+ def delete_trade(
125
+ trade_id: int,
126
+ db: Session = Depends(get_db),
127
+ current_user: models.User = Depends(get_current_user)
128
+ ):
129
+ logger.info(f"Attempting soft-delete for trade ID: {trade_id} by user: {current_user.id}")
130
+ db_trade = db.query(models.Trade).filter(
131
+ models.Trade.id == trade_id,
132
+ models.Trade.user_id == current_user.id,
133
+ models.Trade.deleted_at.is_(None) # Ensure it's not already deleted
134
+ ).first()
135
+
136
+ if not db_trade:
137
+ logger.warning(f"Trade ID: {trade_id} not found or already deleted for user: {current_user.id}")
138
+ raise HTTPException(status_code=404, detail="Trade not found or already deleted")
139
+
140
+ db_trade.deleted_at = datetime.utcnow() # Set deletion timestamp
141
+ db.add(db_trade)
142
+ db.commit()
143
+ db.refresh(db_trade)
144
+ logger.info(f"Successfully soft-deleted trade ID: {trade_id} for user: {current_user.id}")
145
+ return db_trade