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
58 changes: 58 additions & 0 deletions backend/app/api/beneficiarios.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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.beneficiario import BeneficiarioResponse, CreateBeneficiarioRequest, UpdateBeneficiarioRequest
from app.repositories.beneficiario_repository import BeneficiarioRepository

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


@router.post("", response_model=BeneficiarioResponse, status_code=status.HTTP_201_CREATED)
async def create_beneficiario(
body: CreateBeneficiarioRequest, db: AsyncSession = Depends(get_db)
) -> BeneficiarioResponse:
repo = BeneficiarioRepository(db)
return await repo.create(user_id=body.user_id, account_number=body.account_number, bank_name=body.bank_name)


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


@router.get("/{beneficiario_id}", response_model=BeneficiarioResponse)
async def get_beneficiario(beneficiario_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> BeneficiarioResponse:
repo = BeneficiarioRepository(db)
beneficiario = await repo.get_by_id(beneficiario_id)
if not beneficiario:
raise HTTPException(status_code=404, detail="Beneficiario not found")
return beneficiario


@router.patch("/{beneficiario_id}", response_model=BeneficiarioResponse)
async def update_beneficiario(
beneficiario_id: uuid.UUID,
body: UpdateBeneficiarioRequest,
db: AsyncSession = Depends(get_db),
) -> BeneficiarioResponse:
repo = BeneficiarioRepository(db)
beneficiario = await repo.get_by_id(beneficiario_id)
if not beneficiario:
raise HTTPException(status_code=404, detail="Beneficiario not found")
return await repo.update(beneficiario, account_number=body.account_number, bank_name=body.bank_name)


@router.delete("/{beneficiario_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_beneficiario(beneficiario_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> None:
repo = BeneficiarioRepository(db)
beneficiario = await repo.get_by_id(beneficiario_id)
if not beneficiario:
raise HTTPException(status_code=404, detail="Beneficiario not found")
await repo.delete(beneficiario)
2 changes: 2 additions & 0 deletions backend/app/api/router.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter

from app.api.beneficiarios import router as beneficiarios_router
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
Expand All @@ -10,3 +11,4 @@
api_router.include_router(usuarios_router)
api_router.include_router(cuentas_router)
api_router.include_router(dispositivos_router)
api_router.include_router(beneficiarios_router)
17 changes: 17 additions & 0 deletions backend/app/entities/beneficiario.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import uuid
from datetime import UTC, datetime

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

from app.database.base import Base


class Beneficiario(Base):
__tablename__ = "beneficiario"

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)
account_number: Mapped[str] = mapped_column(String, nullable=False)
bank_name: Mapped[str] = mapped_column(String, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
25 changes: 25 additions & 0 deletions backend/app/models/beneficiario.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import uuid
from datetime import datetime

from pydantic import BaseModel


class CreateBeneficiarioRequest(BaseModel):
user_id: uuid.UUID
account_number: str
bank_name: str


class UpdateBeneficiarioRequest(BaseModel):
account_number: str | None = None
bank_name: str | None = None


class BeneficiarioResponse(BaseModel):
id: uuid.UUID
user_id: uuid.UUID
account_number: str
bank_name: str
created_at: datetime

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

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

from app.entities.beneficiario import Beneficiario


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

async def create(self, user_id: uuid.UUID, account_number: str, bank_name: str) -> Beneficiario:
beneficiario = Beneficiario(user_id=user_id, account_number=account_number, bank_name=bank_name)
self.db.add(beneficiario)
await self.db.commit()
await self.db.refresh(beneficiario)
return beneficiario

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

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

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

async def delete(self, beneficiario: Beneficiario) -> None:
await self.db.delete(beneficiario)
await self.db.commit()
117 changes: 117 additions & 0 deletions backend/tests/test_beneficiarios.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import uuid

import pytest
from httpx import AsyncClient


async def _create_usuario(client: AsyncClient) -> str:
resp = await client.post("/api/usuarios", json={"email": f"{uuid.uuid4()}@example.com"})
return resp.json()["id"]


@pytest.mark.asyncio
async def test_create_beneficiario(client: AsyncClient) -> None:
user_id = await _create_usuario(client)
response = await client.post(
"/api/beneficiarios",
json={"user_id": user_id, "account_number": "001122334455", "bank_name": "BBVA"},
)
assert response.status_code == 201
data = response.json()
assert data["user_id"] == user_id
assert data["account_number"] == "001122334455"
assert data["bank_name"] == "BBVA"
assert "id" in data
assert "created_at" in data


@pytest.mark.asyncio
async def test_get_beneficiario(client: AsyncClient) -> None:
user_id = await _create_usuario(client)
create_resp = await client.post(
"/api/beneficiarios",
json={"user_id": user_id, "account_number": "999888777", "bank_name": "Banamex"},
)
beneficiario_id = create_resp.json()["id"]

response = await client.get(f"/api/beneficiarios/{beneficiario_id}")
assert response.status_code == 200
assert response.json()["bank_name"] == "Banamex"


@pytest.mark.asyncio
async def test_get_beneficiario_not_found(client: AsyncClient) -> None:
response = await client.get(f"/api/beneficiarios/{uuid.uuid4()}")
assert response.status_code == 404


@pytest.mark.asyncio
async def test_list_beneficiarios(client: AsyncClient) -> None:
user_id = await _create_usuario(client)
await client.post("/api/beneficiarios", json={"user_id": user_id, "account_number": "111", "bank_name": "BBVA"})
await client.post("/api/beneficiarios", json={"user_id": user_id, "account_number": "222", "bank_name": "HSBC"})

response = await client.get("/api/beneficiarios")
assert response.status_code == 200
assert len(response.json()) == 2


@pytest.mark.asyncio
async def test_list_beneficiarios_filtered_by_user(client: AsyncClient) -> None:
user_a = await _create_usuario(client)
user_b = await _create_usuario(client)
await client.post("/api/beneficiarios", json={"user_id": user_a, "account_number": "111", "bank_name": "BBVA"})
await client.post("/api/beneficiarios", json={"user_id": user_b, "account_number": "222", "bank_name": "HSBC"})

response = await client.get(f"/api/beneficiarios?user_id={user_a}")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["user_id"] == user_a


@pytest.mark.asyncio
async def test_update_beneficiario(client: AsyncClient) -> None:
user_id = await _create_usuario(client)
create_resp = await client.post(
"/api/beneficiarios",
json={"user_id": user_id, "account_number": "000", "bank_name": "OldBank"},
)
beneficiario_id = create_resp.json()["id"]

response = await client.patch(
f"/api/beneficiarios/{beneficiario_id}",
json={"account_number": "999", "bank_name": "NewBank"},
)
assert response.status_code == 200
data = response.json()
assert data["account_number"] == "999"
assert data["bank_name"] == "NewBank"


@pytest.mark.asyncio
async def test_update_beneficiario_not_found(client: AsyncClient) -> None:
response = await client.patch(f"/api/beneficiarios/{uuid.uuid4()}", json={"bank_name": "X"})
assert response.status_code == 404


@pytest.mark.asyncio
async def test_delete_beneficiario(client: AsyncClient) -> None:
user_id = await _create_usuario(client)
create_resp = await client.post(
"/api/beneficiarios",
json={"user_id": user_id, "account_number": "DEL001", "bank_name": "DeleteBank"},
)
beneficiario_id = create_resp.json()["id"]

response = await client.delete(f"/api/beneficiarios/{beneficiario_id}")
assert response.status_code == 204

get_resp = await client.get(f"/api/beneficiarios/{beneficiario_id}")
assert get_resp.status_code == 404


@pytest.mark.asyncio
async def test_delete_beneficiario_not_found(client: AsyncClient) -> None:
response = await client.delete(f"/api/beneficiarios/{uuid.uuid4()}")
assert response.status_code == 404
Loading