@marcos_feitoza/personal-finance-backen-trades-assets 1.1.0 → 1.2.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,18 @@
1
+ # [1.2.0](https://github.com/MarcosOps/personal-finance-backend-trades-assets/compare/v1.1.1...v1.2.0) (2025-11-28)
2
+
3
+
4
+ ### Features
5
+
6
+ * add confirmation dialog for deletion operations ([1a568c8](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/1a568c880a79b438dd421e9314b5785c88db6c36))
7
+ * update readme.me ([82c5d27](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/82c5d279aeea11ca73103178c84727c1a0d33e7a))
8
+
9
+ ## [1.1.1](https://github.com/MarcosOps/personal-finance-backend-trades-assets/compare/v1.1.0...v1.1.1) (2025-11-26)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * update logs ([5bfcc96](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/5bfcc965be907ec04583dc86e8a04caeb1ef8cb5))
15
+
1
16
  # [1.1.0](https://github.com/MarcosOps/personal-finance-backend-trades-assets/compare/v1.0.4...v1.1.0) (2025-11-25)
2
17
 
3
18
 
package/README.md CHANGED
@@ -1,22 +1,34 @@
1
- # Biblioteca de Trades e Ativos - Personal Finance
1
+ # Biblioteca de Trades e Ativos - Personal Finance Backend
2
2
 
3
- Este projeto contém a lógica de negócios específica para o gerenciamento de `Trades` (transações de compra/venda) e `Assets` (ativos como ações, ETFs, etc.).
3
+ Este projeto contém a lógica de negócios e os modelos de dados específicos para o gerenciamento de `Trades` (transações de compra/venda de ativos) e `Assets` (ativos como ações, criptomoedas, etc.).
4
4
 
5
5
  ## Propósito
6
6
 
7
- Esta biblioteca encapsula as regras para criar e validar trades. Sua principal responsabilidade é, ao receber um novo trade para um símbolo de ação que ainda não existe no banco de dados, buscar os detalhes desse ativo (nome, setor, etc.) usando uma API externa e salvá-lo corretamente.
7
+ O objetivo desta biblioteca é encapsular toda a lógica de criação, leitura, atualização e exclusão (soft-delete) de trades e ativos. Ela interage com serviços externos (como o `market-data-service` para buscar detalhes de ativos) e com o banco de dados via `personal-finance-backend-shared`.
8
+
9
+ ## Conteúdo Principal
10
+
11
+ - **`routers/trades.py`**: Define endpoints para a criação, listagem e exclusão (soft-delete) de trades.
12
+ - **`routers/assets.py`**: Define endpoints para a listagem e gerenciamento de ativos.
8
13
 
9
14
  ## Tecnologias
10
15
 
11
- - **Busca de Dados:** Usa a biblioteca `yfinance` para buscar os detalhes de novos ativos.
12
- - **Banco de Dados:** Interage com o banco de dados através da `personal-finance-backend-shared`.
16
+ - **Framework**: FastAPI (seus routers são usados por outros serviços)
17
+ - **Busca de Dados Externos**: Utiliza `httpx` para comunicação com o `market-data-service`.
18
+ - **Banco de Dados**: Interage com o PostgreSQL através da `personal-finance-backend-shared`.
19
+ - **Dependências Locais**:
20
+ - `personal-finance-backend-shared`: Para acesso aos modelos, `crud` e utilitários.
13
21
 
14
- ## Como Usar
22
+ ---
15
23
 
16
- Este projeto não é um serviço executável. Ele é projetado para ser instalado como uma dependência local pelo serviço principal (`core-transactions-service`).
24
+ ## Como Usar (Instalação como Dependência)
17
25
 
18
- No `requirements.txt` do serviço principal, adicione a seguinte linha para incluí-lo:
26
+ Este projeto não é um serviço executável por si só. Ele é projetado para ser instalado como uma dependência local por outros serviços do backend (ex: `personal-finance-backend-core`).
27
+
28
+ Para incluir esta biblioteca como uma dependência editável em outro serviço (por exemplo, no `requirements.txt` do `personal-finance-backend-core`), adicione a seguinte linha no `requirements.txt` do serviço consumidor:
19
29
 
20
30
  ```
21
- -e ./trades-assets-library
31
+ -e ./personal-finance-backend-trades-assets
22
32
  ```
33
+
34
+ **Importante:** Certifique-se de que o contexto de build do Docker (`Dockerfile`) ou o script de build (`dockerbuild.sh`) do serviço consumidor está copiando corretamente o diretório `personal-finance-backend-trades-assets` para o local esperado para que o `pip install -e` funcione.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-backen-trades-assets",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -3,15 +3,13 @@ from sqlalchemy.orm import Session
3
3
  from datetime import datetime
4
4
  from personal_finance_shared import models, schemas, crud
5
5
  from personal_finance_shared.database import get_db
6
- import logging
7
6
  import requests
8
7
  import os
9
8
  import httpx
10
9
  from personal_finance_shared.dependencies import get_current_user, get_current_token
10
+ from personal_finance_shared.logging_utils import get_logger, REQUEST_ID_CTX
11
11
 
12
-
13
- logging.basicConfig(level=logging.INFO)
14
- logger = logging.getLogger(__name__)
12
+ logger = get_logger("personal_finance_trades_assets.routers.trades")
15
13
 
16
14
  router = APIRouter(
17
15
  prefix="/api/trades",
@@ -21,18 +19,6 @@ router = APIRouter(
21
19
  MARKET_DATA_SERVICE_URL = os.getenv("MARKET_DATA_SERVICE_URL", "http://personal-finance-backend-market-data.app.svc.cluster.local:8000")
22
20
  BALANCE_SERVICE_URL = os.getenv("BALANCE_SERVICE_URL", "http://personal-finance-backend-balance-service.app.svc.cluster.local:8000")
23
21
 
24
- def _get_asset_details_from_service(symbol: str) -> dict:
25
- """Fetches asset details from the market-data service."""
26
- try:
27
- url = f"{MARKET_DATA_SERVICE_URL}/api/market-data/details/{symbol.upper()}"
28
- logger.info(f"Fetching details from: {url}")
29
- response = requests.post(url, timeout=10)
30
- response.raise_for_status()
31
- return response.json()
32
- except requests.exceptions.RequestException as e:
33
- logger.error(f"Error calling market-data service for {symbol}: {e}")
34
- return {}
35
-
36
22
  @router.post("/", response_model=schemas.TradeResponse)
37
23
  async def create_trade(
38
24
  trade: schemas.TradeCreate,
@@ -40,64 +26,72 @@ async def create_trade(
40
26
  current_user: models.User = Depends(get_current_user),
41
27
  token: str = Depends(get_current_token)
42
28
  ):
29
+ correlation_id = REQUEST_ID_CTX.get()
43
30
  trade_symbol = trade.symbol.upper()
44
- logger.info(f"Processing trade for user {current_user.id}, symbol: {trade_symbol}")
31
+ logger.info("Processing trade", extra={'user_id': current_user.id, 'symbol': trade_symbol, 'trade_data': trade.dict()})
45
32
 
46
- # Search for the asset globally, not by user_id
47
33
  db_asset = db.query(models.Asset).filter(
48
34
  models.Asset.symbol == trade_symbol
49
35
  ).first()
50
36
 
51
37
  if not db_asset:
52
- logger.info(f"Asset with symbol {trade_symbol} not found globally. Creating new asset...")
38
+ logger.info("Asset not found globally, creating new asset", extra={'user_id': current_user.id, 'symbol': trade_symbol})
53
39
  db_asset = models.Asset(
54
40
  symbol=trade_symbol,
55
41
  name=trade.name,
56
42
  asset_type=trade.asset_type,
57
43
  industry=trade.industry,
58
44
  id_crypto=trade.id_crypto if hasattr(trade, 'id_crypto') else None,
59
- # user_id is no longer part of Asset model
60
45
  )
61
46
  db.add(db_asset)
62
47
  db.commit()
63
48
  db.refresh(db_asset)
64
- logger.info(f"Created new global asset with ID: {db_asset.id}")
49
+ logger.info("Created new global asset", extra={'user_id': current_user.id, 'asset_id': db_asset.id, 'symbol': db_asset.symbol})
65
50
  else:
66
- logger.info(f"Found existing global asset with ID: {db_asset.id}")
51
+ logger.info("Found existing global asset", extra={'user_id': current_user.id, 'asset_id': db_asset.id, 'symbol': db_asset.symbol})
67
52
 
68
53
  if trade.trade_type == 'buy':
69
54
  investment_account_name = trade.investment_account
70
55
  trade_cost = trade.shares * trade.price
71
56
  current_balance = 0
72
57
  headers = {"Authorization": f"Bearer {token}"}
58
+ if correlation_id:
59
+ headers["X-Correlation-ID"] = correlation_id
73
60
  try:
74
61
  async with httpx.AsyncClient() as client:
75
62
  response = await client.get(f"{BALANCE_SERVICE_URL}/api/balance/{investment_account_name}", headers=headers)
76
63
  response.raise_for_status()
77
64
  data = response.json()
78
65
  current_balance = data['balance']
79
- logger.info(f"Calculated cash balance for '{investment_account_name}' is {current_balance}")
66
+ logger.info("Calculated cash balance for account", extra={'user_id': current_user.id, 'account': investment_account_name, 'balance': current_balance})
80
67
  except Exception as e:
81
- logger.error(f"Could not retrieve balance for validation for user {current_user.id}: {e}")
68
+ logger.error("Could not retrieve balance for validation", extra={'user_id': current_user.id, 'error': str(e), 'account': investment_account_name})
82
69
 
83
70
  if float(current_balance) < float(trade_cost):
84
- logger.warning(f"Insufficient funds in '{investment_account_name}' for user {current_user.id}. Balance: {current_balance}, Required: {trade_cost}")
71
+ error_detail = f"Insufficient cash balance in '{investment_account_name}'. Balance: {current_balance}, Required: {trade_cost}"
72
+ logger.warning("Insufficient funds for trade", extra={'user_id': current_user.id, 'account': investment_account_name, 'balance': current_balance, 'required': trade_cost})
85
73
  raise HTTPException(
86
74
  status_code=400,
87
- detail=f"Insufficient cash balance in '{investment_account_name}' to complete trade."
75
+ detail=error_detail
88
76
  )
89
77
 
90
78
  trade_data = trade.dict(exclude={'symbol', 'name', 'asset_type', 'industry', 'id_crypto'})
91
79
  db_trade = models.Trade(**trade_data, asset_id=db_asset.id, user_id=current_user.id)
92
80
 
93
81
  if db_trade.trade_type == 'buy':
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
+ logger.info("STOCK PURCHASE recorded", extra={'user_id': current_user.id, 'shares': float(db_trade.shares), 'symbol': trade_symbol, 'price': float(db_trade.price)})
95
83
  elif db_trade.trade_type == 'sell':
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
+ logger.info("STOCK SALE recorded", extra={'user_id': current_user.id, 'shares': float(db_trade.shares), 'symbol': trade_symbol, 'price': float(db_trade.price)})
97
85
 
98
86
  db.add(db_trade)
99
87
  db.commit()
100
88
  db.refresh(db_trade)
89
+ logger.info("Trade created successfully", extra={'user_id': current_user.id, 'trade_id': db_trade.id})
90
+
91
+ # Get and log asset summary after trade
92
+ account_summary = crud.get_account_summary(db=db, user_id=current_user.id, account_name=trade.investment_account)
93
+ logger.info("Account asset summary after trade", extra={'user_id': current_user.id, 'account': trade.investment_account, 'summary': account_summary})
94
+
101
95
  return db_trade
102
96
 
103
97
  @router.get("/", response_model=list[schemas.TradeResponse])
@@ -108,16 +102,16 @@ def read_trades(
108
102
  db: Session = Depends(get_db),
109
103
  current_user: models.User = Depends(get_current_user)
110
104
  ):
111
- logger.info(f"Reading trades for user {current_user.id}, account: {investment_account}")
105
+ logger.info("Reading trades", extra={'user_id': current_user.id, 'account': investment_account, 'skip': skip, 'limit': limit})
112
106
  query = db.query(models.Trade).filter(
113
107
  models.Trade.user_id == current_user.id,
114
- models.Trade.deleted_at.is_(None) # Filter out soft-deleted trades
108
+ models.Trade.deleted_at.is_(None)
115
109
  )
116
110
  if investment_account:
117
111
  query = query.filter(models.Trade.investment_account == investment_account)
118
112
 
119
113
  trades = query.order_by(models.Trade.date.desc()).offset(skip).limit(limit).all()
120
- logger.info(f"Found {len(trades)} trades for user {current_user.id}")
114
+ logger.info("Trades found", extra={'user_id': current_user.id, 'count': len(trades), 'account': investment_account})
121
115
  return trades
122
116
 
123
117
  @router.delete("/{trade_id}", response_model=schemas.TradeResponse)
@@ -126,20 +120,20 @@ def delete_trade(
126
120
  db: Session = Depends(get_db),
127
121
  current_user: models.User = Depends(get_current_user)
128
122
  ):
129
- logger.info(f"Attempting soft-delete for trade ID: {trade_id} by user: {current_user.id}")
123
+ logger.info("Attempting soft-delete for trade", extra={'user_id': current_user.id, 'trade_id': trade_id})
130
124
  db_trade = db.query(models.Trade).filter(
131
125
  models.Trade.id == trade_id,
132
126
  models.Trade.user_id == current_user.id,
133
- models.Trade.deleted_at.is_(None) # Ensure it's not already deleted
127
+ models.Trade.deleted_at.is_(None)
134
128
  ).first()
135
129
 
136
130
  if not db_trade:
137
- logger.warning(f"Trade ID: {trade_id} not found or already deleted for user: {current_user.id}")
131
+ logger.warning("Trade not found or already deleted for soft-delete", extra={'user_id': current_user.id, 'trade_id': trade_id})
138
132
  raise HTTPException(status_code=404, detail="Trade not found or already deleted")
139
133
 
140
- db_trade.deleted_at = datetime.utcnow() # Set deletion timestamp
134
+ db_trade.deleted_at = datetime.utcnow()
141
135
  db.add(db_trade)
142
136
  db.commit()
143
137
  db.refresh(db_trade)
144
- logger.info(f"Successfully soft-deleted trade ID: {trade_id} for user: {current_user.id}")
138
+ logger.info("Successfully soft-deleted trade", extra={'user_id': current_user.id, 'trade_id': trade_id})
145
139
  return db_trade