Skip to content

Commit 86f85d8

Browse files
committed
Finished implementation
1 parent 6b42ec6 commit 86f85d8

File tree

4 files changed

+421
-3
lines changed

4 files changed

+421
-3
lines changed

backend/app/api/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
from fastapi import APIRouter
44

5-
from app.api.routes import items, login, misc, private, users
5+
from app.api.routes import items, login, misc, private, users, wallets
66
from app.core.config import settings
77

88
api_router = APIRouter()
99
api_router.include_router(login.router)
1010
api_router.include_router(users.router)
1111
api_router.include_router(misc.router)
1212
api_router.include_router(items.router)
13+
api_router.include_router(wallets.router)
1314

1415

1516
if settings.ENVIRONMENT == "local":

backend/app/api/routes/wallets.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Wallet management API endpoints."""
2+
3+
import uuid
4+
5+
from fastapi import APIRouter, HTTPException
6+
from sqlmodel import func, select
7+
8+
from app.api.deps import CurrentUser, SessionDep
9+
from app.constants import BAD_REQUEST_CODE, NOT_FOUND_CODE
10+
from app.crud import (
11+
create_transaction,
12+
create_wallet,
13+
get_user_wallets,
14+
get_wallet_by_id,
15+
get_wallet_transactions,
16+
)
17+
from app.models import (
18+
Transaction,
19+
TransactionCreate,
20+
TransactionPublic,
21+
TransactionsPublic,
22+
WalletCreate,
23+
WalletPublic,
24+
WalletsPublic,
25+
)
26+
27+
router = APIRouter(prefix="/wallets", tags=["wallets"])
28+
29+
30+
@router.post("/")
31+
def create_user_wallet(
32+
*,
33+
session: SessionDep,
34+
current_user: CurrentUser,
35+
wallet_in: WalletCreate,
36+
) -> WalletPublic:
37+
"""Create new wallet for the current user."""
38+
try:
39+
db_wallet = create_wallet(
40+
session=session,
41+
wallet_in=wallet_in,
42+
user_id=current_user.id,
43+
)
44+
return WalletPublic.model_validate(db_wallet)
45+
except ValueError as e:
46+
raise HTTPException(status_code=BAD_REQUEST_CODE, detail=str(e)) from e
47+
48+
49+
@router.get("/")
50+
def read_user_wallets(
51+
session: SessionDep,
52+
current_user: CurrentUser,
53+
) -> WalletsPublic:
54+
"""Retrieve current user's wallets."""
55+
wallets = get_user_wallets(session=session, user_id=current_user.id)
56+
wallet_publics = [WalletPublic.model_validate(wallet) for wallet in wallets]
57+
return WalletsPublic(wallet_data=wallet_publics, count=len(wallet_publics))
58+
59+
60+
@router.get("/{wallet_id}")
61+
def read_wallet(
62+
session: SessionDep,
63+
current_user: CurrentUser,
64+
wallet_id: uuid.UUID,
65+
) -> WalletPublic:
66+
"""Get wallet by ID."""
67+
db_wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
68+
if not db_wallet:
69+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
70+
71+
# Check ownership
72+
if db_wallet.user_id != current_user.id:
73+
raise HTTPException(
74+
status_code=BAD_REQUEST_CODE,
75+
detail="Not enough permissions",
76+
)
77+
78+
return WalletPublic.model_validate(db_wallet)
79+
80+
81+
@router.post("/{wallet_id}/transactions/")
82+
def create_wallet_transaction(
83+
*,
84+
session: SessionDep,
85+
current_user: CurrentUser,
86+
wallet_id: uuid.UUID,
87+
transaction_in: TransactionCreate,
88+
) -> TransactionPublic:
89+
"""Create new transaction for a wallet."""
90+
# Check wallet exists and belongs to user
91+
db_wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
92+
if not db_wallet:
93+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
94+
95+
if db_wallet.user_id != current_user.id:
96+
raise HTTPException(
97+
status_code=BAD_REQUEST_CODE,
98+
detail="Not enough permissions",
99+
)
100+
101+
try:
102+
db_transaction = create_transaction(
103+
session=session,
104+
transaction_in=transaction_in,
105+
wallet_id=wallet_id,
106+
)
107+
return TransactionPublic.model_validate(db_transaction)
108+
except ValueError as e:
109+
raise HTTPException(status_code=BAD_REQUEST_CODE, detail=str(e)) from e
110+
111+
112+
@router.get("/{wallet_id}/transactions/")
113+
def read_wallet_transactions(
114+
session: SessionDep,
115+
current_user: CurrentUser,
116+
wallet_id: uuid.UUID,
117+
skip: int = 0,
118+
limit: int = 100,
119+
) -> TransactionsPublic:
120+
"""Get transactions for a wallet."""
121+
# Check wallet exists and belongs to user
122+
db_wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
123+
if not db_wallet:
124+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
125+
126+
if db_wallet.user_id != current_user.id:
127+
raise HTTPException(
128+
status_code=BAD_REQUEST_CODE,
129+
detail="Not enough permissions",
130+
)
131+
132+
transactions = get_wallet_transactions(
133+
session=session,
134+
wallet_id=wallet_id,
135+
skip=skip,
136+
limit=limit,
137+
)
138+
139+
# Get total count for pagination
140+
count_statement = (
141+
select(func.count())
142+
.select_from(Transaction)
143+
.where(Transaction.wallet_id == wallet_id)
144+
)
145+
count = session.exec(count_statement).one()
146+
147+
transaction_publics = [TransactionPublic.model_validate(tx) for tx in transactions]
148+
return TransactionsPublic(transaction_data=transaction_publics, count=count)

backend/app/crud.py

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
"""CRUD operations for database models."""
22

33
import uuid
4+
from decimal import Decimal
45

5-
from sqlmodel import Session, select
6+
from sqlmodel import Session, desc, select
67

78
from app.core.security import get_password_hash, verify_password
8-
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
9+
from app.models import (
10+
CurrencyType,
11+
Item,
12+
ItemCreate,
13+
Transaction,
14+
TransactionCreate,
15+
TransactionType,
16+
User,
17+
UserCreate,
18+
UserUpdate,
19+
Wallet,
20+
WalletCreate,
21+
)
922

1023

1124
def create_user(*, session: Session, user_create: UserCreate) -> User:
@@ -57,3 +70,145 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
5770
session.commit()
5871
session.refresh(db_item)
5972
return db_item
73+
74+
75+
# Exchange rates for currency conversion (hardcoded as per requirements)
76+
EXCHANGE_RATES = {
77+
(CurrencyType.USD, CurrencyType.EUR): Decimal("0.85"),
78+
(CurrencyType.EUR, CurrencyType.USD): Decimal("1.18"),
79+
(CurrencyType.USD, CurrencyType.RUB): Decimal("75.0"),
80+
(CurrencyType.RUB, CurrencyType.USD): Decimal("0.013"),
81+
(CurrencyType.EUR, CurrencyType.RUB): Decimal("88.0"),
82+
(CurrencyType.RUB, CurrencyType.EUR): Decimal("0.011"),
83+
}
84+
85+
# Transaction fee percentage
86+
TRANSACTION_FEE_RATE = Decimal("0.02") # 2% fee for cross-currency transactions
87+
88+
# Wallet constraints
89+
MAX_WALLETS_PER_USER = 3
90+
91+
# Error messages
92+
WALLET_LIMIT_EXCEEDED = "User cannot have more than 3 wallets"
93+
WALLET_NOT_FOUND = "Wallet not found"
94+
INSUFFICIENT_BALANCE = "Insufficient balance for debit transaction"
95+
96+
97+
def create_wallet(
98+
*,
99+
session: Session,
100+
wallet_in: WalletCreate,
101+
user_id: uuid.UUID,
102+
) -> Wallet:
103+
"""Create a new wallet for a user."""
104+
# Check if user already has 3 wallets
105+
existing_wallets = session.exec(
106+
select(Wallet).where(Wallet.user_id == user_id),
107+
).all()
108+
109+
if len(existing_wallets) >= MAX_WALLETS_PER_USER:
110+
raise ValueError(WALLET_LIMIT_EXCEEDED)
111+
112+
# Check if user already has a wallet with this currency
113+
for wallet in existing_wallets:
114+
if wallet.currency == wallet_in.currency:
115+
msg = f"User already has a {wallet_in.currency} wallet"
116+
raise ValueError(msg)
117+
118+
db_wallet = Wallet.model_validate(
119+
wallet_in,
120+
update={"user_id": user_id, "balance": Decimal("0.00")},
121+
)
122+
session.add(db_wallet)
123+
session.commit()
124+
session.refresh(db_wallet)
125+
return db_wallet
126+
127+
128+
def get_wallet_by_id(*, session: Session, wallet_id: uuid.UUID) -> Wallet | None:
129+
"""Get wallet by ID."""
130+
return session.get(Wallet, wallet_id)
131+
132+
133+
def get_user_wallets(*, session: Session, user_id: uuid.UUID) -> list[Wallet]:
134+
"""Get all wallets for a user."""
135+
statement = select(Wallet).where(Wallet.user_id == user_id)
136+
return list(session.exec(statement).all())
137+
138+
139+
def create_transaction(
140+
*,
141+
session: Session,
142+
transaction_in: TransactionCreate,
143+
wallet_id: uuid.UUID,
144+
) -> Transaction:
145+
"""Create a new transaction for a wallet."""
146+
# Get the wallet
147+
wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
148+
if not wallet:
149+
raise ValueError(WALLET_NOT_FOUND)
150+
151+
# Determine transaction currency (default to wallet currency if not specified)
152+
transaction_currency = transaction_in.currency or wallet.currency
153+
amount = transaction_in.amount
154+
155+
# Handle currency conversion and fees if needed
156+
if transaction_currency != wallet.currency:
157+
if (transaction_currency, wallet.currency) not in EXCHANGE_RATES:
158+
msg = (
159+
f"Currency conversion from {transaction_currency} "
160+
f"to {wallet.currency} not supported"
161+
)
162+
raise ValueError(msg)
163+
164+
# Convert amount to wallet currency
165+
rate = EXCHANGE_RATES[(transaction_currency, wallet.currency)]
166+
amount = amount * rate
167+
168+
# Apply transaction fee
169+
fee = amount * TRANSACTION_FEE_RATE
170+
amount = amount - fee
171+
172+
# Check balance for debit transactions
173+
if transaction_in.transaction_type == TransactionType.DEBIT:
174+
if wallet.balance < amount:
175+
raise ValueError(INSUFFICIENT_BALANCE)
176+
new_balance = wallet.balance - amount
177+
else: # Credit transaction
178+
new_balance = wallet.balance + amount
179+
180+
# Update wallet balance
181+
wallet.balance = new_balance.quantize(Decimal("0.01"))
182+
session.add(wallet)
183+
184+
# Create transaction record
185+
db_transaction = Transaction.model_validate(
186+
transaction_in,
187+
update={
188+
"wallet_id": wallet_id,
189+
"amount": amount.quantize(Decimal("0.01")),
190+
"currency": transaction_currency,
191+
},
192+
)
193+
session.add(db_transaction)
194+
session.commit()
195+
session.refresh(db_transaction)
196+
return db_transaction
197+
198+
199+
def get_wallet_transactions(
200+
*,
201+
session: Session,
202+
wallet_id: uuid.UUID,
203+
skip: int = 0,
204+
limit: int = 100,
205+
) -> list[Transaction]:
206+
"""Get transactions for a wallet."""
207+
statement = (
208+
select(Transaction)
209+
.where(Transaction.wallet_id == wallet_id)
210+
.order_by(desc(Transaction.timestamp))
211+
.offset(skip)
212+
.limit(limit)
213+
)
214+
return list(session.exec(statement).all())

0 commit comments

Comments
 (0)