From b4cef352a87fe2bcc18e76941d841c1482d02c5b Mon Sep 17 00:00:00 2001 From: herrdelta83 Date: Fri, 10 Apr 2026 14:52:19 -0600 Subject: [PATCH 1/5] =?UTF-8?q?file=20created=20beneficiario.py=20ORM=20mo?= =?UTF-8?q?del=20=E2=80=94=20FK=20to=20usuario.id,=20created=5Fat=20auto-s?= =?UTF-8?q?et?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/entities/beneficiario.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 backend/app/entities/beneficiario.py diff --git a/backend/app/entities/beneficiario.py b/backend/app/entities/beneficiario.py new file mode 100644 index 0000000..88276da --- /dev/null +++ b/backend/app/entities/beneficiario.py @@ -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)) From 289755508ad2c8fb3bfcd0ce7868dcf9f8de5c41 Mon Sep 17 00:00:00 2001 From: herrdelta83 Date: Fri, 10 Apr 2026 14:52:46 -0600 Subject: [PATCH 2/5] file beneficiario.py CreateBeneficiarioRequest, UpdateBeneficiarioRequest, BeneficiarioResponse --- backend/app/models/beneficiario.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/app/models/beneficiario.py diff --git a/backend/app/models/beneficiario.py b/backend/app/models/beneficiario.py new file mode 100644 index 0000000..222b4b7 --- /dev/null +++ b/backend/app/models/beneficiario.py @@ -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} From ade3eb9cf893be679210c61226d3d53161d675c0 Mon Sep 17 00:00:00 2001 From: herrdelta83 Date: Fri, 10 Apr 2026 14:53:29 -0600 Subject: [PATCH 3/5] file created beneficiario_repository.py create, get_by_id, get_all, update, delete --- .../repositories/beneficiario_repository.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 backend/app/repositories/beneficiario_repository.py diff --git a/backend/app/repositories/beneficiario_repository.py b/backend/app/repositories/beneficiario_repository.py new file mode 100644 index 0000000..e2645aa --- /dev/null +++ b/backend/app/repositories/beneficiario_repository.py @@ -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() From 0a086ab0997f4e4838855eb92843268988b39b5f Mon Sep 17 00:00:00 2001 From: herrdelta83 Date: Fri, 10 Apr 2026 14:53:58 -0600 Subject: [PATCH 4/5] file created beneficiarios.py 5 routes under /api/beneficiarios --- backend/app/api/beneficiarios.py | 58 ++++++++++++++++++++++++++++++++ backend/app/api/router.py | 2 ++ 2 files changed, 60 insertions(+) create mode 100644 backend/app/api/beneficiarios.py diff --git a/backend/app/api/beneficiarios.py b/backend/app/api/beneficiarios.py new file mode 100644 index 0000000..7d67d8b --- /dev/null +++ b/backend/app/api/beneficiarios.py @@ -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) diff --git a/backend/app/api/router.py b/backend/app/api/router.py index eca6b13..dcab2fc 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -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 @@ -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) From 53862a6a9cf3fb41c10fefc6dafc51db714f99c8 Mon Sep 17 00:00:00 2001 From: herrdelta83 Date: Fri, 10 Apr 2026 14:54:44 -0600 Subject: [PATCH 5/5] file created test_beneficiarios.py 9 tests) --- backend/tests/test_beneficiarios.py | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 backend/tests/test_beneficiarios.py diff --git a/backend/tests/test_beneficiarios.py b/backend/tests/test_beneficiarios.py new file mode 100644 index 0000000..ec94e19 --- /dev/null +++ b/backend/tests/test_beneficiarios.py @@ -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