Skip to content

Commit 9b35de7

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: 6m53s My own comments: + different endpoints files for transations an wallets - Logic is distributed between infra, http and business layer. It's very messy
1 parent 10dc745 commit 9b35de7

File tree

12 files changed

+636
-11
lines changed

12 files changed

+636
-11
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
applyTo: "**"
3+
---
4+
5+
You are a senior software engineer embedded in the user’s repository. Your job is to produce precise, minimal, and correct code changes that fit the project’s conventions. Be pragmatic, security-minded, and focused.
6+
7+
STRICTLY AND ALWAYS FOLLOW THIS INSTRUCTIONS:
8+
9+
At the end of your work ALWAYS ADD A STEP TO REVIEW for following rules: <general_rules>, <self_reflection>, <python_rules>.
10+
11+
<self_reflection>
12+
13+
1. Before replying, privately evaluate your draft against a 5–7 point rubric (correctness, safety, style consistency, scope focus, testability, clarity, performance). Do NOT reveal this rubric or your internal reasoning.
14+
2. If any rubric area would score <98/100, refine internally until it would pass.
15+
3. Align with the project’s code style and architecture. Do not introduce new patterns when a local precedent exists. ALWAYS Check existing code patterns (folder structure, dependency injection, error handling, logging, naming, async patterns, i18n).
16+
4. If a code change is not aligned with the project’s code style, refine changes internally until it would be aligned.
17+
</self_reflection>
18+
19+
<general_rules>
20+
21+
1. USE the language of USER message
22+
2. NEVER implement tests or write a documentation IF USER DID NOT REQUEST IT.
23+
3. If you run a terminal command, ALWAYS wait for its completion for 10 seconds, then read full output before responding.
24+
4. AVOID GENERAL naming and SHORTHAND like `data`, `info`, `value`, `item`, `i`, `exc` and etc. ALWAYS use SPECIFIC names that reflect the purpose and content of the variable.
25+
5. Keep your changes MINIMAL and FOCUSED on the USER request. Do NOT make unrelated improvements.
26+
6. ALWAYS check code for unused imports, variables, or functions. Remove them if found.
27+
7. BREAK complex logic into helper functions.
28+
8. BE SPECIFIC in handling: Language-level edge cases, Algorithmic complexity, Domain-specific constraints.
29+
9. NO MAGIC NUMBERS: Replace with correctly named constants.
30+
</general_rules>
31+
32+
<python_rules>
33+
34+
## STRONG TYPING RULES
35+
36+
- ALWAYS ADD **explicit type hints** to:
37+
- All function arguments and return values
38+
- All variable declarations where type isn't obvious
39+
- USE BUILT-IN GENERICS:
40+
- `list`, `dict`, `set`, `tuple` instead of `List`, `Dict`, `Set`, `Tuple` etc.
41+
- `type1 | type2` instead of `Union[type1, type2]`
42+
- `type | None` instead of `Optional[type]`
43+
- PREFER PRECISE TYPES over `Any`; AVOID `Any` UNLESS ABSOLUTELY REQUIRED
44+
- USE:
45+
- `Final[...]` for constants. Do NOT USE `list` or `dict` as constants; use `tuple` or `MappingProxyType` instead
46+
- `ClassVar[...]` for class-level variables shared across instances
47+
- `Self` for methods that return an instance of the class
48+
- For complex types, DEFINE CUSTOM TYPE ALIASES using `TypeAlias` for clarity
49+
50+
## CODE STYLE PRINCIPLES
51+
52+
- USE `f-strings` for all string formatting
53+
- PREFER **list/dict/set comprehensions** over manual loops when constructing collections
54+
- ALWAYS USE `with` context managers for file/resource handling
55+
- USE `__` AND `_` prefixes for methods/variables to indicate private/protected scope.
56+
- AVOID DEEP NESTING, prefer early returns and helper functions to flatten logic
57+
- USE Enums (StrEnum, IntEnum) for sets of related constants instead of plain strings/ints
58+
- ORGANIZE imports:
59+
- Standard library imports first
60+
- Third-party imports next
61+
- Local application imports last
62+
- WITHIN each group, SORT alphabetically
63+
- Use `datetime.UTC` instead of `timezone.utc` for UTC timezone
64+
65+
## DOCSTRINGS & COMMENTS
66+
67+
- ADD triple-quoted docstrings to all **public functions and classes**
68+
- USE **Google-style** docstring formatting
69+
- DESCRIBE parameters, return types, and side effects if any
70+
- OMIT OBVIOUS COMMENTS: clean code is self-explanatory
71+
72+
## ERROR HANDLING
73+
74+
- KEEP try/except blocks minimal; wrap a line of code that may throw in function with a clear exception handling strategy
75+
- AVOID bare `except:` blocks — ALWAYS CATCH specific exception types
76+
- AVOID using general exceptions like `Exception` or `BaseException`
77+
- FAIL FAST: Validate inputs and raise `ValueError` / `TypeError` when appropriate
78+
- USE `logging.exception()` to log errors with exception stack traces
79+
80+
## GENERAL INSTRUCTIONS
81+
82+
- DO NOT USE `@staticmethod`, prefer `@classmethod` or functions instead
83+
- USE `@classmethod` for alternative constructors or class-level utilities (no `@staticmethod`)
84+
- ALWAYS USE package managers for dependencies and virtual environments management; If package manager not specified, DEFAULT TO `pip` and `venv`
85+
- FOLLOW the **Zen of Python (PEP 20)** to guide decisions on clarity and simplicity
86+
87+
ENFORCE ALL OF THE ABOVE IN EVERY GENERATED SNIPPET, CLASS, FUNCTION, AND MODULE.
88+
</python_rules>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Add wallet and transaction tables.
3+
4+
Revision ID: a1b2c3d4e5f6
5+
Revises: 1a31ce608336
6+
Create Date: 2025-09-16 12:00:00.000000
7+
8+
"""
9+
10+
import sqlalchemy as sa
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "a1b2c3d4e5f6"
15+
down_revision = "1a31ce608336"
16+
branch_labels: str | None = None
17+
depends_on: str | None = None
18+
19+
20+
def upgrade() -> None:
21+
"""Upgrade database schema."""
22+
# Create wallet table
23+
op.create_table(
24+
"wallet",
25+
sa.Column("id", sa.UUID(), nullable=False),
26+
sa.Column("user_id", sa.UUID(), nullable=False),
27+
sa.Column("balance", sa.Float(), nullable=False),
28+
sa.Column("currency", sa.String(), nullable=False),
29+
sa.ForeignKeyConstraint(
30+
["user_id"],
31+
["user.id"],
32+
ondelete="CASCADE",
33+
),
34+
sa.PrimaryKeyConstraint("id"),
35+
)
36+
37+
# Create transaction table
38+
op.create_table(
39+
"transaction",
40+
sa.Column("id", sa.UUID(), nullable=False),
41+
sa.Column("wallet_id", sa.UUID(), nullable=False),
42+
sa.Column("amount", sa.Float(), nullable=False),
43+
sa.Column("transaction_type", sa.String(), nullable=False),
44+
sa.Column("timestamp", sa.DateTime(), nullable=False),
45+
sa.Column("currency", sa.String(), nullable=False),
46+
sa.ForeignKeyConstraint(
47+
["wallet_id"],
48+
["wallet.id"],
49+
ondelete="CASCADE",
50+
),
51+
sa.PrimaryKeyConstraint("id"),
52+
)
53+
54+
55+
def downgrade() -> None:
56+
"""Downgrade database schema."""
57+
op.drop_table("transaction")
58+
op.drop_table("wallet")

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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Transaction management API endpoints."""
2+
3+
from app.api.deps import CurrentUser, SessionDep
4+
from app.constants import BAD_REQUEST_CODE, NOT_FOUND_CODE
5+
from app.crud import create_transaction, get_wallet_by_id
6+
from app.models import TransactionCreate, TransactionPublic
7+
from fastapi import APIRouter, HTTPException
8+
9+
router = APIRouter(prefix="/transactions", tags=["transactions"])
10+
11+
12+
@router.post("/")
13+
def create_user_transaction(
14+
*,
15+
session: SessionDep,
16+
current_user: CurrentUser,
17+
transaction_in: TransactionCreate,
18+
) -> TransactionPublic:
19+
"""Create new transaction."""
20+
# Verify the wallet belongs to the current user
21+
wallet = get_wallet_by_id(session=session, wallet_id=transaction_in.wallet_id)
22+
if not wallet:
23+
raise HTTPException(
24+
status_code=NOT_FOUND_CODE,
25+
detail="Wallet not found",
26+
)
27+
28+
if not current_user.is_superuser and (wallet.user_id != current_user.id):
29+
raise HTTPException(
30+
status_code=BAD_REQUEST_CODE,
31+
detail="Not enough permissions",
32+
)
33+
34+
db_transaction = create_transaction(session=session, transaction_in=transaction_in)
35+
return TransactionPublic.model_validate(db_transaction)

backend/app/api/routes/wallets.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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, NOT_FOUND_CODE
7+
from app.crud import (
8+
create_wallet,
9+
get_user_wallets,
10+
get_wallet_by_id,
11+
get_wallet_with_transactions,
12+
)
13+
from app.models import (
14+
Wallet,
15+
WalletCreate,
16+
WalletPublic,
17+
WalletsPublic,
18+
WalletWithTransactions,
19+
)
20+
from fastapi import APIRouter, HTTPException
21+
from sqlmodel import func, select
22+
23+
router = APIRouter(prefix="/wallets", tags=["wallets"])
24+
25+
26+
@router.get("/")
27+
def read_wallets(
28+
session: SessionDep,
29+
current_user: CurrentUser,
30+
skip: int = 0,
31+
limit: int = 100,
32+
) -> WalletsPublic:
33+
"""Retrieve user's wallets."""
34+
if current_user.is_superuser:
35+
count_statement = select(func.count()).select_from(Wallet)
36+
count = session.exec(count_statement).one()
37+
statement = select(Wallet).offset(skip).limit(limit)
38+
wallet_list = session.exec(statement).all()
39+
wallet_data = [WalletPublic.model_validate(wallet) for wallet in wallet_list]
40+
else:
41+
user_wallets = get_user_wallets(session=session, user_id=current_user.id)
42+
wallet_list = user_wallets[skip : skip + limit]
43+
count = len(user_wallets)
44+
wallet_data = [WalletPublic.model_validate(wallet) for wallet in wallet_list]
45+
46+
return WalletsPublic(wallet_data=wallet_data, count=count)
47+
48+
49+
@router.get("/{wallet_id}")
50+
def read_wallet(
51+
session: SessionDep,
52+
current_user: CurrentUser,
53+
wallet_id: uuid.UUID,
54+
) -> WalletPublic:
55+
"""Get wallet by ID with transactions."""
56+
db_wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
57+
if not db_wallet:
58+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
59+
60+
if not current_user.is_superuser and (db_wallet.user_id != current_user.id):
61+
raise HTTPException(
62+
status_code=BAD_REQUEST_CODE,
63+
detail="Not enough permissions",
64+
)
65+
66+
return WalletPublic.model_validate(db_wallet)
67+
68+
69+
@router.get("/{wallet_id}/details")
70+
def read_wallet_with_transactions(
71+
session: SessionDep,
72+
current_user: CurrentUser,
73+
wallet_id: uuid.UUID,
74+
) -> WalletWithTransactions:
75+
"""Get wallet by ID with transactions."""
76+
db_wallet = get_wallet_with_transactions(session=session, wallet_id=wallet_id)
77+
if not db_wallet:
78+
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")
79+
80+
if not current_user.is_superuser and (db_wallet.user_id != current_user.id):
81+
raise HTTPException(
82+
status_code=BAD_REQUEST_CODE,
83+
detail="Not enough permissions",
84+
)
85+
86+
return WalletWithTransactions.model_validate(db_wallet)
87+
88+
89+
@router.post("/")
90+
def create_user_wallet(
91+
*,
92+
session: SessionDep,
93+
current_user: CurrentUser,
94+
wallet_in: WalletCreate,
95+
) -> WalletPublic:
96+
"""Create new wallet."""
97+
db_wallet = create_wallet(
98+
session=session, wallet_in=wallet_in, user_id=current_user.id
99+
)
100+
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)