Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/instructions/general.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
applyTo: "**"
---

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.

STRICTLY AND ALWAYS FOLLOW THIS INSTRUCTIONS:

At the end of your work ALWAYS ADD A STEP TO REVIEW for following rules: <general_rules>, <self_reflection>, <python_rules>.

<self_reflection>

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.
2. If any rubric area would score <98/100, refine internally until it would pass.
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).
4. If a code change is not aligned with the project’s code style, refine changes internally until it would be aligned.
</self_reflection>

<general_rules>

1. USE the language of USER message
2. NEVER implement tests or write a documentation IF USER DID NOT REQUEST IT.
3. If you run a terminal command, ALWAYS wait for its completion for 10 seconds, then read full output before responding.
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.
5. Keep your changes MINIMAL and FOCUSED on the USER request. Do NOT make unrelated improvements.
6. ALWAYS check code for unused imports, variables, or functions. Remove them if found.
7. BREAK complex logic into helper functions.
8. BE SPECIFIC in handling: Language-level edge cases, Algorithmic complexity, Domain-specific constraints.
9. NO MAGIC NUMBERS: Replace with correctly named constants.
</general_rules>

<python_rules>

## STRONG TYPING RULES

- ALWAYS ADD **explicit type hints** to:
- All function arguments and return values
- All variable declarations where type isn't obvious
- USE BUILT-IN GENERICS:
- `list`, `dict`, `set`, `tuple` instead of `List`, `Dict`, `Set`, `Tuple` etc.
- `type1 | type2` instead of `Union[type1, type2]`
- `type | None` instead of `Optional[type]`
- PREFER PRECISE TYPES over `Any`; AVOID `Any` UNLESS ABSOLUTELY REQUIRED
- USE:
- `Final[...]` for constants. Do NOT USE `list` or `dict` as constants; use `tuple` or `MappingProxyType` instead
- `ClassVar[...]` for class-level variables shared across instances
- `Self` for methods that return an instance of the class
- For complex types, DEFINE CUSTOM TYPE ALIASES using `TypeAlias` for clarity

## CODE STYLE PRINCIPLES

- USE `f-strings` for all string formatting
- PREFER **list/dict/set comprehensions** over manual loops when constructing collections
- ALWAYS USE `with` context managers for file/resource handling
- USE `__` AND `_` prefixes for methods/variables to indicate private/protected scope.
- AVOID DEEP NESTING, prefer early returns and helper functions to flatten logic
- USE Enums (StrEnum, IntEnum) for sets of related constants instead of plain strings/ints
- ORGANIZE imports:
- Standard library imports first
- Third-party imports next
- Local application imports last
- WITHIN each group, SORT alphabetically
- Use `datetime.UTC` instead of `timezone.utc` for UTC timezone

## DOCSTRINGS & COMMENTS

- ADD triple-quoted docstrings to all **public functions and classes**
- USE **Google-style** docstring formatting
- DESCRIBE parameters, return types, and side effects if any
- OMIT OBVIOUS COMMENTS: clean code is self-explanatory

## ERROR HANDLING

- KEEP try/except blocks minimal; wrap a line of code that may throw in function with a clear exception handling strategy
- AVOID bare `except:` blocks — ALWAYS CATCH specific exception types
- AVOID using general exceptions like `Exception` or `BaseException`
- FAIL FAST: Validate inputs and raise `ValueError` / `TypeError` when appropriate
- USE `logging.exception()` to log errors with exception stack traces

## GENERAL INSTRUCTIONS

- DO NOT USE `@staticmethod`, prefer `@classmethod` or functions instead
- USE `@classmethod` for alternative constructors or class-level utilities (no `@staticmethod`)
- ALWAYS USE package managers for dependencies and virtual environments management; If package manager not specified, DEFAULT TO `pip` and `venv`
- FOLLOW the **Zen of Python (PEP 20)** to guide decisions on clarity and simplicity

ENFORCE ALL OF THE ABOVE IN EVERY GENERATED SNIPPET, CLASS, FUNCTION, AND MODULE.
</python_rules>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Add wallet and transaction tables.

Revision ID: a1b2c3d4e5f6
Revises: 1a31ce608336
Create Date: 2025-09-16 12:00:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "a1b2c3d4e5f6"
down_revision = "1a31ce608336"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
"""Upgrade database schema."""
# Create wallet table
op.create_table(
"wallet",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("balance", sa.Float(), nullable=False),
sa.Column("currency", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
)

# Create transaction table
op.create_table(
"transaction",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("wallet_id", sa.UUID(), nullable=False),
sa.Column("amount", sa.Float(), nullable=False),
sa.Column("transaction_type", sa.String(), nullable=False),
sa.Column("timestamp", sa.DateTime(), nullable=False),
sa.Column("currency", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["wallet_id"],
["wallet.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
)


def downgrade() -> None:
"""Downgrade database schema."""
op.drop_table("transaction")
op.drop_table("wallet")
7 changes: 4 additions & 3 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""API router configuration."""

from fastapi import APIRouter

from app.api.routes import items, login, misc, private, users
from app.api.routes import items, login, misc, private, transactions, users, wallets
from app.core.config import settings
from fastapi import APIRouter

api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(users.router)
api_router.include_router(misc.router)
api_router.include_router(items.router)
api_router.include_router(wallets.router)
api_router.include_router(transactions.router)


if settings.ENVIRONMENT == "local":
Expand Down
35 changes: 35 additions & 0 deletions backend/app/api/routes/transactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Transaction management API endpoints."""

from app.api.deps import CurrentUser, SessionDep
from app.constants import BAD_REQUEST_CODE, NOT_FOUND_CODE
from app.crud import create_transaction, get_wallet_by_id
from app.models import TransactionCreate, TransactionPublic
from fastapi import APIRouter, HTTPException

router = APIRouter(prefix="/transactions", tags=["transactions"])


@router.post("/")
def create_user_transaction(
*,
session: SessionDep,
current_user: CurrentUser,
transaction_in: TransactionCreate,
) -> TransactionPublic:
"""Create new transaction."""
# Verify the wallet belongs to the current user
wallet = get_wallet_by_id(session=session, wallet_id=transaction_in.wallet_id)
if not wallet:
raise HTTPException(
status_code=NOT_FOUND_CODE,
detail="Wallet not found",
)

if not current_user.is_superuser and (wallet.user_id != current_user.id):
raise HTTPException(
status_code=BAD_REQUEST_CODE,
detail="Not enough permissions",
)

db_transaction = create_transaction(session=session, transaction_in=transaction_in)
return TransactionPublic.model_validate(db_transaction)
100 changes: 100 additions & 0 deletions backend/app/api/routes/wallets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Wallet management API endpoints."""

import uuid

from app.api.deps import CurrentUser, SessionDep
from app.constants import BAD_REQUEST_CODE, NOT_FOUND_CODE
from app.crud import (
create_wallet,
get_user_wallets,
get_wallet_by_id,
get_wallet_with_transactions,
)
from app.models import (
Wallet,
WalletCreate,
WalletPublic,
WalletsPublic,
WalletWithTransactions,
)
from fastapi import APIRouter, HTTPException
from sqlmodel import func, select

router = APIRouter(prefix="/wallets", tags=["wallets"])


@router.get("/")
def read_wallets(
session: SessionDep,
current_user: CurrentUser,
skip: int = 0,
limit: int = 100,
) -> WalletsPublic:
"""Retrieve user's wallets."""
if current_user.is_superuser:
count_statement = select(func.count()).select_from(Wallet)
count = session.exec(count_statement).one()
statement = select(Wallet).offset(skip).limit(limit)
wallet_list = session.exec(statement).all()
wallet_data = [WalletPublic.model_validate(wallet) for wallet in wallet_list]
else:
user_wallets = get_user_wallets(session=session, user_id=current_user.id)
wallet_list = user_wallets[skip : skip + limit]
count = len(user_wallets)
wallet_data = [WalletPublic.model_validate(wallet) for wallet in wallet_list]

return WalletsPublic(wallet_data=wallet_data, count=count)


@router.get("/{wallet_id}")
def read_wallet(
session: SessionDep,
current_user: CurrentUser,
wallet_id: uuid.UUID,
) -> WalletPublic:
"""Get wallet by ID with transactions."""
db_wallet = get_wallet_by_id(session=session, wallet_id=wallet_id)
if not db_wallet:
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")

if not current_user.is_superuser and (db_wallet.user_id != current_user.id):
raise HTTPException(
status_code=BAD_REQUEST_CODE,
detail="Not enough permissions",
)

return WalletPublic.model_validate(db_wallet)


@router.get("/{wallet_id}/details")
def read_wallet_with_transactions(
session: SessionDep,
current_user: CurrentUser,
wallet_id: uuid.UUID,
) -> WalletWithTransactions:
"""Get wallet by ID with transactions."""
db_wallet = get_wallet_with_transactions(session=session, wallet_id=wallet_id)
if not db_wallet:
raise HTTPException(status_code=NOT_FOUND_CODE, detail="Wallet not found")

if not current_user.is_superuser and (db_wallet.user_id != current_user.id):
raise HTTPException(
status_code=BAD_REQUEST_CODE,
detail="Not enough permissions",
)

return WalletWithTransactions.model_validate(db_wallet)


@router.post("/")
def create_user_wallet(
*,
session: SessionDep,
current_user: CurrentUser,
wallet_in: WalletCreate,
) -> WalletPublic:
"""Create new wallet."""
db_wallet = create_wallet(
session=session, wallet_in=wallet_in, user_id=current_user.id
)
return WalletPublic.model_validate(db_wallet)
6 changes: 3 additions & 3 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ class Settings(BaseSettings): # type: ignore[explicit-any]
FIRST_SUPERUSER: EmailStr
FIRST_SUPERUSER_PASSWORD: str

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

@computed_field # type: ignore[prop-decorator]
@computed_field # type: ignore[prop-decorator]
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: # noqa: N802
"""Build database URI from configuration."""
Expand All @@ -99,7 +99,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: # noqa: N802
path=self.POSTGRES_DB,
)

@computed_field # type: ignore[prop-decorator]
@computed_field # type: ignore[prop-decorator]
@property
def emails_enabled(self) -> bool:
"""Check if email configuration is enabled."""
Expand Down
Loading