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
61 changes: 61 additions & 0 deletions backend/app/api/cuentas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import uuid

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.database.session import get_db
from app.models.cuenta import CreateCuentaRequest, CuentaResponse, UpdateCuentaRequest
from app.repositories.cuenta_repository import CuentaRepository

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


@router.post("", response_model=CuentaResponse, status_code=status.HTTP_201_CREATED)
async def create_cuenta(body: CreateCuentaRequest, db: AsyncSession = Depends(get_db)) -> CuentaResponse:
repo = CuentaRepository(db)
return await repo.create(
user_id=body.user_id,
balance=body.balance,
currency=body.currency,
status=body.status,
)


@router.get("", response_model=list[CuentaResponse])
async def list_cuentas(
user_id: uuid.UUID | None = Query(default=None),
db: AsyncSession = Depends(get_db),
) -> list[CuentaResponse]:
repo = CuentaRepository(db)
return await repo.get_all(user_id=user_id)


@router.get("/{cuenta_id}", response_model=CuentaResponse)
async def get_cuenta(cuenta_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> CuentaResponse:
repo = CuentaRepository(db)
cuenta = await repo.get_by_id(cuenta_id)
if not cuenta:
raise HTTPException(status_code=404, detail="Cuenta not found")
return cuenta


@router.patch("/{cuenta_id}", response_model=CuentaResponse)
async def update_cuenta(
cuenta_id: uuid.UUID,
body: UpdateCuentaRequest,
db: AsyncSession = Depends(get_db),
) -> CuentaResponse:
repo = CuentaRepository(db)
cuenta = await repo.get_by_id(cuenta_id)
if not cuenta:
raise HTTPException(status_code=404, detail="Cuenta not found")
return await repo.update(cuenta, balance=body.balance, currency=body.currency, status=body.status)


@router.delete("/{cuenta_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_cuenta(cuenta_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> None:
repo = CuentaRepository(db)
cuenta = await repo.get_by_id(cuenta_id)
if not cuenta:
raise HTTPException(status_code=404, detail="Cuenta not found")
await repo.delete(cuenta)
72 changes: 72 additions & 0 deletions backend/app/api/dispositivos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import uuid

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.database.session import get_db
from app.models.dispositivo import CreateDispositivoRequest, DispositivoResponse, UpdateDispositivoRequest
from app.repositories.dispositivo_repository import DispositivoRepository

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


@router.post("", response_model=DispositivoResponse, status_code=status.HTTP_201_CREATED)
async def create_dispositivo(
body: CreateDispositivoRequest, db: AsyncSession = Depends(get_db)
) -> DispositivoResponse:
repo = DispositivoRepository(db)
return await repo.create(user_id=body.user_id, fingerprint=body.fingerprint, trusted=body.trusted)


@router.get("", response_model=list[DispositivoResponse])
async def list_dispositivos(
user_id: uuid.UUID | None = Query(default=None),
db: AsyncSession = Depends(get_db),
) -> list[DispositivoResponse]:
repo = DispositivoRepository(db)
return await repo.get_all(user_id=user_id)


@router.get("/{dispositivo_id}", response_model=DispositivoResponse)
async def get_dispositivo(dispositivo_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> DispositivoResponse:
repo = DispositivoRepository(db)
dispositivo = await repo.get_by_id(dispositivo_id)
if not dispositivo:
raise HTTPException(status_code=404, detail="Dispositivo not found")
return dispositivo


@router.patch("/{dispositivo_id}", response_model=DispositivoResponse)
async def update_dispositivo(
dispositivo_id: uuid.UUID,
body: UpdateDispositivoRequest,
db: AsyncSession = Depends(get_db),
) -> DispositivoResponse:
repo = DispositivoRepository(db)
dispositivo = await repo.get_by_id(dispositivo_id)
if not dispositivo:
raise HTTPException(status_code=404, detail="Dispositivo not found")
return await repo.update(
dispositivo,
fingerprint=body.fingerprint,
trusted=body.trusted,
last_seen=body.last_seen,
)


@router.post("/{dispositivo_id}/touch", response_model=DispositivoResponse)
async def touch_dispositivo(dispositivo_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> DispositivoResponse:
repo = DispositivoRepository(db)
dispositivo = await repo.get_by_id(dispositivo_id)
if not dispositivo:
raise HTTPException(status_code=404, detail="Dispositivo not found")
return await repo.touch(dispositivo)


@router.delete("/{dispositivo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dispositivo(dispositivo_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> None:
repo = DispositivoRepository(db)
dispositivo = await repo.get_by_id(dispositivo_id)
if not dispositivo:
raise HTTPException(status_code=404, detail="Dispositivo not found")
await repo.delete(dispositivo)
4 changes: 4 additions & 0 deletions backend/app/api/router.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from fastapi import APIRouter

from app.api.cuentas import router as cuentas_router
from app.api.dispositivos import router as dispositivos_router
from app.api.health import router as health_router
from app.api.usuarios import router as usuarios_router

api_router = APIRouter(prefix="/api")
api_router.include_router(health_router)
api_router.include_router(usuarios_router)
api_router.include_router(cuentas_router)
api_router.include_router(dispositivos_router)
17 changes: 17 additions & 0 deletions backend/app/entities/cuenta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import uuid
from decimal import Decimal

from sqlalchemy import ForeignKey, Numeric, String
from sqlalchemy.orm import Mapped, mapped_column

from app.database.base import Base


class Cuenta(Base):
__tablename__ = "cuenta"

id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("usuario.id", ondelete="CASCADE"), nullable=False)
balance: Mapped[Decimal] = mapped_column(Numeric(precision=18, scale=2), default=Decimal("0.00"))
currency: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, default="active")
18 changes: 18 additions & 0 deletions backend/app/entities/dispositivo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import uuid
from datetime import UTC, datetime

from sqlalchemy import Boolean, DateTime, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column

from app.database.base import Base


class Dispositivo(Base):
__tablename__ = "dispositivo"

id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("usuario.id", ondelete="CASCADE"), nullable=False)
fingerprint: Mapped[str] = mapped_column(String, nullable=False)
trusted: Mapped[bool] = mapped_column(Boolean, default=False)
first_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
27 changes: 27 additions & 0 deletions backend/app/models/cuenta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import uuid
from decimal import Decimal

from pydantic import BaseModel, Field


class CreateCuentaRequest(BaseModel):
user_id: uuid.UUID
balance: Decimal = Field(default=Decimal("0.00"), ge=0)
currency: str
status: str = "active"


class UpdateCuentaRequest(BaseModel):
balance: Decimal | None = Field(default=None, ge=0)
currency: str | None = None
status: str | None = None


class CuentaResponse(BaseModel):
id: uuid.UUID
user_id: uuid.UUID
balance: Decimal
currency: str
status: str

model_config = {"from_attributes": True}
27 changes: 27 additions & 0 deletions backend/app/models/dispositivo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import uuid
from datetime import datetime

from pydantic import BaseModel


class CreateDispositivoRequest(BaseModel):
user_id: uuid.UUID
fingerprint: str
trusted: bool = False


class UpdateDispositivoRequest(BaseModel):
fingerprint: str | None = None
trusted: bool | None = None
last_seen: datetime | None = None


class DispositivoResponse(BaseModel):
id: uuid.UUID
user_id: uuid.UUID
fingerprint: str
trusted: bool
first_seen: datetime
last_seen: datetime

model_config = {"from_attributes": True}
42 changes: 42 additions & 0 deletions backend/app/repositories/cuenta_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import uuid
from decimal import Decimal

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

from app.entities.cuenta import Cuenta


class CuentaRepository:
def __init__(self, db: AsyncSession) -> None:
self.db = db

async def create(self, user_id: uuid.UUID, balance: Decimal, currency: str, status: str) -> Cuenta:
cuenta = Cuenta(user_id=user_id, balance=balance, currency=currency, status=status)
self.db.add(cuenta)
await self.db.commit()
await self.db.refresh(cuenta)
return cuenta

async def get_by_id(self, cuenta_id: uuid.UUID) -> Cuenta | None:
result = await self.db.execute(select(Cuenta).where(Cuenta.id == cuenta_id))
return result.scalar_one_or_none()

async def get_all(self, user_id: uuid.UUID | None = None) -> list[Cuenta]:
query = select(Cuenta)
if user_id is not None:
query = query.where(Cuenta.user_id == user_id)
result = await self.db.execute(query)
return list(result.scalars().all())

async def update(self, cuenta: Cuenta, **fields: object) -> Cuenta:
for key, value in fields.items():
if value is not None:
setattr(cuenta, key, value)
await self.db.commit()
await self.db.refresh(cuenta)
return cuenta

async def delete(self, cuenta: Cuenta) -> None:
await self.db.delete(cuenta)
await self.db.commit()
49 changes: 49 additions & 0 deletions backend/app/repositories/dispositivo_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import uuid
from datetime import UTC, datetime

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

from app.entities.dispositivo import Dispositivo


class DispositivoRepository:
def __init__(self, db: AsyncSession) -> None:
self.db = db

async def create(self, user_id: uuid.UUID, fingerprint: str, trusted: bool) -> Dispositivo:
dispositivo = Dispositivo(user_id=user_id, fingerprint=fingerprint, trusted=trusted)
self.db.add(dispositivo)
await self.db.commit()
await self.db.refresh(dispositivo)
return dispositivo

async def get_by_id(self, dispositivo_id: uuid.UUID) -> Dispositivo | None:
result = await self.db.execute(select(Dispositivo).where(Dispositivo.id == dispositivo_id))
return result.scalar_one_or_none()

async def get_all(self, user_id: uuid.UUID | None = None) -> list[Dispositivo]:
query = select(Dispositivo)
if user_id is not None:
query = query.where(Dispositivo.user_id == user_id)
result = await self.db.execute(query)
return list(result.scalars().all())

async def update(self, dispositivo: Dispositivo, **fields: object) -> Dispositivo:
for key, value in fields.items():
if value is not None:
setattr(dispositivo, key, value)
await self.db.commit()
await self.db.refresh(dispositivo)
return dispositivo

async def touch(self, dispositivo: Dispositivo) -> Dispositivo:
"""Update last_seen to now."""
dispositivo.last_seen = datetime.now(UTC)
await self.db.commit()
await self.db.refresh(dispositivo)
return dispositivo

async def delete(self, dispositivo: Dispositivo) -> None:
await self.db.delete(dispositivo)
await self.db.commit()
Loading
Loading