From 5e55b64c510f0eb897c7d77ba485327014b457e5 Mon Sep 17 00:00:00 2001 From: shivamg9 Date: Tue, 28 May 2024 04:05:17 +0530 Subject: [PATCH 1/3] G2P-2391: Exclude archived and reimbursement programs from all program API --- src/openg2p_portal_api/models/orm/program_orm.py | 12 +++++++++++- src/openg2p_portal_api/models/program.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/openg2p_portal_api/models/orm/program_orm.py b/src/openg2p_portal_api/models/orm/program_orm.py index 861d970..29da052 100644 --- a/src/openg2p_portal_api/models/orm/program_orm.py +++ b/src/openg2p_portal_api/models/orm/program_orm.py @@ -22,6 +22,8 @@ class ProgramORM(BaseORMModelWithId): description: Mapped[str] = mapped_column(String()) state: Mapped[str] = mapped_column(String()) is_multiple_form_submission: Mapped[str] = mapped_column() + is_reimbursement_program: Mapped[bool] = mapped_column() + active: Mapped[bool] = mapped_column() membership: Mapped[Optional[List["ProgramMembershipORM"]]] = relationship( back_populates="program" @@ -40,7 +42,15 @@ async def get_all_programs(cls) -> List["ProgramORM"]: async with async_session_maker() as session: stmt = ( select(cls) - .filter(cls.state != "inactive", cls.state != "ended") + .filter( + cls.state != "inactive", + cls.state != "ended", + cls.active.is_(True), + or_( + cls.is_reimbursement_program.is_(False), + cls.is_reimbursement_program.is_(None), + ), + ) .options(selectinload(cls.membership)) .order_by(cls.id.asc()) ) diff --git a/src/openg2p_portal_api/models/program.py b/src/openg2p_portal_api/models/program.py index 5d3b8b8..d8179bc 100644 --- a/src/openg2p_portal_api/models/program.py +++ b/src/openg2p_portal_api/models/program.py @@ -29,6 +29,8 @@ class Program(ProgramBase): is_portal_form_mapped: Optional[bool] = False is_multiple_form_submission: Optional[bool] = False last_application_status: Optional[str] = None + is_reimbursement_program: Optional[bool] = None + active: Optional[bool] = None @validator("is_portal_form_mapped", pre=True, always=True) def is_program_form_mapped(cls, v, values): From 0031cdadbe49e607e04e9fe89eb4720ea0c778bd Mon Sep 17 00:00:00 2001 From: shivamg9 Date: Tue, 28 May 2024 20:11:03 +0530 Subject: [PATCH 2/3] G2P-2411 G2P-2381 Fixed display name and resolved odoo server error during entitlement approval --- src/openg2p_portal_api/app.py | 2 +- .../controllers/auth_controller.py | 23 +++--- .../models/orm/partner_orm.py | 2 +- src/openg2p_portal_api/models/profile.py | 17 +++-- .../services/form_service.py | 57 ++++---------- .../services/partner_service.py | 75 +++++++++++++------ 6 files changed, 90 insertions(+), 86 deletions(-) diff --git a/src/openg2p_portal_api/app.py b/src/openg2p_portal_api/app.py index 61385c6..7e04c61 100644 --- a/src/openg2p_portal_api/app.py +++ b/src/openg2p_portal_api/app.py @@ -24,10 +24,10 @@ class Initializer(Initializer): def initialize(self, **kwargs): super().initialize() # Initialize all Services, Controllers, any utils here. + PartnerService() MembershipService() ProgramService() FormService() - PartnerService() DiscoveryController().post_init() ProgramController().post_init() diff --git a/src/openg2p_portal_api/controllers/auth_controller.py b/src/openg2p_portal_api/controllers/auth_controller.py index 03de81a..e2e7161 100644 --- a/src/openg2p_portal_api/controllers/auth_controller.py +++ b/src/openg2p_portal_api/controllers/auth_controller.py @@ -4,6 +4,7 @@ from openg2p_fastapi_auth.controllers.auth_controller import AuthController from openg2p_fastapi_auth.models.orm.login_provider import LoginProvider from openg2p_fastapi_common.errors.http_exceptions import UnauthorizedError +from sqlalchemy.exc import IntegrityError from ..config import Settings from ..dependencies import JwtBearerAuth @@ -16,7 +17,7 @@ PartnerPhoneNoORM, ) from ..models.orm.reg_id_orm import RegIDORM, RegIDTypeORM -from ..models.profile import Profile +from ..models.profile import GetProfile, UpdateProfile from ..services.partner_service import PartnerService _config = Settings.get_config() @@ -37,13 +38,12 @@ def __init__(self, **kwargs): self.router.add_api_route( "/profile", self.get_profile, - responses={200: {"model": Profile}}, + responses={200: {"model": GetProfile}}, methods=["GET"], ) self.router.add_api_route( "/profile", self.update_profile, - responses={200: {"model": Profile}}, methods=["PUT"], ) @@ -125,7 +125,7 @@ async def get_profile( } ) - return Profile( + return GetProfile( id=partner_data.id, ids=partner_ids, email=partner_data.email, @@ -142,7 +142,7 @@ async def get_profile( async def update_profile( self, - userdata: Profile, + userdata: UpdateProfile, auth: Annotated[AuthCredentials, Depends(JwtBearerAuth())], ): """ @@ -158,14 +158,13 @@ async def update_profile( Confirmation or updated profile data after the update. """ - if userdata.id is None or userdata.id != auth.partner_id: - raise UnauthorizedError( - message="Unauthorized. Partner Not Found in Registry." + try: + await self.partner_service.update_partner_info( + auth.partner_id, userdata.model_dump(exclude={"id"}) ) - - return await self.partner_service.update_partner_data( - auth.partner_id, userdata.model_dump(exclude={"id"}) - ) + except IntegrityError: + return "Could not add to registrant to program!!" + return "Updated the partner info" async def get_login_providers_db(self) -> List[LoginProvider]: return [ diff --git a/src/openg2p_portal_api/models/orm/partner_orm.py b/src/openg2p_portal_api/models/orm/partner_orm.py index 5d0c2e7..8d83290 100644 --- a/src/openg2p_portal_api/models/orm/partner_orm.py +++ b/src/openg2p_portal_api/models/orm/partner_orm.py @@ -32,7 +32,7 @@ class PartnerORM(BaseORMModelWithId): birth_place: Mapped[str] = mapped_column() phone: Mapped[str] = mapped_column() company_id: Mapped[Optional[int]] = mapped_column() - registration_date: Mapped[date] = mapped_column(Date()) + registration_date: Mapped[date] = mapped_column(Date(), default=date.today) reg_ids: Mapped[Optional[List[RegIDORM]]] = relationship(back_populates="partner") create_date: Mapped[datetime] = mapped_column(DateTime(), default=datetime.utcnow) diff --git a/src/openg2p_portal_api/models/profile.py b/src/openg2p_portal_api/models/profile.py index 71694c1..5f669ed 100644 --- a/src/openg2p_portal_api/models/profile.py +++ b/src/openg2p_portal_api/models/profile.py @@ -27,17 +27,22 @@ class PhoneNumber(BaseModel): class Profile(BaseModel): - model_config = ConfigDict() - - id: Optional[int] = None - ids: List[RegistrantID] + ids: Optional[List[RegistrantID]] = None email: Optional[str] = None gender: Optional[str] = None # address: Optional[dict] = {} - bank_ids: List[BankDetails] + bank_ids: Optional[List[BankDetails]] = None addl_name: Optional[str] = None given_name: Optional[str] = None family_name: Optional[str] = None birthdate: Optional[date] = None - phone_numbers: List[PhoneNumber] + phone_numbers: Optional[List[PhoneNumber]] = None birth_place: Optional[str] = None + + +class UpdateProfile(Profile): + pass + + +class GetProfile(Profile): + id: Optional[int] = None diff --git a/src/openg2p_portal_api/services/form_service.py b/src/openg2p_portal_api/services/form_service.py index 5492e2a..02b4a40 100644 --- a/src/openg2p_portal_api/services/form_service.py +++ b/src/openg2p_portal_api/services/form_service.py @@ -3,25 +3,24 @@ from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService -from sqlalchemy import text from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import async_sessionmaker -from ..context import partner_fields_cache from ..models.form import ProgramForm -from ..models.orm.partner_orm import PartnerORM from ..models.orm.program_orm import ProgramORM from ..models.orm.program_registrant_info_orm import ( ProgramRegistrantInfoDraftORM, ProgramRegistrantInfoORM, ) from .membership_service import MembershipService +from .partner_service import PartnerService class FormService(BaseService): def __init__(self, **kwargs): super().__init__(**kwargs) self.membership_service = MembershipService.get_component() + self.partner_service = PartnerService.get_component() async def get_program_form(self, program_id: int, registrant_id: int): response_dict = {} @@ -107,17 +106,12 @@ async def submit_application_form( application_id = self._compute_application_id() create_date = datetime.now() - partner_info = await PartnerORM.get_partner_data(registrant_id) - if partner_info: - updated_partner_info = await self.update_partner_info( - session, partner_info, form_data.program_registrant_info - ) - - cleaned_program_registrant_info = self.clean_program_registrant_info( - form_data.program_registrant_info, updated_partner_info - ) - else: - cleaned_program_registrant_info = form_data.program_registrant_info + updated_partner_info = await self.partner_service.update_partner_info( + registrant_id, form_data.program_registrant_info, session=session + ) + cleaned_program_registrant_info = self.clean_program_registrant_info( + form_data.program_registrant_info, updated_partner_info + ) program_registrant_info = ProgramRegistrantInfoORM( program_id=program_id, @@ -151,38 +145,15 @@ def _compute_application_id(self): random_number = str(random.randint(1, 100000)) return d + m + y + random_number.zfill(5) - async def update_partner_info(self, session, partner_info, program_registrant_info): - # Update partner_info with fields from program_registrant_info - updated_fields = {} - partner_fields = await self.get_partner_fields() - for key, value in program_registrant_info.items(): - # if hasattr(partner_info, key) and getattr(partner_info, key) != value: - if key in partner_fields: - updated_fields[key] = value - if updated_fields: - set_clause = ", ".join( - [f"{key} = '{value}'" for key, value in updated_fields.items()] - ) - await session.execute( - text( - f"UPDATE {PartnerORM.__tablename__} SET {set_clause} WHERE id='{partner_info.id}'" - ) - ) - return updated_fields - - def clean_program_registrant_info(self, program_registrant_info, updated_fields): + def clean_program_registrant_info( + self, program_registrant_info, updated_partner_fields + ): # Remove updated fields from program_registrant_info + if not updated_partner_fields: + return program_registrant_info cleaned_info = { key: value for key, value in program_registrant_info.items() - if key not in updated_fields + if key not in updated_partner_fields } return cleaned_info - - async def get_partner_fields(self): - partner_field = partner_fields_cache.get() - if partner_field: - return partner_field - partner_field = await PartnerORM.get_partner_fields() - partner_fields_cache.set(partner_field) - return partner_field diff --git a/src/openg2p_portal_api/services/partner_service.py b/src/openg2p_portal_api/services/partner_service.py index 3a1bbf0..5888dbf 100644 --- a/src/openg2p_portal_api/services/partner_service.py +++ b/src/openg2p_portal_api/services/partner_service.py @@ -1,18 +1,22 @@ +import logging from datetime import datetime import orjson from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.errors.http_exceptions import InternalServerError from openg2p_fastapi_common.service import BaseService +from sqlalchemy import text from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import async_sessionmaker from ..config import Settings +from ..context import partner_fields_cache from ..models.orm.auth_oauth_provider import AuthOauthProviderORM from ..models.orm.partner_orm import PartnerORM, PartnerPhoneNoORM from ..models.orm.reg_id_orm import RegIDORM _config = Settings.get_config(strict=False) +_logger = logging.getLogger(_config.logging_default_logger_name) class PartnerService(BaseService): @@ -74,7 +78,9 @@ async def check_and_create_partner( ) ) - async_session_maker = async_sessionmaker(dbengine.get()) + async_session_maker = async_sessionmaker( + dbengine.get(), expire_on_commit=False + ) async with async_session_maker() as session: try: partner = PartnerORM(**partner_dict) @@ -92,34 +98,41 @@ async def check_and_create_partner( session.add(phone_number) await session.commit() + await self.create_partner_add_display_name(partner, session) except IntegrityError as e: raise InternalServerError( message=f"Could not create partner. {repr(e)}", ) from e - async def update_partner_data(self, id, data: dict): - async_session_maker = async_sessionmaker(dbengine.get()) - async with async_session_maker() as session: - partner = await PartnerORM.get_partner_data(id) - if partner: - for field_name in partner.__dict__: - if field_name in data: - setattr(partner, field_name, data[field_name]) - - partner.name = ( - data["given_name"] - + " " - + data["family_name"] - + " " - + data["addl_name"] + async def update_partner_info(self, partner_id, data, session=None): + # Update partner_info with fields from program_registrant_info + is_create_session = False + if not session: + session = async_sessionmaker(dbengine.get())() + is_create_session = True + updated_fields = {} + partner_fields = await self.get_partner_fields() + for key, value in data.items(): + # if hasattr(partner_info, key) and getattr(partner_info, key) != value: + # TODO: handle deleted values + + if key in partner_fields and data.get(key, None): + updated_fields[key] = value + # TODO: handle the name change + # name=self.create_partner_process_name(data["family_name"],data["given_name"],data["addl_name"]) + if updated_fields: + set_clause = ", ".join( + [f"{key} = '{value}'" for key, value in updated_fields.items()] + ) + await session.execute( + text( + f"UPDATE {PartnerORM.__tablename__} SET {set_clause} WHERE id='{partner_id}'" ) - - try: - session.add(partner) - await session.commit() - except IntegrityError: - return "Could not add to registrant to program!!" - return "Updated the partner info" + ) + await session.commit() + if is_create_session: + await session.close() + return updated_fields def create_partner_process_gender(self, gender): return gender.capitalize() @@ -157,3 +170,19 @@ def create_partner_process_other_fields(self, validation: dict, mapping: str): else: res[key] = value return res + + async def create_partner_add_display_name(self, partner, session): + try: + await self.update_partner_info( + partner.id, {"display_name": partner.name}, session=session + ) + except IntegrityError: + _logger.warning("Failed to insert display name. Odoo version maybe 17.0") + + async def get_partner_fields(self): + partner_field = partner_fields_cache.get() + if partner_field: + return partner_field + partner_field = await PartnerORM.get_partner_fields() + partner_fields_cache.set(partner_field) + return partner_field From 6b0779d210452808e258281976b3f4baa86a309a Mon Sep 17 00:00:00 2001 From: shivamg9 Date: Thu, 30 May 2024 00:12:29 +0530 Subject: [PATCH 3/3] G2P-2185 G2P-2404 Restrict submission from beneficiary portal api --- .../services/form_service.py | 22 +++++++++++++++++-- .../services/membership_service.py | 3 ++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/openg2p_portal_api/services/form_service.py b/src/openg2p_portal_api/services/form_service.py index 02b4a40..db90441 100644 --- a/src/openg2p_portal_api/services/form_service.py +++ b/src/openg2p_portal_api/services/form_service.py @@ -3,6 +3,7 @@ from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService +from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import async_sessionmaker @@ -95,9 +96,26 @@ async def submit_application_form( ): async_session_maker = async_sessionmaker(dbengine.get()) async with async_session_maker() as session: - program_membership_id = await self.membership_service.check_and_create_mem( - program_id, registrant_id + existing_application = await session.execute( + select(ProgramRegistrantInfoORM).filter( + ProgramRegistrantInfoORM.program_id == program_id, + ProgramRegistrantInfoORM.registrant_id == registrant_id, + ProgramRegistrantInfoORM.state.in_( + ["active", "inprogress", "applied"] + ), + ) ) + if existing_application.scalars().first(): + return "Error: There is already an active or in-progress application for this program." + + try: + program_membership_id = ( + await self.membership_service.check_and_create_mem( + program_id, registrant_id + ) + ) + except ValueError as e: + return str(e) get_draft_reg_info = ( await ProgramRegistrantInfoDraftORM.get_draft_reg_info_by_id( program_id, registrant_id diff --git a/src/openg2p_portal_api/services/membership_service.py b/src/openg2p_portal_api/services/membership_service.py index d86bb7b..4305655 100644 --- a/src/openg2p_portal_api/services/membership_service.py +++ b/src/openg2p_portal_api/services/membership_service.py @@ -10,7 +10,7 @@ class MembershipService(BaseService): def __init__(self, **kwargs): super().__init__(**kwargs) - async def check_and_create_mem(self, programid: int, partnerid: int): + async def check_and_create_mem(self, programid: int, partnerid: int) -> int: async_session_maker = async_sessionmaker(dbengine.get()) async with async_session_maker() as session: membership = await ProgramMembershipORM.get_membership_by_id( @@ -25,6 +25,7 @@ async def check_and_create_mem(self, programid: int, partnerid: int): try: session.add(membership) await session.commit() + await session.refresh(membership) except IntegrityError: return "Could not add to registrant to program!!"