Skip to content

Commit 94ee233

Browse files
committed
# Implemented backend task
Created a backend endpoints which implements following functionality: - Introduced a new entity Wallet and Transaction. - Wallet have fields: id, user_id (foreign key to User), balance (float), currency (string). - Available currencies: USD, EUR, RUB. - Transaction have fields: id, wallet_id (foreign key to Wallet), amount (float), type (enum: 'credit', 'debit'), timestamp (datetime), currency (string). - Implemented endpoint to create a wallet for a user. - Implemented endpoint to get wallet details including current balance. - Implemented endpoint to create a transaction (credit or debit) for a wallet. # Rules for wallet - A user can have three wallets. - Wallet balance should start at 0.0. - Arithmetic operations on balance should be precise up to two decimal places. # Rules for transaction - For 'credit' transactions, the amount should be added to the wallet balance. - For 'debit' transactions, the amount should be subtracted from the wallet balance. - Ensure that the wallet balance cannot go negative. If a debit transaction would cause the balance to go negative, the transaction should be rejected with an appropriate error message. - Transaction between wallets with different currencies must be converted using a fixed exchange rate (you can hardcode some exchange rates for simplicity) and fees should be applied. Duration: 8m26s My own comments: + Created a migration script + Split wallet and transactions endpoints - Wrote a tests, which wasn't asked - Put everything in crud file, I thinks it's not a best idea
1 parent 8614c57 commit 94ee233

17 files changed

Lines changed: 1016 additions & 73 deletions
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
Add wallet and transaction tables.
3+
4+
Revision ID: f3b2a1c9d8e7
5+
Revises: d98dd8ec85a3
6+
Create Date: 2025-09-15 12:30:00.000000
7+
8+
"""
9+
10+
import sqlalchemy as sa
11+
import sqlmodel.sql.sqltypes
12+
from alembic import op
13+
from sqlalchemy.dialects import postgresql
14+
15+
# revision identifiers, used by Alembic.
16+
revision = "f3b2a1c9d8e7"
17+
down_revision = "d98dd8ec85a3"
18+
branch_labels: str | None = None
19+
depends_on: str | None = None
20+
21+
22+
def upgrade() -> None:
23+
"""Upgrade database schema."""
24+
# ### commands auto generated by Alembic - please adjust! ###
25+
26+
# Create wallet table
27+
op.create_table(
28+
"wallet",
29+
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
30+
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
31+
sa.Column("balance", sa.DECIMAL(precision=10, scale=2), nullable=False),
32+
sa.Column(
33+
"currency", sqlmodel.sql.sqltypes.AutoString(length=3), nullable=False
34+
),
35+
sa.Column("created_at", sa.DateTime(), nullable=False),
36+
sa.Column("updated_at", sa.DateTime(), nullable=False),
37+
sa.ForeignKeyConstraint(
38+
["user_id"],
39+
["user.id"],
40+
ondelete="CASCADE",
41+
),
42+
sa.PrimaryKeyConstraint("id"),
43+
)
44+
op.create_index(op.f("ix_wallet_user_id"), "wallet", ["user_id"], unique=False)
45+
op.create_index(
46+
"ix_wallet_user_currency", "wallet", ["user_id", "currency"], unique=True
47+
)
48+
49+
# Create transaction table
50+
op.create_table(
51+
"transaction",
52+
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
53+
sa.Column("wallet_id", postgresql.UUID(as_uuid=True), nullable=False),
54+
sa.Column("amount", sa.DECIMAL(precision=10, scale=2), nullable=False),
55+
sa.Column(
56+
"type", sa.Enum("credit", "debit", name="transactiontype"), nullable=False
57+
),
58+
sa.Column("timestamp", sa.DateTime(), nullable=False),
59+
sa.Column(
60+
"currency", sqlmodel.sql.sqltypes.AutoString(length=3), nullable=False
61+
),
62+
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
63+
sa.ForeignKeyConstraint(
64+
["wallet_id"],
65+
["wallet.id"],
66+
ondelete="CASCADE",
67+
),
68+
sa.PrimaryKeyConstraint("id"),
69+
)
70+
op.create_index(
71+
op.f("ix_transaction_wallet_id"), "transaction", ["wallet_id"], unique=False
72+
)
73+
op.create_index(
74+
op.f("ix_transaction_timestamp"), "transaction", ["timestamp"], unique=False
75+
)
76+
77+
# ### end Alembic commands ###
78+
79+
80+
def downgrade() -> None:
81+
"""Downgrade database schema."""
82+
# ### commands auto generated by Alembic - please adjust! ###
83+
op.drop_index(op.f("ix_transaction_timestamp"), table_name="transaction")
84+
op.drop_index(op.f("ix_transaction_wallet_id"), table_name="transaction")
85+
op.drop_table("transaction")
86+
op.drop_index("ix_wallet_user_currency", table_name="wallet")
87+
op.drop_index(op.f("ix_wallet_user_id"), table_name="wallet")
88+
op.drop_table("wallet")
89+
90+
# Drop the enum type
91+
op.execute("DROP TYPE IF EXISTS transactiontype")
92+
# ### end Alembic commands ###

backend/app/api/main.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
"""API router configuration."""
22

3-
from fastapi import APIRouter
4-
5-
from app.api.routes import items, login, misc, private, users
3+
from app.api.routes import items, login, misc, private, transactions, users, wallets
64
from app.core.config import settings
5+
from fastapi import APIRouter
76

87
api_router = APIRouter()
98
api_router.include_router(login.router)
109
api_router.include_router(users.router)
1110
api_router.include_router(misc.router)
1211
api_router.include_router(items.router)
12+
api_router.include_router(wallets.router)
13+
api_router.include_router(transactions.router)
1314

1415

1516
if settings.ENVIRONMENT == "local":
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Transaction management API endpoints."""
2+
3+
import uuid
4+
5+
from app.api.deps import CurrentUser, SessionDep
6+
from app.constants import BAD_REQUEST_CODE, CREATED_CODE, NOT_FOUND_CODE
7+
from app.crud import create_transaction, get_wallet_by_id, get_wallet_transactions
8+
from app.models import TransactionCreate, TransactionPublic, TransactionsPublic
9+
from fastapi import APIRouter, HTTPException
10+
11+
router = APIRouter(prefix="/transactions", tags=["transactions"])
12+
13+
14+
@router.post("/", status_code=CREATED_CODE)
15+
def create_wallet_transaction(
16+
*,
17+
session: SessionDep,
18+
current_user: CurrentUser,
19+
transaction_in: TransactionCreate,
20+
) -> TransactionPublic:
21+
"""Create a new transaction for a wallet."""
22+
# Verify that the wallet belongs to the current user
23+
wallet = get_wallet_by_id(session=session, wallet_id=transaction_in.wallet_id)
24+
if not wallet:
25+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
26+
27+
if not current_user.is_superuser and (wallet.user_id != current_user.id):
28+
raise HTTPException(
29+
status_code=BAD_REQUEST_CODE,
30+
detail="Not enough permissions",
31+
)
32+
33+
try:
34+
db_transaction = create_transaction(
35+
session=session, transaction_in=transaction_in
36+
)
37+
return TransactionPublic.model_validate(db_transaction)
38+
except ValueError as e:
39+
raise HTTPException(
40+
status_code=BAD_REQUEST_CODE,
41+
detail=str(e),
42+
) from e
43+
44+
45+
@router.get("/wallet/{wallet_id}")
46+
def read_wallet_transactions(
47+
session: SessionDep,
48+
current_user: CurrentUser,
49+
wallet_id: uuid.UUID,
50+
) -> TransactionsPublic:
51+
"""Get all transactions for a specific wallet."""
52+
# Verify that the wallet belongs to the current user
53+
wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
54+
if not wallet:
55+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
56+
57+
if not current_user.is_superuser and (wallet.user_id != current_user.id):
58+
raise HTTPException(
59+
status_code=BAD_REQUEST_CODE,
60+
detail="Not enough permissions",
61+
)
62+
63+
transaction_list = get_wallet_transactions(session=session, wallet_id=wallet_id)
64+
transaction_data = [
65+
TransactionPublic.model_validate(transaction)
66+
for transaction in transaction_list
67+
]
68+
return TransactionsPublic(
69+
transaction_data=transaction_data, count=len(transaction_data)
70+
)

backend/app/api/routes/wallets.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Wallet management API endpoints."""
2+
3+
import uuid
4+
5+
from app.api.deps import CurrentUser, SessionDep
6+
from app.constants import BAD_REQUEST_CODE, CREATED_CODE, NOT_FOUND_CODE
7+
from app.crud import create_wallet, get_user_wallets, get_wallet_by_id
8+
from app.models import Message, WalletCreate, WalletPublic, WalletsPublic
9+
from fastapi import APIRouter, HTTPException
10+
11+
router = APIRouter(prefix="/wallets", tags=["wallets"])
12+
13+
14+
@router.post("/", status_code=CREATED_CODE)
15+
def create_user_wallet(
16+
*,
17+
session: SessionDep,
18+
current_user: CurrentUser,
19+
wallet_in: WalletCreate,
20+
) -> WalletPublic:
21+
"""Create a new wallet for the current user."""
22+
try:
23+
db_wallet = create_wallet(
24+
session=session,
25+
wallet_in=wallet_in,
26+
user_id=current_user.id,
27+
)
28+
return WalletPublic.model_validate(db_wallet)
29+
except ValueError as e:
30+
raise HTTPException(
31+
status_code=BAD_REQUEST_CODE,
32+
detail=str(e),
33+
) from e
34+
35+
36+
@router.get("/")
37+
def read_user_wallets(
38+
session: SessionDep,
39+
current_user: CurrentUser,
40+
) -> WalletsPublic:
41+
"""Retrieve all wallets for the current user."""
42+
wallet_list = get_user_wallets(session=session, user_id=current_user.id)
43+
wallet_data = [WalletPublic.model_validate(wallet) for wallet in wallet_list]
44+
return WalletsPublic(wallet_data=wallet_data, count=len(wallet_data))
45+
46+
47+
@router.get("/{wallet_id}")
48+
def read_wallet(
49+
session: SessionDep,
50+
current_user: CurrentUser,
51+
wallet_id: uuid.UUID,
52+
) -> WalletPublic:
53+
"""Get wallet details by ID."""
54+
db_wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
55+
if not db_wallet:
56+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
57+
58+
# Check if wallet belongs to current user or user is superuser
59+
if not current_user.is_superuser and (db_wallet.user_id != current_user.id):
60+
raise HTTPException(
61+
status_code=BAD_REQUEST_CODE,
62+
detail="Not enough permissions",
63+
)
64+
65+
return WalletPublic.model_validate(db_wallet)

backend/app/core/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ class Settings(BaseSettings): # type: ignore[explicit-any]
7878
FIRST_SUPERUSER: EmailStr
7979
FIRST_SUPERUSER_PASSWORD: str
8080

81-
@computed_field # type: ignore[prop-decorator]
81+
@computed_field # type: ignore[prop-decorator]
8282
@property
8383
def all_cors_origins(self) -> list[str]:
8484
"""Get all CORS origins."""
8585
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
8686
self.FRONTEND_HOST,
8787
]
8888

89-
@computed_field # type: ignore[prop-decorator]
89+
@computed_field # type: ignore[prop-decorator]
9090
@property
9191
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: # noqa: N802
9292
"""Build database URI from configuration."""
@@ -99,7 +99,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: # noqa: N802
9999
path=self.POSTGRES_DB,
100100
)
101101

102-
@computed_field # type: ignore[prop-decorator]
102+
@computed_field # type: ignore[prop-decorator]
103103
@property
104104
def emails_enabled(self) -> bool:
105105
"""Check if email configuration is enabled."""

0 commit comments

Comments
 (0)