Skip to content
Merged
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
1 change: 1 addition & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from app.core.config import settings
from app.db.base import Base
import app.models.application # noqa: F401
import app.models.deposit # noqa: F401
import app.models.email_verification # noqa: F401
import app.models.event # noqa: F401
import app.models.review_image # noqa: F401
Expand Down
42 changes: 42 additions & 0 deletions alembic/versions/e12a0231a096_add_deposits_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""add deposits table

Revision ID: e12a0231a096
Revises: 23d53081763c
Create Date: 2026-06-15 13:48:03.481192

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'e12a0231a096'
down_revision: Union[str, Sequence[str], None] = '23d53081763c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('deposits',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('amount', sa.Integer(), nullable=False),
sa.Column('balance', sa.Integer(), nullable=False),
sa.Column('deposited_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_deposits_user_id'), 'deposits', ['user_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_deposits_user_id'), table_name='deposits')
op.drop_table('deposits')
# ### end Alembic commands ###
20 changes: 20 additions & 0 deletions app/api/v1/endpoints/deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.v1.deps import require_login
from app.db.session import get_db
from app.schemas.common import SuccessResponse
from app.schemas.deposit import DepositReq, DepositResp
from app.services import deposit as deposit_service

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


@router.post("", response_model=SuccessResponse[DepositResp])
async def create_deposit(
body: DepositReq,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(require_login),
) -> SuccessResponse[DepositResp]:
result = await deposit_service.create_deposit(db, user_id, body)
return SuccessResponse(data=result)
2 changes: 2 additions & 0 deletions app/api/v1/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from app.api.v1.endpoints.application import router as application_router
from app.api.v1.endpoints.auth import router as auth_router
from app.api.v1.endpoints.deposit import router as deposit_router
from app.api.v1.endpoints.event import router as event_router
from app.api.v1.endpoints.store import router as store_router

Expand All @@ -10,3 +11,4 @@
router.include_router(store_router)
router.include_router(event_router)
router.include_router(application_router)
router.include_router(deposit_router)
22 changes: 22 additions & 0 deletions app/models/deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from datetime import datetime
from uuid import UUID, uuid4

from sqlalchemy import DateTime, ForeignKey, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func

from app.db.base import Base


class Deposit(Base):
__tablename__ = "deposits"

id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"), index=True)
amount: Mapped[int] = mapped_column(Integer)
balance: Mapped[int] = mapped_column(Integer)
deposited_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)

user: Mapped["User"] = relationship("User", back_populates="deposits") # type: ignore[name-defined] # noqa: F821
8 changes: 7 additions & 1 deletion app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from datetime import datetime
from typing import TYPE_CHECKING
from uuid import UUID, uuid4

from sqlalchemy import DateTime, Enum, String
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func

from app.db.base import Base

if TYPE_CHECKING:
from app.models.deposit import Deposit


class User(Base):
__tablename__ = "users"
Expand All @@ -19,3 +23,5 @@ class User(Base):
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)

deposits: Mapped[list["Deposit"]] = relationship("Deposit", back_populates="user")
10 changes: 10 additions & 0 deletions app/schemas/deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from pydantic import BaseModel


class DepositReq(BaseModel):
amount: int


class DepositResp(BaseModel):
balance: int
depositedAt: str
32 changes: 32 additions & 0 deletions app/services/deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from uuid import UUID

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.deposit import Deposit
from app.schemas.deposit import DepositReq, DepositResp


async def create_deposit(
db: AsyncSession, user_id: str, data: DepositReq
) -> DepositResp:
current_balance = await db.scalar(
select(func.coalesce(func.max(Deposit.balance), 0)).where(
Deposit.user_id == UUID(user_id)
)
)
new_balance = (current_balance or 0) + data.amount

deposit = Deposit(
user_id=UUID(user_id),
amount=data.amount,
balance=new_balance,
)
db.add(deposit)
await db.commit()
await db.refresh(deposit)

return DepositResp(
balance=deposit.balance,
depositedAt=deposit.deposited_at.isoformat(),
)
76 changes: 76 additions & 0 deletions tests/test_deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.deposit import Deposit
from tests.conftest import auth_headers, create_user


@pytest.mark.asyncio
class TestDeposit:
async def test_deposit_returns_balance_and_deposited_at(
self, client: AsyncClient, db: AsyncSession
) -> None:
user = await create_user(db)
res = await client.post(
"/api/deposit",
json={"amount": 5000},
headers=auth_headers(user.id),
)
assert res.status_code == 200
data = res.json()["data"]
assert data["balance"] == 5000
assert "depositedAt" in data

async def test_deposit_accumulates_balance(
self, client: AsyncClient, db: AsyncSession
) -> None:
user = await create_user(db)
await client.post(
"/api/deposit",
json={"amount": 3000},
headers=auth_headers(user.id),
)
res = await client.post(
"/api/deposit",
json={"amount": 2000},
headers=auth_headers(user.id),
)
assert res.status_code == 200
assert res.json()["data"]["balance"] == 5000

async def test_deposit_saved_to_db(
self, client: AsyncClient, db: AsyncSession
) -> None:
user = await create_user(db)
await client.post(
"/api/deposit",
json={"amount": 10000},
headers=auth_headers(user.id),
)
saved = await db.scalar(select(Deposit).where(Deposit.user_id == user.id))
assert saved is not None
assert saved.amount == 10000
assert saved.balance == 10000

async def test_each_deposit_creates_a_record(
self, client: AsyncClient, db: AsyncSession
) -> None:
user = await create_user(db)
await client.post(
"/api/deposit", json={"amount": 1000}, headers=auth_headers(user.id)
)
await client.post(
"/api/deposit", json={"amount": 2000}, headers=auth_headers(user.id)
)
records = (
await db.scalars(select(Deposit).where(Deposit.user_id == user.id))
).all()
assert len(records) == 2
amounts = {r.amount for r in records}
assert amounts == {1000, 2000}

async def test_no_auth_returns_4xx(self, client: AsyncClient) -> None:
res = await client.post("/api/deposit", json={"amount": 1000})
assert res.status_code in (401, 403)
Loading