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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Add wallet and transaction tables.
Revision ID: f3b2a1c9d8e7
Revises: d98dd8ec85a3
Create Date: 2025-09-15 12:30:00.000000
"""

import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "f3b2a1c9d8e7"
down_revision = "d98dd8ec85a3"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
"""Upgrade database schema."""
# ### commands auto generated by Alembic - please adjust! ###

# Create wallet table
op.create_table(
"wallet",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("balance", sa.DECIMAL(precision=10, scale=2), nullable=False),
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration script includes created_at and updated_at columns that are not defined in the SQLModel classes. This mismatch between the migration and model definitions will cause issues.

Copilot uses AI. Check for mistakes.

sa.Column(
"currency", sqlmodel.sql.sqltypes.AutoString(length=3), nullable=False
),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
Comment on lines +35 to +36
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration script includes created_at and updated_at columns that are not defined in the SQLModel classes. This mismatch between the migration and model definitions will cause issues.

Copilot uses AI. Check for mistakes.

sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_wallet_user_id"), "wallet", ["user_id"], unique=False)
op.create_index(
"ix_wallet_user_currency", "wallet", ["user_id", "currency"], unique=True
)

# Create transaction table
op.create_table(
"transaction",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("wallet_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("amount", sa.DECIMAL(precision=10, scale=2), nullable=False),
sa.Column(
"type", sa.Enum("credit", "debit", name="transactiontype"), nullable=False
),
sa.Column("timestamp", sa.DateTime(), nullable=False),
sa.Column(
"currency", sqlmodel.sql.sqltypes.AutoString(length=3), nullable=False
),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.ForeignKeyConstraint(
["wallet_id"],
["wallet.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_transaction_wallet_id"), "transaction", ["wallet_id"], unique=False
)
op.create_index(
op.f("ix_transaction_timestamp"), "transaction", ["timestamp"], unique=False
)

# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade database schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_transaction_timestamp"), table_name="transaction")
op.drop_index(op.f("ix_transaction_wallet_id"), table_name="transaction")
op.drop_table("transaction")
op.drop_index("ix_wallet_user_currency", table_name="wallet")
op.drop_index(op.f("ix_wallet_user_id"), table_name="wallet")
op.drop_table("wallet")

# Drop the enum type
op.execute("DROP TYPE IF EXISTS transactiontype")
# ### end Alembic commands ###
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
70 changes: 70 additions & 0 deletions backend/app/api/routes/transactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Transaction management API endpoints."""

import uuid

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

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


@router.post("/", status_code=CREATED_CODE)
def create_wallet_transaction(
*,
session: SessionDep,
current_user: CurrentUser,
transaction_in: TransactionCreate,
) -> TransactionPublic:
"""Create a new transaction for a wallet."""
# Verify that 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",
)

try:
db_transaction = create_transaction(
session=session, transaction_in=transaction_in
)
return TransactionPublic.model_validate(db_transaction)
except ValueError as e:
raise HTTPException(
status_code=BAD_REQUEST_CODE,
detail=str(e),
) from e


@router.get("/wallet/{wallet_id}")
def read_wallet_transactions(
session: SessionDep,
current_user: CurrentUser,
wallet_id: uuid.UUID,
) -> TransactionsPublic:
"""Get all transactions for a specific wallet."""
# Verify that the wallet belongs to the current user
wallet = get_wallet_by_id(session=session, wallet_id=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",
)

transaction_list = get_wallet_transactions(session=session, wallet_id=wallet_id)
transaction_data = [
TransactionPublic.model_validate(transaction)
for transaction in transaction_list
]
return TransactionsPublic(
transaction_data=transaction_data, count=len(transaction_data)
)
65 changes: 65 additions & 0 deletions backend/app/api/routes/wallets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Wallet management API endpoints."""

import uuid

from app.api.deps import CurrentUser, SessionDep
from app.constants import BAD_REQUEST_CODE, CREATED_CODE, NOT_FOUND_CODE
from app.crud import create_wallet, get_user_wallets, get_wallet_by_id
from app.models import Message, WalletCreate, WalletPublic, WalletsPublic
from fastapi import APIRouter, HTTPException

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


@router.post("/", status_code=CREATED_CODE)
def create_user_wallet(
*,
session: SessionDep,
current_user: CurrentUser,
wallet_in: WalletCreate,
) -> WalletPublic:
"""Create a new wallet for the current user."""
try:
db_wallet = create_wallet(
session=session,
wallet_in=wallet_in,
user_id=current_user.id,
)
return WalletPublic.model_validate(db_wallet)
except ValueError as e:
raise HTTPException(
status_code=BAD_REQUEST_CODE,
detail=str(e),
) from e


@router.get("/")
def read_user_wallets(
session: SessionDep,
current_user: CurrentUser,
) -> WalletsPublic:
"""Retrieve all wallets for the current user."""
wallet_list = get_user_wallets(session=session, user_id=current_user.id)
wallet_data = [WalletPublic.model_validate(wallet) for wallet in wallet_list]
return WalletsPublic(wallet_data=wallet_data, count=len(wallet_data))


@router.get("/{wallet_id}")
def read_wallet(
session: SessionDep,
current_user: CurrentUser,
wallet_id: uuid.UUID,
) -> WalletPublic:
"""Get wallet details by ID."""
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")

# Check if wallet belongs to current user or user is superuser
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)
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