diff --git a/db_revisions/versions/045aa502e050_update_financial_institutions_table.py b/db_revisions/versions/045aa502e050_update_financial_institutions_table.py index 7932387..4a4122f 100644 --- a/db_revisions/versions/045aa502e050_update_financial_institutions_table.py +++ b/db_revisions/versions/045aa502e050_update_financial_institutions_table.py @@ -84,7 +84,9 @@ def downgrade() -> None: op.drop_constraint( constraint_name="fk_federal_regulator_financial_institutions", table_name="financial_institutions" ) - op.drop_constraint(constraint_name="fk_address_state_financial_institutions", table_name="financial_institutions") + op.drop_constraint( + constraint_name="fk_address_state_code_financial_institutions", table_name="financial_institutions" + ) op.drop_constraint( constraint_name="fk_hmda_institution_type_financial_institutions", table_name="financial_institutions" ) diff --git a/db_revisions/versions/383ab402c8c2_231227_add_active_field_to_institutions.py b/db_revisions/versions/383ab402c8c2_231227_add_active_field_to_institutions.py new file mode 100644 index 0000000..ee3d69e --- /dev/null +++ b/db_revisions/versions/383ab402c8c2_231227_add_active_field_to_institutions.py @@ -0,0 +1,31 @@ +"""231227 add active field to institutions table + +Revision ID: 383ab402c8c2 +Revises: a41281b1e109 +Create Date: 2023-12-27 14:21:33.567414 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "383ab402c8c2" +down_revision: Union[str, None] = "a41281b1e109" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.batch_alter_table("financial_institutions") as batch_op: + batch_op.add_column(sa.Column(name="is_active", type_=sa.Boolean(), nullable=False, server_default=sa.true())) + batch_op.create_index( + index_name=batch_op.f("ix_financial_institutions_is_active"), columns=["is_active"], unique=False + ) + + +def downgrade() -> None: + op.drop_index(index_name="ix_financial_institutions_is_active", table_name="financial_institutions") + op.drop_column(table_name="financial_institutions", column_name="is_active") diff --git a/src/entities/models/__init__.py b/src/entities/models/__init__.py index 1415f83..e43e50e 100644 --- a/src/entities/models/__init__.py +++ b/src/entities/models/__init__.py @@ -3,7 +3,7 @@ "FinancialInstitutionDao", "FinancialInstitutionDomainDao", "FinancialInstitutionDto", - "FinancialInstitutionWithDomainsDto", + "FinancialInstitutionWithRelationsDto", "FinancialInsitutionDomainDto", "FinancialInsitutionDomainCreate", "FinanicialInstitutionAssociationDto", @@ -34,7 +34,7 @@ ) from .dto import ( FinancialInstitutionDto, - FinancialInstitutionWithDomainsDto, + FinancialInstitutionWithRelationsDto, FinancialInsitutionDomainDto, FinancialInsitutionDomainCreate, FinanicialInstitutionAssociationDto, diff --git a/src/entities/models/dao.py b/src/entities/models/dao.py index 1a9d643..6e11577 100644 --- a/src/entities/models/dao.py +++ b/src/entities/models/dao.py @@ -18,6 +18,7 @@ class FinancialInstitutionDao(AuditMixin, Base): __tablename__ = "financial_institutions" lei: Mapped[str] = mapped_column(unique=True, index=True, primary_key=True) name: Mapped[str] = mapped_column(index=True) + is_active: Mapped[bool] = mapped_column(index=True) domains: Mapped[List["FinancialInstitutionDomainDao"]] = relationship( "FinancialInstitutionDomainDao", back_populates="fi" ) diff --git a/src/entities/models/dto.py b/src/entities/models/dto.py index de46dc1..2510e9e 100644 --- a/src/entities/models/dto.py +++ b/src/entities/models/dto.py @@ -21,6 +21,7 @@ class Config: class FinancialInstitutionBase(BaseModel): lei: str name: str + is_active: bool class FinancialInstitutionDto(FinancialInstitutionBase): @@ -45,10 +46,6 @@ class Config: from_attributes = True -class FinancialInstitutionWithDomainsDto(FinancialInstitutionDto): - domains: List[FinancialInsitutionDomainDto] = [] - - class DeniedDomainDto(BaseModel): domain: str @@ -65,10 +62,6 @@ def to_keycloak_user(self): return {"firstName": self.first_name, "lastName": self.last_name} -class FinanicialInstitutionAssociationDto(FinancialInstitutionDto): - approved: bool - - class FederalRegulatorBase(BaseModel): id: str @@ -88,30 +81,12 @@ class Config: from_attributes = True -# Let this in here just in case the 'generic' InstitutionTypeDto approach isn't desired -# -# class HMDAInstitutionTypeBase(BaseModel): -# id: str - - -# class HMDAInstitutionTypeDto(HMDAInstitutionTypeBase): -# name: str -# -# class Config: -# from_attributes = True - - -# class SBLInstitutionTypeBase(BaseModel): -# id: str - - -# class SBLInstitutionTypeDto(SBLInstitutionTypeBase): -# name: str -# -# class Config: -# from_attributes = True - +class HMDAInstitutionTypeDto(InstitutionTypeDto): + pass +class SBLInstitutionTypeDto(InstitutionTypeDto): + pass + class AddressStateBase(BaseModel): code: str @@ -123,6 +98,18 @@ class Config: from_attributes = True +class FinancialInstitutionWithRelationsDto(FinancialInstitutionDto): + primary_federal_regulator: FederalRegulatorDto | None = None + hmda_institution_type: HMDAInstitutionTypeDto | None = None + sbl_institution_type: SBLInstitutionTypeDto | None = None + hq_address_state: AddressStateDto + domains: List[FinancialInsitutionDomainDto] = [] + + +class FinanicialInstitutionAssociationDto(FinancialInstitutionWithRelationsDto): + approved: bool + + class AuthenticatedUser(BaseUser, BaseModel): claims: Dict[str, Any] name: str diff --git a/src/entities/repos/institutions_repo.py b/src/entities/repos/institutions_repo.py index db3bf3e..38555d8 100644 --- a/src/entities/repos/institutions_repo.py +++ b/src/entities/repos/institutions_repo.py @@ -82,46 +82,14 @@ async def upsert_institution(session: AsyncSession, fi: FinancialInstitutionDto) stmt = select(FinancialInstitutionDao).filter(FinancialInstitutionDao.lei == fi.lei) res = await session.execute(stmt) db_fi = res.scalar_one_or_none() + fi_data = fi.__dict__.copy() + fi_data.pop("_sa_instance_state", None) if db_fi is None: - db_fi = FinancialInstitutionDao( - lei=fi.lei, - name=fi.name, - tax_id=fi.tax_id, - rssd_id=fi.rssd_id, - primary_federal_regulator_id=fi.primary_federal_regulator_id, - hmda_institution_type_id=fi.hmda_institution_type_id, - sbl_institution_type_id=fi.sbl_institution_type_id, - hq_address_street_1=fi.hq_address_street_1, - hq_address_street_2=fi.hq_address_street_2, - hq_address_city=fi.hq_address_city, - hq_address_state_code=fi.hq_address_state_code, - hq_address_zip=fi.hq_address_zip, - parent_lei=fi.parent_lei, - parent_legal_name=fi.parent_legal_name, - parent_rssd_id=fi.parent_rssd_id, - top_holder_lei=fi.top_holder_lei, - top_holder_legal_name=fi.top_holder_legal_name, - top_holder_rssd_id=fi.top_holder_rssd_id, - ) + db_fi = FinancialInstitutionDao(**fi_data) session.add(db_fi) else: - db_fi.name = fi.name - db_fi.tax_id = fi.tax_id - db_fi.rssd_id = fi.rssd_id - db_fi.primary_federal_regulator_id = fi.primary_federal_regulator_id - db_fi.hmda_institution_type_id = fi.hmda_institution_type_id - db_fi.sbl_institution_type_id = fi.sbl_institution_type_id - db_fi.hq_address_street_1 = fi.hq_address_street_1 - db_fi.hq_address_street_2 = fi.hq_address_street_2 - db_fi.hq_address_city = fi.hq_address_city - db_fi.hq_address_state_code = fi.hq_address_state_code - db_fi.hq_address_zip = fi.hq_address_zip - db_fi.parent_lei = fi.parent_lei - db_fi.parent_legal_name = fi.parent_legal_name - db_fi.parent_rssd_id = fi.parent_rssd_id - db_fi.top_holder_lei = fi.top_holder_lei - db_fi.top_holder_legal_name = fi.top_holder_legal_name - db_fi.top_holder_rssd_id = fi.top_holder_rssd_id + for key, value in fi_data.items(): + setattr(db_fi, key, value) await session.commit() return db_fi diff --git a/src/routers/institutions.py b/src/routers/institutions.py index 0b1b8ec..b207217 100644 --- a/src/routers/institutions.py +++ b/src/routers/institutions.py @@ -8,7 +8,7 @@ from entities.repos import institutions_repo as repo from entities.models import ( FinancialInstitutionDto, - FinancialInstitutionWithDomainsDto, + FinancialInstitutionWithRelationsDto, FinancialInsitutionDomainDto, FinancialInsitutionDomainCreate, FinanicialInstitutionAssociationDto, @@ -32,7 +32,7 @@ async def set_db(request: Request, session: Annotated[AsyncSession, Depends(get_ router = Router(dependencies=[Depends(set_db)]) -@router.get("/", response_model=List[FinancialInstitutionWithDomainsDto]) +@router.get("/", response_model=List[FinancialInstitutionWithRelationsDto]) @requires("authenticated") async def get_institutions( request: Request, @@ -44,7 +44,7 @@ async def get_institutions( return await repo.get_institutions(request.state.db_session, leis, domain, page, count) -@router.post("/", response_model=Tuple[str, FinancialInstitutionDto], dependencies=[Depends(check_domain)]) +@router.post("/", response_model=Tuple[str, FinancialInstitutionWithRelationsDto], dependencies=[Depends(check_domain)]) @requires(["query-groups", "manage-users"]) async def create_institution( request: Request, @@ -63,24 +63,7 @@ async def get_associated_institutions(request: Request): associated_institutions = await repo.get_institutions(request.state.db_session, user.institutions) return [ FinanicialInstitutionAssociationDto( - name=institution.name, - lei=institution.lei, - tax_id=institution.tax_id, - rssd_id=institution.rssd_id, - primary_federal_regulator_id=institution.primary_federal_regulator_id, - hmda_institution_type_id=institution.hmda_institution_type_id, - sbl_institution_type_id=institution.sbl_institution_type_id, - hq_address_street_1=institution.hq_address_street_1, - hq_address_street_2=institution.hq_address_street_2, - hq_address_city=institution.hq_address_city, - hq_address_state_code=institution.hq_address_state_code, - hq_address_zip=institution.hq_address_zip, - parent_lei=institution.parent_lei, - parent_legal_name=institution.parent_legal_name, - parent_rssd_id=institution.parent_rssd_id, - top_holder_lei=institution.top_holder_lei, - top_holder_legal_name=institution.top_holder_legal_name, - top_holder_rssd_id=institution.top_holder_rssd_id, + **institution.__dict__, approved=email_domain in [inst_domain.domain for inst_domain in institution.domains], ) for institution in associated_institutions @@ -108,7 +91,7 @@ async def get_federal_regulators(request: Request): return await repo.get_federal_regulators(request.state.db_session) -@router.get("/{lei}", response_model=FinancialInstitutionWithDomainsDto) +@router.get("/{lei}", response_model=FinancialInstitutionWithRelationsDto) @requires("authenticated") async def get_institution( request: Request, diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 73577f4..1c95982 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -5,7 +5,15 @@ from pytest_mock import MockerFixture from starlette.authentication import AuthCredentials, UnauthenticatedUser -from entities.models import AuthenticatedUser, FinancialInstitutionDao, FinancialInstitutionDomainDao +from entities.models import ( + AuthenticatedUser, + FinancialInstitutionDao, + FinancialInstitutionDomainDao, + FederalRegulatorDao, + AddressStateDao, + HMDAInstitutionTypeDao, + SBLInstitutionTypeDao, +) @pytest.fixture @@ -54,16 +62,21 @@ def get_institutions_mock(mocker: MockerFixture) -> Mock: FinancialInstitutionDao( name="Test Bank 123", lei="TESTBANK123", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test.bank", lei="TESTBANK123")], tax_id="123456789", rssd_id=1234, primary_federal_regulator_id="FRI1", + primary_federal_regulator=FederalRegulatorDao(id="FRI1", name="FRI1"), hmda_institution_type_id="HIT1", + hmda_institution_type=HMDAInstitutionTypeDao(id="HIT1", name="HIT1"), sbl_institution_type_id="SIT1", + sbl_institution_type=SBLInstitutionTypeDao(id="SIT1", name="SIT1"), hq_address_street_1="Test Address Street 1", hq_address_street_2="", hq_address_city="Test City 1", hq_address_state_code="GA", + hq_address_state=AddressStateDao(code="GA", name="Georgia"), hq_address_zip="00000", parent_lei="PARENTTESTBANK123", parent_legal_name="PARENT TEST BANK 123", diff --git a/tests/api/routers/test_institutions_api.py b/tests/api/routers/test_institutions_api.py index 0e55bcf..d5f0abd 100644 --- a/tests/api/routers/test_institutions_api.py +++ b/tests/api/routers/test_institutions_api.py @@ -5,7 +5,14 @@ from pytest_mock import MockerFixture from starlette.authentication import AuthCredentials from oauth2.oauth2_backend import AuthenticatedUser -from entities.models import FinancialInstitutionDao, FinancialInstitutionDomainDao +from entities.models import ( + FinancialInstitutionDao, + FinancialInstitutionDomainDao, + FederalRegulatorDao, + AddressStateDao, + HMDAInstitutionTypeDao, + SBLInstitutionTypeDao, +) class TestInstitutionsApi: @@ -32,16 +39,21 @@ def test_create_institution_authed(self, mocker: MockerFixture, app_fixture: Fas upsert_institution_mock.return_value = FinancialInstitutionDao( name="testName", lei="testLei", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test.bank", lei="TESTBANK123")], tax_id="123456789", rssd_id=1234, primary_federal_regulator_id="FRI2", + primary_federal_regulator=FederalRegulatorDao(id="FRI2", name="FRI2"), hmda_institution_type_id="HIT2", + hmda_institution_type=HMDAInstitutionTypeDao(id="HIT2", name="HIT2"), sbl_institution_type_id="SIT2", + sbl_institution_type=SBLInstitutionTypeDao(id="SIT2", name="SIT2"), hq_address_street_1="Test Address Street 1", hq_address_street_2="", hq_address_city="Test City 1", hq_address_state_code="VA", + hq_address_state=AddressStateDao(code="VA", name="Virginia"), hq_address_zip="00000", parent_lei="PARENTTESTBANK123", parent_legal_name="PARENT TEST BANK 123", @@ -58,6 +70,7 @@ def test_create_institution_authed(self, mocker: MockerFixture, app_fixture: Fas json={ "name": "testName", "lei": "testLei", + "is_active": True, "tax_id": "123456789", "rssd_id": 12344, "primary_federal_regulator_id": "FRI2", @@ -86,9 +99,11 @@ def test_create_institution_only_required_fields( upsert_institution_mock.return_value = FinancialInstitutionDao( name="testName", lei="testLei", + is_active=True, hq_address_street_1="Test Address Street 1", hq_address_city="Test City 1", hq_address_state_code="VA", + hq_address_state=AddressStateDao(code="VA", name="Virginia"), hq_address_zip="00000", ) upsert_group_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.upsert_group") @@ -99,6 +114,7 @@ def test_create_institution_only_required_fields( json={ "name": "testName", "lei": "testLei", + "is_active": True, "hq_address_street_1": "Test Address Street 1", "hq_address_city": "Test City 1", "hq_address_state_code": "VA", @@ -142,6 +158,7 @@ def test_create_institution_authed_no_permission(self, app_fixture: FastAPI, aut json={ "name": "testName", "lei": "testLei", + "is_active": True, "tax_id": "123456789", "rssd_id": 12344, "primary_federal_regulator_id": "FIR2", @@ -173,16 +190,21 @@ def test_get_institution_authed(self, mocker: MockerFixture, app_fixture: FastAP get_institution_mock.return_value = FinancialInstitutionDao( name="Test Bank 123", lei="TESTBANK123", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test.bank", lei="TESTBANK123")], tax_id="123456789", rssd_id=1234, primary_federal_regulator_id="FRI1", + primary_federal_regulator=FederalRegulatorDao(id="FRI1", name="FRI1"), hmda_institution_type_id="HIT1", + hmda_institution_type=HMDAInstitutionTypeDao(id="HIT1", name="HIT1"), sbl_institution_type_id="SIT1", + sbl_institution_type=SBLInstitutionTypeDao(id="SIT1", name="SIT1"), hq_address_street_1="Test Address Street 1", hq_address_street_2="", hq_address_city="Test City 1", hq_address_state_code="GA", + hq_address_state=AddressStateDao(code="GA", name="Georgia"), hq_address_zip="00000", parent_lei="PARENTTESTBANK123", parent_legal_name="PARENT TEST BANK 123", @@ -266,16 +288,21 @@ def test_get_associated_institutions( FinancialInstitutionDao( name="Test Bank 123", lei="TESTBANK123", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test123.bank", lei="TESTBANK123")], tax_id="123456789", rssd_id=1234, primary_federal_regulator_id="FRI1", + primary_federal_regulator=FederalRegulatorDao(id="FRI1", name="FRI1"), hmda_institution_type_id="HIT1", + hmda_institution_type=HMDAInstitutionTypeDao(id="HIT1", name="HIT1"), sbl_institution_type_id="SIT1", + sbl_institution_type=SBLInstitutionTypeDao(id="SIT1", name="SIT1"), hq_address_street_1="Test Address Street 1", hq_address_street_2="", hq_address_city="Test City 1", hq_address_state_code="GA", + hq_address_state=AddressStateDao(code="GA", name="Georgia"), hq_address_zip="00000", parent_lei="PARENTTESTBANK123", parent_legal_name="PARENT TEST BANK 123", @@ -287,16 +314,21 @@ def test_get_associated_institutions( FinancialInstitutionDao( name="Test Bank 234", lei="TESTBANK234", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test234.bank", lei="TESTBANK234")], tax_id="123456879", rssd_id=6879, primary_federal_regulator_id="FRI1", + primary_federal_regulator=FederalRegulatorDao(id="FRI1", name="FRI1"), hmda_institution_type_id="HIT1", + hmda_institution_type=HMDAInstitutionTypeDao(id="HIT1", name="HIT1"), sbl_institution_type_id="SIT1", + sbl_institution_type=SBLInstitutionTypeDao(id="SIT1", name="SIT1"), hq_address_street_1="Test Address Street 2", hq_address_street_2="", hq_address_city="Test City 2", hq_address_state_code="GA", + hq_address_state=AddressStateDao(code="GA", name="Georgia"), hq_address_zip="00000", parent_lei="PARENTTESTBANK123", parent_legal_name="PARENT TEST BANK 123", diff --git a/tests/entities/repos/test_institutions_repo.py b/tests/entities/repos/test_institutions_repo.py index e0e4d4e..e8c9d8e 100644 --- a/tests/entities/repos/test_institutions_repo.py +++ b/tests/entities/repos/test_institutions_repo.py @@ -46,6 +46,7 @@ async def setup( FinancialInstitutionDao( name="Test Bank 123", lei="TESTBANK123", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test.bank.1", lei="TESTBANK123")], tax_id="123456789", rssd_id=1234, @@ -67,6 +68,7 @@ async def setup( FinancialInstitutionDao( name="Test Bank 456", lei="TESTBANK456", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="test.bank.2", lei="TESTBANK456")], tax_id="987654321", rssd_id=4321, @@ -88,6 +90,7 @@ async def setup( FinancialInstitutionDao( name="Test Sub Bank 456", lei="TESTSUBBANK456", + is_active=True, domains=[FinancialInstitutionDomainDao(domain="sub.test.bank.2", lei="TESTSUBBANK456")], tax_id="765432198", rssd_id=2134, @@ -190,6 +193,7 @@ async def test_add_institution(self, transaction_session: AsyncSession): FinancialInstitutionDao( name="New Bank 123", lei="NEWBANK123", + is_active=True, tax_id="654321987", rssd_id=6543, primary_federal_regulator_id="FRI3", @@ -219,6 +223,7 @@ async def test_add_institution_only_required_fields( FinancialInstitutionDao( name="Minimal Bank 123", lei="MINBANK123", + is_active=True, hq_address_street_1="Minimal Address Street 1", hq_address_city="Minimal City 1", hq_address_state_code="FL", @@ -250,6 +255,7 @@ async def test_update_institution(self, transaction_session: AsyncSession): FinancialInstitutionDao( name="Test Bank 234", lei="TESTBANK123", + is_active=True, hq_address_street_1="Test Address Street 1", hq_address_city="Test City 1", hq_address_state_code="GA",