@marcos_feitoza/personal-finance-backen-trades-assets 1.0.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.
@@ -0,0 +1,67 @@
1
+ version: 2.1
2
+
3
+ jobs:
4
+ build-and-push:
5
+ machine: true
6
+ steps:
7
+ - checkout
8
+ - run:
9
+ name: Check for PR or main branch
10
+ command: |
11
+ if [ -z "${CIRCLE_PULL_REQUEST}" ] && [ "${CIRCLE_BRANCH}" != "main" ]; then
12
+ echo "Not a PR and not main. Halting job."
13
+ circleci-agent step halt
14
+ fi
15
+ - run:
16
+ name: Install buildx and QEMU for multi-arch builds
17
+ command: |
18
+ sudo apt-get update
19
+ sudo apt-get install -y qemu-user-static
20
+ docker run --privileged --rm tonistiigi/binfmt --install all
21
+ docker buildx create --name mybuilder --use
22
+ docker buildx inspect --bootstrap
23
+ - run:
24
+ name: Login to Docker Hub
25
+ command: |
26
+ echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
27
+ - run:
28
+ name: Build and push frontend image
29
+ command: |
30
+ COMMIT_SHA=${CIRCLE_SHA1:0:7}
31
+ IMAGE_TAG="$DOCKER_REPO/$CIRCLE_PROJECT_REPONAME:$COMMIT_SHA"
32
+
33
+ echo "Building image for frontend"
34
+ echo "Tag: $IMAGE_TAG"
35
+
36
+ docker buildx build --platform=linux/amd64,linux/arm64,linux/arm/v7 \
37
+ --tag $IMAGE_TAG \
38
+ --push .
39
+
40
+ release:
41
+ docker:
42
+ - image: circleci/node:latest
43
+ steps:
44
+ - checkout
45
+ - run:
46
+ name: Install dependencies
47
+ command: npm install
48
+ - run:
49
+ name: Run semantic-release
50
+ command: npx semantic-release
51
+
52
+ workflows:
53
+ build-and-release:
54
+ jobs:
55
+ - build-and-push:
56
+ filters:
57
+ branches:
58
+ only:
59
+ - main
60
+ - /.*/ # permite todas as branches, mas o job se auto-interrompe se não for PR
61
+ - release:
62
+ requires:
63
+ - build-and-push
64
+ filters:
65
+ branches:
66
+ only:
67
+ - main
@@ -0,0 +1,23 @@
1
+ version: 2.1
2
+
3
+ jobs:
4
+ release:
5
+ docker:
6
+ - image: circleci/node:latest
7
+ steps:
8
+ - checkout
9
+ - run:
10
+ name: Install dependencies
11
+ command: npm install
12
+ - run:
13
+ name: Run semantic-release
14
+ command: npx semantic-release
15
+
16
+ workflows:
17
+ build-and-release:
18
+ jobs:
19
+ - release:
20
+ filters:
21
+ branches:
22
+ only:
23
+ - main
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # 1.0.0 (2025-09-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * remove files que nao usaremos ([d5ab4eb](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/d5ab4ebcb540472b46a6021230855c34587ffa63))
7
+ * update docker (recovery point) ([fe0b8d5](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/fe0b8d510c5f252066b996ade940203aa87575f1))
8
+
9
+
10
+ ### Features
11
+
12
+ * general fix ([cf1a13a](https://github.com/MarcosOps/personal-finance-backend-trades-assets/commit/cf1a13a42740bbb49e26ae3eb85d9c8fde158581))
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # Biblioteca de Trades e Ativos - Personal Finance
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.).
4
+
5
+ ## Propósito
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.
8
+
9
+ ## Tecnologias
10
+
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`.
13
+
14
+ ## Como Usar
15
+
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`).
17
+
18
+ No `requirements.txt` do serviço principal, adicione a seguinte linha para incluí-lo:
19
+
20
+ ```
21
+ -e ./trades-assets-library
22
+ ```
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@marcos_feitoza/personal-finance-backen-trades-assets",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "scripts": {
8
+ "release": "semantic-release"
9
+ },
10
+ "devDependencies": {
11
+ "semantic-release": "^18.0.0",
12
+ "@semantic-release/changelog": "^6.0.0",
13
+ "@semantic-release/git": "^10.0.0",
14
+ "@semantic-release/github": "^8.0.0"
15
+ },
16
+ "release": {
17
+ "branches": [
18
+ "main"
19
+ ],
20
+ "plugins": [
21
+ "@semantic-release/commit-analyzer",
22
+ "@semantic-release/release-notes-generator",
23
+ "@semantic-release/changelog",
24
+ "@semantic-release/npm",
25
+ "@semantic-release/github",
26
+ "@semantic-release/git"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .routers import trades, assets
4
+
5
+ app = FastAPI()
6
+
7
+ app.add_middleware(
8
+ CORSMiddleware,
9
+ allow_origins=["*"], # Allows all origins
10
+ allow_credentials=True,
11
+ allow_methods=["*"], # Allows all methods
12
+ allow_headers=["*"], # Allows all headers
13
+ )
14
+
15
+ app.include_router(trades.router)
16
+ app.include_router(assets.router)
17
+
18
+ @app.get("/api/")
19
+ def read_root():
20
+ return {"message": "Trades and Assets API"}
@@ -0,0 +1,45 @@
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from sqlalchemy import distinct
4
+ from personal_finance_shared import models, schemas
5
+ from personal_finance_shared.database import get_db
6
+ import logging
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter(
12
+ prefix="/api/assets",
13
+ tags=["assets"],
14
+ )
15
+
16
+ @router.post("/", response_model=schemas.AssetResponse)
17
+ def create_asset(asset: schemas.AssetCreate, db: Session = Depends(get_db)):
18
+ db_asset = db.query(models.Asset).filter(models.Asset.symbol == asset.symbol).first()
19
+ if db_asset:
20
+ raise HTTPException(status_code=400, detail="Asset with this symbol already exists")
21
+ db_asset = models.Asset(**asset.dict())
22
+ db.add(db_asset)
23
+ db.commit()
24
+ db.refresh(db_asset)
25
+ return db_asset
26
+
27
+ @router.get("/", response_model=list[schemas.AssetResponse])
28
+ def read_assets(investment_account: str = None, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
29
+ logger.info(f"Reading assets for investment account: {investment_account}")
30
+ query = db.query(models.Asset)
31
+
32
+ if investment_account:
33
+ # Get asset_ids from trades for the given investment_account
34
+ asset_ids_in_account = db.query(distinct(models.Trade.asset_id)).filter(
35
+ models.Trade.investment_account == investment_account
36
+ ).all()
37
+ # Extract just the IDs from the list of tuples
38
+ asset_ids = [id for (id,) in asset_ids_in_account]
39
+
40
+ # Filter assets by these IDs
41
+ query = query.filter(models.Asset.id.in_(asset_ids))
42
+
43
+ assets = query.offset(skip).limit(limit).all()
44
+ logger.info(f"Found {len(assets)} assets")
45
+ return assets
@@ -0,0 +1,114 @@
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from personal_finance_shared import models, schemas, crud
4
+ from personal_finance_shared.database import get_db
5
+ import logging
6
+ import requests
7
+ import os
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter(
13
+ prefix="/api/trades",
14
+ tags=["trades"],
15
+ )
16
+
17
+ MARKET_DATA_SERVICE_URL = os.getenv("MARKET_DATA_SERVICE_URL", "http://personal-finance-backend-market-data:8000")
18
+
19
+ def _get_asset_details_from_service(symbol: str) -> dict:
20
+ """Fetches asset details from the market-data service."""
21
+ try:
22
+ url = f"{MARKET_DATA_SERVICE_URL}/api/market-data/details/{symbol.upper()}"
23
+ logger.info(f"Fetching details from: {url}")
24
+ response = requests.post(url, timeout=10) # Using POST as per the new endpoint
25
+ response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
26
+ return response.json()
27
+ except requests.exceptions.RequestException as e:
28
+ logger.error(f"Error calling market-data service for {symbol}: {e}")
29
+ return {}
30
+
31
+
32
+
33
+ @router.post("/", response_model=schemas.TradeResponse)
34
+ def create_trade(trade: schemas.TradeCreate, db: Session = Depends(get_db)):
35
+ trade_symbol = trade.symbol.upper()
36
+ logger.info(f"Processing trade for symbol: {trade_symbol}")
37
+
38
+ # Check if asset exists with the provided symbol or its canonical variations
39
+ db_asset = db.query(models.Asset).filter(models.Asset.symbol == trade_symbol).first()
40
+ if not db_asset and '.' not in trade_symbol:
41
+ db_asset = db.query(models.Asset).filter(models.Asset.symbol == f"{trade_symbol}.TO").first()
42
+
43
+ if not db_asset:
44
+ logger.info(f"Asset with symbol {trade_symbol} not found. Fetching details...")
45
+
46
+ asset_details = _get_asset_details_from_service(trade_symbol)
47
+ canonical_symbol = asset_details.get("canonical_symbol", trade_symbol)
48
+
49
+ # Double-check if an asset with the canonical symbol already exists
50
+ db_asset = db.query(models.Asset).filter(models.Asset.symbol == canonical_symbol).first()
51
+
52
+ if not db_asset:
53
+ logger.info(f"Creating new asset with canonical symbol: {canonical_symbol}")
54
+ db_asset = models.Asset(
55
+ symbol=canonical_symbol,
56
+ name=asset_details.get("name", canonical_symbol),
57
+ asset_type=asset_details.get("asset_type", "Unknown"),
58
+ industry=asset_details.get("industry", "N/A")
59
+ )
60
+ db.add(db_asset)
61
+ db.commit()
62
+ db.refresh(db_asset)
63
+ logger.info(f"Created new asset with ID: {db_asset.id}")
64
+ else:
65
+ logger.info(f"Found existing asset with canonical symbol: {canonical_symbol}")
66
+ else:
67
+ logger.info(f"Found existing asset with ID: {db_asset.id}")
68
+
69
+ # --- Balance Validation for 'buy' trades ---
70
+ if trade.trade_type == 'buy':
71
+ investment_account_name = trade.investment_account
72
+ trade_cost = trade.shares * trade.price
73
+
74
+ current_balance = crud.get_investment_account_balance(db=db, account_name=investment_account_name)
75
+ logger.info(f"Calculated cash balance for '{investment_account_name}' is {current_balance}")
76
+
77
+ if current_balance < trade_cost:
78
+ logger.warning(f"Insufficient funds in '{investment_account_name}'. Balance: {current_balance}, Required: {trade_cost}")
79
+ raise HTTPException(
80
+ status_code=400,
81
+ detail=f"Insufficient cash balance in '{investment_account_name}' to complete trade."
82
+ )
83
+ # --- End Validation Logic ---
84
+
85
+ # Create the trade record, linking it to the asset via asset_id
86
+ trade_data = trade.dict(exclude={'symbol', 'name', 'asset_type', 'industry'})
87
+ db_trade = models.Trade(**trade_data, asset_id=db_asset.id)
88
+
89
+ # Log for stock purchase/sale
90
+ if db_trade.trade_type == 'buy':
91
+ logger.info(f"STOCK PURCHASE: Bought {db_trade.shares} shares of {trade_symbol} at ${db_trade.price:.2f} each.")
92
+ elif db_trade.trade_type == 'sell':
93
+ logger.info(f"STOCK SALE: Sold {db_trade.shares} shares of {trade_symbol} at ${db_trade.price:.2f} each.")
94
+
95
+ db.add(db_trade)
96
+ db.commit()
97
+ db.refresh(db_trade)
98
+ return db_trade
99
+
100
+ @router.get("/", response_model=list[schemas.TradeResponse])
101
+ def read_trades(
102
+ investment_account: str = None,
103
+ skip: int = 0,
104
+ limit: int = 100,
105
+ db: Session = Depends(get_db)
106
+ ):
107
+ logger.info(f"Reading trades for account: {investment_account}")
108
+ query = db.query(models.Trade)
109
+ if investment_account:
110
+ query = query.filter(models.Trade.investment_account == investment_account)
111
+
112
+ trades = query.order_by(models.Trade.date.desc()).offset(skip).limit(limit).all()
113
+ logger.info(f"Found {len(trades)} trades")
114
+ return trades
@@ -0,0 +1,6 @@
1
+ psycopg2-binary
2
+ sqlalchemy
3
+ alembic
4
+ python-dotenv
5
+ -e ./shared-library
6
+ requests
package/setup.py ADDED
@@ -0,0 +1,11 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="personal_finance_trades_assets",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ description="Library for trades and assets business logic.",
8
+ install_requires=[
9
+ 'requests',
10
+ ],
11
+ )