From 509eb53c3206428141fc0bdcfec3705288c8f7e2 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Wed, 3 Jul 2024 19:01:21 +0530 Subject: [PATCH 01/39] Disbursement Envelope API --- .gitignore | 162 -------- LICENSE | 373 ------------------ README.md | 16 +- .../controllers/disbursement_envelope.py | 72 ++++ src/openg2p_g2p_bridge_api/errors/codes.py | 39 ++ .../errors/exceptions.py | 24 ++ .../models/disbursement_envelope.py | 92 +++++ .../schemas/__init__.py | 15 + .../schemas/disbursement_envelope.py | 28 ++ src/openg2p_g2p_bridge_api/schemas/request.py | 18 + .../schemas/response.py | 30 ++ .../services/disbursement_envelope.py | 234 +++++++++++ 12 files changed, 566 insertions(+), 537 deletions(-) delete mode 100644 .gitignore delete mode 100644 LICENSE create mode 100644 src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py create mode 100644 src/openg2p_g2p_bridge_api/errors/codes.py create mode 100644 src/openg2p_g2p_bridge_api/errors/exceptions.py create mode 100644 src/openg2p_g2p_bridge_api/models/disbursement_envelope.py create mode 100644 src/openg2p_g2p_bridge_api/schemas/__init__.py create mode 100644 src/openg2p_g2p_bridge_api/schemas/disbursement_envelope.py create mode 100644 src/openg2p_g2p_bridge_api/schemas/request.py create mode 100644 src/openg2p_g2p_bridge_api/schemas/response.py create mode 100644 src/openg2p_g2p_bridge_api/services/disbursement_envelope.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 82f9275..0000000 --- a/.gitignore +++ /dev/null @@ -1,162 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a612ad9..0000000 --- a/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index 5b1c94c..15fc796 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# openg2p-g2p-bridge-api -FastAPI based microservice to facilitate Cash Transfers - Bridge between PBMS and Sponsor Bank +# openg2p-g2p-bridge-api + +[![Pre-commit Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml?query=branch%3Adevelop) +[![Build Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml?query=branch%3Adevelop) +[![codecov](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api/branch/develop/graph/badge.svg)](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api) +[![openapi](https://img.shields.io/badge/open--API-swagger-brightgreen)](https://validator.swagger.io/?url=https://raw.githubusercontent.com/OpenG2P/openg2p-g2p-bridge-api/develop/api-docs/generated/openapi.json) +![PyPI](https://img.shields.io/pypi/v/openg2p-g2p-bridge-api?label=pypi%20package) +![PyPI - Downloads](https://img.shields.io/pypi/dm/openg2p-g2p-bridge-api) + + + +## Licenses + +This repository is licensed under [MPL-2.0](LICENSE). diff --git a/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py b/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py new file mode 100644 index 0000000..d5282a3 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py @@ -0,0 +1,72 @@ +from openg2p_fastapi_common.controller import BaseController + +from ..errors import DisbursementEnvelopeException +from ..schemas import ( + DisbursementEnvelopePayload, + DisbursementEnvelopeRequest, + DisbursementEnvelopeResponse, +) +from ..services import DisbursementEnvelopeService + + +class DisbursementEnvelopeController(BaseController): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.disbursement_envelope_service = DisbursementEnvelopeService.get_component() + self.router.tags += ["G2P Bridge Disbursement Envelope"] + + self.router.add_api_route( + "/create_disbursement_envelope", + self.create_disbursement_envelope, + responses={200: {"model": DisbursementEnvelopeRequest}}, + methods=["POST"], + ) + self.router.add_api_route( + "/cancel_disbursement_envelope", + self.cancel_disbursement_envelope, + responses={200: {"model": DisbursementEnvelopeRequest}}, + methods=["POST"], + ) + + async def create_disbursement_envelope( + self, disbursement_envelope_request: DisbursementEnvelopeRequest + ) -> DisbursementEnvelopeResponse: + try: + disbursement_envelope_payload: DisbursementEnvelopePayload = ( + await self.disbursement_envelope_service.create_disbursement_envelope( + disbursement_envelope_request + ) + ) + except DisbursementEnvelopeException as e: + error_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_error_response( + e.code + ) + return error_response + + disbursement_envelope_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_success_response( + disbursement_envelope_payload + ) + + return disbursement_envelope_response + + async def cancel_disbursement_envelope( + self, disbursement_envelope_request: DisbursementEnvelopeRequest + ) -> DisbursementEnvelopeResponse: + try: + disbursement_envelope_payload: DisbursementEnvelopePayload = ( + await self.disbursement_envelope_service.cancel_disbursement_envelope( + disbursement_envelope_request + ) + ) + except DisbursementEnvelopeException as e: + error_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_error_response( + e.code + ) + return error_response + + disbursement_envelope_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_success_response( + disbursement_envelope_payload + ) + + return disbursement_envelope_response diff --git a/src/openg2p_g2p_bridge_api/errors/codes.py b/src/openg2p_g2p_bridge_api/errors/codes.py new file mode 100644 index 0000000..354282a --- /dev/null +++ b/src/openg2p_g2p_bridge_api/errors/codes.py @@ -0,0 +1,39 @@ +import enum + + +class G2PBridgeErrorCodes(enum.Enum): + # Disbursement Envelope Errors + INVALID_PROGRAM_MNEMONIC = "INVALID_PROGRAM_MNEMONIC" + INVALID_DISBURSEMENT_FREQUENCY = "INVALID_DISBURSEMENT_FREQUENCY" + INVALID_CYCLE_CODE_MNEMONIC = "INVALID_CYCLE_CODE_MNEMONIC" + INVALID_NO_OF_BENEFICIARIES = "INVALID_NO_OF_BENEFICIARIES" + INVALID_NO_OF_DISBURSEMENTS = "INVALID_NO_OF_DISBURSEMENTS" + INVALID_TOTAL_DISBURSEMENT_AMOUNT = "INVALID_TOTAL_DISBURSEMENT_AMOUNT" + INVALID_DISBURSEMENT_CURRENCY_CODE = "INVALID_DISBURSEMENT_CURRENCY_CODE" + INVALID_DISBURSEMENT_SCHEDULE_DATE = "INVALID_DISBURSEMENT_SCHEDULE_DATE" + DISBURSEMENT_ENVELOPE_NOT_FOUND = "DISBURSEMENT_ENVELOPE_NOT_FOUND" + DISBURSEMENT_ENVELOPE_ALREADY_CANCELED = "DISBURSEMENT_ENVELOPE_ALREADY_CANCELED" + INVALID_DISBURSEMENT_ENVELOPE_ID = "INVALID_DISBURSEMENT_ENVELOPE_ID" + INVALID_DISBURSEMENT_AMOUNT = "INVALID_DISBURSEMENT_AMOUNT" + INVALID_BENEFICIARY_ID = "INVALID_BENEFICIARY_ID" + INVALID_BENEFICIARY_NAME = "INVALID_BENEFICIARY_NAME" + INVALID_NARRATIVE = "INVALID_NARRATIVE" + INVALID_RECEIPT_TIME_STAMP = "INVALID_RECEIPT_TIME_STAMP" + + # Disbursement Errors + INVALID_DISBURSEMENT_ID = "INVALID_DISBURSEMENT_ID" + INVALID_DISBURSEMENT_PAYLOAD = "INVALID_DISBURSEMENT_PAYLOAD" + ENVELOPE_ALREADY_CANCELLED = "ENVELOPE_ALREADY_CANCELLED" + NO_OF_DISBURSEMENTS_EXCEEDS_DECLARED = "NO_OF_DISBUESEMENTS_EXCEEDS_DECLARED" + NO_OF_DISBURSEMENTS_LESS_THAN_ZERO = "NO_OF_DISBURSEMENTS_LESS_THAN_ZERO" + TOTAL_DISBURSEMENT_AMOUNT_EXCEEDS_DECLARED = ( + "TOTAL_DISBURSEMENT_AMOUNT_EXCEEDS_DECLARED" + ) + TOTAL_DISBURSEMENT_AMOUNT_LESS_THAN_ZERO = ( + "TOTAL_DISBURSEMENT_AMOUNT_LESS_THAN_ZERO" + ) + MULTIPLE_ENVELOPES_FOUND = "MULTIPLE_ENVELOPES_FOUND" + DISBURSEMENT_ENVELOPE_SCHEDULE_DATE_REACHED = ( + "DISBURSEMENT_ENVELOPE_SCHEDULE_DATE_REACHED" + ) + DISBURSEMENT_ALREADY_CANCELED = "DISBURSEMENT_ALREADY_CANCELED" diff --git a/src/openg2p_g2p_bridge_api/errors/exceptions.py b/src/openg2p_g2p_bridge_api/errors/exceptions.py new file mode 100644 index 0000000..ca23761 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/errors/exceptions.py @@ -0,0 +1,24 @@ +from typing import List, Optional + +from ..schemas import DisbursementPayload +from .codes import G2PBridgeErrorCodes + + +class DisbursementEnvelopeException(Exception): + def __init__(self, code: G2PBridgeErrorCodes, message: Optional[str] = None): + self.code: G2PBridgeErrorCodes = code + self.message: Optional[str] = message + super().__init__(self.message) + + +class DisbursementException(Exception): + def __init__( + self, + code: G2PBridgeErrorCodes, + disbursement_payloads: List[DisbursementPayload], + message: Optional[str] = None, + ): + self.code: G2PBridgeErrorCodes = code + self.message: Optional[str] = message + self.disbursement_payloads: List[DisbursementPayload] = disbursement_payloads + super().__init__(code, self.message) diff --git a/src/openg2p_g2p_bridge_api/models/disbursement_envelope.py b/src/openg2p_g2p_bridge_api/models/disbursement_envelope.py new file mode 100644 index 0000000..8133342 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/models/disbursement_envelope.py @@ -0,0 +1,92 @@ +from datetime import datetime +from enum import Enum + +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import UUID, Boolean, Date, DateTime, Integer, String +from sqlalchemy.orm import Mapped, mapped_column + + +class FundsAvailableWithBankEnum(Enum): + PENDING_CHECK = "PENDING_CHECK" + FUNDS_NOT_AVAILABLE = "FUNDS_NOT_AVAILABLE" + FUNDS_AVAILABLE = "FUNDS_AVAILABLE" + + +class FundsBlockedWithBankEnum(Enum): + PENDING_CHECK = "PENDING_CHECK" + FUNDS_BLOCK_SUCCESS = "FUNDS_BLOCK_SUCCESS" + FUNDS_BLOCK_FAILURE = "FUNDS_BLOCK_FAILURE" + + +class DisbursementFrequency(Enum): + Daily = "Daily" + Weekly = "Weekly" + Fortnightly = "Fortnightly" + Monthly = "Monthly" + BiMonthly = "BiMonthly" + Quarterly = "Quarterly" + SemiAnnually = "SemiAnnually" + Annually = "Annually" + OnDemand = "OnDemand" + + +class CancellationStatus(Enum): + Not_Canceled = "Not_Canceled" + Canceled = "Canceled" + + +class DisbursementEnvelope(BaseORMModelWithTimes): + __tablename__ = "disbursement_envelopes" + id = mapped_column(UUID, primary_key=True) + disbursement_envelope_id: Mapped[str] = mapped_column(String, unique=True) + benefit_program_mnemonic: Mapped[str] = mapped_column(String) + disbursement_frequency: Mapped[DisbursementFrequency] = mapped_column(String) + cycle_code_mnemonic: Mapped[str] = mapped_column(String) + number_of_beneficiaries: Mapped[int] = mapped_column(Integer) + number_of_disbursements: Mapped[int] = mapped_column(Integer) + total_disbursement_amount: Mapped[float] = mapped_column(Integer) + disbursement_schedule_date: Mapped[datetime.date] = mapped_column(Date()) + receipt_time_stamp: Mapped[datetime] = mapped_column( + DateTime(), default=datetime.utcnow() + ) + cancellation_status: Mapped[CancellationStatus] = mapped_column( + String, default=CancellationStatus.Not_Canceled + ) + cancellation_timestamp: Mapped[datetime] = mapped_column( + DateTime(), nullable=True, default=None + ) + + +class DisbursementEnvelopeBatchStatus(BaseORMModelWithTimes): + __tablename__ = "disbursement_envelope_batch_statuses" + id = mapped_column(UUID, primary_key=True) + disbursement_envelope_id: Mapped[str] = mapped_column(String, unique=True) + number_of_disbursements_received: Mapped[int] = mapped_column(Integer) + total_disbursement_amount_received: Mapped[int] = mapped_column(Integer) + + funds_available_with_bank: Mapped[FundsAvailableWithBankEnum] = mapped_column( + String + ) + funds_available_latest_timestamp: Mapped[datetime] = mapped_column( + DateTime(), default=None, nullable=True + ) + funds_available_latest_error_code: Mapped[str] = mapped_column( + String, nullable=True + ) + funds_available_retries: Mapped[int] = mapped_column(Integer, default=0) + + funds_blocked_with_bank: Mapped[FundsBlockedWithBankEnum] = mapped_column(String) + funds_blocked_latest_timestamp: Mapped[datetime] = mapped_column( + DateTime(), default=None, nullable=True + ) + funds_blocked_latest_error_code: Mapped[str] = mapped_column(String, nullable=True) + funds_blocked_retries: Mapped[int] = mapped_column(Integer, default=0) + funds_blocked_reference_number: Mapped[str] = mapped_column(String, nullable=True) + + id_mapper_resolution_required: Mapped[bool] = mapped_column( + Boolean, default=True + ) # TODO: Business logic to be added + + number_of_disbursements_shipped: Mapped[int] = mapped_column(Integer, default=0) + number_of_disbursements_successful: Mapped[int] = mapped_column(Integer, default=0) + number_of_disbursements_failed: Mapped[int] = mapped_column(Integer, default=0) diff --git a/src/openg2p_g2p_bridge_api/schemas/__init__.py b/src/openg2p_g2p_bridge_api/schemas/__init__.py new file mode 100644 index 0000000..9a41509 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/schemas/__init__.py @@ -0,0 +1,15 @@ +from .disbursement import ( + DisbursementBatchStatusPayload, + DisbursementBatchStatusRequest, + DisbursementBatchStatusResponse, + DisbursementPayload, + DisbursementRequest, + DisbursementResponse, +) +from .disbursement_envelope import ( + DisbursementEnvelopePayload, + DisbursementEnvelopeRequest, + DisbursementEnvelopeResponse, +) +from .request import BridgeRequest +from .response import ResponseStatus diff --git a/src/openg2p_g2p_bridge_api/schemas/disbursement_envelope.py b/src/openg2p_g2p_bridge_api/schemas/disbursement_envelope.py new file mode 100644 index 0000000..68c680a --- /dev/null +++ b/src/openg2p_g2p_bridge_api/schemas/disbursement_envelope.py @@ -0,0 +1,28 @@ +import datetime +from typing import Optional + +from pydantic import BaseModel + +from ..models import DisbursementFrequency +from .request import BridgeRequest +from .response import BridgeResponse + + +class DisbursementEnvelopePayload(BaseModel): + id: Optional[str] = None + disbursement_envelope_id: Optional[str] = None + benefit_program_mnemonic: Optional[str] = None + disbursement_frequency: Optional[DisbursementFrequency] = None + cycle_code_mnemonic: Optional[str] = None + number_of_beneficiaries: Optional[int] = None + number_of_disbursements: Optional[int] = None + total_disbursement_amount: Optional[float] = None + disbursement_schedule_date: Optional[datetime.date] = None + + +class DisbursementEnvelopeRequest(BridgeRequest): + request_payload: DisbursementEnvelopePayload + + +class DisbursementEnvelopeResponse(BridgeResponse): + response_payload: Optional[DisbursementEnvelopePayload] = None diff --git a/src/openg2p_g2p_bridge_api/schemas/request.py b/src/openg2p_g2p_bridge_api/schemas/request.py new file mode 100644 index 0000000..c450e3d --- /dev/null +++ b/src/openg2p_g2p_bridge_api/schemas/request.py @@ -0,0 +1,18 @@ +from typing import Optional + +from pydantic import BaseModel + + +class RequestHeader(BaseModel): + pass + + +class RequestPagination(BaseModel): + request_page: Optional[int] + page_size: Optional[int] + + +class BridgeRequest(BaseModel): + request_header: RequestHeader + request_pagination: RequestPagination + request_payload: object diff --git a/src/openg2p_g2p_bridge_api/schemas/response.py b/src/openg2p_g2p_bridge_api/schemas/response.py new file mode 100644 index 0000000..99f0b55 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/schemas/response.py @@ -0,0 +1,30 @@ +from enum import Enum +from typing import Optional + +from pydantic import BaseModel + +from ..errors.codes import G2PBridgeErrorCodes + + +class ResponseHeader(BaseModel): + pass + + +class ResponseStatus(Enum): + SUCCESS = "success" + FAILURE = "failure" + + +class ResponsePagination(BaseModel): + current_page: Optional[int] = None + page_size: Optional[int] = None + total_elements: Optional[int] = None + + +class BridgeResponse(BaseModel): + response_header: Optional[ResponseHeader] = None + response_status: ResponseStatus + response_error_code: Optional[G2PBridgeErrorCodes] = None + response_message: Optional[str] = None + response_pagination: Optional[ResponsePagination] = ResponsePagination() + response_payload: Optional[object] = None diff --git a/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py b/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py new file mode 100644 index 0000000..7e4f71c --- /dev/null +++ b/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py @@ -0,0 +1,234 @@ +import time +import uuid +from datetime import datetime + +from openg2p_fastapi_common.context import dbengine +from openg2p_fastapi_common.service import BaseService +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select + +from ..errors import DisbursementEnvelopeException, G2PBridgeErrorCodes +from ..models import ( + CancellationStatus, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, + DisbursementFrequency, + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) +from ..schemas import ( + DisbursementEnvelopePayload, + DisbursementEnvelopeRequest, + DisbursementEnvelopeResponse, + ResponseStatus, +) + + +class DisbursementEnvelopeService(BaseService): + async def create_disbursement_envelope( + self, disbursement_envelope_request: DisbursementEnvelopeRequest + ) -> DisbursementEnvelopePayload: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + try: + await self.validate_envelope_request(disbursement_envelope_request) + except DisbursementEnvelopeException as e: + raise e + + # ----------------------------- + # First construct Persistence Models - DisbursementEnvelope and DisbursementEnvelopeBatchStatus + # ----------------------------- + disbursement_envelope: DisbursementEnvelope = await self.construct_disbursement_envelope( + disbursement_envelope_payload=disbursement_envelope_request.request_payload + ) + + disbursement_envelope_batch_status: DisbursementEnvelopeBatchStatus = ( + await self.construct_disbursement_envelope_batch_status( + disbursement_envelope + ) + ) + + session.add(disbursement_envelope) + session.add(disbursement_envelope_batch_status) + + await session.commit() + + disbursement_envelope_payload: DisbursementEnvelopePayload = ( + disbursement_envelope_request.request_payload + ) + disbursement_envelope_payload.disbursement_envelope_id = ( + disbursement_envelope.disbursement_envelope_id + ) + + return disbursement_envelope_payload + + async def cancel_disbursement_envelope( + self, disbursement_envelope_request: DisbursementEnvelopeRequest + ) -> DisbursementEnvelopePayload: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + disbursement_envelope_payload: DisbursementEnvelopePayload = ( + disbursement_envelope_request.request_payload + ) + disbursement_envelope_id: str = ( + disbursement_envelope_payload.disbursement_envelope_id + ) + + disbursement_envelope: DisbursementEnvelope = ( + await session.execute( + select(DisbursementEnvelope).where( + DisbursementEnvelope.disbursement_envelope_id + == disbursement_envelope_id + ) + ) + ).scalar() + + if disbursement_envelope is None: + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND + ) + + if ( + disbursement_envelope.cancellation_status + == CancellationStatus.Canceled.value + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED + ) + + disbursement_envelope.cancellation_status = ( + CancellationStatus.Canceled.value + ) + disbursement_envelope.cancellation_timestamp = datetime.utcnow() + + await session.commit() + + return disbursement_envelope_payload + + async def construct_disbursement_envelope_success_response( + self, disbursement_envelope_payload: DisbursementEnvelopePayload + ) -> DisbursementEnvelopeResponse: + disbursement_envelope_response: DisbursementEnvelopeResponse = ( + DisbursementEnvelopeResponse( + response_status=ResponseStatus.SUCCESS, + response_payload=disbursement_envelope_payload, + ) + ) + return disbursement_envelope_response + + async def construct_disbursement_envelope_error_response( + self, error_code: G2PBridgeErrorCodes + ) -> DisbursementEnvelopeResponse: + disbursement_envelope_response: DisbursementEnvelopeResponse = ( + DisbursementEnvelopeResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=error_code.value, + ) + ) + + return disbursement_envelope_response + + # noinspection PyMethodMayBeStatic + async def validate_envelope_request( + self, disbursement_envelope_request: DisbursementEnvelopeRequest + ) -> bool: + disbursement_envelope_payload: DisbursementEnvelopePayload = ( + disbursement_envelope_request.request_payload + ) + if ( + disbursement_envelope_payload.benefit_program_mnemonic is None + or disbursement_envelope_payload.benefit_program_mnemonic == "" + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC + ) + if ( + disbursement_envelope_payload.disbursement_frequency + not in DisbursementFrequency + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_FREQUENCY + ) + if ( + disbursement_envelope_payload.cycle_code_mnemonic is None + or disbursement_envelope_payload.cycle_code_mnemonic == "" + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_CYCLE_CODE_MNEMONIC + ) + if ( + disbursement_envelope_payload.number_of_beneficiaries is None + or disbursement_envelope_payload.number_of_beneficiaries < 1 + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_NO_OF_BENEFICIARIES + ) + if ( + disbursement_envelope_payload.number_of_disbursements is None + or disbursement_envelope_payload.number_of_disbursements < 1 + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_NO_OF_DISBURSEMENTS + ) + if ( + disbursement_envelope_payload.total_disbursement_amount is None + or disbursement_envelope_payload.total_disbursement_amount < 0 + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_TOTAL_DISBURSEMENT_AMOUNT + ) + if ( + disbursement_envelope_payload.disbursement_schedule_date is None + or disbursement_envelope_payload.disbursement_schedule_date + < datetime.date(datetime.utcnow()) # TODO: Add a delta of x days + ): + raise DisbursementEnvelopeException( + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_SCHEDULE_DATE + ) + + return True + + # noinspection PyMethodMayBeStatic + async def construct_disbursement_envelope( + self, disbursement_envelope_payload: DisbursementEnvelopePayload + ) -> DisbursementEnvelope: + disbursement_envelope: DisbursementEnvelope = DisbursementEnvelope( + id=uuid.uuid4(), + disbursement_envelope_id=str(int(time.time() * 1000)), + benefit_program_mnemonic=disbursement_envelope_payload.benefit_program_mnemonic, + disbursement_frequency=disbursement_envelope_payload.disbursement_frequency.value, + cycle_code_mnemonic=disbursement_envelope_payload.cycle_code_mnemonic, + number_of_beneficiaries=disbursement_envelope_payload.number_of_beneficiaries, + number_of_disbursements=disbursement_envelope_payload.number_of_disbursements, + total_disbursement_amount=disbursement_envelope_payload.total_disbursement_amount, + disbursement_schedule_date=disbursement_envelope_payload.disbursement_schedule_date, + receipt_time_stamp=datetime.utcnow(), + cancellation_status=CancellationStatus.Not_Canceled.value, + active=True, + ) + disbursement_envelope_payload.id = disbursement_envelope.id + disbursement_envelope_payload.disbursement_envelope_id = ( + disbursement_envelope.disbursement_envelope_id + ) + return disbursement_envelope + + # noinspection PyMethodMayBeStatic + async def construct_disbursement_envelope_batch_status( + self, disbursement_envelope: DisbursementEnvelope + ) -> DisbursementEnvelopeBatchStatus: + disbursement_envelope_batch_status: DisbursementEnvelopeBatchStatus = DisbursementEnvelopeBatchStatus( + id=disbursement_envelope.id, + disbursement_envelope_id=disbursement_envelope.disbursement_envelope_id, + number_of_disbursements_received=0, + total_disbursement_amount_received=0, + funds_available_with_bank=FundsAvailableWithBankEnum.PENDING_CHECK.value, + funds_available_latest_timestamp=datetime.utcnow(), + funds_available_latest_error_code="", + funds_available_retries=0, + funds_blocked_with_bank=FundsBlockedWithBankEnum.PENDING_CHECK.value, + funds_blocked_latest_timestamp=datetime.utcnow(), + funds_blocked_retries=0, + funds_blocked_latest_error_code="", + active=True, + ) + return disbursement_envelope_batch_status From 8ace13e7e136e61703a67b7916d87a18cfaf3b13 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Wed, 3 Jul 2024 19:01:26 +0530 Subject: [PATCH 02/39] Disbursement API --- .../controllers/disbursement.py | 82 +++ .../models/disbursement.py | 87 +++ .../schemas/disbursement.py | 56 ++ .../services/disbursement.py | 513 ++++++++++++++++++ 4 files changed, 738 insertions(+) create mode 100644 src/openg2p_g2p_bridge_api/controllers/disbursement.py create mode 100644 src/openg2p_g2p_bridge_api/models/disbursement.py create mode 100644 src/openg2p_g2p_bridge_api/schemas/disbursement.py create mode 100644 src/openg2p_g2p_bridge_api/services/disbursement.py diff --git a/src/openg2p_g2p_bridge_api/controllers/disbursement.py b/src/openg2p_g2p_bridge_api/controllers/disbursement.py new file mode 100644 index 0000000..0e3cb77 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/controllers/disbursement.py @@ -0,0 +1,82 @@ +from typing import List + +from openg2p_fastapi_common.controller import BaseController + +from ..errors import DisbursementException +from ..schemas import ( + DisbursementPayload, + DisbursementRequest, + DisbursementResponse, +) +from ..services import DisbursementService + + +class DisbursementController(BaseController): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.disbursement_service = DisbursementService.get_component() + self.router.tags += ["G2P Bridge Disbursement Envelope"] + + self.router.add_api_route( + "/create_disbursements", + self.create_disbursements, + responses={200: {"model": DisbursementRequest}}, + methods=["POST"], + ) + self.router.add_api_route( + "/cancel_disbursements", + self.cancel_disbursements, + responses={200: {"model": DisbursementRequest}}, + methods=["POST"], + ) + + async def create_disbursements( + self, disbursement_request: DisbursementRequest + ) -> DisbursementResponse: + try: + disbursement_payloads: List[ + DisbursementPayload + ] = await self.disbursement_service.create_disbursements( + disbursement_request + ) + except DisbursementException as e: + error_response: DisbursementResponse = ( + await self.disbursement_service.construct_disbursement_error_response( + e.code, e.disbursement_payloads + ) + ) + return error_response + + disbursement_response: DisbursementResponse = ( + await self.disbursement_service.construct_disbursement_success_response( + disbursement_payloads + ) + ) + + return disbursement_response + + async def cancel_disbursements( + self, disbursement_request: DisbursementRequest + ) -> DisbursementResponse: + try: + disbursement_payloads: List[ + DisbursementPayload + ] = await self.disbursement_service.cancel_disbursements( + disbursement_request + ) + except DisbursementException as e: + error_response: DisbursementResponse = ( + await self.disbursement_service.construct_disbursement_error_response( + e.code, e.disbursement_payloads + ) + ) + return error_response + + disbursement_response: DisbursementResponse = ( + await self.disbursement_service.construct_disbursement_success_response( + disbursement_payloads + ) + ) + + return disbursement_response diff --git a/src/openg2p_g2p_bridge_api/models/disbursement.py b/src/openg2p_g2p_bridge_api/models/disbursement.py new file mode 100644 index 0000000..1b4aab2 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/models/disbursement.py @@ -0,0 +1,87 @@ +from datetime import datetime +from enum import Enum + +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import UUID, DateTime, Float, Integer, String +from sqlalchemy import Enum as SqlEnum +from sqlalchemy.orm import Mapped, mapped_column + + +class CancellationStatus(Enum): + NOT_CANCELLED = "NOT_CANCELLED" + CANCELLED = "CANCELLED" + + +class ShipmentStatus(Enum): + PENDING = "PENDING" + PROCESSED = "PROCESSED" + + +class ReplyStatus(Enum): + PENDING = "PENDING" + SUCCESS = "SUCCESS" + FAILURE = "FAILURE" + + +class Disbursement(BaseORMModelWithTimes): + __tablename__ = "disbursements" + id = mapped_column(UUID, primary_key=True) + disbursement_id: Mapped[str] = mapped_column( + String, unique=True + ) # TODO: Add unique constraint with composite key + disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) + beneficiary_id: Mapped[int] = mapped_column(Integer) + beneficiary_name: Mapped[str] = mapped_column(String) + disbursement_amount: Mapped[float] = mapped_column(Float) + narrative: Mapped[str] = mapped_column(String) + receipt_time_stamp: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow + ) + cancellation_status: Mapped[CancellationStatus] = mapped_column( + SqlEnum(CancellationStatus), default=CancellationStatus.NOT_CANCELLED + ) + cancellation_time_stamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + + +class DisbursementBatchStatus(BaseORMModelWithTimes): + __tablename__ = "disbursement_batch_statuses" + id = mapped_column(UUID, primary_key=True) + disbursement_id: Mapped[str] = mapped_column(String, unique=True) + disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) + shipment_to_bank_status: Mapped[ShipmentStatus] = mapped_column( + SqlEnum(ShipmentStatus), default=ShipmentStatus.PENDING + ) + shipment_to_bank_time_stamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + reply_status_from_bank: Mapped[ReplyStatus] = mapped_column( + SqlEnum(ReplyStatus), default=ReplyStatus.PENDING + ) + reply_from_bank_time_stamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + reply_failure_error_code: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + reply_failure_error_message: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + reply_success_fsp_code: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + reply_success_fa: Mapped[str] = mapped_column(String, nullable=True, default=None) + mapper_resolved_fa: Mapped[str] = mapped_column(String, nullable=True, default=None) + mapper_resolved_phone_number: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + mapper_resolved_name: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + mapper_resolved_timestamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + mapper_resolved_retries: Mapped[int] = mapped_column( + Integer, nullable=True, default=0 + ) diff --git a/src/openg2p_g2p_bridge_api/schemas/disbursement.py b/src/openg2p_g2p_bridge_api/schemas/disbursement.py new file mode 100644 index 0000000..b560609 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/schemas/disbursement.py @@ -0,0 +1,56 @@ +import datetime +from typing import List, Optional + +from pydantic import BaseModel + +from ..models import CancellationStatus, ReplyStatus, ShipmentStatus +from .request import BridgeRequest +from .response import BridgeResponse + + +class DisbursementPayload(BaseModel): + id: Optional[str] = None + disbursement_id: Optional[str] = None + disbursement_envelope_id: Optional[str] = None + beneficiary_id: Optional[int] = None + beneficiary_name: Optional[str] = None + disbursement_amount: Optional[float] = None + narrative: Optional[str] = None + receipt_time_stamp: Optional[datetime.datetime] = None + cancellation_status: Optional[CancellationStatus] = None + cancellation_time_stamp: Optional[datetime.datetime] = None + response_error_codes: Optional[List[str]] = None + + +class DisbursementRequest(BridgeRequest): + request_payload: List[DisbursementPayload] + + +class DisbursementResponse(BridgeResponse): + response_payload: Optional[List[DisbursementPayload]] = None + + +class DisbursementBatchStatusPayload(BaseModel): + disbursement_id: Optional[int] = None + disbursement_envelope_id: Optional[int] = None + shipment_to_bank_status: Optional[ShipmentStatus] = None + shipment_to_bank_time_stamp: Optional[datetime.datetime] = None + reply_status_from_bank: Optional[ReplyStatus] = None + reply_from_bank_time_stamp: Optional[datetime.datetime] = None + reply_failure_error_code: Optional[str] = None + reply_failure_error_message: Optional[str] = None + reply_success_fsp_code: Optional[str] = None + reply_success_fa: Optional[str] = None + mapper_resolved_fa: Optional[str] = None + mapper_resolved_phone_number: Optional[str] = None + mapper_resolved_name: Optional[str] = None + mapper_resolved_timestamp: Optional[datetime.datetime] = None + mapper_resolved_retries: Optional[int] = None + + +class DisbursementBatchStatusRequest(BaseModel): + request_payload: DisbursementBatchStatusPayload + + +class DisbursementBatchStatusResponse(BaseModel): + response_payload: Optional[DisbursementBatchStatusPayload] = None diff --git a/src/openg2p_g2p_bridge_api/services/disbursement.py b/src/openg2p_g2p_bridge_api/services/disbursement.py new file mode 100644 index 0000000..555e1cf --- /dev/null +++ b/src/openg2p_g2p_bridge_api/services/disbursement.py @@ -0,0 +1,513 @@ +import time +import uuid +from datetime import datetime +from typing import List + +from openg2p_fastapi_common.context import dbengine +from openg2p_fastapi_common.service import BaseService +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select + +from ..errors import ( + DisbursementException, + G2PBridgeErrorCodes, +) +from ..models import ( + CancellationStatus, + Disbursement, + DisbursementBatchStatus, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, +) +from ..schemas import ( + DisbursementPayload, + DisbursementRequest, + DisbursementResponse, + ResponseStatus, +) + + +class DisbursementService(BaseService): + async def create_disbursements( + self, disbursement_request: DisbursementRequest + ) -> List[DisbursementPayload]: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + try: + await self.validate_disbursement_envelope( + session=session, + disbursement_payloads=disbursement_request.request_payload, + ) + except DisbursementException as e: + raise e + + is_error_free = await self.validate_disbursement_request( + disbursement_payloads=disbursement_request.request_payload + ) + + if not is_error_free: + raise DisbursementException( + code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + disbursement_payloads=disbursement_request.request_payload, + ) + + disbursements: List[Disbursement] = await self.construct_disbursements( + disbursement_payloads=disbursement_request.request_payload + ) + disbursement_batch_statuses: List[ + DisbursementBatchStatus + ] = await self.construct_disbursement_batch_statuses( + disbursements=disbursements + ) + + disbursement_envelope_batch_status = ( + await self.update_disbursement_envelope_batch_status( + disbursement_request, disbursements, session + ) + ) + + session.add_all(disbursements) + session.add_all(disbursement_batch_statuses) + session.add(disbursement_envelope_batch_status) + await session.commit() + + return disbursement_request.request_payload + + async def update_disbursement_envelope_batch_status( + self, disbursement_request, disbursements, session + ): + disbursement_envelope_batch_status = ( + ( + await session.execute( + select(DisbursementEnvelopeBatchStatus).where( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == str(disbursements[0].disbursement_envelope_id) + ) + ) + ) + .scalars() + .first() + ) + disbursement_envelope_batch_status.number_of_disbursements_received += len( + disbursements + ) + disbursement_envelope_batch_status.total_disbursement_amount_received += sum( + [disbursement.disbursement_amount for disbursement in disbursements] + ) + return disbursement_envelope_batch_status + + async def construct_disbursements( + self, disbursement_payloads: List[DisbursementPayload] + ) -> List[Disbursement]: + disbursements: List[Disbursement] = [] + for disbursement_payload in disbursement_payloads: + disbursement = Disbursement( + id=uuid.uuid4(), + disbursement_id=str(int(time.time() * 1000)), + disbursement_envelope_id=str( + disbursement_payload.disbursement_envelope_id + ), + beneficiary_id=disbursement_payload.beneficiary_id, + beneficiary_name=disbursement_payload.beneficiary_name, + disbursement_amount=disbursement_payload.disbursement_amount, + narrative=disbursement_payload.narrative, + active=True, + ) + disbursement_payload.id = disbursement.id + disbursement_payload.disbursement_id = disbursement.disbursement_id + disbursements.append(disbursement) + return disbursements + + async def construct_disbursement_batch_statuses( + self, disbursements: List[Disbursement] + ): + disbursement_batch_statuses = [] + for disbursement in disbursements: + disbursement_batch_status = DisbursementBatchStatus( + id=disbursement.id, + disbursement_id=disbursement.disbursement_id, + disbursement_envelope_id=str(disbursement.disbursement_envelope_id), + active=True, + ) + disbursement_batch_statuses.append(disbursement_batch_status) + return disbursement_batch_statuses + + async def validate_disbursement_request( + self, disbursement_payloads: List[DisbursementPayload] + ): + absolutely_no_error = True + + for disbursement_payload in disbursement_payloads: + disbursement_payload.response_error_codes = [] + if disbursement_payload.disbursement_envelope_id is None: + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ENVELOPE_ID + ) + if disbursement_payload.disbursement_amount <= 0: + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_AMOUNT + ) + if disbursement_payload.beneficiary_id is None: + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_BENEFICIARY_ID + ) + if ( + disbursement_payload.beneficiary_name is None + or disbursement_payload.beneficiary_name == "" + ): + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_BENEFICIARY_NAME + ) + if ( + disbursement_payload.narrative is None + or disbursement_payload.narrative == "" + ): + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_NARRATIVE + ) + + if len(disbursement_payload.response_error_codes) > 0: + absolutely_no_error = False + + return absolutely_no_error + + async def validate_disbursement_envelope( + self, session, disbursement_payloads: List[DisbursementPayload] + ): + disbursement_envelope_id = disbursement_payloads[0].disbursement_envelope_id + if not all( + disbursement_payload.disbursement_envelope_id + == disbursement_envelope_id + for disbursement_payload in disbursement_payloads + ): + raise DisbursementException( + G2PBridgeErrorCodes.MULTIPLE_ENVELOPES_FOUND, + disbursement_payloads, + ) + disbursement_envelope = ( + ( + await session.execute( + select(DisbursementEnvelope).where( + DisbursementEnvelope.disbursement_envelope_id + == str(disbursement_envelope_id) + ) + ) + ) + .scalars() + .first() + ) + if not disbursement_envelope: + raise DisbursementException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, + disbursement_payloads, + ) + + if disbursement_envelope.cancellation_status == CancellationStatus.Canceled: + raise DisbursementException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, + disbursement_payloads, + ) + + disbursement_envelope_batch_status = ( + ( + await session.execute( + select(DisbursementEnvelopeBatchStatus).where( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == str(disbursement_envelope_id) + ) + ) + ) + .scalars() + .first() + ) + + no_of_disbursements_after_this_request = ( + len(disbursement_payloads) + + disbursement_envelope_batch_status.number_of_disbursements_received + ) + total_disbursement_amount_after_this_request = ( + sum( + [ + disbursement_payload.disbursement_amount + for disbursement_payload in disbursement_payloads + ] + ) + + disbursement_envelope_batch_status.total_disbursement_amount_received + ) + + if ( + no_of_disbursements_after_this_request + > disbursement_envelope.number_of_disbursements + ): + raise DisbursementException( + G2PBridgeErrorCodes.NO_OF_DISBURSEMENTS_EXCEEDS_DECLARED, + disbursement_payloads, + ) + + if ( + total_disbursement_amount_after_this_request + > disbursement_envelope.total_disbursement_amount + ): + raise DisbursementException( + G2PBridgeErrorCodes.TOTAL_DISBURSEMENT_AMOUNT_EXCEEDS_DECLARED, + disbursement_payloads, + ) + + return True + + async def construct_disbursement_error_response( + self, + code: G2PBridgeErrorCodes, + disbursement_payloads: List[DisbursementPayload], + ) -> DisbursementResponse: + disbursement_response: DisbursementResponse = DisbursementResponse( + response_status=ResponseStatus.FAILURE, + response_payload=disbursement_payloads, + response_error_code=code.value, + ) + + return disbursement_response + + async def construct_disbursement_success_response( + self, disbursement_payloads: List[DisbursementPayload] + ) -> DisbursementResponse: + disbursement_response: DisbursementResponse = DisbursementResponse( + response_status=ResponseStatus.SUCCESS, + response_payload=disbursement_payloads, + ) + + return disbursement_response + + async def cancel_disbursements( + self, disbursement_request: DisbursementRequest + ) -> List[DisbursementPayload]: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + + is_payload_valid = ( + await self.validate_request_payload( + disbursement_payloads=disbursement_request.request_payload + ) + ) + + if not is_payload_valid: + raise DisbursementException( + code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + disbursement_payloads=disbursement_request.request_payload, + ) + + disbursements_in_db: List[Disbursement] = await self.fetch_disbursements_from_db( + disbursement_request, session + ) + if not disbursements_in_db: + raise DisbursementException( + code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID, + disbursement_payloads=disbursement_request.request_payload, + ) + + try: + await self.check_for_single_envelope( + disbursements_in_db, disbursement_request.request_payload + ) + except DisbursementException as e: + raise e + + try: + await self.validate_envelope_for_disbursement_cancellation( + disbursements_in_db=disbursements_in_db, + disbursement_payloads=disbursement_request.request_payload, + session=session, + ) + except DisbursementException as e: + raise e + + invalid_disbursements_exist = await self.check_for_invalid_disbursements( + disbursement_request, + disbursements_in_db, + ) + + if invalid_disbursements_exist: + raise DisbursementException( + code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + disbursement_payloads=disbursement_request.request_payload, + ) + + for disbursement in disbursements_in_db: + disbursement.cancellation_status = CancellationStatus.Canceled + disbursement.cancellation_time_stamp = datetime.now() + + disbursement_envelope_batch_status = ( + ( + await session.execute( + select(DisbursementEnvelopeBatchStatus).where( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == str(disbursements_in_db[0].disbursement_envelope_id) + ) + ) + ) + .scalars() + .first() + ) + + disbursement_envelope_batch_status.number_of_disbursements_received -= len( + disbursements_in_db + ) + disbursement_envelope_batch_status.total_disbursement_amount_received -= ( + sum( + [ + disbursement.disbursement_amount + for disbursement in disbursements_in_db + ] + ) + ) + + session.add_all(disbursements_in_db) + session.add(disbursement_envelope_batch_status) + await session.commit() + + return disbursement_request.request_payload + + + async def check_for_single_envelope(self, disbursements_in_db, disbursement_payloads): + disbursement_envelope_ids = set( + [disbursement.disbursement_envelope_id for disbursement in disbursements_in_db] + ) + if len(disbursement_envelope_ids) > 1: + raise DisbursementException( + G2PBridgeErrorCodes.MULTIPLE_ENVELOPES_FOUND, + disbursement_payloads, + ) + return True + + async def check_for_invalid_disbursements( + self, disbursement_request, disbursements_in_db + ) -> bool: + invalid_disbursements_exist = False + for disbursement_payload in disbursement_request.request_payload: + if disbursement_payload.id not in [ + disbursement.id for disbursement in disbursements_in_db + ]: + invalid_disbursements_exist = True + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID.value + ) + if disbursement_payload.id in [ + disbursement.id + for disbursement in disbursements_in_db + if disbursement.cancellation_status == CancellationStatus.Canceled + ]: + invalid_disbursements_exist = True + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED.value + ) + return invalid_disbursements_exist + + async def fetch_disbursements_from_db(self, disbursement_request, session) -> List[Disbursement]: + disbursements_in_db = ( + ( + await session.execute( + select(Disbursement).where( + Disbursement.disbursement_id.in_( + [ + str(disbursement_payload.disbursement_id) + for disbursement_payload in disbursement_request.request_payload + ] + ) + ) + ) + ) + .scalars() + .all() + ) + return disbursements_in_db + + async def validate_envelope_for_disbursement_cancellation( + self, disbursements_in_db, disbursement_payloads: List[DisbursementPayload], session + ): + disbursement_envelope = ( + ( + await session.execute( + select(DisbursementEnvelope).where( + DisbursementEnvelope.disbursement_envelope_id + == str(disbursements_in_db[0].disbursement_envelope_id) + ) + ) + ) + .scalars() + .first() + ) + if not disbursement_envelope: + raise DisbursementException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, + disbursement_payloads, + ) + + if disbursement_envelope.cancellation_status == CancellationStatus.Canceled: + raise DisbursementException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, + disbursement_payloads, + ) + + if disbursement_envelope.disbursement_schedule_date <= datetime.now().date(): + raise DisbursementException( + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_SCHEDULE_DATE_REACHED, + disbursement_payloads, + ) + + disbursement_envelope_batch_status = ( + ( + await session.execute( + select(DisbursementEnvelopeBatchStatus).where( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == str(disbursements_in_db[0].disbursement_envelope_id) + ) + ) + ) + .scalars() + .first() + ) + + no_of_disbursements_after_this_request = ( + len(disbursement_payloads) + - disbursement_envelope_batch_status.number_of_disbursements_received + ) + total_disbursement_amount_after_this_request = ( + sum( + [ + disbursement.disbursement_amount + for disbursement in disbursements_in_db + ] + ) + - disbursement_envelope_batch_status.total_disbursement_amount_received + ) + + if no_of_disbursements_after_this_request < 0: + raise DisbursementException( + G2PBridgeErrorCodes.NO_OF_DISBURSEMENTS_LESS_THAN_ZERO, + disbursement_payloads, + ) + + if total_disbursement_amount_after_this_request < 0: + raise DisbursementException( + G2PBridgeErrorCodes.TOTAL_DISBURSEMENT_AMOUNT_LESS_THAN_ZERO, + disbursement_payloads, + ) + + return True + + async def validate_request_payload( + self, disbursement_payloads: List[DisbursementPayload] + ): + absolutely_no_error = True + + for disbursement_payload in disbursement_payloads: + disbursement_payload.response_error_codes = [] + if disbursement_payload.disbursement_id is None or disbursement_payload.disbursement_id == "": + disbursement_payload.response_error_codes.append( + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID + ) + + if len(disbursement_payload.response_error_codes) > 0: + absolutely_no_error = False + + return absolutely_no_error From 45cc55d6d923304769766f946679000b111dfd22 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Wed, 3 Jul 2024 19:01:58 +0530 Subject: [PATCH 03/39] Project Copier Template Structure --- .copier-answers.yml | 18 + .dockerignore | 89 +++++ .editorconfig | 20 + .github/workflows/docker-build.yml | 44 +++ .github/workflows/openapi-push.yml | 51 +++ .github/workflows/pre-commit.yml | 16 + .github/workflows/publish.yml | 22 ++ .github/workflows/test.yml | 50 +++ .gitignore | 81 ++++ .pre-commit-config.yaml | 49 +++ .ruff.toml | 16 + CODE-OF-CONDUCT.md | 114 ++++++ CONTRIBUTING.md | 2 + Dockerfile | 27 ++ LICENSE | 373 ++++++++++++++++++ main.py | 15 + pyproject.toml | 31 ++ src/openg2p_g2p_bridge_api/__init__.py | 1 + src/openg2p_g2p_bridge_api/app.py | 38 ++ src/openg2p_g2p_bridge_api/config.py | 18 + .../controllers/__init__.py | 2 + src/openg2p_g2p_bridge_api/errors/__init__.py | 2 + src/openg2p_g2p_bridge_api/models/__init__.py | 2 + .../services/__init__.py | 2 + src/openg2p_g2p_bridge_api/utils/__init__.py | 0 test-requirements.txt | 3 + tests/__init__.py | 0 27 files changed, 1086 insertions(+) create mode 100644 .copier-answers.yml create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/openapi-push.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .ruff.toml create mode 100644 CODE-OF-CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100755 main.py create mode 100644 pyproject.toml create mode 100644 src/openg2p_g2p_bridge_api/__init__.py create mode 100644 src/openg2p_g2p_bridge_api/app.py create mode 100644 src/openg2p_g2p_bridge_api/config.py create mode 100644 src/openg2p_g2p_bridge_api/controllers/__init__.py create mode 100644 src/openg2p_g2p_bridge_api/errors/__init__.py create mode 100644 src/openg2p_g2p_bridge_api/models/__init__.py create mode 100644 src/openg2p_g2p_bridge_api/services/__init__.py create mode 100644 src/openg2p_g2p_bridge_api/utils/__init__.py create mode 100644 test-requirements.txt create mode 100644 tests/__init__.py diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..5bb2aa6 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,18 @@ +# Do NOT update manually; changes here will be overwritten by Copier +_commit: af08ec1 +_src_path: https://github.com/openg2p/openg2p-fastapi-template +github_ci_docker_build: true +github_ci_openapi_publish: true +github_ci_precommit: true +github_ci_pypi_publish: true +github_ci_tests: true +github_ci_tests_codecov: true +module_name: openg2p_g2p_bridge_api +org_name: OpenG2P +org_slug: OpenG2P +package_name: openg2p-g2p-bridge-api +repo_name: ' openg2p-g2p-bridge-api + + ' +repo_slug: openg2p-g2p-bridge-api + diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0a53e44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,89 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7d8f3a5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml,toml,jinja}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml,rst,md,jinja}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..fe97faa --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,44 @@ +name: Build Docker and Push + +on: + push: + workflow_dispatch: + +jobs: + docker-build: + name: Docker Build and Push + runs-on: ubuntu-latest + env: + NAMESPACE: ${{ secrets.docker_hub_organisation || 'openg2p' }} + SERVICE_NAME: openg2p-g2p-bridge-api + steps: + - uses: actions/checkout@v3 + - name: Docker build + run: | + BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,') + + IMAGE_ID=$NAMESPACE/$SERVICE_NAME + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + VERSION=$BRANCH_NAME + if [[ $BRANCH_NAME == master || $BRANCH_NAME == main ]]; then + VERSION=develop + fi + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + echo IMAGE_ID=$IMAGE_ID >> $GITHUB_ENV + echo VERSION=$VERSION >> $GITHUB_ENV + + docker build . \ + --file Dockerfile \ + --tag $IMAGE_ID:$VERSION + if [[ '${{ secrets.docker_hub_token }}' != '' && '${{ secrets.docker_hub_actor }}' != '' ]]; then + export DOCKER_PUSH="true" + echo DOCKER_PUSH=$DOCKER_PUSH >> $GITHUB_ENV + fi + - name: Docker push + if: env.DOCKER_PUSH == 'true' + run: | + echo "${{ secrets.docker_hub_token }}" | docker login -u ${{ secrets.docker_hub_actor }} --password-stdin + docker push ${{ env.IMAGE_ID }}:${{ env.VERSION }} diff --git a/.github/workflows/openapi-push.yml b/.github/workflows/openapi-push.yml new file mode 100644 index 0000000..0cefd09 --- /dev/null +++ b/.github/workflows/openapi-push.yml @@ -0,0 +1,51 @@ +name: openapi publish on push + +on: + push: + workflow_dispatch: + +jobs: + openapi-publish: + name: OpenAPI Generate and Publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Get branch name (merge) + run: | + echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV + - name: Setup python for openapi generate + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install app + run: | + python -m pip install git+https://github.com/openg2p/openg2p-fastapi-common@develop\#subdirectory=openg2p-fastapi-common + python -m pip install git+https://github.com/openg2p/openg2p-fastapi-common@develop\#subdirectory=openg2p-fastapi-auth + python -m pip install . + - name: Generate openapi json + run: | + mkdir -p api-docs/generated + python3 main.py getOpenAPI api-docs/generated/openapi.json + if ! [ -z "$(git status --porcelain=v1 2>/dev/null -- api-docs/generated/openapi.json)" ]; then + shopt -s nocasematch + if [[ ${{ github.repository_owner }} == 'OpenG2P' ]]; then + export OPENAPI_CHANGED="true" + echo OPENAPI_CHANGED=$OPENAPI_CHANGED >> $GITHUB_ENV + fi + fi + - name: Commit Changes + uses: EndBug/add-and-commit@v7 + if: env.OPENAPI_CHANGED == 'true' + with: + default_author: github_actions + message: "Generated new openapi.json on push to ${{ github.event.inputs.git-ref }}" + add: "api-docs/generated/openapi.json" + - name: Setup nodejs + uses: actions/setup-node@v4 + if: env.OPENAPI_CHANGED == 'true' + with: + node-version: '18' + - name: Publish to stoplight + if: env.OPENAPI_CHANGED == 'true' + run: | + npx @stoplight/cli@5 push --ci-token ${{ secrets.STOPLIGHT_PROJECT_TOKEN }} --url https://openg2p.stoplight.io --branch ${{ env.BRANCH_NAME }} --directory api-docs/generated diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..affa8a8 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 + with: + extra_args: --all-files --show-diff-on-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..797fccb --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,22 @@ +name: Publish to PyPI + +on: + workflow_dispatch + +jobs: + publish-to-pypi: + runs-on: ubuntu-latest + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - uses: actions/checkout@v3 + - name: Install build dependencies + run: pip install build + - name: Build distribution + run: python -m build + - name: Publish + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d42dfc8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Test and coverage + +on: + pull_request: + push: + workflow_dispatch: + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: test with ${{ matrix.py }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + py: + - "3.10" + os: + - ubuntu-latest + services: + postgres: + image: postgres:9.6 + env: + POSTGRES_USER: openg2p + POSTGRES_PASSWORD: openg2p + POSTGRES_DB: openg2p + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup python for test ${{ matrix.py }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py }} + - name: Install test requirements + run: | + python -m pip install -r test-requirements.txt + python -m pip install -e . + - name: Run test suite + run: | + pytest --cov-branch --cov-report=term-missing --cov=openg2p_g2p_bridge_api --cov=tests + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5204522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +/.venv +/.pytest_cache + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store + +# Django stuff: +*.log + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Sphinx documentation +docs/_build/ + +# Backup files +*~ +*.swp + +# OCA rules +!static/lib/ + +# Ruff stuff +.ruff_cache + +# Ignore secret files and env +.secrets.* +.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..336b7e1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: | + (?x) + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.11.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.289 + hooks: + - id: ruff + args: + - --fix diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..aa1fc5b --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,16 @@ +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[per-file-ignores] +"__init__.py" = ["F401"] diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..e1949e1 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,114 @@ +# Code of Conduct + +## Contributor Covenant Code of Conduct + +### Preamble + +OpenG2P was created to foster an open, innovative and inclusive community around open source & open standard. +To clarify expected behaviour in our communities we have adopted the Contributor Covenant. This code of +conduct has been adopted by many other open source communities and we feel it expresses our values well. + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free +experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy +community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit + permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will +take appropriate and fair corrective action in response to any behavior that they deem inappropriate, +threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki +edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate +reasons for moderation decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially +representing the community in public spaces. Examples of representing our community include using an official +e-mail address, posting via an official social media account, or acting as an appointed representative at an +online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders +responsible for enforcement at \[INSERT CONTACT METHOD]. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action +they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in +the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the +violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, +including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. +This includes avoiding interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a +specified period of time. No public or private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may +lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained +inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +### Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the +[FAQ](https://www.contributor-covenant.org/faq). Translations are available +[here](https://www.contributor-covenant.org/translations). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c187f93 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,2 @@ +Refer to contribution guidelines +[here](https://github.com/OpenG2P/openg2p-documentation/blob/1.0.0/community/contributing-to-openg2p.md). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e441cb3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM bitnami/python:3.10.13-debian-11-r24 + +ARG container_user=openg2p +ARG container_user_group=openg2p +ARG container_user_uid=1001 +ARG container_user_gid=1001 + +RUN groupadd -g ${container_user_gid} ${container_user_group} \ + && useradd -mN -u ${container_user_uid} -G ${container_user_group} -s /bin/bash ${container_user} + +WORKDIR /app + +RUN install_packages libpq-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +RUN chown -R ${container_user}:${container_user_group} /app +USER ${container_user} + +ADD --chown=${container_user}:${container_user_group} openg2p-g2p-bridge-api /app/src +ADD --chown=${container_user}:${container_user_group} main.py /app + +RUN python3 -m venv venv \ + && . ./venv/bin/activate +RUN python3 -m pip install ./src + +CMD python3 main.py migrate; \ + python3 main.py run diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/main.py b/main.py new file mode 100755 index 0000000..859f215 --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +# ruff: noqa: I001 + +from openg2p_g2p_bridge_api.app import Initializer +from openg2p_fastapi_common.ping import PingInitializer + +initializer = Initializer() +PingInitializer() + +app = initializer.return_app() + +if __name__ == "__main__": + initializer.main() + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bcc5183 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "openg2p-g2p-bridge-api" +authors = [ + { name="OpenG2P", email="info@openg2p.org" }, +] +description = "OpenG2P G2P Bridge API" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: OS Independent", +] +dependencies = [ + "openg2p-fastapi-common", + "openg2p-fastapi-auth", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://openg2p.org" +Documentation = "https://docs.openg2p.org/" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-api" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-api" + +[tool.hatch.version] +path = "src/openg2p_g2p_bridge_api/__init__.py" diff --git a/src/openg2p_g2p_bridge_api/__init__.py b/src/openg2p_g2p_bridge_api/__init__.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/src/openg2p_g2p_bridge_api/app.py b/src/openg2p_g2p_bridge_api/app.py new file mode 100644 index 0000000..b312863 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/app.py @@ -0,0 +1,38 @@ +# ruff: noqa: E402 +import asyncio + +from .config import Settings + +_config = Settings.get_config() + +from openg2p_fastapi_common.app import Initializer as BaseInitializer + +from .controllers import ( + DisbursementEnvelopeController, + DisbursementController, +) +from .models import DisbursementEnvelope, DisbursementEnvelopeBatchStatus +from .services import ( + DisbursementEnvelopeService, + DisbursementService, +) + +class Initializer(BaseInitializer): + def initialize(self, **kwargs): + super().initialize() + + DisbursementEnvelopeService() + DisbursementService() + DisbursementEnvelopeController().post_init() + DisbursementController().post_init() + + def migrate_database(self, args): + super().migrate_database(args) + + async def migrate(): + print("Migrating database") + await DisbursementEnvelope.create_migrate() + await DisbursementEnvelopeBatchStatus.create_migrate() + + + asyncio.run(migrate()) diff --git a/src/openg2p_g2p_bridge_api/config.py b/src/openg2p_g2p_bridge_api/config.py new file mode 100644 index 0000000..39717fd --- /dev/null +++ b/src/openg2p_g2p_bridge_api/config.py @@ -0,0 +1,18 @@ +from openg2p_fastapi_common.config import Settings as BaseSettings +from pydantic_settings import SettingsConfigDict +from . import __version__ + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="g2p_bridge_", env_file=".env", extra="allow") + + openapi_title: str = "OpenG2P G2P Bridge API" + openapi_description: str = """ + This module enables cash transfers from PBMS + *********************************** + Further details goes here + *********************************** + """ + openapi_version: str = __version__ + + db_dbname: str = "openg2p_g2p_bridge_db" diff --git a/src/openg2p_g2p_bridge_api/controllers/__init__.py b/src/openg2p_g2p_bridge_api/controllers/__init__.py new file mode 100644 index 0000000..67024f6 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/controllers/__init__.py @@ -0,0 +1,2 @@ +from .disbursement_envelope import DisbursementEnvelopeController +from .disbursement import DisbursementController diff --git a/src/openg2p_g2p_bridge_api/errors/__init__.py b/src/openg2p_g2p_bridge_api/errors/__init__.py new file mode 100644 index 0000000..be2518b --- /dev/null +++ b/src/openg2p_g2p_bridge_api/errors/__init__.py @@ -0,0 +1,2 @@ +from .exceptions import DisbursementEnvelopeException, DisbursementException +from .codes import G2PBridgeErrorCodes diff --git a/src/openg2p_g2p_bridge_api/models/__init__.py b/src/openg2p_g2p_bridge_api/models/__init__.py new file mode 100644 index 0000000..0642630 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/models/__init__.py @@ -0,0 +1,2 @@ +from .disbursement_envelope import FundsAvailableWithBankEnum, FundsBlockedWithBankEnum, DisbursementFrequency, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, CancellationStatus +from .disbursement import Disbursement, DisbursementBatchStatus, CancellationStatus as DisbursementCancellationStatus, ShipmentStatus, ReplyStatus diff --git a/src/openg2p_g2p_bridge_api/services/__init__.py b/src/openg2p_g2p_bridge_api/services/__init__.py new file mode 100644 index 0000000..2d57335 --- /dev/null +++ b/src/openg2p_g2p_bridge_api/services/__init__.py @@ -0,0 +1,2 @@ +from .disbursement_envelope import DisbursementEnvelopeService +from .disbursement import DisbursementService diff --git a/src/openg2p_g2p_bridge_api/utils/__init__.py b/src/openg2p_g2p_bridge_api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..4f53afa --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +pytest-cov +git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-common +git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-auth diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 4bebc2ddacc83a0f2be288f57404a9250df7e95e Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 4 Jul 2024 12:22:09 +0530 Subject: [PATCH 04/39] Disbursement Cancellation API --- src/openg2p_g2p_bridge_api/services/disbursement.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/openg2p_g2p_bridge_api/services/disbursement.py b/src/openg2p_g2p_bridge_api/services/disbursement.py index 555e1cf..a20312e 100644 --- a/src/openg2p_g2p_bridge_api/services/disbursement.py +++ b/src/openg2p_g2p_bridge_api/services/disbursement.py @@ -14,6 +14,7 @@ ) from ..models import ( CancellationStatus, + DisbursementCancellationStatus, Disbursement, DisbursementBatchStatus, DisbursementEnvelope, @@ -333,7 +334,7 @@ async def cancel_disbursements( ) for disbursement in disbursements_in_db: - disbursement.cancellation_status = CancellationStatus.Canceled + disbursement.cancellation_status = DisbursementCancellationStatus.CANCELLED disbursement.cancellation_time_stamp = datetime.now() disbursement_envelope_batch_status = ( @@ -384,17 +385,17 @@ async def check_for_invalid_disbursements( ) -> bool: invalid_disbursements_exist = False for disbursement_payload in disbursement_request.request_payload: - if disbursement_payload.id not in [ - disbursement.id for disbursement in disbursements_in_db + if disbursement_payload.disbursement_id not in [ + disbursement.disbursement_id for disbursement in disbursements_in_db ]: invalid_disbursements_exist = True disbursement_payload.response_error_codes.append( G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID.value ) - if disbursement_payload.id in [ - disbursement.id + if disbursement_payload.disbursement_id in [ + disbursement.disbursement_id for disbursement in disbursements_in_db - if disbursement.cancellation_status == CancellationStatus.Canceled + if disbursement.cancellation_status == DisbursementCancellationStatus.CANCELLED ]: invalid_disbursements_exist = True disbursement_payload.response_error_codes.append( From 73856af12d0b5b5a0a5a5dd7f59984c45b5be7ad Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 4 Jul 2024 14:30:20 +0530 Subject: [PATCH 05/39] Disbursement Controller Test --- tests/test_disbursement_envelope.py | 111 ++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/test_disbursement_envelope.py diff --git a/tests/test_disbursement_envelope.py b/tests/test_disbursement_envelope.py new file mode 100644 index 0000000..26fb3c1 --- /dev/null +++ b/tests/test_disbursement_envelope.py @@ -0,0 +1,111 @@ +from datetime import datetime +from unittest.mock import AsyncMock, patch + +import pytest +from openg2p_g2p_bridge_api.controllers import DisbursementEnvelopeController +from openg2p_g2p_bridge_api.errors import ( + DisbursementEnvelopeException, + G2PBridgeErrorCodes, +) +from openg2p_g2p_bridge_api.schemas import ( + DisbursementEnvelopePayload, + DisbursementEnvelopeRequest, + DisbursementEnvelopeResponse, + ResponseStatus, +) + + +def mock_create_disbursement_envelope(is_valid): + if not is_valid: + raise DisbursementEnvelopeException( + code=G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC, + message="Invalid program mnemonic provided.", + ) + return DisbursementEnvelopePayload( + disbursement_envelope_id="env123", + benefit_program_mnemonic="TEST123", + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +async def test_create_disbursement_envelope_success(mock_service_get_component): + mock_service_instance = AsyncMock() + mock_service_instance.create_disbursement_envelope = AsyncMock( + return_value=mock_create_disbursement_envelope(True) + ) + mock_service_instance.construct_disbursement_envelope_success_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + expected_payload = mock_create_disbursement_envelope(True) + expected_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.SUCCESS, response_payload=expected_payload + ) + mock_service_instance.construct_disbursement_envelope_success_response.return_value = ( + expected_response + ) + controller = DisbursementEnvelopeController() + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload( + benefit_program_mnemonic="TEST123", + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + ) + + actual_response = await controller.create_disbursement_envelope(request_payload) + + assert actual_response == expected_response + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +async def test_create_disbursement_envelope_failure(mock_service_get_component): + mock_service_instance = AsyncMock() + + mock_service_instance.create_disbursement_envelope.side_effect = ( + lambda request: mock_create_disbursement_envelope(False) + ) + mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + error_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC, + ) + + mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( + error_response + ) + + controller = DisbursementEnvelopeController() + + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload( + benefit_program_mnemonic="", # This should trigger the error + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + ) + + actual_response = await controller.create_disbursement_envelope(request_payload) + + assert ( + actual_response == error_response + ), "The response did not match the expected error response." From 641328e779c6e50f93c67272ed938d276158144f Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 4 Jul 2024 14:30:44 +0530 Subject: [PATCH 06/39] Pre-commit fixes --- main.py | 1 - src/openg2p_g2p_bridge_api/app.py | 4 +- src/openg2p_g2p_bridge_api/config.py | 5 +- .../controllers/__init__.py | 2 +- src/openg2p_g2p_bridge_api/errors/__init__.py | 2 +- src/openg2p_g2p_bridge_api/models/__init__.py | 19 ++++++- .../services/__init__.py | 2 +- .../services/disbursement.py | 53 +++++++++++-------- .../services/disbursement_envelope.py | 3 -- 9 files changed, 57 insertions(+), 34 deletions(-) diff --git a/main.py b/main.py index 859f215..9ba2291 100755 --- a/main.py +++ b/main.py @@ -12,4 +12,3 @@ if __name__ == "__main__": initializer.main() - diff --git a/src/openg2p_g2p_bridge_api/app.py b/src/openg2p_g2p_bridge_api/app.py index b312863..e20b4ff 100644 --- a/src/openg2p_g2p_bridge_api/app.py +++ b/src/openg2p_g2p_bridge_api/app.py @@ -8,8 +8,8 @@ from openg2p_fastapi_common.app import Initializer as BaseInitializer from .controllers import ( - DisbursementEnvelopeController, DisbursementController, + DisbursementEnvelopeController, ) from .models import DisbursementEnvelope, DisbursementEnvelopeBatchStatus from .services import ( @@ -17,6 +17,7 @@ DisbursementService, ) + class Initializer(BaseInitializer): def initialize(self, **kwargs): super().initialize() @@ -34,5 +35,4 @@ async def migrate(): await DisbursementEnvelope.create_migrate() await DisbursementEnvelopeBatchStatus.create_migrate() - asyncio.run(migrate()) diff --git a/src/openg2p_g2p_bridge_api/config.py b/src/openg2p_g2p_bridge_api/config.py index 39717fd..43b4f24 100644 --- a/src/openg2p_g2p_bridge_api/config.py +++ b/src/openg2p_g2p_bridge_api/config.py @@ -1,10 +1,13 @@ from openg2p_fastapi_common.config import Settings as BaseSettings from pydantic_settings import SettingsConfigDict + from . import __version__ class Settings(BaseSettings): - model_config = SettingsConfigDict(env_prefix="g2p_bridge_", env_file=".env", extra="allow") + model_config = SettingsConfigDict( + env_prefix="g2p_bridge_", env_file=".env", extra="allow" + ) openapi_title: str = "OpenG2P G2P Bridge API" openapi_description: str = """ diff --git a/src/openg2p_g2p_bridge_api/controllers/__init__.py b/src/openg2p_g2p_bridge_api/controllers/__init__.py index 67024f6..f265e87 100644 --- a/src/openg2p_g2p_bridge_api/controllers/__init__.py +++ b/src/openg2p_g2p_bridge_api/controllers/__init__.py @@ -1,2 +1,2 @@ -from .disbursement_envelope import DisbursementEnvelopeController from .disbursement import DisbursementController +from .disbursement_envelope import DisbursementEnvelopeController diff --git a/src/openg2p_g2p_bridge_api/errors/__init__.py b/src/openg2p_g2p_bridge_api/errors/__init__.py index be2518b..9ddf790 100644 --- a/src/openg2p_g2p_bridge_api/errors/__init__.py +++ b/src/openg2p_g2p_bridge_api/errors/__init__.py @@ -1,2 +1,2 @@ -from .exceptions import DisbursementEnvelopeException, DisbursementException from .codes import G2PBridgeErrorCodes +from .exceptions import DisbursementEnvelopeException, DisbursementException diff --git a/src/openg2p_g2p_bridge_api/models/__init__.py b/src/openg2p_g2p_bridge_api/models/__init__.py index 0642630..0f22d2b 100644 --- a/src/openg2p_g2p_bridge_api/models/__init__.py +++ b/src/openg2p_g2p_bridge_api/models/__init__.py @@ -1,2 +1,17 @@ -from .disbursement_envelope import FundsAvailableWithBankEnum, FundsBlockedWithBankEnum, DisbursementFrequency, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, CancellationStatus -from .disbursement import Disbursement, DisbursementBatchStatus, CancellationStatus as DisbursementCancellationStatus, ShipmentStatus, ReplyStatus +from .disbursement import ( + CancellationStatus as DisbursementCancellationStatus, +) +from .disbursement import ( + Disbursement, + DisbursementBatchStatus, + ReplyStatus, + ShipmentStatus, +) +from .disbursement_envelope import ( + CancellationStatus, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, + DisbursementFrequency, + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) diff --git a/src/openg2p_g2p_bridge_api/services/__init__.py b/src/openg2p_g2p_bridge_api/services/__init__.py index 2d57335..60026d0 100644 --- a/src/openg2p_g2p_bridge_api/services/__init__.py +++ b/src/openg2p_g2p_bridge_api/services/__init__.py @@ -1,2 +1,2 @@ -from .disbursement_envelope import DisbursementEnvelopeService from .disbursement import DisbursementService +from .disbursement_envelope import DisbursementEnvelopeService diff --git a/src/openg2p_g2p_bridge_api/services/disbursement.py b/src/openg2p_g2p_bridge_api/services/disbursement.py index a20312e..b37ab6a 100644 --- a/src/openg2p_g2p_bridge_api/services/disbursement.py +++ b/src/openg2p_g2p_bridge_api/services/disbursement.py @@ -14,9 +14,9 @@ ) from ..models import ( CancellationStatus, - DisbursementCancellationStatus, Disbursement, DisbursementBatchStatus, + DisbursementCancellationStatus, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, ) @@ -177,9 +177,8 @@ async def validate_disbursement_envelope( ): disbursement_envelope_id = disbursement_payloads[0].disbursement_envelope_id if not all( - disbursement_payload.disbursement_envelope_id - == disbursement_envelope_id - for disbursement_payload in disbursement_payloads + disbursement_payload.disbursement_envelope_id == disbursement_envelope_id + for disbursement_payload in disbursement_payloads ): raise DisbursementException( G2PBridgeErrorCodes.MULTIPLE_ENVELOPES_FOUND, @@ -284,11 +283,8 @@ async def cancel_disbursements( ) -> List[DisbursementPayload]: session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: - - is_payload_valid = ( - await self.validate_request_payload( - disbursement_payloads=disbursement_request.request_payload - ) + is_payload_valid = await self.validate_request_payload( + disbursement_payloads=disbursement_request.request_payload ) if not is_payload_valid: @@ -297,9 +293,9 @@ async def cancel_disbursements( disbursement_payloads=disbursement_request.request_payload, ) - disbursements_in_db: List[Disbursement] = await self.fetch_disbursements_from_db( - disbursement_request, session - ) + disbursements_in_db: List[ + Disbursement + ] = await self.fetch_disbursements_from_db(disbursement_request, session) if not disbursements_in_db: raise DisbursementException( code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID, @@ -334,7 +330,9 @@ async def cancel_disbursements( ) for disbursement in disbursements_in_db: - disbursement.cancellation_status = DisbursementCancellationStatus.CANCELLED + disbursement.cancellation_status = ( + DisbursementCancellationStatus.CANCELLED + ) disbursement.cancellation_time_stamp = datetime.now() disbursement_envelope_batch_status = ( @@ -368,11 +366,13 @@ async def cancel_disbursements( return disbursement_request.request_payload - - async def check_for_single_envelope(self, disbursements_in_db, disbursement_payloads): - disbursement_envelope_ids = set( - [disbursement.disbursement_envelope_id for disbursement in disbursements_in_db] - ) + async def check_for_single_envelope( + self, disbursements_in_db, disbursement_payloads + ): + disbursement_envelope_ids = { + disbursement.disbursement_envelope_id + for disbursement in disbursements_in_db + } if len(disbursement_envelope_ids) > 1: raise DisbursementException( G2PBridgeErrorCodes.MULTIPLE_ENVELOPES_FOUND, @@ -395,7 +395,8 @@ async def check_for_invalid_disbursements( if disbursement_payload.disbursement_id in [ disbursement.disbursement_id for disbursement in disbursements_in_db - if disbursement.cancellation_status == DisbursementCancellationStatus.CANCELLED + if disbursement.cancellation_status + == DisbursementCancellationStatus.CANCELLED ]: invalid_disbursements_exist = True disbursement_payload.response_error_codes.append( @@ -403,7 +404,9 @@ async def check_for_invalid_disbursements( ) return invalid_disbursements_exist - async def fetch_disbursements_from_db(self, disbursement_request, session) -> List[Disbursement]: + async def fetch_disbursements_from_db( + self, disbursement_request, session + ) -> List[Disbursement]: disbursements_in_db = ( ( await session.execute( @@ -423,7 +426,10 @@ async def fetch_disbursements_from_db(self, disbursement_request, session) -> Li return disbursements_in_db async def validate_envelope_for_disbursement_cancellation( - self, disbursements_in_db, disbursement_payloads: List[DisbursementPayload], session + self, + disbursements_in_db, + disbursement_payloads: List[DisbursementPayload], + session, ): disbursement_envelope = ( ( @@ -503,7 +509,10 @@ async def validate_request_payload( for disbursement_payload in disbursement_payloads: disbursement_payload.response_error_codes = [] - if disbursement_payload.disbursement_id is None or disbursement_payload.disbursement_id == "": + if ( + disbursement_payload.disbursement_id is None + or disbursement_payload.disbursement_id == "" + ): disbursement_payload.response_error_codes.append( G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID ) diff --git a/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py b/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py index 7e4f71c..4dd5219 100644 --- a/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py +++ b/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py @@ -35,9 +35,6 @@ async def create_disbursement_envelope( except DisbursementEnvelopeException as e: raise e - # ----------------------------- - # First construct Persistence Models - DisbursementEnvelope and DisbursementEnvelopeBatchStatus - # ----------------------------- disbursement_envelope: DisbursementEnvelope = await self.construct_disbursement_envelope( disbursement_envelope_payload=disbursement_envelope_request.request_payload ) From 8ee1f2bd9d282e78da838b8b6a5c36f22242066a Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 4 Jul 2024 14:45:18 +0530 Subject: [PATCH 07/39] Make request header and pagination optional --- src/openg2p_g2p_bridge_api/schemas/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openg2p_g2p_bridge_api/schemas/request.py b/src/openg2p_g2p_bridge_api/schemas/request.py index c450e3d..6b6c3cc 100644 --- a/src/openg2p_g2p_bridge_api/schemas/request.py +++ b/src/openg2p_g2p_bridge_api/schemas/request.py @@ -13,6 +13,6 @@ class RequestPagination(BaseModel): class BridgeRequest(BaseModel): - request_header: RequestHeader - request_pagination: RequestPagination + request_header: Optional[RequestHeader] = None + request_pagination: Optional[RequestPagination] = None request_payload: object From 90e632f6857db6addda127b84eaa5e703fc6ac48 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 4 Jul 2024 19:18:32 +0530 Subject: [PATCH 08/39] Update disbursement envelope test to include all error codes --- tests/test_disbursement_envelope.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_disbursement_envelope.py b/tests/test_disbursement_envelope.py index 26fb3c1..05381b9 100644 --- a/tests/test_disbursement_envelope.py +++ b/tests/test_disbursement_envelope.py @@ -15,11 +15,10 @@ ) -def mock_create_disbursement_envelope(is_valid): +def mock_create_disbursement_envelope(is_valid, error_code=None): if not is_valid: raise DisbursementEnvelopeException( - code=G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC, - message="Invalid program mnemonic provided.", + code=error_code, message=f"{error_code} error." ) return DisbursementEnvelopePayload( disbursement_envelope_id="env123", @@ -71,11 +70,13 @@ async def test_create_disbursement_envelope_success(mock_service_get_component): @pytest.mark.asyncio @patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") -async def test_create_disbursement_envelope_failure(mock_service_get_component): +@pytest.mark.parametrize("error_code", list(G2PBridgeErrorCodes)) +async def test_create_disbursement_envelope_errors( + mock_service_get_component, error_code +): mock_service_instance = AsyncMock() - mock_service_instance.create_disbursement_envelope.side_effect = ( - lambda request: mock_create_disbursement_envelope(False) + lambda request: mock_create_disbursement_envelope(False, error_code) ) mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() @@ -83,7 +84,7 @@ async def test_create_disbursement_envelope_failure(mock_service_get_component): error_response = DisbursementEnvelopeResponse( response_status=ResponseStatus.FAILURE, - response_error_code=G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC, + response_error_code=error_code, ) mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( @@ -94,7 +95,7 @@ async def test_create_disbursement_envelope_failure(mock_service_get_component): request_payload = DisbursementEnvelopeRequest( request_payload=DisbursementEnvelopePayload( - benefit_program_mnemonic="", # This should trigger the error + benefit_program_mnemonic="", # Trigger the error disbursement_frequency="Monthly", cycle_code_mnemonic="CYCLE42", number_of_beneficiaries=100, @@ -108,4 +109,4 @@ async def test_create_disbursement_envelope_failure(mock_service_get_component): assert ( actual_response == error_response - ), "The response did not match the expected error response." + ), f"The response did not match the expected error response for {error_code}." From 0384c2c4fc7dbdf6a7ce1eb338ec7c1a6f6afe44 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 4 Jul 2024 21:14:57 +0530 Subject: [PATCH 09/39] Cancel disbursement envelope tests --- tests/test_disbursement_envelope.py | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/test_disbursement_envelope.py b/tests/test_disbursement_envelope.py index 05381b9..1af0e1c 100644 --- a/tests/test_disbursement_envelope.py +++ b/tests/test_disbursement_envelope.py @@ -110,3 +110,90 @@ async def test_create_disbursement_envelope_errors( assert ( actual_response == error_response ), f"The response did not match the expected error response for {error_code}." + + +def mock_cancel_disbursement_envelope(is_valid, error_code=None): + if not is_valid: + raise DisbursementEnvelopeException( + code=error_code, message=f"{error_code} error." + ) + + return DisbursementEnvelopePayload( + disbursement_envelope_id="env123", + benefit_program_mnemonic="TEST123", + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +async def test_cancel_disbursement_envelope_success(mock_service_get_component): + mock_service_instance = AsyncMock() + mock_service_instance.cancel_disbursement_envelope = AsyncMock( + return_value=mock_cancel_disbursement_envelope(True) + ) + mock_service_instance.construct_disbursement_envelope_success_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + successful_payload = mock_cancel_disbursement_envelope(True) + expected_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.SUCCESS, response_payload=successful_payload + ) + mock_service_instance.construct_disbursement_envelope_success_response.return_value = ( + expected_response + ) + + controller = DisbursementEnvelopeController() + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload(disbursement_envelope_id="env123") + ) + + actual_response = await controller.cancel_disbursement_envelope(request_payload) + assert actual_response == expected_response + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +@pytest.mark.parametrize( + "error_code", + [ + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, + ], +) +async def test_cancel_disbursement_envelope_failure( + mock_service_get_component, error_code +): + mock_service_instance = AsyncMock() + mock_service_instance.cancel_disbursement_envelope.side_effect = ( + lambda request: mock_cancel_disbursement_envelope(False, error_code) + ) + mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + error_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=error_code.value, + ) + mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( + error_response + ) + + controller = DisbursementEnvelopeController() + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload( + disbursement_envelope_id="env123" # Assuming this ID triggers the error + ) + ) + + actual_response = await controller.cancel_disbursement_envelope(request_payload) + assert ( + actual_response == error_response + ), f"The response for {error_code} did not match the expected error response." From 23baedbccc6e3cb1a68a4b804b6496f6b692d6da Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 5 Jul 2024 11:33:28 +0530 Subject: [PATCH 10/39] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5204522..f5e0368 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -/.venv +/venv /.pytest_cache # C extensions From b63865b588ad72825a463bc74040f2afc22d89f8 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Sun, 7 Jul 2024 13:51:38 +0530 Subject: [PATCH 11/39] disbursement tests --- tests/test_disbursement.py | 177 +++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/test_disbursement.py diff --git a/tests/test_disbursement.py b/tests/test_disbursement.py new file mode 100644 index 0000000..59486f8 --- /dev/null +++ b/tests/test_disbursement.py @@ -0,0 +1,177 @@ +import datetime +from unittest.mock import AsyncMock, patch + +import pytest +from openg2p_g2p_bridge_api.controllers import DisbursementController +from openg2p_g2p_bridge_api.errors import ( + DisbursementException, + G2PBridgeErrorCodes, +) +from openg2p_g2p_bridge_api.models import CancellationStatus +from openg2p_g2p_bridge_api.schemas import ( + DisbursementPayload, + DisbursementRequest, + DisbursementResponse, + ResponseStatus, +) + + +def mock_create_disbursements(is_valid, disbursement_payloads): + if not is_valid: + raise DisbursementException( + code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + disbursement_payloads=disbursement_payloads, + ) + return disbursement_payloads + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_create_disbursements_success(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_envelope_id="env123", + beneficiary_id=123, + disbursement_amount=1000, + ) + ] + mock_service_instance.create_disbursements = AsyncMock( + return_value=mock_create_disbursements(True, disbursement_payloads) + ) + mock_service_instance.construct_disbursement_success_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.SUCCESS, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.create_disbursements(request_payload) + + assert response.response_status == ResponseStatus.SUCCESS + assert response.response_payload == disbursement_payloads + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_create_disbursements_failure(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_envelope_id="env123", + beneficiary_id=123, + disbursement_amount=1000, + ) + ] + mock_service_instance.create_disbursements = AsyncMock( + side_effect=lambda req: mock_create_disbursements(False, req.request_payload) + ) + mock_service_instance.construct_disbursement_error_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.create_disbursements(request_payload) + + assert response.response_status == ResponseStatus.FAILURE + assert ( + response.response_error_code == G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD + ) + + +def mock_cancel_disbursements(is_valid, disbursement_payloads): + if not is_valid: + raise DisbursementException( + code=G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED, + disbursement_payloads=disbursement_payloads, + ) + for payload in disbursement_payloads: + payload.cancellation_status = CancellationStatus.Canceled + payload.cancellation_time_stamp = datetime.datetime.utcnow() + return disbursement_payloads + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_cancel_disbursements_success(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_id="123", + beneficiary_id=123, + disbursement_amount=1000, + cancellation_status=None, + ) + ] + mock_service_instance.cancel_disbursements = AsyncMock( + return_value=mock_cancel_disbursements(True, disbursement_payloads) + ) + mock_service_instance.construct_disbursement_success_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.SUCCESS, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.cancel_disbursements(request_payload) + + assert response.response_status == ResponseStatus.SUCCESS + assert all( + payload.cancellation_status == CancellationStatus.Canceled + for payload in response.response_payload + ) + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_cancel_disbursements_failure(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_id="123", + beneficiary_id=123, + disbursement_amount=1000, + cancellation_status=None, + ) + ] + mock_service_instance.cancel_disbursements = AsyncMock( + side_effect=lambda req: mock_cancel_disbursements(False, req.request_payload) + ) + mock_service_instance.construct_disbursement_error_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.cancel_disbursements(request_payload) + + assert response.response_status == ResponseStatus.FAILURE + assert ( + response.response_error_code + == G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED + ) From c67b0b07f3a6dba59040f2e6cfcdc8a971239199 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 19 Jul 2024 17:03:30 +0530 Subject: [PATCH 12/39] Openg2p Bridge Models --- openg2p-g2p-bridge-models/.gitignore | 81 ++++ .../.pre-commit-config.yaml | 49 +++ openg2p-g2p-bridge-models/.ruff.toml | 16 + openg2p-g2p-bridge-models/CODE-OF-CONDUCT.md | 114 ++++++ openg2p-g2p-bridge-models/CONTRIBUTING.md | 2 + openg2p-g2p-bridge-models/LICENSE | 373 ++++++++++++++++++ openg2p-g2p-bridge-models/README.md | 14 + openg2p-g2p-bridge-models/pyproject.toml | 31 ++ .../src/openg2p_g2p_bridge_models/__init__.py | 1 + .../errors/__init__.py | 2 + .../errors/codes.py | 0 .../errors/exceptions.py | 0 .../models/__init__.py | 20 + .../models/benefit_program_configuration.py | 19 + .../models/disbursement.py | 120 ++++++ .../models/disbursement_envelope.py | 24 +- .../schemas/__init__.py | 3 - .../schemas/disbursement.py | 30 ++ .../schemas/disbursement_envelope.py | 0 .../schemas/request.py | 0 .../schemas/response.py | 0 src/openg2p_g2p_bridge_api/errors/__init__.py | 2 - src/openg2p_g2p_bridge_api/models/__init__.py | 17 - .../models/disbursement.py | 87 ---- .../schemas/disbursement.py | 56 --- 25 files changed, 883 insertions(+), 178 deletions(-) create mode 100644 openg2p-g2p-bridge-models/.gitignore create mode 100644 openg2p-g2p-bridge-models/.pre-commit-config.yaml create mode 100644 openg2p-g2p-bridge-models/.ruff.toml create mode 100644 openg2p-g2p-bridge-models/CODE-OF-CONDUCT.md create mode 100644 openg2p-g2p-bridge-models/CONTRIBUTING.md create mode 100644 openg2p-g2p-bridge-models/LICENSE create mode 100644 openg2p-g2p-bridge-models/README.md create mode 100644 openg2p-g2p-bridge-models/pyproject.toml create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/__init__.py create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/errors/codes.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/errors/exceptions.py (100%) create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/benefit_program_configuration.py create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/models/disbursement_envelope.py (83%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/schemas/__init__.py (74%) create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement.py rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/schemas/disbursement_envelope.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/schemas/request.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models}/schemas/response.py (100%) delete mode 100644 src/openg2p_g2p_bridge_api/errors/__init__.py delete mode 100644 src/openg2p_g2p_bridge_api/models/__init__.py delete mode 100644 src/openg2p_g2p_bridge_api/models/disbursement.py delete mode 100644 src/openg2p_g2p_bridge_api/schemas/disbursement.py diff --git a/openg2p-g2p-bridge-models/.gitignore b/openg2p-g2p-bridge-models/.gitignore new file mode 100644 index 0000000..f5e0368 --- /dev/null +++ b/openg2p-g2p-bridge-models/.gitignore @@ -0,0 +1,81 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +/venv +/.pytest_cache + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store + +# Django stuff: +*.log + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Sphinx documentation +docs/_build/ + +# Backup files +*~ +*.swp + +# OCA rules +!static/lib/ + +# Ruff stuff +.ruff_cache + +# Ignore secret files and env +.secrets.* +.env diff --git a/openg2p-g2p-bridge-models/.pre-commit-config.yaml b/openg2p-g2p-bridge-models/.pre-commit-config.yaml new file mode 100644 index 0000000..336b7e1 --- /dev/null +++ b/openg2p-g2p-bridge-models/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: | + (?x) + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.11.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.289 + hooks: + - id: ruff + args: + - --fix diff --git a/openg2p-g2p-bridge-models/.ruff.toml b/openg2p-g2p-bridge-models/.ruff.toml new file mode 100644 index 0000000..aa1fc5b --- /dev/null +++ b/openg2p-g2p-bridge-models/.ruff.toml @@ -0,0 +1,16 @@ +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[per-file-ignores] +"__init__.py" = ["F401"] diff --git a/openg2p-g2p-bridge-models/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-models/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..e1949e1 --- /dev/null +++ b/openg2p-g2p-bridge-models/CODE-OF-CONDUCT.md @@ -0,0 +1,114 @@ +# Code of Conduct + +## Contributor Covenant Code of Conduct + +### Preamble + +OpenG2P was created to foster an open, innovative and inclusive community around open source & open standard. +To clarify expected behaviour in our communities we have adopted the Contributor Covenant. This code of +conduct has been adopted by many other open source communities and we feel it expresses our values well. + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free +experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy +community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit + permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will +take appropriate and fair corrective action in response to any behavior that they deem inappropriate, +threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki +edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate +reasons for moderation decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially +representing the community in public spaces. Examples of representing our community include using an official +e-mail address, posting via an official social media account, or acting as an appointed representative at an +online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders +responsible for enforcement at \[INSERT CONTACT METHOD]. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action +they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in +the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the +violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, +including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. +This includes avoiding interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a +specified period of time. No public or private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may +lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained +inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +### Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the +[FAQ](https://www.contributor-covenant.org/faq). Translations are available +[here](https://www.contributor-covenant.org/translations). diff --git a/openg2p-g2p-bridge-models/CONTRIBUTING.md b/openg2p-g2p-bridge-models/CONTRIBUTING.md new file mode 100644 index 0000000..c187f93 --- /dev/null +++ b/openg2p-g2p-bridge-models/CONTRIBUTING.md @@ -0,0 +1,2 @@ +Refer to contribution guidelines +[here](https://github.com/OpenG2P/openg2p-documentation/blob/1.0.0/community/contributing-to-openg2p.md). diff --git a/openg2p-g2p-bridge-models/LICENSE b/openg2p-g2p-bridge-models/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/openg2p-g2p-bridge-models/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/openg2p-g2p-bridge-models/README.md b/openg2p-g2p-bridge-models/README.md new file mode 100644 index 0000000..81f9d8d --- /dev/null +++ b/openg2p-g2p-bridge-models/README.md @@ -0,0 +1,14 @@ +# openg2p-g2p-bridge-models + +[![Pre-commit Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml?query=branch%3Adevelop) +[![Build Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml?query=branch%3Adevelop) +[![codecov](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api/branch/develop/graph/badge.svg)](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api) +[![openapi](https://img.shields.io/badge/open--API-swagger-brightgreen)](https://validator.swagger.io/?url=https://raw.githubusercontent.com/OpenG2P/openg2p-g2p-bridge-api/develop/api-docs/generated/openapi.json) +![PyPI](https://img.shields.io/pypi/v/openg2p-g2p-bridge-api?label=pypi%20package) +![PyPI - Downloads](https://img.shields.io/pypi/dm/openg2p-g2p-bridge-api) + + + +## Licenses + +This repository is licensed under [MPL-2.0](LICENSE). diff --git a/openg2p-g2p-bridge-models/pyproject.toml b/openg2p-g2p-bridge-models/pyproject.toml new file mode 100644 index 0000000..a162b6c --- /dev/null +++ b/openg2p-g2p-bridge-models/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "openg2p-g2p-bridge-models" +authors = [ + { name="OpenG2P", email="info@openg2p.org" }, +] +description = "OpenG2P G2P Bridge Models" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: OS Independent", +] +dependencies = [ + "openg2p-fastapi-common", + "openg2p-fastapi-auth", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://openg2p.org" +Documentation = "https://docs.openg2p.org/" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-models" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-models" + +[tool.hatch.version] +path = "src/openg2p_g2p_bridge_models/__init__.py" diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/__init__.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py new file mode 100644 index 0000000..22b18dc --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py @@ -0,0 +1,2 @@ +# from .codes import G2PBridgeErrorCodes +# from .exceptions import DisbursementEnvelopeException, DisbursementException diff --git a/src/openg2p_g2p_bridge_api/errors/codes.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/codes.py similarity index 100% rename from src/openg2p_g2p_bridge_api/errors/codes.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/codes.py diff --git a/src/openg2p_g2p_bridge_api/errors/exceptions.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/exceptions.py similarity index 100% rename from src/openg2p_g2p_bridge_api/errors/exceptions.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/exceptions.py diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py new file mode 100644 index 0000000..ee88dd2 --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py @@ -0,0 +1,20 @@ +from .benefit_program_configuration import BenefitProgramConfiguration +from .disbursement import ( + BankDisbursementBatchStatus, + Disbursement, + DisbursementBatchControl, + DisbursementCancellationStatus, + ProcessStatus, + MapperResolutionBatchStatus, + MapperResolutionDetails, + MapperResolvedFaType, + ProcessStatus, +) +from .disbursement_envelope import ( + CancellationStatus, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, + DisbursementFrequency, + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/benefit_program_configuration.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/benefit_program_configuration.py new file mode 100644 index 0000000..92bd02d --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/benefit_program_configuration.py @@ -0,0 +1,19 @@ +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import Boolean, String +from sqlalchemy.orm import Mapped, mapped_column + + +class BenefitProgramConfiguration(BaseORMModelWithTimes): + __tablename__ = "benefit_program_configurations" + + benefit_program_mnemonic: Mapped[str] = mapped_column(String, unique=True) + benefit_program_name: Mapped[str] = mapped_column(String, nullable=False) + funding_org_code: Mapped[str] = mapped_column(String, nullable=False) + funding_org_name: Mapped[str] = mapped_column(String, nullable=False) + sponsor_bank_code: Mapped[str] = mapped_column(String, nullable=False) + sponsor_bank_account_number: Mapped[str] = mapped_column( + String, unique=True, nullable=False + ) + sponsor_bank_branch_code: Mapped[str] = mapped_column(String, nullable=False) + sponsor_bank_account_currency: Mapped[str] = mapped_column(String, nullable=False) + id_mapper_resolution_required: Mapped[bool] = mapped_column(Boolean, default=False) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py new file mode 100644 index 0000000..4771a4d --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py @@ -0,0 +1,120 @@ +from datetime import datetime +from enum import Enum + +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import UUID, DateTime, Float, Integer, String +from sqlalchemy import Enum as SqlEnum +from sqlalchemy.orm import Mapped, mapped_column + + +class DisbursementCancellationStatus(Enum): + NOT_CANCELLED = "NOT_CANCELLED" + CANCELLED = "CANCELLED" + + +class MapperResolvedFaType(Enum): + BANK_ACCOUNT = "BANK_ACCOUNT" + MOBILE_WALLET = "MOBILE_WALLET" + EMAIL_WALLET = "EMAIL_WALLET" + + +class ProcessStatus(Enum): + PENDING = "PENDING" + PROCESSED = "PROCESSED" + + +class Disbursement(BaseORMModelWithTimes): + __tablename__ = "disbursements" + disbursement_id: Mapped[str] = mapped_column( + String, unique=True + ) # TODO: Add unique constraint with composite key + disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) + beneficiary_id: Mapped[str] = mapped_column(String) + beneficiary_name: Mapped[str] = mapped_column(String) + disbursement_amount: Mapped[float] = mapped_column(Float) + narrative: Mapped[str] = mapped_column(String) + receipt_time_stamp: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow + ) + cancellation_status: Mapped[DisbursementCancellationStatus] = mapped_column( + SqlEnum(DisbursementCancellationStatus), + default=DisbursementCancellationStatus.NOT_CANCELLED, + ) + cancellation_time_stamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + + +class DisbursementBatchControl(BaseORMModelWithTimes): + __tablename__ = "disbursement_batch_control" + + disbursement_id: Mapped[str] = mapped_column(String, unique=True) + disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) + beneficiary_id: Mapped[str] = mapped_column(String) + bank_disbursement_batch_id = mapped_column(UUID, nullable=True, default=None, index=True, unique=True) + mapper_resolution_batch_id = mapped_column(UUID, nullable=True, default=None, index=True, unique=True) + + +class MapperResolutionBatchStatus(BaseORMModelWithTimes): + __tablename__ = "mapper_resolution_batch_statuses" + + mapper_resolution_batch_id = mapped_column( + UUID, nullable=True, default=None, index=True, unique=True + ) + resolution_status: Mapped[ProcessStatus] = mapped_column( + SqlEnum(ProcessStatus), default=ProcessStatus.PENDING + ) + resolution_time_stamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + latest_error_code: Mapped[str] = mapped_column(String, nullable=True, default=None) + resolution_attempts: Mapped[int] = mapped_column(Integer, nullable=True, default=0) + + +class MapperResolutionDetails(BaseORMModelWithTimes): + __tablename__ = "mapper_resolution_details" + + mapper_resolution_batch_id = mapped_column( + UUID, nullable=True, default=None, index=True + ) + disbursement_id: Mapped[str] = mapped_column(String, index=True, unique=True) + beneficiary_id: Mapped[str] = mapped_column(String, index=True) + mapper_resolved_fa: Mapped[str] = mapped_column(String, nullable=True, default=None) + mapper_resolved_name: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + mapper_resolved_fa_type: Mapped[MapperResolvedFaType] = mapped_column( + SqlEnum(MapperResolvedFaType), nullable=True, default=None + ) + bank_account_number: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + bank_code: Mapped[str] = mapped_column(String, nullable=True, default=None) + branch_code: Mapped[str] = mapped_column(String, nullable=True, default=None) + mobile_number: Mapped[str] = mapped_column(String, nullable=True, default=None) + mobile_wallet_provider: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + email_address: Mapped[str] = mapped_column(String, nullable=True, default=None) + email_wallet_provider: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + + +class BankDisbursementBatchStatus(BaseORMModelWithTimes): + __tablename__ = "bank_disbursement_batch_statuses" + + bank_disbursement_batch_id: Mapped[UUID] = mapped_column( + UUID, nullable=True, default=None, index=True, unique=True + ) + disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) + disbursement_status: Mapped[ProcessStatus] = mapped_column( + SqlEnum(ProcessStatus), default=ProcessStatus.PENDING + ) + disbursement_timestamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + latest_error_code: Mapped[str] = mapped_column(String, nullable=True, default=None) + disbursement_attempts: Mapped[int] = mapped_column( + Integer, nullable=False, default=0 + ) diff --git a/src/openg2p_g2p_bridge_api/models/disbursement_envelope.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py similarity index 83% rename from src/openg2p_g2p_bridge_api/models/disbursement_envelope.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py index 8133342..6afda90 100644 --- a/src/openg2p_g2p_bridge_api/models/disbursement_envelope.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py @@ -2,20 +2,22 @@ from enum import Enum from openg2p_fastapi_common.models import BaseORMModelWithTimes -from sqlalchemy import UUID, Boolean, Date, DateTime, Integer, String +from sqlalchemy import Boolean, Date, DateTime, Integer, String from sqlalchemy.orm import Mapped, mapped_column class FundsAvailableWithBankEnum(Enum): PENDING_CHECK = "PENDING_CHECK" - FUNDS_NOT_AVAILABLE = "FUNDS_NOT_AVAILABLE" FUNDS_AVAILABLE = "FUNDS_AVAILABLE" + FUNDS_NOT_AVAILABLE = "FUNDS_NOT_AVAILABLE" + ERROR = "ERROR" class FundsBlockedWithBankEnum(Enum): PENDING_CHECK = "PENDING_CHECK" FUNDS_BLOCK_SUCCESS = "FUNDS_BLOCK_SUCCESS" FUNDS_BLOCK_FAILURE = "FUNDS_BLOCK_FAILURE" + ERROR = "ERROR" class DisbursementFrequency(Enum): @@ -31,13 +33,12 @@ class DisbursementFrequency(Enum): class CancellationStatus(Enum): - Not_Canceled = "Not_Canceled" - Canceled = "Canceled" + Not_Cancelled = "Not_Cancelled" + Cancelled = "Cancelled" class DisbursementEnvelope(BaseORMModelWithTimes): __tablename__ = "disbursement_envelopes" - id = mapped_column(UUID, primary_key=True) disbursement_envelope_id: Mapped[str] = mapped_column(String, unique=True) benefit_program_mnemonic: Mapped[str] = mapped_column(String) disbursement_frequency: Mapped[DisbursementFrequency] = mapped_column(String) @@ -45,12 +46,13 @@ class DisbursementEnvelope(BaseORMModelWithTimes): number_of_beneficiaries: Mapped[int] = mapped_column(Integer) number_of_disbursements: Mapped[int] = mapped_column(Integer) total_disbursement_amount: Mapped[float] = mapped_column(Integer) + disbursement_currency_code: Mapped[str] = mapped_column(String) disbursement_schedule_date: Mapped[datetime.date] = mapped_column(Date()) receipt_time_stamp: Mapped[datetime] = mapped_column( DateTime(), default=datetime.utcnow() ) cancellation_status: Mapped[CancellationStatus] = mapped_column( - String, default=CancellationStatus.Not_Canceled + String, default=CancellationStatus.Not_Cancelled ) cancellation_timestamp: Mapped[datetime] = mapped_column( DateTime(), nullable=True, default=None @@ -59,7 +61,6 @@ class DisbursementEnvelope(BaseORMModelWithTimes): class DisbursementEnvelopeBatchStatus(BaseORMModelWithTimes): __tablename__ = "disbursement_envelope_batch_statuses" - id = mapped_column(UUID, primary_key=True) disbursement_envelope_id: Mapped[str] = mapped_column(String, unique=True) number_of_disbursements_received: Mapped[int] = mapped_column(Integer) total_disbursement_amount_received: Mapped[int] = mapped_column(Integer) @@ -73,20 +74,17 @@ class DisbursementEnvelopeBatchStatus(BaseORMModelWithTimes): funds_available_latest_error_code: Mapped[str] = mapped_column( String, nullable=True ) - funds_available_retries: Mapped[int] = mapped_column(Integer, default=0) + funds_available_attempts: Mapped[int] = mapped_column(Integer, default=0) funds_blocked_with_bank: Mapped[FundsBlockedWithBankEnum] = mapped_column(String) funds_blocked_latest_timestamp: Mapped[datetime] = mapped_column( DateTime(), default=None, nullable=True ) funds_blocked_latest_error_code: Mapped[str] = mapped_column(String, nullable=True) - funds_blocked_retries: Mapped[int] = mapped_column(Integer, default=0) + funds_blocked_attempts: Mapped[int] = mapped_column(Integer, default=0) funds_blocked_reference_number: Mapped[str] = mapped_column(String, nullable=True) - id_mapper_resolution_required: Mapped[bool] = mapped_column( - Boolean, default=True - ) # TODO: Business logic to be added + id_mapper_resolution_required: Mapped[bool] = mapped_column(Boolean, default=True) number_of_disbursements_shipped: Mapped[int] = mapped_column(Integer, default=0) - number_of_disbursements_successful: Mapped[int] = mapped_column(Integer, default=0) number_of_disbursements_failed: Mapped[int] = mapped_column(Integer, default=0) diff --git a/src/openg2p_g2p_bridge_api/schemas/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py similarity index 74% rename from src/openg2p_g2p_bridge_api/schemas/__init__.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py index 9a41509..d6b770d 100644 --- a/src/openg2p_g2p_bridge_api/schemas/__init__.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py @@ -1,7 +1,4 @@ from .disbursement import ( - DisbursementBatchStatusPayload, - DisbursementBatchStatusRequest, - DisbursementBatchStatusResponse, DisbursementPayload, DisbursementRequest, DisbursementResponse, diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement.py new file mode 100644 index 0000000..57f5e8b --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement.py @@ -0,0 +1,30 @@ +import datetime +from typing import List, Optional + +from pydantic import BaseModel + +from ..models import CancellationStatus +from .request import BridgeRequest +from .response import BridgeResponse + + +class DisbursementPayload(BaseModel): + id: Optional[str] = None + disbursement_id: Optional[str] = None + disbursement_envelope_id: Optional[str] = None + beneficiary_id: Optional[str] = None + beneficiary_name: Optional[str] = None + disbursement_amount: Optional[float] = None + narrative: Optional[str] = None + receipt_time_stamp: Optional[datetime.datetime] = None + cancellation_status: Optional[CancellationStatus] = None + cancellation_time_stamp: Optional[datetime.datetime] = None + response_error_codes: Optional[List[str]] = None + + +class DisbursementRequest(BridgeRequest): + request_payload: List[DisbursementPayload] + + +class DisbursementResponse(BridgeResponse): + response_payload: Optional[List[DisbursementPayload]] = None diff --git a/src/openg2p_g2p_bridge_api/schemas/disbursement_envelope.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement_envelope.py similarity index 100% rename from src/openg2p_g2p_bridge_api/schemas/disbursement_envelope.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement_envelope.py diff --git a/src/openg2p_g2p_bridge_api/schemas/request.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/request.py similarity index 100% rename from src/openg2p_g2p_bridge_api/schemas/request.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/request.py diff --git a/src/openg2p_g2p_bridge_api/schemas/response.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/response.py similarity index 100% rename from src/openg2p_g2p_bridge_api/schemas/response.py rename to openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/response.py diff --git a/src/openg2p_g2p_bridge_api/errors/__init__.py b/src/openg2p_g2p_bridge_api/errors/__init__.py deleted file mode 100644 index 9ddf790..0000000 --- a/src/openg2p_g2p_bridge_api/errors/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .codes import G2PBridgeErrorCodes -from .exceptions import DisbursementEnvelopeException, DisbursementException diff --git a/src/openg2p_g2p_bridge_api/models/__init__.py b/src/openg2p_g2p_bridge_api/models/__init__.py deleted file mode 100644 index 0f22d2b..0000000 --- a/src/openg2p_g2p_bridge_api/models/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from .disbursement import ( - CancellationStatus as DisbursementCancellationStatus, -) -from .disbursement import ( - Disbursement, - DisbursementBatchStatus, - ReplyStatus, - ShipmentStatus, -) -from .disbursement_envelope import ( - CancellationStatus, - DisbursementEnvelope, - DisbursementEnvelopeBatchStatus, - DisbursementFrequency, - FundsAvailableWithBankEnum, - FundsBlockedWithBankEnum, -) diff --git a/src/openg2p_g2p_bridge_api/models/disbursement.py b/src/openg2p_g2p_bridge_api/models/disbursement.py deleted file mode 100644 index 1b4aab2..0000000 --- a/src/openg2p_g2p_bridge_api/models/disbursement.py +++ /dev/null @@ -1,87 +0,0 @@ -from datetime import datetime -from enum import Enum - -from openg2p_fastapi_common.models import BaseORMModelWithTimes -from sqlalchemy import UUID, DateTime, Float, Integer, String -from sqlalchemy import Enum as SqlEnum -from sqlalchemy.orm import Mapped, mapped_column - - -class CancellationStatus(Enum): - NOT_CANCELLED = "NOT_CANCELLED" - CANCELLED = "CANCELLED" - - -class ShipmentStatus(Enum): - PENDING = "PENDING" - PROCESSED = "PROCESSED" - - -class ReplyStatus(Enum): - PENDING = "PENDING" - SUCCESS = "SUCCESS" - FAILURE = "FAILURE" - - -class Disbursement(BaseORMModelWithTimes): - __tablename__ = "disbursements" - id = mapped_column(UUID, primary_key=True) - disbursement_id: Mapped[str] = mapped_column( - String, unique=True - ) # TODO: Add unique constraint with composite key - disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) - beneficiary_id: Mapped[int] = mapped_column(Integer) - beneficiary_name: Mapped[str] = mapped_column(String) - disbursement_amount: Mapped[float] = mapped_column(Float) - narrative: Mapped[str] = mapped_column(String) - receipt_time_stamp: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow - ) - cancellation_status: Mapped[CancellationStatus] = mapped_column( - SqlEnum(CancellationStatus), default=CancellationStatus.NOT_CANCELLED - ) - cancellation_time_stamp: Mapped[datetime] = mapped_column( - DateTime, nullable=True, default=None - ) - - -class DisbursementBatchStatus(BaseORMModelWithTimes): - __tablename__ = "disbursement_batch_statuses" - id = mapped_column(UUID, primary_key=True) - disbursement_id: Mapped[str] = mapped_column(String, unique=True) - disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) - shipment_to_bank_status: Mapped[ShipmentStatus] = mapped_column( - SqlEnum(ShipmentStatus), default=ShipmentStatus.PENDING - ) - shipment_to_bank_time_stamp: Mapped[datetime] = mapped_column( - DateTime, nullable=True, default=None - ) - reply_status_from_bank: Mapped[ReplyStatus] = mapped_column( - SqlEnum(ReplyStatus), default=ReplyStatus.PENDING - ) - reply_from_bank_time_stamp: Mapped[datetime] = mapped_column( - DateTime, nullable=True, default=None - ) - reply_failure_error_code: Mapped[str] = mapped_column( - String, nullable=True, default=None - ) - reply_failure_error_message: Mapped[str] = mapped_column( - String, nullable=True, default=None - ) - reply_success_fsp_code: Mapped[str] = mapped_column( - String, nullable=True, default=None - ) - reply_success_fa: Mapped[str] = mapped_column(String, nullable=True, default=None) - mapper_resolved_fa: Mapped[str] = mapped_column(String, nullable=True, default=None) - mapper_resolved_phone_number: Mapped[str] = mapped_column( - String, nullable=True, default=None - ) - mapper_resolved_name: Mapped[str] = mapped_column( - String, nullable=True, default=None - ) - mapper_resolved_timestamp: Mapped[datetime] = mapped_column( - DateTime, nullable=True, default=None - ) - mapper_resolved_retries: Mapped[int] = mapped_column( - Integer, nullable=True, default=0 - ) diff --git a/src/openg2p_g2p_bridge_api/schemas/disbursement.py b/src/openg2p_g2p_bridge_api/schemas/disbursement.py deleted file mode 100644 index b560609..0000000 --- a/src/openg2p_g2p_bridge_api/schemas/disbursement.py +++ /dev/null @@ -1,56 +0,0 @@ -import datetime -from typing import List, Optional - -from pydantic import BaseModel - -from ..models import CancellationStatus, ReplyStatus, ShipmentStatus -from .request import BridgeRequest -from .response import BridgeResponse - - -class DisbursementPayload(BaseModel): - id: Optional[str] = None - disbursement_id: Optional[str] = None - disbursement_envelope_id: Optional[str] = None - beneficiary_id: Optional[int] = None - beneficiary_name: Optional[str] = None - disbursement_amount: Optional[float] = None - narrative: Optional[str] = None - receipt_time_stamp: Optional[datetime.datetime] = None - cancellation_status: Optional[CancellationStatus] = None - cancellation_time_stamp: Optional[datetime.datetime] = None - response_error_codes: Optional[List[str]] = None - - -class DisbursementRequest(BridgeRequest): - request_payload: List[DisbursementPayload] - - -class DisbursementResponse(BridgeResponse): - response_payload: Optional[List[DisbursementPayload]] = None - - -class DisbursementBatchStatusPayload(BaseModel): - disbursement_id: Optional[int] = None - disbursement_envelope_id: Optional[int] = None - shipment_to_bank_status: Optional[ShipmentStatus] = None - shipment_to_bank_time_stamp: Optional[datetime.datetime] = None - reply_status_from_bank: Optional[ReplyStatus] = None - reply_from_bank_time_stamp: Optional[datetime.datetime] = None - reply_failure_error_code: Optional[str] = None - reply_failure_error_message: Optional[str] = None - reply_success_fsp_code: Optional[str] = None - reply_success_fa: Optional[str] = None - mapper_resolved_fa: Optional[str] = None - mapper_resolved_phone_number: Optional[str] = None - mapper_resolved_name: Optional[str] = None - mapper_resolved_timestamp: Optional[datetime.datetime] = None - mapper_resolved_retries: Optional[int] = None - - -class DisbursementBatchStatusRequest(BaseModel): - request_payload: DisbursementBatchStatusPayload - - -class DisbursementBatchStatusResponse(BaseModel): - response_payload: Optional[DisbursementBatchStatusPayload] = None From d445bcaeb11bc747bedb88f2fbe219798351075b Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 19 Jul 2024 17:05:05 +0530 Subject: [PATCH 13/39] Openg2p Bridge APIs --- .../.copier-answers.yml | 1 - .../.dockerignore | 2 +- .../.editorconfig | 0 openg2p-g2p-bridge-api/.gitignore | 81 ++++++++++++++++ .../.pre-commit-config.yaml | 0 .../.ruff.toml | 0 .../CODE-OF-CONDUCT.md | 0 .../CONTRIBUTING.md | 0 .../Dockerfile | 0 LICENSE => openg2p-g2p-bridge-api/LICENSE | 0 README.md => openg2p-g2p-bridge-api/README.md | 0 .../__init__.py | 0 main.py => openg2p-g2p-bridge-api/main.py | 0 .../pyproject.toml | 0 .../__init__.py | 0 .../app.py | 11 ++- .../celery_app.py | 7 ++ .../config.py | 0 .../controllers/__init__.py | 0 .../controllers/disbursement.py | 6 +- .../controllers/disbursement_envelope.py | 6 +- .../services/__init__.py | 0 .../services/disbursement.py | 96 +++++++++++++------ .../services/disbursement_envelope.py | 39 +++++--- .../utils/__init__.py | 1 + .../utils/model_serializer.py | 6 ++ .../test-requirements.txt | 0 .../tests}/__init__.py | 0 .../tests}/test_disbursement.py | 22 ++--- .../tests}/test_disbursement_envelope.py | 8 +- 30 files changed, 214 insertions(+), 72 deletions(-) rename .copier-answers.yml => openg2p-g2p-bridge-api/.copier-answers.yml (99%) rename .dockerignore => openg2p-g2p-bridge-api/.dockerignore (99%) rename .editorconfig => openg2p-g2p-bridge-api/.editorconfig (100%) create mode 100644 openg2p-g2p-bridge-api/.gitignore rename .pre-commit-config.yaml => openg2p-g2p-bridge-api/.pre-commit-config.yaml (100%) rename .ruff.toml => openg2p-g2p-bridge-api/.ruff.toml (100%) rename CODE-OF-CONDUCT.md => openg2p-g2p-bridge-api/CODE-OF-CONDUCT.md (100%) rename CONTRIBUTING.md => openg2p-g2p-bridge-api/CONTRIBUTING.md (100%) rename Dockerfile => openg2p-g2p-bridge-api/Dockerfile (100%) rename LICENSE => openg2p-g2p-bridge-api/LICENSE (100%) rename README.md => openg2p-g2p-bridge-api/README.md (100%) rename {src/openg2p_g2p_bridge_api/utils => openg2p-g2p-bridge-api}/__init__.py (100%) rename main.py => openg2p-g2p-bridge-api/main.py (100%) rename pyproject.toml => openg2p-g2p-bridge-api/pyproject.toml (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/__init__.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/app.py (78%) create mode 100644 openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/config.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/controllers/__init__.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/controllers/disbursement.py (95%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/controllers/disbursement_envelope.py (95%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/services/__init__.py (100%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/services/disbursement.py (86%) rename {src/openg2p_g2p_bridge_api => openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api}/services/disbursement_envelope.py (89%) create mode 100644 openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py create mode 100644 openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/model_serializer.py rename test-requirements.txt => openg2p-g2p-bridge-api/test-requirements.txt (100%) rename {tests => openg2p-g2p-bridge-api/tests}/__init__.py (100%) rename {tests => openg2p-g2p-bridge-api/tests}/test_disbursement.py (92%) rename {tests => openg2p-g2p-bridge-api/tests}/test_disbursement_envelope.py (97%) diff --git a/.copier-answers.yml b/openg2p-g2p-bridge-api/.copier-answers.yml similarity index 99% rename from .copier-answers.yml rename to openg2p-g2p-bridge-api/.copier-answers.yml index 5bb2aa6..60cfca7 100644 --- a/.copier-answers.yml +++ b/openg2p-g2p-bridge-api/.copier-answers.yml @@ -15,4 +15,3 @@ repo_name: ' openg2p-g2p-bridge-api ' repo_slug: openg2p-g2p-bridge-api - diff --git a/.dockerignore b/openg2p-g2p-bridge-api/.dockerignore similarity index 99% rename from .dockerignore rename to openg2p-g2p-bridge-api/.dockerignore index 0a53e44..d47f7ee 100644 --- a/.dockerignore +++ b/openg2p-g2p-bridge-api/.dockerignore @@ -71,7 +71,7 @@ docs/_build/ target/ # Virtual environment -.env +../.env .venv/ venv/ diff --git a/.editorconfig b/openg2p-g2p-bridge-api/.editorconfig similarity index 100% rename from .editorconfig rename to openg2p-g2p-bridge-api/.editorconfig diff --git a/openg2p-g2p-bridge-api/.gitignore b/openg2p-g2p-bridge-api/.gitignore new file mode 100644 index 0000000..c633cec --- /dev/null +++ b/openg2p-g2p-bridge-api/.gitignore @@ -0,0 +1,81 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +/venv +/.pytest_cache + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store + +# Django stuff: +*.log + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Sphinx documentation +docs/_build/ + +# Backup files +*~ +*.swp + +# OCA rules +!static/lib/ + +# Ruff stuff +.ruff_cache + +# Ignore secret files and env +.secrets.* +../.env diff --git a/.pre-commit-config.yaml b/openg2p-g2p-bridge-api/.pre-commit-config.yaml similarity index 100% rename from .pre-commit-config.yaml rename to openg2p-g2p-bridge-api/.pre-commit-config.yaml diff --git a/.ruff.toml b/openg2p-g2p-bridge-api/.ruff.toml similarity index 100% rename from .ruff.toml rename to openg2p-g2p-bridge-api/.ruff.toml diff --git a/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-api/CODE-OF-CONDUCT.md similarity index 100% rename from CODE-OF-CONDUCT.md rename to openg2p-g2p-bridge-api/CODE-OF-CONDUCT.md diff --git a/CONTRIBUTING.md b/openg2p-g2p-bridge-api/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to openg2p-g2p-bridge-api/CONTRIBUTING.md diff --git a/Dockerfile b/openg2p-g2p-bridge-api/Dockerfile similarity index 100% rename from Dockerfile rename to openg2p-g2p-bridge-api/Dockerfile diff --git a/LICENSE b/openg2p-g2p-bridge-api/LICENSE similarity index 100% rename from LICENSE rename to openg2p-g2p-bridge-api/LICENSE diff --git a/README.md b/openg2p-g2p-bridge-api/README.md similarity index 100% rename from README.md rename to openg2p-g2p-bridge-api/README.md diff --git a/src/openg2p_g2p_bridge_api/utils/__init__.py b/openg2p-g2p-bridge-api/__init__.py similarity index 100% rename from src/openg2p_g2p_bridge_api/utils/__init__.py rename to openg2p-g2p-bridge-api/__init__.py diff --git a/main.py b/openg2p-g2p-bridge-api/main.py similarity index 100% rename from main.py rename to openg2p-g2p-bridge-api/main.py diff --git a/pyproject.toml b/openg2p-g2p-bridge-api/pyproject.toml similarity index 100% rename from pyproject.toml rename to openg2p-g2p-bridge-api/pyproject.toml diff --git a/src/openg2p_g2p_bridge_api/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py similarity index 100% rename from src/openg2p_g2p_bridge_api/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py diff --git a/src/openg2p_g2p_bridge_api/app.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/app.py similarity index 78% rename from src/openg2p_g2p_bridge_api/app.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/app.py index e20b4ff..67865a7 100644 --- a/src/openg2p_g2p_bridge_api/app.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/app.py @@ -1,27 +1,32 @@ # ruff: noqa: E402 import asyncio +import logging from .config import Settings _config = Settings.get_config() from openg2p_fastapi_common.app import Initializer as BaseInitializer +from openg2p_g2p_bridge_models.models import ( + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, +) from .controllers import ( DisbursementController, DisbursementEnvelopeController, ) -from .models import DisbursementEnvelope, DisbursementEnvelopeBatchStatus from .services import ( DisbursementEnvelopeService, DisbursementService, ) +_logger = logging.getLogger(_config.logging_default_logger_name) + class Initializer(BaseInitializer): def initialize(self, **kwargs): super().initialize() - DisbursementEnvelopeService() DisbursementService() DisbursementEnvelopeController().post_init() @@ -31,7 +36,7 @@ def migrate_database(self, args): super().migrate_database(args) async def migrate(): - print("Migrating database") + _logger.info("Migrating database") await DisbursementEnvelope.create_migrate() await DisbursementEnvelopeBatchStatus.create_migrate() diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py new file mode 100644 index 0000000..119a2de --- /dev/null +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py @@ -0,0 +1,7 @@ +from celery import Celery + +celery_app = Celery( + "g2p_bridge_celery_tasks", + broker="redis://localhost:6379/0", + backend="redis://localhost:6379/0", +) diff --git a/src/openg2p_g2p_bridge_api/config.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/config.py similarity index 100% rename from src/openg2p_g2p_bridge_api/config.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/config.py diff --git a/src/openg2p_g2p_bridge_api/controllers/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py similarity index 100% rename from src/openg2p_g2p_bridge_api/controllers/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py diff --git a/src/openg2p_g2p_bridge_api/controllers/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement.py similarity index 95% rename from src/openg2p_g2p_bridge_api/controllers/disbursement.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement.py index 0e3cb77..6562f80 100644 --- a/src/openg2p_g2p_bridge_api/controllers/disbursement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement.py @@ -1,13 +1,13 @@ from typing import List from openg2p_fastapi_common.controller import BaseController - -from ..errors import DisbursementException -from ..schemas import ( +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementException +from openg2p_g2p_bridge_models.schemas import ( DisbursementPayload, DisbursementRequest, DisbursementResponse, ) + from ..services import DisbursementService diff --git a/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement_envelope.py similarity index 95% rename from src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement_envelope.py index d5282a3..fc98a48 100644 --- a/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement_envelope.py @@ -1,11 +1,11 @@ from openg2p_fastapi_common.controller import BaseController - -from ..errors import DisbursementEnvelopeException -from ..schemas import ( +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementEnvelopeException +from openg2p_g2p_bridge_models.schemas import ( DisbursementEnvelopePayload, DisbursementEnvelopeRequest, DisbursementEnvelopeResponse, ) + from ..services import DisbursementEnvelopeService diff --git a/src/openg2p_g2p_bridge_api/services/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/__init__.py similarity index 100% rename from src/openg2p_g2p_bridge_api/services/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/__init__.py diff --git a/src/openg2p_g2p_bridge_api/services/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement.py similarity index 86% rename from src/openg2p_g2p_bridge_api/services/disbursement.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement.py index b37ab6a..93b1c6d 100644 --- a/src/openg2p_g2p_bridge_api/services/disbursement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement.py @@ -1,3 +1,4 @@ +import logging import time import uuid from datetime import datetime @@ -5,27 +6,33 @@ from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService -from sqlalchemy.ext.asyncio import async_sessionmaker -from sqlalchemy.future import select - -from ..errors import ( - DisbursementException, - G2PBridgeErrorCodes, -) -from ..models import ( +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementException +from openg2p_g2p_bridge_models.models import ( + BankDisbursementBatchStatus, CancellationStatus, Disbursement, - DisbursementBatchStatus, + DisbursementBatchControl, DisbursementCancellationStatus, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, + ProcessStatus, + MapperResolutionBatchStatus, + ProcessStatus, ) -from ..schemas import ( +from openg2p_g2p_bridge_models.schemas import ( DisbursementPayload, DisbursementRequest, DisbursementResponse, ResponseStatus, ) +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select + +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) class DisbursementService(BaseService): @@ -41,7 +48,6 @@ async def create_disbursements( ) except DisbursementException as e: raise e - is_error_free = await self.validate_disbursement_request( disbursement_payloads=disbursement_request.request_payload ) @@ -51,32 +57,59 @@ async def create_disbursements( code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, disbursement_payloads=disbursement_request.request_payload, ) - disbursements: List[Disbursement] = await self.construct_disbursements( disbursement_payloads=disbursement_request.request_payload ) - disbursement_batch_statuses: List[ - DisbursementBatchStatus - ] = await self.construct_disbursement_batch_statuses( + disbursement_batch_controls: List[ + DisbursementBatchControl + ] = await self.construct_disbursement_batch_controls( disbursements=disbursements ) disbursement_envelope_batch_status = ( await self.update_disbursement_envelope_batch_status( - disbursement_request, disbursements, session + disbursements, session ) ) - session.add_all(disbursements) - session.add_all(disbursement_batch_statuses) + session.add_all(disbursement_batch_controls) session.add(disbursement_envelope_batch_status) + + if disbursement_envelope_batch_status.id_mapper_resolution_required: + mapper_resolution_batch_status: MapperResolutionBatchStatus = ( + MapperResolutionBatchStatus( + mapper_resolution_batch_id=disbursement_batch_controls[ + 0 + ].mapper_resolution_batch_id, + resolution_status=ProcessStatus.PENDING, + latest_error_code="", + active=True, + ) + ) + session.add(mapper_resolution_batch_status) + _logger.info("ID Mapper Resolution Batch Status Created!") + + bank_disbursement_batch_status: BankDisbursementBatchStatus = ( + BankDisbursementBatchStatus( + bank_disbursement_batch_id=disbursement_batch_controls[ + 0 + ].bank_disbursement_batch_id, + disbursement_envelope_id=disbursement_batch_controls[ + 0 + ].disbursement_envelope_id, + disbursement_status=ProcessStatus.PENDING, + latest_error_code="", + disbursement_attempts=0, + active=True, + ) + ) + + session.add(bank_disbursement_batch_status) await session.commit() return disbursement_request.request_payload - async def update_disbursement_envelope_batch_status( - self, disbursement_request, disbursements, session - ): + async def update_disbursement_envelope_batch_status(self, disbursements, session): disbursement_envelope_batch_status = ( ( await session.execute( @@ -103,7 +136,6 @@ async def construct_disbursements( disbursements: List[Disbursement] = [] for disbursement_payload in disbursement_payloads: disbursement = Disbursement( - id=uuid.uuid4(), disbursement_id=str(int(time.time() * 1000)), disbursement_envelope_id=str( disbursement_payload.disbursement_envelope_id @@ -119,19 +151,23 @@ async def construct_disbursements( disbursements.append(disbursement) return disbursements - async def construct_disbursement_batch_statuses( + async def construct_disbursement_batch_controls( self, disbursements: List[Disbursement] ): - disbursement_batch_statuses = [] + disbursement_batch_controls = [] + mapper_resolution_batch_id = str(uuid.uuid4()) + bank_disbursement_batch_id = str(uuid.uuid4()) for disbursement in disbursements: - disbursement_batch_status = DisbursementBatchStatus( - id=disbursement.id, + disbursement_batch_control = DisbursementBatchControl( disbursement_id=disbursement.disbursement_id, disbursement_envelope_id=str(disbursement.disbursement_envelope_id), + beneficiary_id=disbursement.beneficiary_id, + bank_disbursement_batch_id=bank_disbursement_batch_id, + mapper_resolution_batch_id=mapper_resolution_batch_id, active=True, ) - disbursement_batch_statuses.append(disbursement_batch_status) - return disbursement_batch_statuses + disbursement_batch_controls.append(disbursement_batch_control) + return disbursement_batch_controls async def validate_disbursement_request( self, disbursement_payloads: List[DisbursementPayload] @@ -202,7 +238,7 @@ async def validate_disbursement_envelope( disbursement_payloads, ) - if disbursement_envelope.cancellation_status == CancellationStatus.Canceled: + if disbursement_envelope.cancellation_status == CancellationStatus.Cancelled: raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, disbursement_payloads, @@ -449,7 +485,7 @@ async def validate_envelope_for_disbursement_cancellation( disbursement_payloads, ) - if disbursement_envelope.cancellation_status == CancellationStatus.Canceled: + if disbursement_envelope.cancellation_status == CancellationStatus.Cancelled: raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, disbursement_payloads, diff --git a/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement_envelope.py similarity index 89% rename from src/openg2p_g2p_bridge_api/services/disbursement_envelope.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement_envelope.py index 4dd5219..ff495d2 100644 --- a/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement_envelope.py @@ -1,14 +1,12 @@ import time -import uuid from datetime import datetime from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService -from sqlalchemy.ext.asyncio import async_sessionmaker -from sqlalchemy.future import select - -from ..errors import DisbursementEnvelopeException, G2PBridgeErrorCodes -from ..models import ( +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementEnvelopeException +from openg2p_g2p_bridge_models.models import ( + BenefitProgramConfiguration, CancellationStatus, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, @@ -16,12 +14,14 @@ FundsAvailableWithBankEnum, FundsBlockedWithBankEnum, ) -from ..schemas import ( +from openg2p_g2p_bridge_models.schemas import ( DisbursementEnvelopePayload, DisbursementEnvelopeRequest, DisbursementEnvelopeResponse, ResponseStatus, ) +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select class DisbursementEnvelopeService(BaseService): @@ -41,7 +41,7 @@ async def create_disbursement_envelope( disbursement_envelope_batch_status: DisbursementEnvelopeBatchStatus = ( await self.construct_disbursement_envelope_batch_status( - disbursement_envelope + disbursement_envelope, session ) ) @@ -87,14 +87,14 @@ async def cancel_disbursement_envelope( if ( disbursement_envelope.cancellation_status - == CancellationStatus.Canceled.value + == CancellationStatus.Cancelled.value ): raise DisbursementEnvelopeException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED ) disbursement_envelope.cancellation_status = ( - CancellationStatus.Canceled.value + CancellationStatus.Cancelled.value ) disbursement_envelope.cancellation_timestamp = datetime.utcnow() @@ -190,7 +190,6 @@ async def construct_disbursement_envelope( self, disbursement_envelope_payload: DisbursementEnvelopePayload ) -> DisbursementEnvelope: disbursement_envelope: DisbursementEnvelope = DisbursementEnvelope( - id=uuid.uuid4(), disbursement_envelope_id=str(int(time.time() * 1000)), benefit_program_mnemonic=disbursement_envelope_payload.benefit_program_mnemonic, disbursement_frequency=disbursement_envelope_payload.disbursement_frequency.value, @@ -200,7 +199,7 @@ async def construct_disbursement_envelope( total_disbursement_amount=disbursement_envelope_payload.total_disbursement_amount, disbursement_schedule_date=disbursement_envelope_payload.disbursement_schedule_date, receipt_time_stamp=datetime.utcnow(), - cancellation_status=CancellationStatus.Not_Canceled.value, + cancellation_status=CancellationStatus.Not_Cancelled.value, active=True, ) disbursement_envelope_payload.id = disbursement_envelope.id @@ -211,10 +210,21 @@ async def construct_disbursement_envelope( # noinspection PyMethodMayBeStatic async def construct_disbursement_envelope_batch_status( - self, disbursement_envelope: DisbursementEnvelope + self, disbursement_envelope: DisbursementEnvelope, session ) -> DisbursementEnvelopeBatchStatus: + benefit_program_configuration: BenefitProgramConfiguration = ( + ( + await session.execute( + select(BenefitProgramConfiguration).where( + BenefitProgramConfiguration.benefit_program_mnemonic + == disbursement_envelope.benefit_program_mnemonic + ) + ) + ) + .scalars() + .first() + ) disbursement_envelope_batch_status: DisbursementEnvelopeBatchStatus = DisbursementEnvelopeBatchStatus( - id=disbursement_envelope.id, disbursement_envelope_id=disbursement_envelope.disbursement_envelope_id, number_of_disbursements_received=0, total_disbursement_amount_received=0, @@ -227,5 +237,6 @@ async def construct_disbursement_envelope_batch_status( funds_blocked_retries=0, funds_blocked_latest_error_code="", active=True, + id_mapper_resolution_required=benefit_program_configuration.id_mapper_resolution_required, ) return disbursement_envelope_batch_status diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py new file mode 100644 index 0000000..224d44a --- /dev/null +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py @@ -0,0 +1 @@ +from .model_serializer import serialize_model diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/model_serializer.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/model_serializer.py new file mode 100644 index 0000000..97a93e2 --- /dev/null +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/model_serializer.py @@ -0,0 +1,6 @@ +from sqlalchemy.inspection import inspect + + +def serialize_model(obj): + """Converts SQLAlchemy model instance into a JSON-compliant dictionary.""" + return {c.key: getattr(obj, c.key) for c in inspect(obj).mapper.column_attrs} diff --git a/test-requirements.txt b/openg2p-g2p-bridge-api/test-requirements.txt similarity index 100% rename from test-requirements.txt rename to openg2p-g2p-bridge-api/test-requirements.txt diff --git a/tests/__init__.py b/openg2p-g2p-bridge-api/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to openg2p-g2p-bridge-api/tests/__init__.py diff --git a/tests/test_disbursement.py b/openg2p-g2p-bridge-api/tests/test_disbursement.py similarity index 92% rename from tests/test_disbursement.py rename to openg2p-g2p-bridge-api/tests/test_disbursement.py index 59486f8..8171c3e 100644 --- a/tests/test_disbursement.py +++ b/openg2p-g2p-bridge-api/tests/test_disbursement.py @@ -3,12 +3,10 @@ import pytest from openg2p_g2p_bridge_api.controllers import DisbursementController -from openg2p_g2p_bridge_api.errors import ( - DisbursementException, - G2PBridgeErrorCodes, -) -from openg2p_g2p_bridge_api.models import CancellationStatus -from openg2p_g2p_bridge_api.schemas import ( +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementException +from openg2p_g2p_bridge_models.models import CancellationStatus +from openg2p_g2p_bridge_models.schemas import ( DisbursementPayload, DisbursementRequest, DisbursementResponse, @@ -32,7 +30,7 @@ async def test_create_disbursements_success(mock_service_get_component): disbursement_payloads = [ DisbursementPayload( disbursement_envelope_id="env123", - beneficiary_id=123, + beneficiary_id="123AB", disbursement_amount=1000, ) ] @@ -64,7 +62,7 @@ async def test_create_disbursements_failure(mock_service_get_component): disbursement_payloads = [ DisbursementPayload( disbursement_envelope_id="env123", - beneficiary_id=123, + beneficiary_id="123AB", disbursement_amount=1000, ) ] @@ -99,7 +97,7 @@ def mock_cancel_disbursements(is_valid, disbursement_payloads): disbursement_payloads=disbursement_payloads, ) for payload in disbursement_payloads: - payload.cancellation_status = CancellationStatus.Canceled + payload.cancellation_status = CancellationStatus.Cancelled payload.cancellation_time_stamp = datetime.datetime.utcnow() return disbursement_payloads @@ -111,7 +109,7 @@ async def test_cancel_disbursements_success(mock_service_get_component): disbursement_payloads = [ DisbursementPayload( disbursement_id="123", - beneficiary_id=123, + beneficiary_id="123AB", disbursement_amount=1000, cancellation_status=None, ) @@ -135,7 +133,7 @@ async def test_cancel_disbursements_success(mock_service_get_component): assert response.response_status == ResponseStatus.SUCCESS assert all( - payload.cancellation_status == CancellationStatus.Canceled + payload.cancellation_status == CancellationStatus.Cancelled for payload in response.response_payload ) @@ -147,7 +145,7 @@ async def test_cancel_disbursements_failure(mock_service_get_component): disbursement_payloads = [ DisbursementPayload( disbursement_id="123", - beneficiary_id=123, + beneficiary_id="123AB", disbursement_amount=1000, cancellation_status=None, ) diff --git a/tests/test_disbursement_envelope.py b/openg2p-g2p-bridge-api/tests/test_disbursement_envelope.py similarity index 97% rename from tests/test_disbursement_envelope.py rename to openg2p-g2p-bridge-api/tests/test_disbursement_envelope.py index 1af0e1c..4ccfd8a 100644 --- a/tests/test_disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/tests/test_disbursement_envelope.py @@ -3,11 +3,9 @@ import pytest from openg2p_g2p_bridge_api.controllers import DisbursementEnvelopeController -from openg2p_g2p_bridge_api.errors import ( - DisbursementEnvelopeException, - G2PBridgeErrorCodes, -) -from openg2p_g2p_bridge_api.schemas import ( +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementEnvelopeException +from openg2p_g2p_bridge_models.schemas import ( DisbursementEnvelopePayload, DisbursementEnvelopeRequest, DisbursementEnvelopeResponse, From 91363724a0d615d04206c98f2b3b6c1d59abcc19 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 19 Jul 2024 17:05:14 +0530 Subject: [PATCH 14/39] Openg2p Bridge Celery Tasks --- openg2p-g2p-bridge-celery-tasks/.gitignore | 81 ++++ .../.pre-commit-config.yaml | 49 +++ openg2p-g2p-bridge-celery-tasks/.ruff.toml | 16 + .../CODE-OF-CONDUCT.md | 114 ++++++ .../CONTRIBUTING.md | 2 + openg2p-g2p-bridge-celery-tasks/LICENSE | 373 ++++++++++++++++++ openg2p-g2p-bridge-celery-tasks/README.md | 14 + openg2p-g2p-bridge-celery-tasks/__init__.py | 0 openg2p-g2p-bridge-celery-tasks/main.py | 15 + .../pyproject.toml | 33 ++ .../__init__.py | 1 + .../openg2p_g2p_bridge_celery_tasks/app.py | 60 +++ .../openg2p_g2p_bridge_celery_tasks/config.py | 37 ++ .../helpers/__init__.py | 1 + .../helpers/resolve_helper.py | 99 +++++ .../tasks/__init__.py | 16 + .../tasks/block_funds_with_bank.py | 157 ++++++++ .../tasks/check_funds_with_bank_task.py | 155 ++++++++ .../tasks/disburse_funds_from_bank.py | 211 ++++++++++ .../tasks/mapper_resolution_task.py | 189 +++++++++ .../tests/__init__.py | 0 .../tests/test_mapper_resolve_task.py | 176 +++++++++ 22 files changed, 1799 insertions(+) create mode 100644 openg2p-g2p-bridge-celery-tasks/.gitignore create mode 100644 openg2p-g2p-bridge-celery-tasks/.pre-commit-config.yaml create mode 100644 openg2p-g2p-bridge-celery-tasks/.ruff.toml create mode 100644 openg2p-g2p-bridge-celery-tasks/CODE-OF-CONDUCT.md create mode 100644 openg2p-g2p-bridge-celery-tasks/CONTRIBUTING.md create mode 100644 openg2p-g2p-bridge-celery-tasks/LICENSE create mode 100644 openg2p-g2p-bridge-celery-tasks/README.md create mode 100644 openg2p-g2p-bridge-celery-tasks/__init__.py create mode 100644 openg2p-g2p-bridge-celery-tasks/main.py create mode 100644 openg2p-g2p-bridge-celery-tasks/pyproject.toml create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/__init__.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/__init__.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py create mode 100644 openg2p-g2p-bridge-celery-tasks/tests/__init__.py create mode 100644 openg2p-g2p-bridge-celery-tasks/tests/test_mapper_resolve_task.py diff --git a/openg2p-g2p-bridge-celery-tasks/.gitignore b/openg2p-g2p-bridge-celery-tasks/.gitignore new file mode 100644 index 0000000..f5e0368 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/.gitignore @@ -0,0 +1,81 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +/venv +/.pytest_cache + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store + +# Django stuff: +*.log + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Sphinx documentation +docs/_build/ + +# Backup files +*~ +*.swp + +# OCA rules +!static/lib/ + +# Ruff stuff +.ruff_cache + +# Ignore secret files and env +.secrets.* +.env diff --git a/openg2p-g2p-bridge-celery-tasks/.pre-commit-config.yaml b/openg2p-g2p-bridge-celery-tasks/.pre-commit-config.yaml new file mode 100644 index 0000000..336b7e1 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: | + (?x) + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.11.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.289 + hooks: + - id: ruff + args: + - --fix diff --git a/openg2p-g2p-bridge-celery-tasks/.ruff.toml b/openg2p-g2p-bridge-celery-tasks/.ruff.toml new file mode 100644 index 0000000..aa1fc5b --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/.ruff.toml @@ -0,0 +1,16 @@ +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[per-file-ignores] +"__init__.py" = ["F401"] diff --git a/openg2p-g2p-bridge-celery-tasks/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-celery-tasks/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..e1949e1 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/CODE-OF-CONDUCT.md @@ -0,0 +1,114 @@ +# Code of Conduct + +## Contributor Covenant Code of Conduct + +### Preamble + +OpenG2P was created to foster an open, innovative and inclusive community around open source & open standard. +To clarify expected behaviour in our communities we have adopted the Contributor Covenant. This code of +conduct has been adopted by many other open source communities and we feel it expresses our values well. + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free +experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy +community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit + permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will +take appropriate and fair corrective action in response to any behavior that they deem inappropriate, +threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki +edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate +reasons for moderation decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially +representing the community in public spaces. Examples of representing our community include using an official +e-mail address, posting via an official social media account, or acting as an appointed representative at an +online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders +responsible for enforcement at \[INSERT CONTACT METHOD]. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action +they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in +the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the +violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, +including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. +This includes avoiding interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a +specified period of time. No public or private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may +lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained +inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +### Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the +[FAQ](https://www.contributor-covenant.org/faq). Translations are available +[here](https://www.contributor-covenant.org/translations). diff --git a/openg2p-g2p-bridge-celery-tasks/CONTRIBUTING.md b/openg2p-g2p-bridge-celery-tasks/CONTRIBUTING.md new file mode 100644 index 0000000..c187f93 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/CONTRIBUTING.md @@ -0,0 +1,2 @@ +Refer to contribution guidelines +[here](https://github.com/OpenG2P/openg2p-documentation/blob/1.0.0/community/contributing-to-openg2p.md). diff --git a/openg2p-g2p-bridge-celery-tasks/LICENSE b/openg2p-g2p-bridge-celery-tasks/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/openg2p-g2p-bridge-celery-tasks/README.md b/openg2p-g2p-bridge-celery-tasks/README.md new file mode 100644 index 0000000..17c08f0 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/README.md @@ -0,0 +1,14 @@ +# openg2p-g2p-bridge-celery-tasks + +[![Pre-commit Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml?query=branch%3Adevelop) +[![Build Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml?query=branch%3Adevelop) +[![codecov](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api/branch/develop/graph/badge.svg)](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api) +[![openapi](https://img.shields.io/badge/open--API-swagger-brightgreen)](https://validator.swagger.io/?url=https://raw.githubusercontent.com/OpenG2P/openg2p-g2p-bridge-api/develop/api-docs/generated/openapi.json) +![PyPI](https://img.shields.io/pypi/v/openg2p-g2p-bridge-api?label=pypi%20package) +![PyPI - Downloads](https://img.shields.io/pypi/dm/openg2p-g2p-bridge-api) + + + +## Licenses + +This repository is licensed under [MPL-2.0](LICENSE). diff --git a/openg2p-g2p-bridge-celery-tasks/__init__.py b/openg2p-g2p-bridge-celery-tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-celery-tasks/main.py b/openg2p-g2p-bridge-celery-tasks/main.py new file mode 100644 index 0000000..8aaeb2a --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/main.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +# ruff: noqa: I001 + +from openg2p_g2p_bridge_celery_tasks.app import Initializer, celery_app +from openg2p_fastapi_common.ping import PingInitializer + +initializer = Initializer() +PingInitializer() + +app = initializer.return_app() +celery_app = celery_app + +if __name__ == "__main__": + initializer.main() diff --git a/openg2p-g2p-bridge-celery-tasks/pyproject.toml b/openg2p-g2p-bridge-celery-tasks/pyproject.toml new file mode 100644 index 0000000..a122697 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "openg2p-g2p-bridge-celery-tasks" +authors = [ + { name="OpenG2P", email="info@openg2p.org" }, +] +description = "OpenG2P G2P Bridge Celery Tasks" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: OS Independent", +] +dependencies = [ + "openg2p-fastapi-common", + "openg2p-fastapi-auth", + "openg2p-g2pconnect-mapper-lib", + "celery" +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://openg2p.org" +Documentation = "https://docs.openg2p.org/" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-celery-tasks" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-celery-tasks" + +[tool.hatch.version] +path = "src/openg2p_g2p_bridge_celery_tasks/__init__.py" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/__init__.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/__init__.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py new file mode 100644 index 0000000..2f4dd8b --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py @@ -0,0 +1,60 @@ +# ruff: noqa: E402 + +from .config import Settings + +_config = Settings.get_config() + +from celery import Celery +from openg2p_fastapi_common.app import Initializer as BaseInitializer +from openg2p_fastapi_common.exception import BaseExceptionHandler +from openg2p_g2p_bridge_bank_connectors.app import ( + Initializer as BankConnectorInitializer, +) +from openg2p_g2pconnect_mapper_lib.app import Initializer as MapperInitializer +from sqlalchemy import create_engine + +from .helpers import ResolveHelper + + +class Initializer(BaseInitializer): + def initialize(self, **kwargs): + super().init_logger() + super().init_app() + BaseExceptionHandler() + + BankConnectorInitializer() + MapperInitializer() + ResolveHelper() + + +def get_engine(): + if _config.db_datasource: + db_engine = create_engine(_config.db_datasource) + return db_engine + +celery_app = Celery( + "g2p_bridge_celery_tasks", + broker="redis://localhost:6379/0", + backend="redis://localhost:6379/0", + include=["openg2p_g2p_bridge_celery_tasks.tasks.mapper_resolution_task"], +) + +celery_app.conf.beat_schedule = { + "mapper_resolution_beat_producer": { + "task": "mapper_resolution_beat_producer", + "schedule": _config.mapper_resolve_frequency, + }, + "check_funds_with_bank_beat_producer": { + "task": "check_funds_with_bank_beat_producer", + "schedule": _config.funds_available_check_frequency, + }, + "block_funds_with_bank_beat_producer": { + "task": "block_funds_with_bank_beat_producer", + "schedule": _config.funds_blocked_frequency, + }, + "disburse_funds_from_bank_beat_producer": { + "task": "disburse_funds_from_bank_beat_producer", + "schedule": _config.funds_disbursement_frequency, + }, +} +celery_app.conf.timezone = "UTC" \ No newline at end of file diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py new file mode 100644 index 0000000..b084fb4 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py @@ -0,0 +1,37 @@ +from openg2p_fastapi_common.config import Settings as BaseSettings +from pydantic_settings import SettingsConfigDict + +from . import __version__ + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="g2p_bridge_celery_tasks_", env_file=".env", extra="allow" + ) + openapi_title: str = "OpenG2P G2P Bridge Celery Tasks" + openapi_description: str = """ + Celery tasks for OpenG2P G2P Bridge API + *********************************** + Further details goes here + *********************************** + """ + openapi_version: str = __version__ + + db_dbname: str = "openg2p_g2p_bridge_db" + db_driver: str = "postgresql" + + mapper_resolve_api_url: str = "" + + mapper_resolve_attempts: int = 3 + funds_available_check_attempts: int = 3 + funds_blocked_attempts: int = 3 + funds_disbursement_attempts: int = 3 + + mapper_resolve_frequency: int = 3600 + funds_available_check_frequency: int = 3600 + funds_blocked_frequency: int = 3600 + funds_disbursement_frequency: int = 3600 + + bank_deconstruct_strategy: str = "" + mobile_wallet_deconstruct_strategy: str = "" + email_wallet_deconstruct_strategy: str = "" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/__init__.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/__init__.py new file mode 100644 index 0000000..732ae14 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/__init__.py @@ -0,0 +1 @@ +from .resolve_helper import ResolveHelper diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py new file mode 100644 index 0000000..4b2411f --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py @@ -0,0 +1,99 @@ +import enum +import re +import uuid +from datetime import datetime +from typing import List + +from openg2p_fastapi_common.service import BaseService +from openg2p_g2p_bridge_models.models import MapperResolvedFaType +from openg2p_g2pconnect_common_lib.schemas import RequestHeader +from openg2p_g2pconnect_mapper_lib.schemas import ( + ResolveRequest, + ResolveRequestMessage, + SingleResolveRequest, +) +from pydantic import BaseModel + +from ..config import Settings + +_config = Settings.get_config() + + +class FAKeys(enum.Enum): + account_number = "account_number" + bank_code = "bank_code" + branch_code = "branch_code" + account_type = "account_type" + mobile_number = "mobile_number" + mobile_wallet_provider = "mobile_wallet_provider" + email_address = "email_address" + email_wallet_provider = "email_wallet_provider" + + +class KeyValuePair(BaseModel): + key: FAKeys + value: str + + +class ResolveHelper(BaseService): + def construct_single_resolve_request(self, id: str) -> SingleResolveRequest: + single_resolve_request = SingleResolveRequest( + reference_id=str(uuid.uuid4()), + timestamp=datetime.now(), + id=id, + scope="details", + ) + return single_resolve_request + + def construct_resolve_request( + self, single_resolve_requests: List[SingleResolveRequest] + ) -> ResolveRequest: + resolve_request_message = ResolveRequestMessage( + transaction_id=str(uuid.uuid4()), + resolve_request=single_resolve_requests, + ) + + resolve_request = ResolveRequest( + signature="", + header=RequestHeader( + message_id=str(uuid.uuid4()), + message_ts=str(datetime.now()), + action="resolve", + sender_id="", + sender_uri="", + total_count=len(single_resolve_requests), + ), + message=resolve_request_message, + ) + + return resolve_request + + def _deconstruct(self, value: str, strategy: str) -> List[KeyValuePair]: + regex_res = re.match(strategy, value) + deconstructed_list = [] + if regex_res: + regex_res = regex_res.groupdict() + try: + deconstructed_list = [ + KeyValuePair(key=k, value=v) for k, v in regex_res.items() + ] + except Exception as e: + raise ValueError("Error while deconstructing ID/FA") from e + return deconstructed_list + + def deconstruct_fa(self, fa: str) -> dict: + deconstruct_strategy = self.get_deconstruct_strategy(fa) + if deconstruct_strategy: + deconstructed_pairs = self._deconstruct(fa, deconstruct_strategy) + deconstructed_fa = {pair.key: pair.value for pair in deconstructed_pairs} + return deconstructed_fa + return {} + + def get_deconstruct_strategy(self, fa: str) -> str: + if fa.startswith(MapperResolvedFaType.BANK_ACCOUNT.value): + return _config.bank_fa_deconstruct_strategy + elif fa.startswith(MapperResolvedFaType.MOBILE_WALLET.value): + return _config.mobile_wallet_fa_deconstruct_strategy + elif fa.startswith(MapperResolvedFaType.EMAIL_WALLET.value): + return _config.email_wallet_fa_deconstruct_strategy + return "" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py new file mode 100644 index 0000000..be9b971 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py @@ -0,0 +1,16 @@ +from .block_funds_with_bank import ( + block_funds_with_bank_beat_producer, + block_funds_with_bank_worker, +) +from .check_funds_with_bank_task import ( + check_funds_with_bank_beat_producer, + check_funds_with_bank_worker, +) +from .mapper_resolution_task import ( + mapper_resolution_beat_producer, + mapper_resolution_worker, +) +from .disburse_funds_from_bank import ( + disburse_funds_from_bank_beat_producer, + disburse_funds_from_bank_worker, +) \ No newline at end of file diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py new file mode 100644 index 0000000..68ebd13 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py @@ -0,0 +1,157 @@ +import logging +from datetime import datetime + +from openg2p_g2p_bridge_bank_connectors.bank_connectors import BankConnectorFactory +from openg2p_g2p_bridge_bank_connectors.bank_interface import ( + BankConnectorInterface, + BlockFundsResponse, +) +from openg2p_g2p_bridge_models.models import ( + BenefitProgramConfiguration, + CancellationStatus, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) +from sqlalchemy import and_, or_, select +from sqlalchemy.orm import sessionmaker + +from ..app import get_engine, celery_app +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) +_engine = get_engine() + + +@celery_app.task(name="block_funds_with_bank_beat_producer") +def block_funds_with_bank_beat_producer(): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + envelopes = ( + session.execute( + select(DisbursementEnvelope) + .filter( + DisbursementEnvelope.disbursement_schedule_date + <= datetime.utcnow(), + DisbursementEnvelope.cancellation_status + == CancellationStatus.Not_Cancelled.value, + ) + .join( + DisbursementEnvelopeBatchStatus, + DisbursementEnvelope.disbursement_envelope_id + == DisbursementEnvelopeBatchStatus.disbursement_envelope_id, + ) + .filter( + DisbursementEnvelope.number_of_disbursements + == DisbursementEnvelopeBatchStatus.number_of_disbursements_received, + DisbursementEnvelopeBatchStatus.funds_available_with_bank + == FundsAvailableWithBankEnum.FUNDS_AVAILABLE.value, + or_( + and_( + DisbursementEnvelopeBatchStatus.funds_blocked_with_bank + == FundsBlockedWithBankEnum.PENDING_CHECK.value, + DisbursementEnvelopeBatchStatus.funds_blocked_attempts + < _config.funds_blocked_attempts, + ), + and_( + DisbursementEnvelopeBatchStatus.funds_blocked_with_bank + == FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE.value, + DisbursementEnvelopeBatchStatus.funds_blocked_attempts + < _config.funds_blocked_attempts, + ), + ), + ) + ) + .scalars() + .all() + ) + + for envelope in envelopes: + block_funds_with_bank_worker.delay(envelope.disbursement_envelope_id) + + +@celery_app.task(name="block_funds_with_bank_worker") +def block_funds_with_bank_worker(disbursement_envelope_id: str): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + envelope = ( + session.query(DisbursementEnvelope) + .filter( + DisbursementEnvelope.disbursement_envelope_id + == disbursement_envelope_id + ) + .first() + ) + + if not envelope: + return + + batch_status = ( + session.query(DisbursementEnvelopeBatchStatus) + .filter( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == disbursement_envelope_id + ) + .first() + ) + + if not batch_status: + return + + benefit_program_configuration = ( + session.query(BenefitProgramConfiguration) + .filter( + BenefitProgramConfiguration.benefit_program_mnemonic + == envelope.benefit_program_mnemonic + ) + .first() + ) + + total_funds_needed = envelope.total_disbursement_amount + bank_connector: BankConnectorInterface = ( + BankConnectorFactory.get_component().get_bank_connector( + benefit_program_configuration.sponsor_bank_code + ) + ) + + try: + funds_blocked: BlockFundsResponse = bank_connector.block_funds( + benefit_program_configuration.sponsor_bank_account_number, + benefit_program_configuration.sponsor_bank_account_currency, + total_funds_needed, + ) + + if funds_blocked.status == FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS: + batch_status.funds_blocked_with_bank = ( + FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS.value + ) + batch_status.funds_blocked_reference_number = ( + funds_blocked.block_reference_no + ) + batch_status.funds_blocked_latest_error_code = None + else: + batch_status.funds_blocked_with_bank = ( + FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE.value + ) + batch_status.funds_blocked_reference_number = "" + batch_status.funds_blocked_latest_error_code = funds_blocked.error_code + + batch_status.funds_blocked_latest_timestamp = datetime.utcnow() + + batch_status.funds_blocked_attempts += 1 + + except Exception as e: + _logger.error(f"Error blocking funds with bank: {str(e)}") + batch_status.funds_blocked_with_bank = ( + FundsBlockedWithBankEnum.PENDING_CHECK.value + ) + batch_status.funds_blocked_latest_timestamp = datetime.utcnow() + batch_status.funds_blocked_latest_error_code = funds_blocked.error_code + batch_status.funds_blocked_attempts += 1 + batch_status.funds_blocked_reference_number = "" + + session.commit() diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py new file mode 100644 index 0000000..ebc0ec1 --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py @@ -0,0 +1,155 @@ +import logging +from datetime import datetime + +from openg2p_g2p_bridge_bank_connectors.bank_connectors import ( + BankConnectorFactory, +) +from openg2p_g2p_bridge_models.models import ( + BenefitProgramConfiguration, + CancellationStatus, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, + FundsAvailableWithBankEnum, +) +from sqlalchemy import and_, or_, select +from sqlalchemy.orm import sessionmaker + +from ..app import get_engine, celery_app +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) +_engine = get_engine() + + +@celery_app.task(name="check_funds_with_bank_beat_producer") +def check_funds_with_bank_beat_producer(): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + envelopes = ( + session.execute( + select(DisbursementEnvelope) + .filter( + DisbursementEnvelope.disbursement_schedule_date < datetime.utcnow(), + DisbursementEnvelope.cancellation_status + == CancellationStatus.Not_Cancelled.value, + ) + .join( + DisbursementEnvelopeBatchStatus, + DisbursementEnvelope.disbursement_envelope_id + == DisbursementEnvelopeBatchStatus.disbursement_envelope_id, + ) + .filter( + DisbursementEnvelope.number_of_disbursements + == DisbursementEnvelopeBatchStatus.number_of_disbursements_received, + DisbursementEnvelope.total_disbursement_amount + == DisbursementEnvelopeBatchStatus.total_disbursement_amount_received, + or_( + and_( + DisbursementEnvelopeBatchStatus.funds_available_with_bank + == FundsAvailableWithBankEnum.PENDING_CHECK.value, + DisbursementEnvelopeBatchStatus.funds_available_attempts + < _config.funds_available_check_attempts, + ), + and_( + DisbursementEnvelopeBatchStatus.funds_available_with_bank + == FundsAvailableWithBankEnum.FUNDS_NOT_AVAILABLE.value, + DisbursementEnvelopeBatchStatus.funds_available_attempts + < _config.funds_available_check_attempts, + ), + ), + ) + ) + .scalars() + .all() + ) + + for envelope in envelopes: + check_funds_with_bank_worker.delay(envelope.disbursement_envelope_id) + + +@celery_app.task(name="check_funds_with_bank_worker") +def check_funds_with_bank_worker(disbursement_envelope_id: str): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + envelope = ( + session.query(DisbursementEnvelope) + .filter( + DisbursementEnvelope.disbursement_envelope_id + == disbursement_envelope_id + ) + .first() + ) + + if not envelope: + return + + disbursement_envelope_batch_status = ( + session.query(DisbursementEnvelopeBatchStatus) + .filter( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == disbursement_envelope_id + ) + .first() + ) + + if not disbursement_envelope_batch_status: + return + + benefit_program_configuration = ( + session.query(BenefitProgramConfiguration) + .filter( + BenefitProgramConfiguration.benefit_program_mnemonic + == envelope.benefit_program_mnemonic + ) + .first() + ) + + total_funds_needed = envelope.total_disbursement_amount + bank_connector = BankConnectorFactory.get_component().get_bank_connector( + benefit_program_configuration.sponsor_bank_code + ) + + try: + funds_available = ( + bank_connector.check_funds( + benefit_program_configuration.sponsor_bank_account_number, + benefit_program_configuration.sponsor_bank_account_currency, + total_funds_needed, + ).status + == FundsAvailableWithBankEnum.FUNDS_AVAILABLE + ) + + if funds_available: + disbursement_envelope_batch_status.funds_available_with_bank = ( + FundsAvailableWithBankEnum.FUNDS_AVAILABLE.value + ) + else: + disbursement_envelope_batch_status.funds_available_with_bank = ( + FundsAvailableWithBankEnum.FUNDS_NOT_AVAILABLE.value + ) + + disbursement_envelope_batch_status.funds_available_latest_timestamp = ( + datetime.utcnow() + ) + disbursement_envelope_batch_status.funds_available_latest_error_code = None + disbursement_envelope_batch_status.funds_available_attempts += 1 + + except Exception as e: + _logger.error( + f"Error checking funds with bank for envelope {disbursement_envelope_id}: {e}" + ) + disbursement_envelope_batch_status.funds_available_with_bank = ( + FundsAvailableWithBankEnum.PENDING_CHECK.value + ) + disbursement_envelope_batch_status.funds_available_latest_timestamp = ( + datetime.utcnow() + ) + disbursement_envelope_batch_status.funds_available_latest_error_code = str( + e + ) + disbursement_envelope_batch_status.funds_available_attempts += 1 + + session.commit() diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py new file mode 100644 index 0000000..622ef4e --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py @@ -0,0 +1,211 @@ +import logging +from datetime import datetime + +from openg2p_g2p_bridge_bank_connectors.bank_connectors import BankConnectorFactory +from openg2p_g2p_bridge_bank_connectors.bank_interface.bank_connector_interface import ( + PaymentPayload, + PaymentStatus, + BankConnectorInterface +) +from openg2p_g2p_bridge_models.models import ( + BankDisbursementBatchStatus, + BenefitProgramConfiguration, + CancellationStatus, + Disbursement, + DisbursementBatchControl, + DisbursementEnvelope, + DisbursementEnvelopeBatchStatus, + ProcessStatus, + FundsBlockedWithBankEnum, + MapperResolutionDetails, +) +from sqlalchemy import select, and_ +from sqlalchemy.orm import sessionmaker + +from ..app import get_engine, celery_app +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) +_engine = get_engine() + + +@celery_app.task(name="disburse_funds_from_bank_beat_producer") +def disburse_funds_from_bank_beat_producer(): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + with session_maker() as session: + envelopes = ( + session.execute( + select(DisbursementEnvelope) + .filter( + DisbursementEnvelope.disbursement_schedule_date + <= datetime.utcnow(), + DisbursementEnvelope.cancellation_status + == CancellationStatus.Not_Cancelled.value, + ) + .join( + DisbursementEnvelopeBatchStatus, + DisbursementEnvelope.disbursement_envelope_id + == DisbursementEnvelopeBatchStatus.disbursement_envelope_id, + ) + .filter( + DisbursementEnvelope.number_of_disbursements + == DisbursementEnvelopeBatchStatus.number_of_disbursements_received, + DisbursementEnvelopeBatchStatus.funds_blocked_with_bank + == FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS.value, + ) + ) + .scalars() + .all() + ) + + for envelope in envelopes: + pending_batches = ( + session.execute( + select(BankDisbursementBatchStatus).filter( + and_( + BankDisbursementBatchStatus.disbursement_envelope_id + == envelope.disbursement_envelope_id, + BankDisbursementBatchStatus.disbursement_status + == ProcessStatus.PENDING.value, + BankDisbursementBatchStatus.disbursement_attempts + < _config.funds_disbursement_attempts, + ) + ) + ) + .scalars() + .all() + ) + + for batch in pending_batches: + disburse_funds_from_bank_worker.delay(batch.bank_disbursement_batch_id) + + +@celery_app.task(name="disburse_funds_from_bank_worker") +def disburse_funds_from_bank_worker(bank_disbursement_batch_id: str): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + batch_status = ( + session.query(BankDisbursementBatchStatus) + .filter( + BankDisbursementBatchStatus.bank_disbursement_batch_id + == bank_disbursement_batch_id + ) + .first() + ) + + if not batch_status: + return + + disbursement_envelope_id = batch_status.disbursement_envelope_id + envelope = ( + session.query(DisbursementEnvelope) + .filter( + DisbursementEnvelope.disbursement_envelope_id + == disbursement_envelope_id + ) + .first() + ) + + if not envelope: + return + + envelope_batch_status = ( + session.query(DisbursementEnvelopeBatchStatus) + .filter( + DisbursementEnvelopeBatchStatus.disbursement_envelope_id + == disbursement_envelope_id + ) + .first() + ) + + if not envelope_batch_status: + return + + disbursement_batch_controls = ( + session.query(DisbursementBatchControl) + .filter( + DisbursementBatchControl.bank_disbursement_batch_id + == bank_disbursement_batch_id + ) + .all() + ) + + disbursement_ids = [ + control.disbursement_id for control in disbursement_batch_controls + ] + disbursements = ( + session.query(Disbursement) + .filter(Disbursement.disbursement_id.in_(disbursement_ids)) + .all() + ) + + benefit_program_configuration = ( + session.query(BenefitProgramConfiguration) + .filter( + BenefitProgramConfiguration.benefit_program_mnemonic + == envelope.benefit_program_mnemonic + ) + .first() + ) + + payment_payloads = [] + + for disbursement in disbursements: + mapper_details = ( + session.query(MapperResolutionDetails) + .filter( + MapperResolutionDetails.disbursement_id + == disbursement.disbursement_id + ) + .first() + ) + + payment_payloads.append( + PaymentPayload( + remitting_account=benefit_program_configuration.sponsor_bank_account_number, + remitting_account_currency=benefit_program_configuration.sponsor_bank_account_currency, + payment_amount=disbursement.disbursement_amount, + funds_blocked_reference_number=envelope_batch_status.funds_blocked_reference_number, + beneficiary_account=mapper_details.bank_account_number if mapper_details else None, + beneficiary_account_currency=benefit_program_configuration.sponsor_bank_account_currency, + beneficiary_bank_code=mapper_details.bank_code if mapper_details else None, + beneficiary_branch_code=mapper_details.branch_code if mapper_details else None, + payment_date=datetime.utcnow(), + beneficiary_id=disbursement.beneficiary_id, + beneficiary_name=disbursement.beneficiary_name, + beneficiary_account_type=mapper_details.mapper_resolved_fa_type, + beneficiary_phone_no=mapper_details.mobile_number if mapper_details else None, + beneficiary_mobile_wallet_provider=mapper_details.mobile_wallet_provider if mapper_details else None, + beneficiary_email_wallet_provider=mapper_details.email_wallet_provider if mapper_details else None, + beneficiary_email=mapper_details.email_address if mapper_details else None, + benefit_program_mnemonic=envelope.benefit_program_mnemonic, + cycle_code_mnemonic=envelope.cycle_code_mnemonic, + ) + ) + + bank_connector: BankConnectorInterface = BankConnectorFactory.get_component().get_bank_connector( + benefit_program_configuration.sponsor_bank_code + ) + + try: + payment_response = bank_connector.initiate_payment(payment_payloads) + + if payment_response.status == PaymentStatus.SUCCESS: + batch_status.disbursement_status = ProcessStatus.PROCESSED.value + batch_status.latest_error_code = None + else: + batch_status.disbursement_status = ProcessStatus.PENDING.value + batch_status.latest_error_code = payment_response.error_code + + batch_status.disbursement_timestamp = datetime.utcnow() + batch_status.disbursement_attempts += 1 + + except Exception as e: + batch_status.disbursement_status = ProcessStatus.PENDING.value + batch_status.disbursement_timestamp = datetime.utcnow() + batch_status.latest_error_code = str(e) + batch_status.disbursement_attempts += 1 + + session.commit() diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py new file mode 100644 index 0000000..870a39a --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py @@ -0,0 +1,189 @@ +import asyncio +import logging +from datetime import datetime + +from openg2p_g2p_bridge_models.models import ( + DisbursementBatchControl, + MapperResolutionBatchStatus, + MapperResolutionDetails, + ProcessStatus, +) +from openg2p_g2pconnect_mapper_lib.client import MapperResolveClient +from openg2p_g2pconnect_mapper_lib.schemas import ResolveRequest +from sqlalchemy import select, and_ +from sqlalchemy.orm import sessionmaker + +from ..app import get_engine, celery_app +from ..config import Settings +from ..helpers import ResolveHelper + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) +_engine = get_engine() + + +@celery_app.task(name="mapper_resolution_beat_producer") +def mapper_resolution_beat_producer(): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + mapper_resolution_batch_statuses = ( + session.execute( + select(MapperResolutionBatchStatus).filter( + and_( + MapperResolutionBatchStatus.resolution_status + == ProcessStatus.PENDING, + MapperResolutionBatchStatus.resolution_attempts + < _config.mapper_resolve_attempts, + ) + ) + ) + .scalars() + .all() + ) + + for mapper_resolution_batch_status in mapper_resolution_batch_statuses: + mapper_resolution_worker.delay( + mapper_resolution_batch_status.mapper_resolution_batch_id + ) + + +@celery_app.task(name="mapper_resolution_worker") +def mapper_resolution_worker(mapper_resolution_batch_id: str): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + disbursement_batch_controls = ( + session.execute( + select(DisbursementBatchControl).filter( + DisbursementBatchControl.mapper_resolution_batch_id + == mapper_resolution_batch_id + ) + ) + .scalars() + .all() + ) + + beneficiary_disbursement_map = { + control.beneficiary_id: control.disbursement_id + for control in disbursement_batch_controls + } + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + resolve_response, error_msg = loop.run_until_complete( + make_resolve_request(disbursement_batch_controls) + ) + finally: + loop.close() + + if not resolve_response: + session.query(MapperResolutionBatchStatus).filter( + MapperResolutionBatchStatus.mapper_resolution_batch_id + == mapper_resolution_batch_id + ).update( + { + MapperResolutionBatchStatus.resolution_status: ProcessStatus.PENDING, + MapperResolutionBatchStatus.latest_error_code: error_msg, + MapperResolutionBatchStatus.resolution_attempts: MapperResolutionBatchStatus.resolution_attempts + + 1, + } + ) + session.commit() + return + + process_and_store_resolution( + mapper_resolution_batch_id, resolve_response, beneficiary_disbursement_map + ) + + +async def make_resolve_request(disbursement_batch_controls): + + resolve_helper = ResolveHelper.get_component() + + single_resolve_requests = [ + resolve_helper.construct_single_resolve_request(control.beneficiary_id) + for control in disbursement_batch_controls + ] + resolve_request: ResolveRequest = resolve_helper.construct_resolve_request( + single_resolve_requests + ) + resolve_client = MapperResolveClient() + try: + resolve_response = await resolve_client.resolve_request( + resolve_request, _config.mapper_resolve_api_url + ) + return resolve_response, None + except Exception as e: + error_msg = f"Failed to resolve the request: {e}" + return None, error_msg + + +def process_and_store_resolution( + mapper_resolution_batch_id, resolve_response, beneficiary_disbursement_map +): + resolve_helper = ResolveHelper.get_component() + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + with session_maker() as session: + details_list = [] + for single_response in resolve_response.message.resolve_response: + disbursement_id = beneficiary_disbursement_map.get(single_response.id) + if disbursement_id and ( + single_response.fa != "" or single_response.fa is not None + ): + deconstructed_fa = ( + resolve_helper.deconstruct_fa(single_response.fa) + ) + details = MapperResolutionDetails( + mapper_resolution_batch_id=mapper_resolution_batch_id, + disbursement_id=disbursement_id, + beneficiary_id=single_response.id, + mapper_resolved_fa=single_response.fa, + mapper_resolved_name=single_response.account_provider_info.name + if single_response.account_provider_info + else None, + mapper_resolved_fa_type=deconstructed_fa.get("fa_type"), + bank_account_number=deconstructed_fa.get("account_number"), + bank_code=deconstructed_fa.get("bank_code"), + branch_code=deconstructed_fa.get("branch_code"), + mobile_number=deconstructed_fa.get("mobile_number"), + mobile_wallet_provider=deconstructed_fa.get( + "mobile_wallet_provider" + ), + email_address=deconstructed_fa.get("email_address"), + email_wallet_provider=deconstructed_fa.get("email_wallet_provider"), + active=True, + ) + details_list.append(details) + else: + _logger.error( + f"Failed to resolve the request for beneficiary: {single_response.id}" + ) + session.query(MapperResolutionBatchStatus).filter( + MapperResolutionBatchStatus.mapper_resolution_batch_id + == mapper_resolution_batch_id + ).update( + { + MapperResolutionBatchStatus.resolution_status: ProcessStatus.PENDING, + MapperResolutionBatchStatus.latest_error_code: f"Failed to resolve the request for beneficiary: {single_response.id}", + MapperResolutionBatchStatus.resolution_attempts: MapperResolutionBatchStatus.resolution_attempts + + 1, + } + ) + session.commit() + return + + session.add_all(details_list) + session.query(MapperResolutionBatchStatus).filter( + MapperResolutionBatchStatus.mapper_resolution_batch_id + == mapper_resolution_batch_id + ).update( + { + MapperResolutionBatchStatus.resolution_status: ProcessStatus.PROCESSED, + MapperResolutionBatchStatus.resolution_time_stamp: datetime.utcnow(), + MapperResolutionBatchStatus.latest_error_code: None, + MapperResolutionBatchStatus.resolution_attempts: MapperResolutionBatchStatus.resolution_attempts + + 1, + } + ) + session.commit() diff --git a/openg2p-g2p-bridge-celery-tasks/tests/__init__.py b/openg2p-g2p-bridge-celery-tasks/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-celery-tasks/tests/test_mapper_resolve_task.py b/openg2p-g2p-bridge-celery-tasks/tests/test_mapper_resolve_task.py new file mode 100644 index 0000000..3f58aab --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/tests/test_mapper_resolve_task.py @@ -0,0 +1,176 @@ +import uuid +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from openg2p_g2p_bridge_celery_tasks.tasks import mapper_resolve_task +from openg2p_g2p_bridge_models.models import DisbursementBatchControl +from openg2p_g2pconnect_common_lib.schemas import ( + RequestHeader, + StatusEnum, + SyncResponseHeader, +) +from openg2p_g2pconnect_mapper_lib.client import MapperResolveClient +from openg2p_g2pconnect_mapper_lib.schemas import ( + ResolveRequest, + ResolveRequestMessage, + ResolveResponse, + ResolveResponseMessage, + SingleResolveRequest, + SingleResolveResponse, +) + + +@pytest.fixture +def mock_session_maker(): + session_mock = MagicMock() + session_mock.configure_mock( + **{ + "execute.return_value.scalars.return_value.all.return_value": [ + DisbursementBatchControl(beneficiary_id="1", disbursement_id="101") + ], + "__enter__.return_value": session_mock, + "__exit__.return_value": None, + } + ) + return session_mock + + +@pytest.fixture +def mock_resolve_helper(): + helper_mock = MagicMock() + single_resolve_request = SingleResolveRequest( + reference_id=str(uuid.uuid4()), + timestamp=datetime.now(), + id="1", + scope="details", + ) + helper_mock.construct_single_resolve_request.return_value = single_resolve_request + helper_mock.construct_resolve_request.return_value = ResolveRequest( + signature="", + header=RequestHeader( + message_id=str(uuid.uuid4()), + message_ts=str(datetime.now()), + action="resolve", + sender_id="", + sender_uri="", + total_count=1, + ), + message=ResolveRequestMessage( + transaction_id=str(uuid.uuid4()), + resolve_request=[single_resolve_request], + ), + ) + return helper_mock + + +@pytest.fixture +def mock_resolve_client(): + client_mock = MagicMock(spec=MapperResolveClient) + single_response = SingleResolveResponse( + reference_id="ref123", + timestamp=datetime.now(), + fa="FA123", + id="1", + account_provider_info=None, + status=StatusEnum.succ, # Assuming you have an Enum for status + status_reason_message="No issues.", + ) + resolve_response_message = ResolveResponseMessage( + transaction_id="trans123", + correlation_id="corr123", + resolve_response=[single_response], + ) + resolve_response = ResolveResponse( + header=SyncResponseHeader( + version="1.0.0", + message_id="", + message_ts="", + action="resolve", + status=StatusEnum.succ, + ), + message=resolve_response_message, + ) + client_mock.resolve_request.return_value = resolve_response + return client_mock + + +@patch("openg2p_g2p_bridge_celery_tasks.app.get_engine") +@patch("openg2p_g2p_bridge_celery_tasks.helpers.ResolveHelper.get_component") +@patch("openg2p_g2pconnect_mapper_lib.client.MapperResolveClient") +@patch("sqlalchemy.orm.sessionmaker") +def test_mapper_resolve_task_success( + mock_session_maker_func, + mock_resolve_client_cls, + mock_resolve_helper_func, + mock_engine, + mock_session_maker, + mock_resolve_helper, + mock_resolve_client, +): + print("Starting test...") + mock_session_maker_func.return_value = mock_session_maker + mock_resolve_helper_func.return_value = mock_resolve_helper + mock_resolve_client_cls.return_value = mock_resolve_client + + mock_resolve_client.resolve_request.return_value = MagicMock( + message=MagicMock( + resolve_response=[ + MagicMock( + id="1", + fa="Test FA", + account_provider_info=MagicMock(name="Test Provider"), + ) + ] + ) + ) + + valid_uuid = str(uuid.uuid4()) + print("UUID for testing:", valid_uuid) + + try: + mapper_resolve_task(valid_uuid) + except Exception as e: + print("Error during task execution:", str(e)) + + session_mock = mock_session_maker.__enter__.return_value + print("Session mock add called:", session_mock.add.called) + print("Session mock commit called:", session_mock.commit.called) + print("API Call details:", mock_resolve_client.resolve_request.call_args_list) + + assert ( + mock_resolve_client.resolve_request.called + ), "Resolve request was not called as expected." + assert ( + session_mock.add.called + ), "The session.add method was not called, which suggests the task exited early or failed." + assert session_mock.commit.called, "The session.commit method was not called." + + +@patch("openg2p_g2p_bridge_celery_tasks.app.get_engine") +@patch("openg2p_g2p_bridge_celery_tasks.helpers.ResolveHelper.get_component") +@patch("openg2p_g2pconnect_mapper_lib.client.MapperResolveClient") +@patch("sqlalchemy.orm.sessionmaker") +def test_mapper_resolve_task_failure( + mock_session_maker_func, + mock_resolve_client_cls, + mock_resolve_helper_func, + mock_engine, + mock_session_maker, + mock_resolve_helper, + mock_resolve_client, +): + mock_session_maker_func.return_value = mock_session_maker + mock_resolve_helper_func.return_value = mock_resolve_helper + mock_resolve_client_cls.return_value = mock_resolve_client + + mock_resolve_client.resolve_request.side_effect = Exception("API failure") + valid_uuid = str(uuid.uuid4()) # Generate a valid UUID + mapper_resolve_task(valid_uuid) + + session_mock = mock_session_maker.__enter__.return_value + assert session_mock.add.called + assert session_mock.commit.called + assert ( + mock_resolve_client.resolve_request.called + ), "Resolve request was not called as expected" From 8933f8cc52b0fc916d5c5ac4f132c1d47bd6fd78 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 19 Jul 2024 17:05:26 +0530 Subject: [PATCH 15/39] Openg2p Bridge Bank Connectors --- .../.gitignore | 0 .../.pre-commit-config.yaml | 49 +++ openg2p-g2p-bridge-bank-connectors/.ruff.toml | 16 + .../CODE-OF-CONDUCT.md | 114 ++++++ .../CONTRIBUTING.md | 2 + openg2p-g2p-bridge-bank-connectors/LICENSE | 373 ++++++++++++++++++ openg2p-g2p-bridge-bank-connectors/README.md | 14 + .../__init__.py | 0 .../pyproject.toml | 31 ++ .../__init__.py | 1 + .../openg2p_g2p_bridge_bank_connectors/app.py | 16 + .../bank_connectors/__init__.py | 2 + .../bank_connectors/bank_connector_factory.py | 12 + .../bank_connectors/example_bank_connector.py | 40 ++ .../bank_interface/__init__.py | 7 + .../bank_connector_interface.py | 71 ++++ .../config.py | 15 + 17 files changed, 763 insertions(+) rename .gitignore => openg2p-g2p-bridge-bank-connectors/.gitignore (100%) create mode 100644 openg2p-g2p-bridge-bank-connectors/.pre-commit-config.yaml create mode 100644 openg2p-g2p-bridge-bank-connectors/.ruff.toml create mode 100644 openg2p-g2p-bridge-bank-connectors/CODE-OF-CONDUCT.md create mode 100644 openg2p-g2p-bridge-bank-connectors/CONTRIBUTING.md create mode 100644 openg2p-g2p-bridge-bank-connectors/LICENSE create mode 100644 openg2p-g2p-bridge-bank-connectors/README.md create mode 100644 openg2p-g2p-bridge-bank-connectors/__init__.py create mode 100644 openg2p-g2p-bridge-bank-connectors/pyproject.toml create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/__init__.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/__init__.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/bank_connector_factory.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py create mode 100644 openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py diff --git a/.gitignore b/openg2p-g2p-bridge-bank-connectors/.gitignore similarity index 100% rename from .gitignore rename to openg2p-g2p-bridge-bank-connectors/.gitignore diff --git a/openg2p-g2p-bridge-bank-connectors/.pre-commit-config.yaml b/openg2p-g2p-bridge-bank-connectors/.pre-commit-config.yaml new file mode 100644 index 0000000..336b7e1 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: | + (?x) + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.11.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.289 + hooks: + - id: ruff + args: + - --fix diff --git a/openg2p-g2p-bridge-bank-connectors/.ruff.toml b/openg2p-g2p-bridge-bank-connectors/.ruff.toml new file mode 100644 index 0000000..aa1fc5b --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/.ruff.toml @@ -0,0 +1,16 @@ +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[per-file-ignores] +"__init__.py" = ["F401"] diff --git a/openg2p-g2p-bridge-bank-connectors/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-bank-connectors/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..e1949e1 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/CODE-OF-CONDUCT.md @@ -0,0 +1,114 @@ +# Code of Conduct + +## Contributor Covenant Code of Conduct + +### Preamble + +OpenG2P was created to foster an open, innovative and inclusive community around open source & open standard. +To clarify expected behaviour in our communities we have adopted the Contributor Covenant. This code of +conduct has been adopted by many other open source communities and we feel it expresses our values well. + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free +experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy +community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit + permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will +take appropriate and fair corrective action in response to any behavior that they deem inappropriate, +threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki +edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate +reasons for moderation decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially +representing the community in public spaces. Examples of representing our community include using an official +e-mail address, posting via an official social media account, or acting as an appointed representative at an +online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders +responsible for enforcement at \[INSERT CONTACT METHOD]. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action +they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in +the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the +violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, +including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. +This includes avoiding interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a +specified period of time. No public or private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may +lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained +inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +### Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the +[FAQ](https://www.contributor-covenant.org/faq). Translations are available +[here](https://www.contributor-covenant.org/translations). diff --git a/openg2p-g2p-bridge-bank-connectors/CONTRIBUTING.md b/openg2p-g2p-bridge-bank-connectors/CONTRIBUTING.md new file mode 100644 index 0000000..c187f93 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/CONTRIBUTING.md @@ -0,0 +1,2 @@ +Refer to contribution guidelines +[here](https://github.com/OpenG2P/openg2p-documentation/blob/1.0.0/community/contributing-to-openg2p.md). diff --git a/openg2p-g2p-bridge-bank-connectors/LICENSE b/openg2p-g2p-bridge-bank-connectors/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/openg2p-g2p-bridge-bank-connectors/README.md b/openg2p-g2p-bridge-bank-connectors/README.md new file mode 100644 index 0000000..3393014 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/README.md @@ -0,0 +1,14 @@ +# openg2p-g2p-bridge-bank-connectors + +[![Pre-commit Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml?query=branch%3Adevelop) +[![Build Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml?query=branch%3Adevelop) +[![codecov](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api/branch/develop/graph/badge.svg)](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api) +[![openapi](https://img.shields.io/badge/open--API-swagger-brightgreen)](https://validator.swagger.io/?url=https://raw.githubusercontent.com/OpenG2P/openg2p-g2p-bridge-api/develop/api-docs/generated/openapi.json) +![PyPI](https://img.shields.io/pypi/v/openg2p-g2p-bridge-api?label=pypi%20package) +![PyPI - Downloads](https://img.shields.io/pypi/dm/openg2p-g2p-bridge-api) + + + +## Licenses + +This repository is licensed under [MPL-2.0](LICENSE). diff --git a/openg2p-g2p-bridge-bank-connectors/__init__.py b/openg2p-g2p-bridge-bank-connectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-bank-connectors/pyproject.toml b/openg2p-g2p-bridge-bank-connectors/pyproject.toml new file mode 100644 index 0000000..15441f9 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "openg2p-g2p-bridge-bank-connectors" +authors = [ + { name="OpenG2P", email="info@openg2p.org" }, +] +description = "OpenG2P G2P Bridge Bank Connectors" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: OS Independent", +] +dependencies = [ + "openg2p-fastapi-common", + "openg2p-fastapi-auth", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://openg2p.org" +Documentation = "https://docs.openg2p.org/" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-bank-connectors" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-bank-connectors" + +[tool.hatch.version] +path = "src/openg2p_g2p_bridge_bank_connectors/__init__.py" diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/__init__.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/__init__.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py new file mode 100644 index 0000000..7483a5e --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py @@ -0,0 +1,16 @@ +# ruff: noqa: E402 + +from .config import Settings + +_config = Settings.get_config() + +from openg2p_fastapi_common.app import Initializer as BaseInitializer + +from .bank_connectors import BankConnectorFactory, ExampleBankConnector + + +class Initializer(BaseInitializer): + def initialize(self, **kwargs): + + BankConnectorFactory() + ExampleBankConnector() diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/__init__.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/__init__.py new file mode 100644 index 0000000..8c3f7aa --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/__init__.py @@ -0,0 +1,2 @@ +from .bank_connector_factory import BankConnectorFactory +from .example_bank_connector import ExampleBankConnector diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/bank_connector_factory.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/bank_connector_factory.py new file mode 100644 index 0000000..f90e3ad --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/bank_connector_factory.py @@ -0,0 +1,12 @@ +from openg2p_fastapi_common.service import BaseService + +from ..bank_interface.bank_connector_interface import BankConnectorInterface +from .example_bank_connector import ExampleBankConnector + + +class BankConnectorFactory(BaseService): + def get_bank_connector(self, sponsor_bank_code: str) -> BankConnectorInterface: + if sponsor_bank_code == "EXAMPLE": + return ExampleBankConnector() + else: + raise NotImplementedError(f"Bank {sponsor_bank_code} is not supported") diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py new file mode 100644 index 0000000..5fda628 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py @@ -0,0 +1,40 @@ +from typing import List + +from openg2p_g2p_bridge_models.models import ( + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) + +from ..bank_interface.bank_connector_interface import ( + BankConnectorInterface, + BlockFundsResponse, + CheckFundsResponse, + PaymentPayload, + PaymentResponse, + PaymentStatus, +) + + +class ExampleBankConnector(BankConnectorInterface): + def check_funds(self, account_no, currency, amount) -> CheckFundsResponse: + print("EXAMPLE BANK CONNECTOR: Checking funds") + return CheckFundsResponse( + status=FundsAvailableWithBankEnum.FUNDS_AVAILABLE, error_code="" + ) + + def block_funds(self, account_no, currency, amount) -> BlockFundsResponse: + print("EXAMPLE BANK CONNECTOR: Blocking funds") + return BlockFundsResponse( + status=FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS, + block_reference_no="REF123", + error_code="", + ) + + def initiate_payment( + self, payment_payloads: List[PaymentPayload] + ) -> PaymentResponse: + print("EXAMPLE BANK CONNECTOR: Initiating payment") + print("PAYMENT PAYLOADS:", payment_payloads) + return PaymentResponse( + status=PaymentStatus.SUCCESS, error_code="", ack_reference_no="ACK123" + ) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py new file mode 100644 index 0000000..8210d48 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py @@ -0,0 +1,7 @@ +from .bank_connector_interface import ( + BankConnectorInterface, + BlockFundsResponse, + CheckFundsResponse, + PaymentPayload, + PaymentStatus, +) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py new file mode 100644 index 0000000..759a967 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py @@ -0,0 +1,71 @@ +import enum +from datetime import datetime +from typing import List, Optional + +from openg2p_fastapi_common.service import BaseService +from openg2p_g2p_bridge_models.models import ( + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) +from pydantic import BaseModel + + +class CheckFundsResponse(BaseModel): + status: FundsAvailableWithBankEnum + error_code: str + + +class BlockFundsResponse(BaseModel): + status: FundsBlockedWithBankEnum + block_reference_no: str + error_code: str + + +class PaymentPayload(BaseModel): + remitting_account: str + remitting_account_currency: str + payment_amount: float + funds_blocked_reference_number: str + + beneficiary_id: str + beneficiary_name: Optional[str] = None + + beneficiary_account: Optional[str] = None + beneficiary_account_currency: Optional[str] = None + beneficiary_account_type: Optional[str] = None + beneficiary_bank_code: Optional[str] = None + beneficiary_branch_code: Optional[str] = None + + beneficiary_mobile_wallet_provider: Optional[str] = None + beneficiary_phone_no: Optional[str] = None + + beneficiary_email: Optional[str] = None + beneficiary_email_wallet_provider: Optional[str] = None + + benefit_program_mnemonic: Optional[str] = None + cycle_code_mnemonic: Optional[str] = None + payment_date: datetime + + +class PaymentStatus(enum.Enum): + SUCCESS = "SUCCESS" + ERROR = "ERROR" + + +class PaymentResponse(BaseModel): + status: PaymentStatus + error_code: str + ack_reference_no: str + + +class BankConnectorInterface(BaseService): + def check_funds(self, account_no, currency, amount) -> CheckFundsResponse: + raise NotImplementedError() + + def block_funds(self, account_no, currency, amount) -> BlockFundsResponse: + raise NotImplementedError() + + def initiate_payment( + self, payment_payloads: List[PaymentPayload] + ) -> PaymentResponse: + raise NotImplementedError() diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py new file mode 100644 index 0000000..32bf4c1 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py @@ -0,0 +1,15 @@ +from openg2p_fastapi_common.config import Settings as BaseSettings +from pydantic_settings import SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="g2p_bridge_celery_producers_", env_file=".env", extra="allow" + ) + + db_dbname: str = "openg2p_g2p_bridge_db" + + funds_available_check_url_example_bank: int = 3 + funds_blocked_check_url_example_bank: int = 3 + funds_disbursement_url_example_bank: int = 3 + From ab4f57d03ec6a96c18a82df106f53b652f3594e1 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Mon, 22 Jul 2024 11:26:16 +0530 Subject: [PATCH 16/39] Openg2p Bridge API --- .../__init__.py | 0 .../app.py | 0 .../celery_app.py | 0 .../config.py | 0 .../controllers/__init__.py | 0 .../controllers/disbursement.py | 0 .../controllers/disbursement_envelope.py | 0 .../services/__init__.py | 0 .../services/disbursement.py | 0 .../services/disbursement_envelope.py | 0 .../utils/__init__.py | 0 .../utils/model_serializer.py | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/__init__.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/app.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/celery_app.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/config.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/controllers/__init__.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/controllers/disbursement.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/controllers/disbursement_envelope.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/services/__init__.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/services/disbursement.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/services/disbursement_envelope.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/utils/__init__.py (100%) rename openg2p-g2p-bridge-api/src/{openg2p_g2p_bridge_example_bank_api => openg2p_g2p_bridge_api}/utils/model_serializer.py (100%) diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/__init__.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/__init__.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/app.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/app.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/celery_app.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/celery_app.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/config.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/config.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/config.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/config.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/__init__.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/__init__.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/controllers/disbursement_envelope.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/__init__.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/__init__.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/services/disbursement_envelope.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/__init__.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/__init__.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/model_serializer.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/model_serializer.py similarity index 100% rename from openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_example_bank_api/utils/model_serializer.py rename to openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/model_serializer.py From e2dda4246d2e2b3de26a72c2d9fe5c29d0cec7fd Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Mon, 22 Jul 2024 15:59:14 +0530 Subject: [PATCH 17/39] Example Bank APIs --- .../.copier-answers.yml | 17 + .../.dockerignore | 89 +++++ .../.editorconfig | 20 + openg2p-g2p-bridge-example-bank-api/.env | 5 + .../.gitignore | 81 ++++ .../.pre-commit-config.yaml | 49 +++ .../.ruff.toml | 16 + .../CODE-OF-CONDUCT.md | 114 ++++++ .../CONTRIBUTING.md | 2 + openg2p-g2p-bridge-example-bank-api/LICENSE | 373 ++++++++++++++++++ openg2p-g2p-bridge-example-bank-api/README.md | 14 + .../__init__.py | 0 openg2p-g2p-bridge-example-bank-api/main.py | 14 + .../pyproject.toml | 31 ++ .../__init__.py | 1 + .../app.py | 39 ++ .../celery_app.py | 22 ++ .../config.py | 20 + .../controllers/__init__.py | 3 + .../controllers/block_funds.py | 61 +++ .../controllers/check_available_funds.py | 47 +++ .../controllers/initiate_payment.py | 90 +++++ .../models/__init__.py | 2 + .../models/account.py | 50 +++ .../models/benefit_program.py | 10 + .../schemas/__init__.py | 8 + .../schemas/fund_schemas.py | 46 +++ .../utils/__init__.py | 0 .../test-requirements.txt | 3 + .../tests/__init__.py | 0 .../tests/test_disbursement.py | 175 ++++++++ .../tests/test_disbursement_envelope.py | 197 +++++++++ 32 files changed, 1599 insertions(+) create mode 100644 openg2p-g2p-bridge-example-bank-api/.copier-answers.yml create mode 100644 openg2p-g2p-bridge-example-bank-api/.dockerignore create mode 100644 openg2p-g2p-bridge-example-bank-api/.editorconfig create mode 100644 openg2p-g2p-bridge-example-bank-api/.env create mode 100644 openg2p-g2p-bridge-example-bank-api/.gitignore create mode 100644 openg2p-g2p-bridge-example-bank-api/.pre-commit-config.yaml create mode 100644 openg2p-g2p-bridge-example-bank-api/.ruff.toml create mode 100644 openg2p-g2p-bridge-example-bank-api/CODE-OF-CONDUCT.md create mode 100644 openg2p-g2p-bridge-example-bank-api/CONTRIBUTING.md create mode 100644 openg2p-g2p-bridge-example-bank-api/LICENSE create mode 100644 openg2p-g2p-bridge-example-bank-api/README.md create mode 100644 openg2p-g2p-bridge-example-bank-api/__init__.py create mode 100755 openg2p-g2p-bridge-example-bank-api/main.py create mode 100644 openg2p-g2p-bridge-example-bank-api/pyproject.toml create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py create mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py create mode 100644 openg2p-g2p-bridge-example-bank-api/test-requirements.txt create mode 100644 openg2p-g2p-bridge-example-bank-api/tests/__init__.py create mode 100644 openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py create mode 100644 openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py diff --git a/openg2p-g2p-bridge-example-bank-api/.copier-answers.yml b/openg2p-g2p-bridge-example-bank-api/.copier-answers.yml new file mode 100644 index 0000000..60cfca7 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.copier-answers.yml @@ -0,0 +1,17 @@ +# Do NOT update manually; changes here will be overwritten by Copier +_commit: af08ec1 +_src_path: https://github.com/openg2p/openg2p-fastapi-template +github_ci_docker_build: true +github_ci_openapi_publish: true +github_ci_precommit: true +github_ci_pypi_publish: true +github_ci_tests: true +github_ci_tests_codecov: true +module_name: openg2p_g2p_bridge_api +org_name: OpenG2P +org_slug: OpenG2P +package_name: openg2p-g2p-bridge-api +repo_name: ' openg2p-g2p-bridge-api + + ' +repo_slug: openg2p-g2p-bridge-api diff --git a/openg2p-g2p-bridge-example-bank-api/.dockerignore b/openg2p-g2p-bridge-example-bank-api/.dockerignore new file mode 100644 index 0000000..d47f7ee --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.dockerignore @@ -0,0 +1,89 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +../.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ diff --git a/openg2p-g2p-bridge-example-bank-api/.editorconfig b/openg2p-g2p-bridge-example-bank-api/.editorconfig new file mode 100644 index 0000000..7d8f3a5 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml,toml,jinja}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml,rst,md,jinja}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/openg2p-g2p-bridge-example-bank-api/.env b/openg2p-g2p-bridge-example-bank-api/.env new file mode 100644 index 0000000..be5b03f --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.env @@ -0,0 +1,5 @@ +G2P_BRIDGE_DB_DBNAME=bridge_db +G2P_BRIDGE_WORKER_TYPE=gunicorn +G2P_BRIDGE_HOST=0.0.0.0 +G2P_BRIDGE_PORT=8000 +G2P_BRIDGE_NO_OF_WORKERS=1 diff --git a/openg2p-g2p-bridge-example-bank-api/.gitignore b/openg2p-g2p-bridge-example-bank-api/.gitignore new file mode 100644 index 0000000..c633cec --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.gitignore @@ -0,0 +1,81 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +/venv +/.pytest_cache + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store + +# Django stuff: +*.log + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Sphinx documentation +docs/_build/ + +# Backup files +*~ +*.swp + +# OCA rules +!static/lib/ + +# Ruff stuff +.ruff_cache + +# Ignore secret files and env +.secrets.* +../.env diff --git a/openg2p-g2p-bridge-example-bank-api/.pre-commit-config.yaml b/openg2p-g2p-bridge-example-bank-api/.pre-commit-config.yaml new file mode 100644 index 0000000..336b7e1 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: | + (?x) + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.11.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.289 + hooks: + - id: ruff + args: + - --fix diff --git a/openg2p-g2p-bridge-example-bank-api/.ruff.toml b/openg2p-g2p-bridge-example-bank-api/.ruff.toml new file mode 100644 index 0000000..aa1fc5b --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/.ruff.toml @@ -0,0 +1,16 @@ +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[per-file-ignores] +"__init__.py" = ["F401"] diff --git a/openg2p-g2p-bridge-example-bank-api/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-example-bank-api/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..e1949e1 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/CODE-OF-CONDUCT.md @@ -0,0 +1,114 @@ +# Code of Conduct + +## Contributor Covenant Code of Conduct + +### Preamble + +OpenG2P was created to foster an open, innovative and inclusive community around open source & open standard. +To clarify expected behaviour in our communities we have adopted the Contributor Covenant. This code of +conduct has been adopted by many other open source communities and we feel it expresses our values well. + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free +experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy +community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit + permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will +take appropriate and fair corrective action in response to any behavior that they deem inappropriate, +threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki +edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate +reasons for moderation decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially +representing the community in public spaces. Examples of representing our community include using an official +e-mail address, posting via an official social media account, or acting as an appointed representative at an +online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders +responsible for enforcement at \[INSERT CONTACT METHOD]. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action +they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in +the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the +violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, +including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. +This includes avoiding interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a +specified period of time. No public or private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may +lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained +inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +### Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the +[FAQ](https://www.contributor-covenant.org/faq). Translations are available +[here](https://www.contributor-covenant.org/translations). diff --git a/openg2p-g2p-bridge-example-bank-api/CONTRIBUTING.md b/openg2p-g2p-bridge-example-bank-api/CONTRIBUTING.md new file mode 100644 index 0000000..c187f93 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/CONTRIBUTING.md @@ -0,0 +1,2 @@ +Refer to contribution guidelines +[here](https://github.com/OpenG2P/openg2p-documentation/blob/1.0.0/community/contributing-to-openg2p.md). diff --git a/openg2p-g2p-bridge-example-bank-api/LICENSE b/openg2p-g2p-bridge-example-bank-api/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/openg2p-g2p-bridge-example-bank-api/README.md b/openg2p-g2p-bridge-example-bank-api/README.md new file mode 100644 index 0000000..15fc796 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/README.md @@ -0,0 +1,14 @@ +# openg2p-g2p-bridge-api + +[![Pre-commit Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml?query=branch%3Adevelop) +[![Build Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml?query=branch%3Adevelop) +[![codecov](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api/branch/develop/graph/badge.svg)](https://codecov.io/gh/OpenG2P/openg2p-g2p-bridge-api) +[![openapi](https://img.shields.io/badge/open--API-swagger-brightgreen)](https://validator.swagger.io/?url=https://raw.githubusercontent.com/OpenG2P/openg2p-g2p-bridge-api/develop/api-docs/generated/openapi.json) +![PyPI](https://img.shields.io/pypi/v/openg2p-g2p-bridge-api?label=pypi%20package) +![PyPI - Downloads](https://img.shields.io/pypi/dm/openg2p-g2p-bridge-api) + + + +## Licenses + +This repository is licensed under [MPL-2.0](LICENSE). diff --git a/openg2p-g2p-bridge-example-bank-api/__init__.py b/openg2p-g2p-bridge-example-bank-api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-example-bank-api/main.py b/openg2p-g2p-bridge-example-bank-api/main.py new file mode 100755 index 0000000..f97a162 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/main.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +# ruff: noqa: I001 + +from openg2p_g2p_bridge_example_bank_api.app import Initializer +from openg2p_fastapi_common.ping import PingInitializer + +initializer = Initializer() +PingInitializer() + +app = initializer.return_app() + +if __name__ == "__main__": + initializer.main() diff --git a/openg2p-g2p-bridge-example-bank-api/pyproject.toml b/openg2p-g2p-bridge-example-bank-api/pyproject.toml new file mode 100644 index 0000000..afe215a --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "openg2p-g2p-bridge-example-bank-api" +authors = [ + { name="OpenG2P", email="info@openg2p.org" }, +] +description = "OpenG2P G2P Bridge API" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: OS Independent", +] +dependencies = [ + "openg2p-fastapi-common", + "openg2p-fastapi-auth", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://openg2p.org" +Documentation = "https://docs.openg2p.org/" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-example-bank-api" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-example-bank-api" + +[tool.hatch.version] +path = "src/openg2p_g2p_bridge_example_bank_api/__init__.py" diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py new file mode 100644 index 0000000..8459562 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py @@ -0,0 +1,39 @@ +# ruff: noqa: E402 +import asyncio +import logging + +from .config import Settings + +_config = Settings.get_config() + +from openg2p_fastapi_common.app import Initializer as BaseInitializer + +from .controllers import ( + BlockFundsController, + FundAvailabilityController, + PaymentController, +) +from .models import Account, BenefitProgram, FundBlock, InitiatePaymentRequest + +_logger = logging.getLogger(_config.logging_default_logger_name) + + +class Initializer(BaseInitializer): + def initialize(self, **kwargs): + super().initialize() + + BlockFundsController().post_init() + FundAvailabilityController().post_init() + PaymentController().post_init() + + def migrate_database(self, args): + super().migrate_database(args) + + async def migrate(): + _logger.info("Migrating database") + await BenefitProgram.create_migrate() + await Account.create_migrate() + await FundBlock.create_migrate() + await InitiatePaymentRequest.create_migrate() + + asyncio.run(migrate()) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py new file mode 100644 index 0000000..a3789f5 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py @@ -0,0 +1,22 @@ +from celery import Celery + +celery_app = Celery( + "example_bank_celery_tasks", + broker="redis://localhost:6379/0", + backend="redis://localhost:6379/0", +) + +celery_app.conf.beat_schedule = { + "initiate_fund_check_beat_producer": { + "task": "initiate_fund_check_beat_producer", + "schedule": 10, + } +} + +celery_app.conf.timezone = "UTC" + + +@celery_app.task +def initiate_fund_check_beat_producer(): + print("Initiating fund check beat producer") + return True diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py new file mode 100644 index 0000000..71148dc --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py @@ -0,0 +1,20 @@ +from openg2p_fastapi_common.config import Settings as BaseSettings +from pydantic_settings import SettingsConfigDict + +from . import __version__ + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="g2p_bridge_", env_file=".env", extra="allow" + ) + + openapi_title: str = "Example Bank APIs for Cash Transfer" + openapi_description: str = """ + *********************************** + Further details goes here + *********************************** + """ + openapi_version: str = __version__ + + db_dbname: str = "openg2p_g2p_bridge_db" diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py new file mode 100644 index 0000000..41dd4d4 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py @@ -0,0 +1,3 @@ +from .block_funds import BlockFundsController +from .check_available_funds import FundAvailabilityController +from .initiate_payment import PaymentController diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py new file mode 100644 index 0000000..515062a --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py @@ -0,0 +1,61 @@ +import uuid + +from fastapi import HTTPException +from openg2p_fastapi_common.context import dbengine +from openg2p_fastapi_common.controller import BaseController +from sqlalchemy import update +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select + +from ..models import Account, FundBlock +from ..schemas import BlockFundsRequest, BlockFundsResponse + + +class BlockFundsController(BaseController): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.router.tags += ["Funds Management"] + + self.router.add_api_route( + "/block_funds", + self.block_funds, + response_model=BlockFundsResponse, + methods=["POST"], + ) + + async def block_funds(self, request: BlockFundsRequest) -> BlockFundsResponse: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + # Check if the account exists and has sufficient funds + stmt = select(Account).where( + (Account.account_number == request.account_no) + & (Account.account_currency == request.currency) + ) + result = await session.execute(stmt) + account_balance = result.scalars().first() + + if not account_balance: + raise HTTPException(status_code=404, detail="Account not found") + if account_balance.book_balance < request.amount: + raise HTTPException(status_code=400, detail="Insufficient funds") + + new_balance = account_balance.book_balance - request.amount + await session.execute( + update(Account) + .where(Account.account_number == request.account_no) + .values(book_balance=new_balance) + ) + + block_reference_no = str(uuid.uuid4()) + fund_block = FundBlock( + block_reference_no=block_reference_no, + account_no=request.account_no, + amount=request.amount, + currency=request.currency, + active=True, + ) + session.add(fund_block) + + await session.commit() + return BlockFundsResponse(block_reference_no=block_reference_no) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py new file mode 100644 index 0000000..dbd63f5 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py @@ -0,0 +1,47 @@ +from fastapi import HTTPException +from openg2p_fastapi_common.context import dbengine +from openg2p_fastapi_common.controller import BaseController +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select + +from ..models import Account +from ..schemas import CheckFundRequest, CheckFundResponse + + +class FundAvailabilityController(BaseController): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.router.tags += ["Fund Availability"] + + self.router.add_api_route( + "/check_funds", + self.check_available_funds, + response_model=CheckFundResponse, + methods=["POST"], + ) + + async def check_available_funds( + self, request: CheckFundRequest + ) -> CheckFundResponse: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + stmt = select(Account).where( + Account.account_number == request.account_number + ) + result = await session.execute(stmt) + account_balance = result.scalars().first() + + if not account_balance: + raise HTTPException(status_code=404, detail="Account not found") + + if account_balance.book_balance >= request.total_funds_needed: + return CheckFundResponse( + account_number=account_balance.account_number, + has_sufficient_funds=True, + ) + else: + return CheckFundResponse( + account_number=account_balance.account_number, + has_sufficient_funds=False, + ) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py new file mode 100644 index 0000000..f062bc2 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py @@ -0,0 +1,90 @@ +from openg2p_fastapi_common.context import dbengine +from openg2p_fastapi_common.controller import BaseController +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.future import select + +from ..models import BenefitProgram, FundBlock, InitiatePaymentRequest +from ..schemas import InitiatePaymentPayload, InitiatorPaymentResponse + + +class PaymentController(BaseController): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.router.tags += ["Payments Management"] + + self.router.add_api_route( + "/initiate_payment", + self.initiate_payment, + response_model=InitiatorPaymentResponse, + methods=["POST"], + ) + + async def initiate_payment( + self, initiate_payment_payload: InitiatePaymentPayload + ) -> InitiatorPaymentResponse: + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + fund_block_stmt = select(FundBlock).where( + FundBlock.block_reference_no + == initiate_payment_payload.funds_blocked_reference_number + ) + fund_block_result = await session.execute(fund_block_stmt) + fund_block = fund_block_result.scalars().first() + + if ( + not fund_block + or fund_block.amount < initiate_payment_payload.payment_amount + or fund_block.currency + != initiate_payment_payload.remitting_account_currency + ): + return InitiatorPaymentResponse( + status="failed", + error_message="Invalid funds block reference or mismatch in details", + ) + + if initiate_payment_payload.benefit_program_mnemonic: + program_stmt = select(BenefitProgram).where( + ( + BenefitProgram.program_mnemonic + == initiate_payment_payload.benefit_program_mnemonic + ) + & ( + BenefitProgram.funding_account_number + == initiate_payment_payload.remitting_account + ) + & ( + BenefitProgram.funding_account_currency + == initiate_payment_payload.remitting_account_currency + ) + ) + program_result = await session.execute(program_stmt) + benefit_program = program_result.scalars().first() + if not benefit_program: + return InitiatorPaymentResponse( + status="failed", + error_message="Invalid benefit program mnemonic", + ) + + payment = InitiatePaymentRequest( + remitting_account=initiate_payment_payload.remitting_account, + remitting_account_currency=initiate_payment_payload.remitting_account_currency, + payment_amount=initiate_payment_payload.payment_amount, + funds_blocked_reference_number=initiate_payment_payload.funds_blocked_reference_number, + beneficiary_id=initiate_payment_payload.beneficiary_id, + beneficiary_name=initiate_payment_payload.beneficiary_name, + beneficiary_account=initiate_payment_payload.beneficiary_account, + beneficiary_account_currency=initiate_payment_payload.beneficiary_account_currency, + beneficiary_account_type=initiate_payment_payload.beneficiary_account_type, + beneficiary_bank_code=initiate_payment_payload.beneficiary_bank_code, + beneficiary_branch_code=initiate_payment_payload.beneficiary_branch_code, + benefit_program_mnemonic=initiate_payment_payload.benefit_program_mnemonic, + cycle_code_mnemonic=initiate_payment_payload.cycle_code_mnemonic, + payment_date=initiate_payment_payload.payment_date, + active=True, + ) + session.add(payment) + + await session.commit() + + return InitiatorPaymentResponse(status="success", error_message="") diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py new file mode 100644 index 0000000..d37ca74 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py @@ -0,0 +1,2 @@ +from .account import Account, FundBlock, InitiatePaymentRequest +from .benefit_program import BenefitProgram diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py new file mode 100644 index 0000000..94bcbd7 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py @@ -0,0 +1,50 @@ +from datetime import datetime +from enum import Enum + +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import DateTime, Float, Integer, String +from sqlalchemy import Enum as SqlEnum +from sqlalchemy.orm import Mapped, mapped_column + + +class PaymentStatus(Enum): + PENDING = "PENDING" + SUCCESS = "SUCCESS" + FAILED = "FAILED" + + +class Account(BaseORMModelWithTimes): + __tablename__ = "accounts" + account_number: Mapped[str] = mapped_column(String) + account_currency: Mapped[str] = mapped_column(String) + book_balance: Mapped[float] = mapped_column(Float) + + +class FundBlock(BaseORMModelWithTimes): + __tablename__ = "fund_blocks" + block_reference_no: Mapped[str] = mapped_column(String, index=True, unique=True) + account_no: Mapped[str] = mapped_column(String) + currency: Mapped[str] = mapped_column(String) + amount: Mapped[float] = mapped_column(Float) + + +class InitiatePaymentRequest(BaseORMModelWithTimes): + __tablename__ = "initiate_payment_requests" + remitting_account: Mapped[str] = mapped_column(String, nullable=False) + remitting_account_currency: Mapped[str] = mapped_column(String, nullable=False) + payment_amount: Mapped[float] = mapped_column(Float, nullable=False) + funds_blocked_reference_number: Mapped[str] = mapped_column(String, nullable=False) + beneficiary_id: Mapped[str] = mapped_column(String, nullable=False) + beneficiary_name: Mapped[str] = mapped_column(String) + beneficiary_account: Mapped[str] = mapped_column(String) + beneficiary_account_currency: Mapped[str] = mapped_column(String) + beneficiary_account_type: Mapped[str] = mapped_column(String) + beneficiary_bank_code: Mapped[str] = mapped_column(String) + beneficiary_branch_code: Mapped[str] = mapped_column(String) + benefit_program_mnemonic: Mapped[str] = mapped_column(String) + cycle_code_mnemonic: Mapped[str] = mapped_column(String) + payment_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False) + payment_initiate_attempts: Mapped[int] = mapped_column(Integer, default=0) + payment_status: Mapped[PaymentStatus] = mapped_column( + SqlEnum(PaymentStatus), default=PaymentStatus.PENDING + ) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py new file mode 100644 index 0000000..3f34eeb --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py @@ -0,0 +1,10 @@ +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + + +class BenefitProgram(BaseORMModelWithTimes): + __tablename__ = "benefit_programs" + program_mnemonic: Mapped[str] = mapped_column(String, primary_key=True) + funding_account_number: Mapped[str] = mapped_column(String, index=True) + funding_account_currency: Mapped[str] = mapped_column(String) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py new file mode 100644 index 0000000..3b47a75 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py @@ -0,0 +1,8 @@ +from .fund_schemas import ( + BlockFundsRequest, + BlockFundsResponse, + CheckFundRequest, + CheckFundResponse, + InitiatePaymentPayload, + InitiatorPaymentResponse, +) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py new file mode 100644 index 0000000..3558f81 --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py @@ -0,0 +1,46 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class CheckFundRequest(BaseModel): + account_number: str + account_currency: str + total_funds_needed: float + + +class CheckFundResponse(BaseModel): + account_number: str + has_sufficient_funds: bool + + +class BlockFundsRequest(BaseModel): + account_no: str + currency: str + amount: float + + +class BlockFundsResponse(BaseModel): + block_reference_no: str + + +class InitiatePaymentPayload(BaseModel): + remitting_account: str + remitting_account_currency: str + payment_amount: float + funds_blocked_reference_number: str + beneficiary_id: str + beneficiary_name: str + beneficiary_account: str + beneficiary_account_currency: str + beneficiary_account_type: str + beneficiary_bank_code: str + beneficiary_branch_code: str + benefit_program_mnemonic: str + cycle_code_mnemonic: str + payment_date: datetime + + +class InitiatorPaymentResponse(BaseModel): + status: str + error_message: str diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-example-bank-api/test-requirements.txt b/openg2p-g2p-bridge-example-bank-api/test-requirements.txt new file mode 100644 index 0000000..4f53afa --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/test-requirements.txt @@ -0,0 +1,3 @@ +pytest-cov +git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-common +git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-auth diff --git a/openg2p-g2p-bridge-example-bank-api/tests/__init__.py b/openg2p-g2p-bridge-example-bank-api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py b/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py new file mode 100644 index 0000000..8171c3e --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py @@ -0,0 +1,175 @@ +import datetime +from unittest.mock import AsyncMock, patch + +import pytest +from openg2p_g2p_bridge_api.controllers import DisbursementController +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementException +from openg2p_g2p_bridge_models.models import CancellationStatus +from openg2p_g2p_bridge_models.schemas import ( + DisbursementPayload, + DisbursementRequest, + DisbursementResponse, + ResponseStatus, +) + + +def mock_create_disbursements(is_valid, disbursement_payloads): + if not is_valid: + raise DisbursementException( + code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + disbursement_payloads=disbursement_payloads, + ) + return disbursement_payloads + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_create_disbursements_success(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_envelope_id="env123", + beneficiary_id="123AB", + disbursement_amount=1000, + ) + ] + mock_service_instance.create_disbursements = AsyncMock( + return_value=mock_create_disbursements(True, disbursement_payloads) + ) + mock_service_instance.construct_disbursement_success_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.SUCCESS, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.create_disbursements(request_payload) + + assert response.response_status == ResponseStatus.SUCCESS + assert response.response_payload == disbursement_payloads + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_create_disbursements_failure(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_envelope_id="env123", + beneficiary_id="123AB", + disbursement_amount=1000, + ) + ] + mock_service_instance.create_disbursements = AsyncMock( + side_effect=lambda req: mock_create_disbursements(False, req.request_payload) + ) + mock_service_instance.construct_disbursement_error_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.create_disbursements(request_payload) + + assert response.response_status == ResponseStatus.FAILURE + assert ( + response.response_error_code == G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD + ) + + +def mock_cancel_disbursements(is_valid, disbursement_payloads): + if not is_valid: + raise DisbursementException( + code=G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED, + disbursement_payloads=disbursement_payloads, + ) + for payload in disbursement_payloads: + payload.cancellation_status = CancellationStatus.Cancelled + payload.cancellation_time_stamp = datetime.datetime.utcnow() + return disbursement_payloads + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_cancel_disbursements_success(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_id="123", + beneficiary_id="123AB", + disbursement_amount=1000, + cancellation_status=None, + ) + ] + mock_service_instance.cancel_disbursements = AsyncMock( + return_value=mock_cancel_disbursements(True, disbursement_payloads) + ) + mock_service_instance.construct_disbursement_success_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.SUCCESS, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.cancel_disbursements(request_payload) + + assert response.response_status == ResponseStatus.SUCCESS + assert all( + payload.cancellation_status == CancellationStatus.Cancelled + for payload in response.response_payload + ) + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") +async def test_cancel_disbursements_failure(mock_service_get_component): + mock_service_instance = AsyncMock() + disbursement_payloads = [ + DisbursementPayload( + disbursement_id="123", + beneficiary_id="123AB", + disbursement_amount=1000, + cancellation_status=None, + ) + ] + mock_service_instance.cancel_disbursements = AsyncMock( + side_effect=lambda req: mock_cancel_disbursements(False, req.request_payload) + ) + mock_service_instance.construct_disbursement_error_response = AsyncMock( + return_value=DisbursementResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED, + response_payload=disbursement_payloads, + ) + ) + + mock_service_get_component.return_value = mock_service_instance + + controller = DisbursementController() + request_payload = DisbursementRequest(request_payload=disbursement_payloads) + + response = await controller.cancel_disbursements(request_payload) + + assert response.response_status == ResponseStatus.FAILURE + assert ( + response.response_error_code + == G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED + ) diff --git a/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py b/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py new file mode 100644 index 0000000..4ccfd8a --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py @@ -0,0 +1,197 @@ +from datetime import datetime +from unittest.mock import AsyncMock, patch + +import pytest +from openg2p_g2p_bridge_api.controllers import DisbursementEnvelopeController +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.errors.exceptions import DisbursementEnvelopeException +from openg2p_g2p_bridge_models.schemas import ( + DisbursementEnvelopePayload, + DisbursementEnvelopeRequest, + DisbursementEnvelopeResponse, + ResponseStatus, +) + + +def mock_create_disbursement_envelope(is_valid, error_code=None): + if not is_valid: + raise DisbursementEnvelopeException( + code=error_code, message=f"{error_code} error." + ) + return DisbursementEnvelopePayload( + disbursement_envelope_id="env123", + benefit_program_mnemonic="TEST123", + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +async def test_create_disbursement_envelope_success(mock_service_get_component): + mock_service_instance = AsyncMock() + mock_service_instance.create_disbursement_envelope = AsyncMock( + return_value=mock_create_disbursement_envelope(True) + ) + mock_service_instance.construct_disbursement_envelope_success_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + expected_payload = mock_create_disbursement_envelope(True) + expected_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.SUCCESS, response_payload=expected_payload + ) + mock_service_instance.construct_disbursement_envelope_success_response.return_value = ( + expected_response + ) + controller = DisbursementEnvelopeController() + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload( + benefit_program_mnemonic="TEST123", + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + ) + + actual_response = await controller.create_disbursement_envelope(request_payload) + + assert actual_response == expected_response + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +@pytest.mark.parametrize("error_code", list(G2PBridgeErrorCodes)) +async def test_create_disbursement_envelope_errors( + mock_service_get_component, error_code +): + mock_service_instance = AsyncMock() + mock_service_instance.create_disbursement_envelope.side_effect = ( + lambda request: mock_create_disbursement_envelope(False, error_code) + ) + mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + error_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=error_code, + ) + + mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( + error_response + ) + + controller = DisbursementEnvelopeController() + + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload( + benefit_program_mnemonic="", # Trigger the error + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + ) + + actual_response = await controller.create_disbursement_envelope(request_payload) + + assert ( + actual_response == error_response + ), f"The response did not match the expected error response for {error_code}." + + +def mock_cancel_disbursement_envelope(is_valid, error_code=None): + if not is_valid: + raise DisbursementEnvelopeException( + code=error_code, message=f"{error_code} error." + ) + + return DisbursementEnvelopePayload( + disbursement_envelope_id="env123", + benefit_program_mnemonic="TEST123", + disbursement_frequency="Monthly", + cycle_code_mnemonic="CYCLE42", + number_of_beneficiaries=100, + number_of_disbursements=100, + total_disbursement_amount=5000.00, + disbursement_schedule_date=datetime.date(datetime.utcnow()), + ) + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +async def test_cancel_disbursement_envelope_success(mock_service_get_component): + mock_service_instance = AsyncMock() + mock_service_instance.cancel_disbursement_envelope = AsyncMock( + return_value=mock_cancel_disbursement_envelope(True) + ) + mock_service_instance.construct_disbursement_envelope_success_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + successful_payload = mock_cancel_disbursement_envelope(True) + expected_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.SUCCESS, response_payload=successful_payload + ) + mock_service_instance.construct_disbursement_envelope_success_response.return_value = ( + expected_response + ) + + controller = DisbursementEnvelopeController() + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload(disbursement_envelope_id="env123") + ) + + actual_response = await controller.cancel_disbursement_envelope(request_payload) + assert actual_response == expected_response + + +@pytest.mark.asyncio +@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") +@pytest.mark.parametrize( + "error_code", + [ + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, + G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, + ], +) +async def test_cancel_disbursement_envelope_failure( + mock_service_get_component, error_code +): + mock_service_instance = AsyncMock() + mock_service_instance.cancel_disbursement_envelope.side_effect = ( + lambda request: mock_cancel_disbursement_envelope(False, error_code) + ) + mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() + + mock_service_get_component.return_value = mock_service_instance + + error_response = DisbursementEnvelopeResponse( + response_status=ResponseStatus.FAILURE, + response_error_code=error_code.value, + ) + mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( + error_response + ) + + controller = DisbursementEnvelopeController() + request_payload = DisbursementEnvelopeRequest( + request_payload=DisbursementEnvelopePayload( + disbursement_envelope_id="env123" # Assuming this ID triggers the error + ) + ) + + actual_response = await controller.cancel_disbursement_envelope(request_payload) + assert ( + actual_response == error_response + ), f"The response for {error_code} did not match the expected error response." From feae2656783caa742ad1bb1e8008ce595d4762d1 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Mon, 22 Jul 2024 15:59:57 +0530 Subject: [PATCH 18/39] Pre-commit fixes --- .../services/disbursement.py | 1 - .../openg2p_g2p_bridge_bank_connectors/app.py | 1 - .../bank_connectors/example_bank_connector.py | 1 + .../config.py | 1 - .../openg2p_g2p_bridge_celery_tasks/app.py | 3 +- .../tasks/__init__.py | 8 ++-- .../tasks/block_funds_with_bank.py | 2 +- .../tasks/check_funds_with_bank_task.py | 2 +- .../tasks/disburse_funds_from_bank.py | 42 +++++++++++++------ .../tasks/mapper_resolution_task.py | 9 ++-- 10 files changed, 41 insertions(+), 29 deletions(-) diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py index 93b1c6d..542b5f7 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py @@ -16,7 +16,6 @@ DisbursementCancellationStatus, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, - ProcessStatus, MapperResolutionBatchStatus, ProcessStatus, ) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py index 7483a5e..50f7dda 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/app.py @@ -11,6 +11,5 @@ class Initializer(BaseInitializer): def initialize(self, **kwargs): - BankConnectorFactory() ExampleBankConnector() diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py index 5fda628..50669ae 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py @@ -18,6 +18,7 @@ class ExampleBankConnector(BankConnectorInterface): def check_funds(self, account_no, currency, amount) -> CheckFundsResponse: print("EXAMPLE BANK CONNECTOR: Checking funds") + return CheckFundsResponse( status=FundsAvailableWithBankEnum.FUNDS_AVAILABLE, error_code="" ) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py index 32bf4c1..614d3df 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py @@ -12,4 +12,3 @@ class Settings(BaseSettings): funds_available_check_url_example_bank: int = 3 funds_blocked_check_url_example_bank: int = 3 funds_disbursement_url_example_bank: int = 3 - diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py index 2f4dd8b..9dd28a4 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py @@ -32,6 +32,7 @@ def get_engine(): db_engine = create_engine(_config.db_datasource) return db_engine + celery_app = Celery( "g2p_bridge_celery_tasks", broker="redis://localhost:6379/0", @@ -57,4 +58,4 @@ def get_engine(): "schedule": _config.funds_disbursement_frequency, }, } -celery_app.conf.timezone = "UTC" \ No newline at end of file +celery_app.conf.timezone = "UTC" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py index be9b971..a14d7f6 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py @@ -6,11 +6,11 @@ check_funds_with_bank_beat_producer, check_funds_with_bank_worker, ) +from .disburse_funds_from_bank import ( + disburse_funds_from_bank_beat_producer, + disburse_funds_from_bank_worker, +) from .mapper_resolution_task import ( mapper_resolution_beat_producer, mapper_resolution_worker, ) -from .disburse_funds_from_bank import ( - disburse_funds_from_bank_beat_producer, - disburse_funds_from_bank_worker, -) \ No newline at end of file diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py index 68ebd13..5f22e45 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py @@ -17,7 +17,7 @@ from sqlalchemy import and_, or_, select from sqlalchemy.orm import sessionmaker -from ..app import get_engine, celery_app +from ..app import celery_app, get_engine from ..config import Settings _config = Settings.get_config() diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py index ebc0ec1..b9da553 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py @@ -14,7 +14,7 @@ from sqlalchemy import and_, or_, select from sqlalchemy.orm import sessionmaker -from ..app import get_engine, celery_app +from ..app import celery_app, get_engine from ..config import Settings _config = Settings.get_config() diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py index 622ef4e..eb0027f 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py @@ -3,9 +3,9 @@ from openg2p_g2p_bridge_bank_connectors.bank_connectors import BankConnectorFactory from openg2p_g2p_bridge_bank_connectors.bank_interface.bank_connector_interface import ( + BankConnectorInterface, PaymentPayload, PaymentStatus, - BankConnectorInterface ) from openg2p_g2p_bridge_models.models import ( BankDisbursementBatchStatus, @@ -15,14 +15,14 @@ DisbursementBatchControl, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, - ProcessStatus, FundsBlockedWithBankEnum, MapperResolutionDetails, + ProcessStatus, ) -from sqlalchemy import select, and_ +from sqlalchemy import and_, select from sqlalchemy.orm import sessionmaker -from ..app import get_engine, celery_app +from ..app import celery_app, get_engine from ..config import Settings _config = Settings.get_config() @@ -168,25 +168,41 @@ def disburse_funds_from_bank_worker(bank_disbursement_batch_id: str): remitting_account_currency=benefit_program_configuration.sponsor_bank_account_currency, payment_amount=disbursement.disbursement_amount, funds_blocked_reference_number=envelope_batch_status.funds_blocked_reference_number, - beneficiary_account=mapper_details.bank_account_number if mapper_details else None, + beneficiary_account=mapper_details.bank_account_number + if mapper_details + else None, beneficiary_account_currency=benefit_program_configuration.sponsor_bank_account_currency, - beneficiary_bank_code=mapper_details.bank_code if mapper_details else None, - beneficiary_branch_code=mapper_details.branch_code if mapper_details else None, + beneficiary_bank_code=mapper_details.bank_code + if mapper_details + else None, + beneficiary_branch_code=mapper_details.branch_code + if mapper_details + else None, payment_date=datetime.utcnow(), beneficiary_id=disbursement.beneficiary_id, beneficiary_name=disbursement.beneficiary_name, beneficiary_account_type=mapper_details.mapper_resolved_fa_type, - beneficiary_phone_no=mapper_details.mobile_number if mapper_details else None, - beneficiary_mobile_wallet_provider=mapper_details.mobile_wallet_provider if mapper_details else None, - beneficiary_email_wallet_provider=mapper_details.email_wallet_provider if mapper_details else None, - beneficiary_email=mapper_details.email_address if mapper_details else None, + beneficiary_phone_no=mapper_details.mobile_number + if mapper_details + else None, + beneficiary_mobile_wallet_provider=mapper_details.mobile_wallet_provider + if mapper_details + else None, + beneficiary_email_wallet_provider=mapper_details.email_wallet_provider + if mapper_details + else None, + beneficiary_email=mapper_details.email_address + if mapper_details + else None, benefit_program_mnemonic=envelope.benefit_program_mnemonic, cycle_code_mnemonic=envelope.cycle_code_mnemonic, ) ) - bank_connector: BankConnectorInterface = BankConnectorFactory.get_component().get_bank_connector( - benefit_program_configuration.sponsor_bank_code + bank_connector: BankConnectorInterface = ( + BankConnectorFactory.get_component().get_bank_connector( + benefit_program_configuration.sponsor_bank_code + ) ) try: diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py index 870a39a..06076b8 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py @@ -10,10 +10,10 @@ ) from openg2p_g2pconnect_mapper_lib.client import MapperResolveClient from openg2p_g2pconnect_mapper_lib.schemas import ResolveRequest -from sqlalchemy import select, and_ +from sqlalchemy import and_, select from sqlalchemy.orm import sessionmaker -from ..app import get_engine, celery_app +from ..app import celery_app, get_engine from ..config import Settings from ..helpers import ResolveHelper @@ -98,7 +98,6 @@ def mapper_resolution_worker(mapper_resolution_batch_id: str): async def make_resolve_request(disbursement_batch_controls): - resolve_helper = ResolveHelper.get_component() single_resolve_requests = [ @@ -131,9 +130,7 @@ def process_and_store_resolution( if disbursement_id and ( single_response.fa != "" or single_response.fa is not None ): - deconstructed_fa = ( - resolve_helper.deconstruct_fa(single_response.fa) - ) + deconstructed_fa = resolve_helper.deconstruct_fa(single_response.fa) details = MapperResolutionDetails( mapper_resolution_batch_id=mapper_resolution_batch_id, disbursement_id=disbursement_id, From f60bb78a39646f0732dfa0208d6794d5c4a32ae4 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Mon, 22 Jul 2024 16:00:08 +0530 Subject: [PATCH 19/39] Pre-commit fixes --- .../src/openg2p_g2p_bridge_models/models/__init__.py | 1 - .../src/openg2p_g2p_bridge_models/models/disbursement.py | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py index ee88dd2..57e974b 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py @@ -4,7 +4,6 @@ Disbursement, DisbursementBatchControl, DisbursementCancellationStatus, - ProcessStatus, MapperResolutionBatchStatus, MapperResolutionDetails, MapperResolvedFaType, diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py index 4771a4d..655ee5e 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py @@ -51,8 +51,12 @@ class DisbursementBatchControl(BaseORMModelWithTimes): disbursement_id: Mapped[str] = mapped_column(String, unique=True) disbursement_envelope_id: Mapped[str] = mapped_column(String, index=True) beneficiary_id: Mapped[str] = mapped_column(String) - bank_disbursement_batch_id = mapped_column(UUID, nullable=True, default=None, index=True, unique=True) - mapper_resolution_batch_id = mapped_column(UUID, nullable=True, default=None, index=True, unique=True) + bank_disbursement_batch_id = mapped_column( + UUID, nullable=True, default=None, index=True, unique=True + ) + mapper_resolution_batch_id = mapped_column( + UUID, nullable=True, default=None, index=True, unique=True + ) class MapperResolutionBatchStatus(BaseORMModelWithTimes): From 5a563e526f0202bcfdf6e2fbe0c5cbf765c9a9a7 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 25 Jul 2024 19:00:19 +0530 Subject: [PATCH 20/39] Bank Connector Tests --- .../bank_connectors/example_bank_connector.py | 92 +++++++++-- .../bank_connector_interface.py | 1 - .../config.py | 2 +- .../tests/__init__.py | 0 .../tests/test_bank_connector.py | 146 ++++++++++++++++++ 5 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 openg2p-g2p-bridge-bank-connectors/tests/__init__.py create mode 100644 openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py index 50669ae..e402fa2 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py @@ -1,5 +1,6 @@ from typing import List +import httpx from openg2p_g2p_bridge_models.models import ( FundsAvailableWithBankEnum, FundsBlockedWithBankEnum, @@ -13,29 +14,90 @@ PaymentResponse, PaymentStatus, ) +from ..config import Settings + +_config = Settings.get_config() class ExampleBankConnector(BankConnectorInterface): def check_funds(self, account_no, currency, amount) -> CheckFundsResponse: - print("EXAMPLE BANK CONNECTOR: Checking funds") + try: + with httpx.Client() as client: + request_data = { + "account_number": account_no, + "account_currency": currency, + "total_funds_needed": amount, + } + response = client.post( + _config.funds_available_check_url_example_bank, json=request_data + ) + response.raise_for_status() - return CheckFundsResponse( - status=FundsAvailableWithBankEnum.FUNDS_AVAILABLE, error_code="" - ) + data = response.json() + if data["status"] == "success": + return CheckFundsResponse( + status=FundsAvailableWithBankEnum.FUNDS_AVAILABLE, error_code="" + ) + return CheckFundsResponse( + status=FundsAvailableWithBankEnum.FUNDS_NOT_AVAILABLE, error_code="" + ) + except httpx.HTTPStatusError as e: + return CheckFundsResponse( + status=FundsAvailableWithBankEnum.PENDING_CHECK, error_code=str(e) + ) def block_funds(self, account_no, currency, amount) -> BlockFundsResponse: - print("EXAMPLE BANK CONNECTOR: Blocking funds") - return BlockFundsResponse( - status=FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS, - block_reference_no="REF123", - error_code="", - ) + try: + with httpx.Client() as client: + request_data = { + "account_number": account_no, + "account_currency": currency, + "total_funds_needed": amount, + } + response = client.post( + _config.funds_block_url_example_bank, json=request_data + ) + response.raise_for_status() + + data = response.json() + if data["status"] == "success": + return BlockFundsResponse( + status=FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS, + block_reference_no=data["block_reference_no"], + error_code="", + ) + return BlockFundsResponse( + status=FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE, + block_reference_no="", + error_code=data.get("error_code", ""), + ) + except httpx.HTTPStatusError as e: + return BlockFundsResponse( + status=FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE, + block_reference_no="", + error_code=str(e), + ) def initiate_payment( self, payment_payloads: List[PaymentPayload] ) -> PaymentResponse: - print("EXAMPLE BANK CONNECTOR: Initiating payment") - print("PAYMENT PAYLOADS:", payment_payloads) - return PaymentResponse( - status=PaymentStatus.SUCCESS, error_code="", ack_reference_no="ACK123" - ) + try: + with httpx.Client() as client: + request_data = { + "payment_payloads": [ + payload.model_dump() for payload in payment_payloads + ] + } + response = client.post( + _config.funds_disbursement_url_example_bank, json=request_data + ) + response.raise_for_status() + + data = response.json() + if data["status"] == "success": + return PaymentResponse(status=PaymentStatus.SUCCESS, error_code="") + return PaymentResponse( + status=PaymentStatus.ERROR, error_code=data.get("error_message", "") + ) + except httpx.HTTPStatusError as e: + return PaymentResponse(status=PaymentStatus.ERROR, error_code=str(e)) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py index 759a967..22c3d1c 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py @@ -55,7 +55,6 @@ class PaymentStatus(enum.Enum): class PaymentResponse(BaseModel): status: PaymentStatus error_code: str - ack_reference_no: str class BankConnectorInterface(BaseService): diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py index 614d3df..4cb6752 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py @@ -10,5 +10,5 @@ class Settings(BaseSettings): db_dbname: str = "openg2p_g2p_bridge_db" funds_available_check_url_example_bank: int = 3 - funds_blocked_check_url_example_bank: int = 3 + funds_block_url_example_bank: int = 3 funds_disbursement_url_example_bank: int = 3 diff --git a/openg2p-g2p-bridge-bank-connectors/tests/__init__.py b/openg2p-g2p-bridge-bank-connectors/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py b/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py new file mode 100644 index 0000000..e2ce670 --- /dev/null +++ b/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py @@ -0,0 +1,146 @@ +import pytest +from unittest.mock import patch, Mock +from httpx import HTTPStatusError +from datetime import datetime +from openg2p_g2p_bridge_bank_connectors.bank_connectors import ExampleBankConnector +from openg2p_g2p_bridge_bank_connectors.bank_interface.bank_connector_interface import ( + PaymentPayload, + PaymentStatus, +) +from openg2p_g2p_bridge_models.models import ( + FundsAvailableWithBankEnum, + FundsBlockedWithBankEnum, +) + + + + +@pytest.fixture +def example_bank_connector(): + return ExampleBankConnector() + + +def test_check_funds_success(example_bank_connector): + with patch('httpx.Client.post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = {"status": "success"} + mock_post.return_value = mock_response + + response = example_bank_connector.check_funds("123456", "USD", 100.0) + + assert response.status == FundsAvailableWithBankEnum.FUNDS_AVAILABLE + assert response.error_code == "" + + +def test_check_funds_failure(example_bank_connector): + with patch('httpx.Client.post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = {"status": "failure"} + mock_post.return_value = mock_response + + response = example_bank_connector.check_funds("123456", "USD", 100.0) + + assert response.status == FundsAvailableWithBankEnum.FUNDS_NOT_AVAILABLE + assert response.error_code == "" + + +def test_check_funds_http_error(example_bank_connector): + with patch('httpx.Client.post', side_effect=HTTPStatusError("Error", request=Mock(), response=Mock())) as mock_post: + response = example_bank_connector.check_funds("123456", "USD", 100.0) + + assert response.status == FundsAvailableWithBankEnum.PENDING_CHECK + assert response.error_code != "" + + +def test_block_funds_success(example_bank_connector): + with patch('httpx.Client.post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = {"status": "success", "block_reference_no": "BR123"} + mock_post.return_value = mock_response + + response = example_bank_connector.block_funds("123456", "USD", 100.0) + + assert response.status == FundsBlockedWithBankEnum.FUNDS_BLOCK_SUCCESS + assert response.block_reference_no == "BR123" + assert response.error_code == "" + + +def test_block_funds_failure(example_bank_connector): + with patch('httpx.Client.post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = {"status": "failure", "error_code": "ERR123"} + mock_post.return_value = mock_response + + response = example_bank_connector.block_funds("123456", "USD", 100.0) + + assert response.status == FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE + assert response.block_reference_no == "" + assert response.error_code == "ERR123" + + +def test_block_funds_http_error(example_bank_connector): + with patch('httpx.Client.post', side_effect=HTTPStatusError("Error", request=Mock(), response=Mock())) as mock_post: + response = example_bank_connector.block_funds("123456", "USD", 100.0) + + assert response.status == FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE + assert response.block_reference_no == "" + assert response.error_code != "" + + +def test_initiate_payment_success(example_bank_connector): + with patch('httpx.Client.post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = {"status": "success"} + mock_post.return_value = mock_response + + payment_payload = PaymentPayload( + remitting_account="123456", + remitting_account_currency="USD", + payment_amount=100.0, + funds_blocked_reference_number="BR123", + beneficiary_id="BID123", + payment_date=datetime.now() + ) + + response = example_bank_connector.initiate_payment([payment_payload]) + + assert response.status == PaymentStatus.SUCCESS + assert response.error_code == "" + + +def test_initiate_payment_failure(example_bank_connector): + with patch('httpx.Client.post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = {"status": "failure", "error_message": "Payment error"} + mock_post.return_value = mock_response + + payment_payload = PaymentPayload( + remitting_account="123456", + remitting_account_currency="USD", + payment_amount=100.0, + funds_blocked_reference_number="BR123", + beneficiary_id="BID123", + payment_date=datetime.now() + ) + + response = example_bank_connector.initiate_payment([payment_payload]) + + assert response.status == PaymentStatus.ERROR + assert response.error_code == "Payment error" + + +def test_initiate_payment_http_error(example_bank_connector): + with patch('httpx.Client.post', side_effect=HTTPStatusError("Error", request=Mock(), response=Mock())) as mock_post: + payment_payload = PaymentPayload( + remitting_account="123456", + remitting_account_currency="USD", + payment_amount=100.0, + funds_blocked_reference_number="BR123", + beneficiary_id="BID123", + payment_date=datetime.now() + ) + + response = example_bank_connector.initiate_payment([payment_payload]) + + assert response.status == PaymentStatus.ERROR + assert response.error_code != "" From a09283a50bde45a1f62a159bcf1ce8c13e2fff35 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 25 Jul 2024 19:01:07 +0530 Subject: [PATCH 21/39] Example Bank Simulator APIs --- .../celery_main.py | 3 + .../app.py | 10 +- .../celery_app.py | 95 +++++++++++++++++-- .../config.py | 5 +- .../controllers/block_funds.py | 33 +++++-- .../controllers/check_available_funds.py | 22 +++-- .../controllers/initiate_payment.py | 86 ++++++----------- .../models/__init__.py | 10 +- .../models/account.py | 49 +++++++++- .../models/benefit_program.py | 10 -- .../schemas/fund_schemas.py | 18 +++- 11 files changed, 241 insertions(+), 100 deletions(-) create mode 100644 openg2p-g2p-bridge-example-bank-api/celery_main.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py diff --git a/openg2p-g2p-bridge-example-bank-api/celery_main.py b/openg2p-g2p-bridge-example-bank-api/celery_main.py new file mode 100644 index 0000000..3bc4a3c --- /dev/null +++ b/openg2p-g2p-bridge-example-bank-api/celery_main.py @@ -0,0 +1,3 @@ +from openg2p_g2p_bridge_example_bank_api.celery_app import celery_app + +celery_app = celery_app diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py index 8459562..15e55da 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py @@ -7,13 +7,14 @@ _config = Settings.get_config() from openg2p_fastapi_common.app import Initializer as BaseInitializer +from sqlalchemy import create_engine from .controllers import ( BlockFundsController, FundAvailabilityController, PaymentController, ) -from .models import Account, BenefitProgram, FundBlock, InitiatePaymentRequest +from .models import Account, FundBlock, InitiatePaymentRequest _logger = logging.getLogger(_config.logging_default_logger_name) @@ -31,9 +32,14 @@ def migrate_database(self, args): async def migrate(): _logger.info("Migrating database") - await BenefitProgram.create_migrate() await Account.create_migrate() await FundBlock.create_migrate() await InitiatePaymentRequest.create_migrate() asyncio.run(migrate()) + + +def get_engine(): + if _config.db_datasource: + db_engine = create_engine(_config.db_datasource) + return db_engine diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py index a3789f5..22fffc9 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py @@ -1,4 +1,22 @@ +import uuid +from datetime import datetime + from celery import Celery +from sqlalchemy import select +from sqlalchemy.orm import sessionmaker + +from .app import get_engine +from .config import Settings +from .models import ( + Account, + AccountingLog, + DebitCreditTypes, + FundBlock, + InitiatePaymentRequest, + PaymentStatus, +) + +_config = Settings.get_config() celery_app = Celery( "example_bank_celery_tasks", @@ -7,16 +25,81 @@ ) celery_app.conf.beat_schedule = { - "initiate_fund_check_beat_producer": { - "task": "initiate_fund_check_beat_producer", + "process_payments": { + "task": "process_payments", "schedule": 10, } } celery_app.conf.timezone = "UTC" +_engine = get_engine() + + +@celery_app.task(name="process_payments") +def process_payments(): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + with session_maker() as session: + initiate_payment_requests = ( + session.execute( + select(InitiatePaymentRequest).where( + (InitiatePaymentRequest.payment_status.in_(["PENDING", "FAILED"])) + & ( + InitiatePaymentRequest.payment_initiate_attempts + < _config.payment_initiate_attempts + ) + ) + ) + .scalars() + .all() + ) + + for payment_request in initiate_payment_requests: + account = ( + session.execute( + select(Account).where( + Account.account_number == payment_request.remitting_account + ) + ) + .scalars() + .first() + ) + + fund_block = ( + session.execute( + select(FundBlock).where( + FundBlock.block_reference_no + == payment_request.funds_blocked_reference_number + ) + ) + .scalars() + .first() + ) + + log = AccountingLog( + reference_no=str(uuid.uuid4()), + debit_credit=DebitCreditTypes.DEBIT, + account_number=payment_request.remitting_account, + transaction_amount=payment_request.payment_amount, + transaction_date=datetime.utcnow(), + transaction_currency=payment_request.remitting_account_currency, + transaction_code="DBT", + narrative_1=payment_request.narrative_1, + narrative_2=payment_request.narrative_2, + narrative_3=payment_request.narrative_3, + narrative_4=payment_request.narrative_4, + narrative_5=payment_request.narrative_5, + narrative_6=payment_request.narrative_6, + active=True, + ) + + session.add(log) + + fund_block.amount_released += payment_request.payment_amount + account.book_balance -= payment_request.payment_amount + account.blocked_amount -= payment_request.payment_amount + account.available_balance = account.book_balance - account.blocked_amount + payment_request.payment_status = PaymentStatus.SUCCESS + payment_request.payment_initiate_attempts += 1 -@celery_app.task -def initiate_fund_check_beat_producer(): - print("Initiating fund check beat producer") - return True + session.commit() diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py index 71148dc..2d6cfef 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py @@ -6,7 +6,7 @@ class Settings(BaseSettings): model_config = SettingsConfigDict( - env_prefix="g2p_bridge_", env_file=".env", extra="allow" + env_prefix="example_bank_", env_file=".env", extra="allow" ) openapi_title: str = "Example Bank APIs for Cash Transfer" @@ -18,3 +18,6 @@ class Settings(BaseSettings): openapi_version: str = __version__ db_dbname: str = "openg2p_g2p_bridge_db" + db_driver: str = "postgresql" + + payment_initiate_attempts: int = 3 diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py index 515062a..4b11683 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py @@ -1,6 +1,5 @@ import uuid -from fastapi import HTTPException from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.controller import BaseController from sqlalchemy import update @@ -27,24 +26,34 @@ def __init__(self, **kwargs): async def block_funds(self, request: BlockFundsRequest) -> BlockFundsResponse: session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: - # Check if the account exists and has sufficient funds stmt = select(Account).where( (Account.account_number == request.account_no) & (Account.account_currency == request.currency) ) result = await session.execute(stmt) - account_balance = result.scalars().first() + account = result.scalars().first() - if not account_balance: - raise HTTPException(status_code=404, detail="Account not found") - if account_balance.book_balance < request.amount: - raise HTTPException(status_code=400, detail="Insufficient funds") + if not account: + return BlockFundsResponse( + status="failed", + block_reference_no="", + error_message="Account not found", + ) + if account.available_balance < request.amount: + return BlockFundsResponse( + status="failed", + block_reference_no="", + error_message="Insufficient funds", + ) - new_balance = account_balance.book_balance - request.amount await session.execute( update(Account) .where(Account.account_number == request.account_no) - .values(book_balance=new_balance) + .values( + available_balance=account.book_balance + - (account.blocked_amount + request.amount), + blocked_amount=account.blocked_amount + request.amount, + ) ) block_reference_no = str(uuid.uuid4()) @@ -58,4 +67,8 @@ async def block_funds(self, request: BlockFundsRequest) -> BlockFundsResponse: session.add(fund_block) await session.commit() - return BlockFundsResponse(block_reference_no=block_reference_no) + return BlockFundsResponse( + status="success", + block_reference_no=block_reference_no, + error_message="", + ) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py index dbd63f5..2b68632 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py @@ -1,4 +1,3 @@ -from fastapi import HTTPException from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.controller import BaseController from sqlalchemy.ext.asyncio import async_sessionmaker @@ -30,18 +29,27 @@ async def check_available_funds( Account.account_number == request.account_number ) result = await session.execute(stmt) - account_balance = result.scalars().first() + account = result.scalars().first() - if not account_balance: - raise HTTPException(status_code=404, detail="Account not found") + if not account: + return CheckFundResponse( + status="failed", + account_number=request.account_number, + has_sufficient_funds=False, + error_message="Account not found", + ) - if account_balance.book_balance >= request.total_funds_needed: + if account.available_balance >= request.total_funds_needed: return CheckFundResponse( - account_number=account_balance.account_number, + status="success", + account_number=account.account_number, has_sufficient_funds=True, + error_message="", ) else: return CheckFundResponse( - account_number=account_balance.account_number, + status="failed", + account_number=account.account_number, has_sufficient_funds=False, + error_message="Insufficient funds", ) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py index f062bc2..5598c4d 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py @@ -2,8 +2,9 @@ from openg2p_fastapi_common.controller import BaseController from sqlalchemy.ext.asyncio import async_sessionmaker from sqlalchemy.future import select +from typing import List -from ..models import BenefitProgram, FundBlock, InitiatePaymentRequest +from ..models import FundBlock, InitiatePaymentRequest from ..schemas import InitiatePaymentPayload, InitiatorPaymentResponse @@ -21,69 +22,44 @@ def __init__(self, **kwargs): ) async def initiate_payment( - self, initiate_payment_payload: InitiatePaymentPayload + self, initiate_payment_payloads: List[InitiatePaymentPayload] ) -> InitiatorPaymentResponse: session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: - fund_block_stmt = select(FundBlock).where( - FundBlock.block_reference_no - == initiate_payment_payload.funds_blocked_reference_number - ) - fund_block_result = await session.execute(fund_block_stmt) - fund_block = fund_block_result.scalars().first() - - if ( - not fund_block - or fund_block.amount < initiate_payment_payload.payment_amount - or fund_block.currency - != initiate_payment_payload.remitting_account_currency - ): - return InitiatorPaymentResponse( - status="failed", - error_message="Invalid funds block reference or mismatch in details", + for initiate_payment_payload in initiate_payment_payloads: + fund_block_stmt = select(FundBlock).where( + FundBlock.block_reference_no + == initiate_payment_payload.funds_blocked_reference_number ) + fund_block_result = await session.execute(fund_block_stmt) + fund_block = fund_block_result.scalars().first() - if initiate_payment_payload.benefit_program_mnemonic: - program_stmt = select(BenefitProgram).where( - ( - BenefitProgram.program_mnemonic - == initiate_payment_payload.benefit_program_mnemonic - ) - & ( - BenefitProgram.funding_account_number - == initiate_payment_payload.remitting_account - ) - & ( - BenefitProgram.funding_account_currency - == initiate_payment_payload.remitting_account_currency - ) - ) - program_result = await session.execute(program_stmt) - benefit_program = program_result.scalars().first() - if not benefit_program: + if ( + not fund_block + or initiate_payment_payload.payment_amount > fund_block.amount + or fund_block.currency + != initiate_payment_payload.remitting_account_currency + ): return InitiatorPaymentResponse( status="failed", - error_message="Invalid benefit program mnemonic", + error_message="Invalid funds block reference or mismatch in details", ) - payment = InitiatePaymentRequest( - remitting_account=initiate_payment_payload.remitting_account, - remitting_account_currency=initiate_payment_payload.remitting_account_currency, - payment_amount=initiate_payment_payload.payment_amount, - funds_blocked_reference_number=initiate_payment_payload.funds_blocked_reference_number, - beneficiary_id=initiate_payment_payload.beneficiary_id, - beneficiary_name=initiate_payment_payload.beneficiary_name, - beneficiary_account=initiate_payment_payload.beneficiary_account, - beneficiary_account_currency=initiate_payment_payload.beneficiary_account_currency, - beneficiary_account_type=initiate_payment_payload.beneficiary_account_type, - beneficiary_bank_code=initiate_payment_payload.beneficiary_bank_code, - beneficiary_branch_code=initiate_payment_payload.beneficiary_branch_code, - benefit_program_mnemonic=initiate_payment_payload.benefit_program_mnemonic, - cycle_code_mnemonic=initiate_payment_payload.cycle_code_mnemonic, - payment_date=initiate_payment_payload.payment_date, - active=True, - ) - session.add(payment) + payment = InitiatePaymentRequest( + remitting_account=initiate_payment_payload.remitting_account, + remitting_account_currency=initiate_payment_payload.remitting_account_currency, + payment_amount=initiate_payment_payload.payment_amount, + funds_blocked_reference_number=initiate_payment_payload.funds_blocked_reference_number, + beneficiary_name=initiate_payment_payload.beneficiary_name, + beneficiary_account=initiate_payment_payload.beneficiary_account, + beneficiary_account_currency=initiate_payment_payload.beneficiary_account_currency, + beneficiary_account_type=initiate_payment_payload.beneficiary_account_type, + beneficiary_bank_code=initiate_payment_payload.beneficiary_bank_code, + beneficiary_branch_code=initiate_payment_payload.beneficiary_branch_code, + payment_date=initiate_payment_payload.payment_date, + active=True, + ) + session.add(payment) await session.commit() diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py index d37ca74..497521c 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py @@ -1,2 +1,8 @@ -from .account import Account, FundBlock, InitiatePaymentRequest -from .benefit_program import BenefitProgram +from .account import ( + Account, + AccountingLog, + DebitCreditTypes, + FundBlock, + InitiatePaymentRequest, + PaymentStatus, +) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py index 94bcbd7..9a1b0e6 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py @@ -13,11 +13,19 @@ class PaymentStatus(Enum): FAILED = "FAILED" +class DebitCreditTypes(Enum): + DEBIT = "debit" + CREDIT = "credit" + + class Account(BaseORMModelWithTimes): __tablename__ = "accounts" + account_holder_name: Mapped[str] = mapped_column(String) account_number: Mapped[str] = mapped_column(String) account_currency: Mapped[str] = mapped_column(String) book_balance: Mapped[float] = mapped_column(Float) + available_balance: Mapped[float] = mapped_column(Float) + blocked_amount: Mapped[float] = mapped_column(Float, default=0) class FundBlock(BaseORMModelWithTimes): @@ -26,6 +34,7 @@ class FundBlock(BaseORMModelWithTimes): account_no: Mapped[str] = mapped_column(String) currency: Mapped[str] = mapped_column(String) amount: Mapped[float] = mapped_column(Float) + amount_released: Mapped[float] = mapped_column(Float, default=0) class InitiatePaymentRequest(BaseORMModelWithTimes): @@ -33,18 +42,50 @@ class InitiatePaymentRequest(BaseORMModelWithTimes): remitting_account: Mapped[str] = mapped_column(String, nullable=False) remitting_account_currency: Mapped[str] = mapped_column(String, nullable=False) payment_amount: Mapped[float] = mapped_column(Float, nullable=False) + payment_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False) funds_blocked_reference_number: Mapped[str] = mapped_column(String, nullable=False) - beneficiary_id: Mapped[str] = mapped_column(String, nullable=False) + beneficiary_name: Mapped[str] = mapped_column(String) beneficiary_account: Mapped[str] = mapped_column(String) beneficiary_account_currency: Mapped[str] = mapped_column(String) beneficiary_account_type: Mapped[str] = mapped_column(String) beneficiary_bank_code: Mapped[str] = mapped_column(String) beneficiary_branch_code: Mapped[str] = mapped_column(String) - benefit_program_mnemonic: Mapped[str] = mapped_column(String) - cycle_code_mnemonic: Mapped[str] = mapped_column(String) - payment_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False) + + narrative_1: Mapped[str] = mapped_column(String, nullable=True) # disbursement id + narrative_2: Mapped[str] = mapped_column(String, nullable=True) # beneficiary id + narrative_3: Mapped[str] = mapped_column(String, nullable=True) # program pneumonic + narrative_4: Mapped[str] = mapped_column( + String, nullable=True + ) # cycle code pneumonic + narrative_5: Mapped[str] = mapped_column(String, nullable=True) # beneficiary email + narrative_6: Mapped[str] = mapped_column( + String, nullable=True + ) # beneficiary phone number + payment_initiate_attempts: Mapped[int] = mapped_column(Integer, default=0) payment_status: Mapped[PaymentStatus] = mapped_column( SqlEnum(PaymentStatus), default=PaymentStatus.PENDING ) + + +class AccountingLog(BaseORMModelWithTimes): + __tablename__ = "accounting_logs" + reference_no: Mapped[str] = mapped_column(String, index=True, unique=True) + debit_credit: Mapped[DebitCreditTypes] = mapped_column(SqlEnum(DebitCreditTypes)) + account_number: Mapped[str] = mapped_column(String, index=True) + transaction_amount: Mapped[float] = mapped_column(Float) + transaction_date: Mapped[datetime] = mapped_column(DateTime) + transaction_currency: Mapped[str] = mapped_column(String) + transaction_code: Mapped[str] = mapped_column(String, nullable=True) + + narrative_1: Mapped[str] = mapped_column(String, nullable=True) # disbursement id + narrative_2: Mapped[str] = mapped_column(String, nullable=True) # beneficiary id + narrative_3: Mapped[str] = mapped_column(String, nullable=True) # program pneumonic + narrative_4: Mapped[str] = mapped_column( + String, nullable=True + ) # cycle code pneumonic + narrative_5: Mapped[str] = mapped_column(String, nullable=True) # beneficiary email + narrative_6: Mapped[str] = mapped_column( + String, nullable=True + ) # beneficiary phone number diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py deleted file mode 100644 index 3f34eeb..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/benefit_program.py +++ /dev/null @@ -1,10 +0,0 @@ -from openg2p_fastapi_common.models import BaseORMModelWithTimes -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column - - -class BenefitProgram(BaseORMModelWithTimes): - __tablename__ = "benefit_programs" - program_mnemonic: Mapped[str] = mapped_column(String, primary_key=True) - funding_account_number: Mapped[str] = mapped_column(String, index=True) - funding_account_currency: Mapped[str] = mapped_column(String) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py index 3558f81..d6c24ea 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Optional from pydantic import BaseModel @@ -10,8 +11,10 @@ class CheckFundRequest(BaseModel): class CheckFundResponse(BaseModel): + status: str account_number: str has_sufficient_funds: bool + error_message: Optional[str] = None class BlockFundsRequest(BaseModel): @@ -21,7 +24,9 @@ class BlockFundsRequest(BaseModel): class BlockFundsResponse(BaseModel): + status: str block_reference_no: str + error_message: Optional[str] = None class InitiatePaymentPayload(BaseModel): @@ -29,18 +34,25 @@ class InitiatePaymentPayload(BaseModel): remitting_account_currency: str payment_amount: float funds_blocked_reference_number: str - beneficiary_id: str beneficiary_name: str beneficiary_account: str beneficiary_account_currency: str beneficiary_account_type: str beneficiary_bank_code: str beneficiary_branch_code: str + benefit_program_mnemonic: str - cycle_code_mnemonic: str + + narrative_1: Optional[str] = None + narrative_2: Optional[str] = None + narrative_3: Optional[str] = None + narrative_4: Optional[str] = None + narrative_5: Optional[str] = None + narrative_6: Optional[str] = None + payment_date: datetime class InitiatorPaymentResponse(BaseModel): status: str - error_message: str + error_message: Optional[str] = None From 843959b037d1b09321ea24615058eadf68cc32ce Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 25 Jul 2024 19:03:59 +0530 Subject: [PATCH 22/39] Example Bank Simulator APIs --- openg2p-g2p-bridge-example-bank-api/.env | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 openg2p-g2p-bridge-example-bank-api/.env diff --git a/openg2p-g2p-bridge-example-bank-api/.env b/openg2p-g2p-bridge-example-bank-api/.env deleted file mode 100644 index be5b03f..0000000 --- a/openg2p-g2p-bridge-example-bank-api/.env +++ /dev/null @@ -1,5 +0,0 @@ -G2P_BRIDGE_DB_DBNAME=bridge_db -G2P_BRIDGE_WORKER_TYPE=gunicorn -G2P_BRIDGE_HOST=0.0.0.0 -G2P_BRIDGE_PORT=8000 -G2P_BRIDGE_NO_OF_WORKERS=1 From ebeb7475a7b95bad3989cf05d8566b8c68748bd0 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 1 Aug 2024 08:34:25 +0530 Subject: [PATCH 23/39] Add example .ENV --- openg2p-g2p-bridge-api/.env.example | 5 +++++ openg2p-g2p-bridge-celery-tasks/.env.example | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 openg2p-g2p-bridge-api/.env.example create mode 100644 openg2p-g2p-bridge-celery-tasks/.env.example diff --git a/openg2p-g2p-bridge-api/.env.example b/openg2p-g2p-bridge-api/.env.example new file mode 100644 index 0000000..3258cab --- /dev/null +++ b/openg2p-g2p-bridge-api/.env.example @@ -0,0 +1,5 @@ +G2P_BRIDGE_DB_DBNAME=openg2p_g2p_bridge_db +G2P_BRIDGE_WORKER_TYPE=gunicorn +G2P_BRIDGE_HOST=0.0.0.0 +G2P_BRIDGE_PORT=8000 +G2P_BRIDGE_NO_OF_WORKERS=1 \ No newline at end of file diff --git a/openg2p-g2p-bridge-celery-tasks/.env.example b/openg2p-g2p-bridge-celery-tasks/.env.example new file mode 100644 index 0000000..ff59a1d --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/.env.example @@ -0,0 +1,10 @@ +G2P_BRIDGE_CELERY_TASKS_MAPPER_RESOLVE_API_URL="http://127.0.0.1:8003/sync/resolve" +G2P_BRIDGE_CELERY_TASKS_MAPPER_RESOLVE_RETRIES=3 +G2P_BRIDGE_CELERY_TASKS_MAPPER_RESOLVE_RETRY_DELAY=5 +G2P_BRIDGE_CELERY_TASKS_PORT=8001 +G2P_BRIDGE_CELERY_TASKS_WORKER_TYPE=gunicorn +G2P_BRIDGE_CELERY_TASKS_NO_OF_WORKERS=1 +G2P_BRIDGE_CELERY_TASKS_DB_DBNAME=openg2p_g2p_bridge_db +G2P_BRIDGE_BANK_DECONSTRUCT_STRATEGY="bank_(?P\d+)_(?P\d+)_(?P\d+)_(?P\w+)" +G2P_BRIDGE_MOBILE_WALLET_DECONSTRUCT_STRATEGY="mobile_(?P\d+)_(?P\w+)" +G2P_BRIDGE_EMAIL_WALLET_DECONSTRUCT_STRATEGY="email_(?P\w+)_(?P\w+)" \ No newline at end of file From f416ffc084b8a04236e5876b76cd28fffab01d9d Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 1 Aug 2024 08:43:41 +0530 Subject: [PATCH 24/39] MT940 Account Statement Controller --- .../src/openg2p_g2p_bridge_api/app.py | 9 +++ .../controllers/__init__.py | 1 + .../controllers/account_statement.py | 46 +++++++++++++ .../services/__init__.py | 1 + .../services/account_statement.py | 66 +++++++++++++++++++ 5 files changed, 123 insertions(+) create mode 100644 openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py create mode 100644 openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py index 67865a7..e04df1e 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py @@ -10,13 +10,19 @@ from openg2p_g2p_bridge_models.models import ( DisbursementEnvelope, DisbursementEnvelopeBatchStatus, + AccountStatement, + AccountStatementLob, + DisbursementRecon, + DisbursementErrorRecon, ) from .controllers import ( + AccountStatementController, DisbursementController, DisbursementEnvelopeController, ) from .services import ( + AccountStatementService, DisbursementEnvelopeService, DisbursementService, ) @@ -29,8 +35,10 @@ def initialize(self, **kwargs): super().initialize() DisbursementEnvelopeService() DisbursementService() + AccountStatementService() DisbursementEnvelopeController().post_init() DisbursementController().post_init() + AccountStatementController().post_init() def migrate_database(self, args): super().migrate_database(args) @@ -39,5 +47,6 @@ async def migrate(): _logger.info("Migrating database") await DisbursementEnvelope.create_migrate() await DisbursementEnvelopeBatchStatus.create_migrate() + await AccountStatement.create_migrate() asyncio.run(migrate()) diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/__init__.py index f265e87..7b6d08e 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/__init__.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/__init__.py @@ -1,2 +1,3 @@ +from .account_statement import AccountStatementController from .disbursement import DisbursementController from .disbursement_envelope import DisbursementEnvelopeController diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py new file mode 100644 index 0000000..66e2261 --- /dev/null +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py @@ -0,0 +1,46 @@ +from fastapi import File, UploadFile +from openg2p_fastapi_common.controller import BaseController +from openg2p_g2p_bridge_models.errors.codes import ( + G2PBridgeErrorCodes, +) +from openg2p_g2p_bridge_models.errors.exceptions import ( + AccountStatementException, +) +from openg2p_g2p_bridge_models.schemas import ( + AccountStatementResponse, +) + +from openg2p_g2p_bridge_api.services import AccountStatementService + + +class AccountStatementController(BaseController): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.router.tags += ["G2P Bridge Account Statement"] + self.account_statement_service = AccountStatementService.get_component() + + self.router.add_api_route( + "/upload_mt940", + self.upload_mt940, + responses={200: {"model": AccountStatementResponse}}, + methods=["POST"], + ) + + async def upload_mt940( + self, + statement_file: UploadFile = File(...), + ) -> AccountStatementResponse: + try: + account_statement_id: str = ( + await self.account_statement_service.upload_mt940(statement_file) + ) + account_statement_response: AccountStatementResponse = await self.account_statement_service.construct_account_statement_success_response( + account_statement_id + ) + except AccountStatementException: + account_statement_response: AccountStatementResponse = await self.account_statement_service.construct_account_statement_error_response( + G2PBridgeErrorCodes.STATEMENT_UPLOAD_ERROR + ) + + return account_statement_response diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/__init__.py index 60026d0..994afe3 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/__init__.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/__init__.py @@ -1,2 +1,3 @@ +from .account_statement import AccountStatementService from .disbursement import DisbursementService from .disbursement_envelope import DisbursementEnvelopeService diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py new file mode 100644 index 0000000..5cc1bc6 --- /dev/null +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py @@ -0,0 +1,66 @@ +import logging +import uuid +from datetime import datetime + +from fastapi import UploadFile +from openg2p_fastapi_common.context import dbengine +from openg2p_fastapi_common.service import BaseService +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.models import AccountStatement, AccountStatementLob +from openg2p_g2p_bridge_models.schemas import AccountStatementResponse, ResponseStatus +from sqlalchemy.ext.asyncio import async_sessionmaker + +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) + + +class AccountStatementService(BaseService): + async def upload_mt940(self, statement_file: UploadFile) -> str: + try: + statement_file = await statement_file.read() + except Exception as e: + _logger.error(f"Error reading file {statement_file.filename}: {str(e)}") + raise e + + statement_id = str(uuid.uuid4()) + statement_date = datetime.utcnow() + + session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) + async with session_maker() as session: + statement = AccountStatement( + statement_id=statement_id, + statement_date=statement_date, + active=True, + ) + session.add(statement) + + statement_lob = AccountStatementLob( + statement_id=statement_id, + statement_lob=str(statement_file), + active=True, + ) + session.add(statement_lob) + + await session.commit() + + return statement_id + + async def construct_account_statement_success_response( + self, statement_id: str + ) -> AccountStatementResponse: + return AccountStatementResponse( + response_status=ResponseStatus.SUCCESS, + statement_id=statement_id, + error_code="", + ) + + async def construct_account_statement_error_response( + self, code: G2PBridgeErrorCodes + ) -> AccountStatementResponse: + return AccountStatementResponse( + response_status=ResponseStatus.FAILURE, + statement_id="", + error_code=code.value, + ) From 34bb4f4214f03d3398d51868c50a17a83eb3fc71 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 1 Aug 2024 08:46:58 +0530 Subject: [PATCH 25/39] Fix DisbursementEnvelopeBatchStatus --- .../services/disbursement_envelope.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py index ff495d2..83e9d4f 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py @@ -192,11 +192,12 @@ async def construct_disbursement_envelope( disbursement_envelope: DisbursementEnvelope = DisbursementEnvelope( disbursement_envelope_id=str(int(time.time() * 1000)), benefit_program_mnemonic=disbursement_envelope_payload.benefit_program_mnemonic, - disbursement_frequency=disbursement_envelope_payload.disbursement_frequency.value, + disbursement_frequency=disbursement_envelope_payload.disbursement_frequency, cycle_code_mnemonic=disbursement_envelope_payload.cycle_code_mnemonic, number_of_beneficiaries=disbursement_envelope_payload.number_of_beneficiaries, number_of_disbursements=disbursement_envelope_payload.number_of_disbursements, total_disbursement_amount=disbursement_envelope_payload.total_disbursement_amount, + disbursement_currency_code=disbursement_envelope_payload.disbursement_currency_code, disbursement_schedule_date=disbursement_envelope_payload.disbursement_schedule_date, receipt_time_stamp=datetime.utcnow(), cancellation_status=CancellationStatus.Not_Cancelled.value, @@ -231,10 +232,10 @@ async def construct_disbursement_envelope_batch_status( funds_available_with_bank=FundsAvailableWithBankEnum.PENDING_CHECK.value, funds_available_latest_timestamp=datetime.utcnow(), funds_available_latest_error_code="", - funds_available_retries=0, + funds_available_attempts=0, funds_blocked_with_bank=FundsBlockedWithBankEnum.PENDING_CHECK.value, funds_blocked_latest_timestamp=datetime.utcnow(), - funds_blocked_retries=0, + funds_blocked_attempts=0, funds_blocked_latest_error_code="", active=True, id_mapper_resolution_required=benefit_program_configuration.id_mapper_resolution_required, From 9ff021b3033df0ba82b93a4892ac640cc96faa69 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 1 Aug 2024 08:47:20 +0530 Subject: [PATCH 26/39] add fa_type to FAKeys --- .../helpers/resolve_helper.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py index 4b2411f..f502f83 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py @@ -28,6 +28,7 @@ class FAKeys(enum.Enum): mobile_wallet_provider = "mobile_wallet_provider" email_address = "email_address" email_wallet_provider = "email_wallet_provider" + fa_type = "fa_type" class KeyValuePair(BaseModel): @@ -85,15 +86,16 @@ def deconstruct_fa(self, fa: str) -> dict: deconstruct_strategy = self.get_deconstruct_strategy(fa) if deconstruct_strategy: deconstructed_pairs = self._deconstruct(fa, deconstruct_strategy) - deconstructed_fa = {pair.key: pair.value for pair in deconstructed_pairs} + deconstructed_fa = {pair.key.value: pair.value for pair in deconstructed_pairs} return deconstructed_fa return {} + # TODO: Update this method to return the correct deconstruct strategy based on the FA type KEY Val pair def get_deconstruct_strategy(self, fa: str) -> str: - if fa.startswith(MapperResolvedFaType.BANK_ACCOUNT.value): + if fa.endswith(MapperResolvedFaType.BANK_ACCOUNT.value): return _config.bank_fa_deconstruct_strategy - elif fa.startswith(MapperResolvedFaType.MOBILE_WALLET.value): + elif fa.endswith(MapperResolvedFaType.MOBILE_WALLET.value): return _config.mobile_wallet_fa_deconstruct_strategy - elif fa.startswith(MapperResolvedFaType.EMAIL_WALLET.value): + elif fa.endswith(MapperResolvedFaType.EMAIL_WALLET.value): return _config.email_wallet_fa_deconstruct_strategy return "" From d780189856f4cddd9da0692ed86171fd67983771 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 1 Aug 2024 10:05:58 +0530 Subject: [PATCH 27/39] Update error codes and exceptions --- .../src/openg2p_g2p_bridge_models/errors/__init__.py | 2 -- .../src/openg2p_g2p_bridge_models/errors/codes.py | 8 ++++++++ .../src/openg2p_g2p_bridge_models/errors/exceptions.py | 7 +++++++ .../src/openg2p_g2p_bridge_models/schemas/__init__.py | 1 + .../schemas/account_statement.py | 8 ++++++++ .../schemas/disbursement_envelope.py | 1 + 6 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/account_statement.py diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py index 22b18dc..e69de29 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/__init__.py @@ -1,2 +0,0 @@ -# from .codes import G2PBridgeErrorCodes -# from .exceptions import DisbursementEnvelopeException, DisbursementException diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/codes.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/codes.py index 354282a..ffaedee 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/codes.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/codes.py @@ -37,3 +37,11 @@ class G2PBridgeErrorCodes(enum.Enum): "DISBURSEMENT_ENVELOPE_SCHEDULE_DATE_REACHED" ) DISBURSEMENT_ALREADY_CANCELED = "DISBURSEMENT_ALREADY_CANCELED" + + # Account Statement Errors + STATEMENT_UPLOAD_ERROR = "STATEMENT_UPLOAD_ERROR" + + # Disbursement Recon Errors + DUPLICATE_DISBURSEMENT = "DUPLICATE_DISBURSEMENT" + INVALID_REVERSAL = "INVALID_REVERSAL" + INVALID_ACCOUNT_NUMBER = "INVALID_ACCOUNT_NUMBER" diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/exceptions.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/exceptions.py index ca23761..b9c01c7 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/exceptions.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/errors/exceptions.py @@ -22,3 +22,10 @@ def __init__( self.message: Optional[str] = message self.disbursement_payloads: List[DisbursementPayload] = disbursement_payloads super().__init__(code, self.message) + + +class AccountStatementException(Exception): + def __init__(self, code: G2PBridgeErrorCodes, message: Optional[str] = None): + self.code: G2PBridgeErrorCodes = code + self.message: Optional[str] = message + super().__init__(self.message) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py index d6b770d..dae3d03 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/__init__.py @@ -1,3 +1,4 @@ +from .account_statement import AccountStatementResponse from .disbursement import ( DisbursementPayload, DisbursementRequest, diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/account_statement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/account_statement.py new file mode 100644 index 0000000..1a0230c --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/account_statement.py @@ -0,0 +1,8 @@ +from typing import Optional + +from .response import BridgeResponse + + +class AccountStatementResponse(BridgeResponse): + statement_id: Optional[str] = None + response_error_code: Optional[str] = None diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement_envelope.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement_envelope.py index 68c680a..624bd2c 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement_envelope.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/schemas/disbursement_envelope.py @@ -17,6 +17,7 @@ class DisbursementEnvelopePayload(BaseModel): number_of_beneficiaries: Optional[int] = None number_of_disbursements: Optional[int] = None total_disbursement_amount: Optional[float] = None + disbursement_currency_code: Optional[str] = None disbursement_schedule_date: Optional[datetime.date] = None From db87f5ce410131dde399ebf560dcd25b5cd955aa Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Thu, 1 Aug 2024 10:06:30 +0530 Subject: [PATCH 28/39] Add account statement --- .../models/__init__.py | 8 +- .../models/account_statement.py | 82 +++++++++++++++++++ .../models/common_enums.py | 7 ++ .../models/disbursement.py | 7 +- .../models/disbursement_envelope.py | 3 +- 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py create mode 100644 openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/common_enums.py diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py index 57e974b..4689b41 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/__init__.py @@ -1,4 +1,11 @@ +from .account_statement import ( + AccountStatement, + AccountStatementLob, + DisbursementErrorRecon, + DisbursementRecon, +) from .benefit_program_configuration import BenefitProgramConfiguration +from .common_enums import ProcessStatus from .disbursement import ( BankDisbursementBatchStatus, Disbursement, @@ -7,7 +14,6 @@ MapperResolutionBatchStatus, MapperResolutionDetails, MapperResolvedFaType, - ProcessStatus, ) from .disbursement_envelope import ( CancellationStatus, diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py new file mode 100644 index 0000000..29c7b51 --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py @@ -0,0 +1,82 @@ +from datetime import datetime + +from openg2p_fastapi_common.models import BaseORMModelWithTimes +from sqlalchemy import Boolean, DateTime, Integer, String, Text +from sqlalchemy import Enum as SqlEnum +from sqlalchemy.orm import Mapped, mapped_column + +from ..errors.codes import G2PBridgeErrorCodes +from .common_enums import ProcessStatus + + +class AccountStatement(BaseORMModelWithTimes): + __tablename__ = "account_statements" + statement_id: Mapped[str] = mapped_column(String, unique=True, index=True) + statement_date: Mapped[datetime] = mapped_column(DateTime) + account_number: Mapped[str] = mapped_column(String, nullable=True) + reference_number: Mapped[str] = mapped_column(String, nullable=True) + statement_number: Mapped[str] = mapped_column(String, nullable=True) + sequence_number: Mapped[str] = mapped_column(String, nullable=True) + statement_upload_timestamp: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow + ) + statement_process_status: Mapped[ProcessStatus] = mapped_column( + SqlEnum(ProcessStatus), default=ProcessStatus.PENDING + ) + statement_process_timestamp: Mapped[datetime] = mapped_column( + DateTime, nullable=True, default=None + ) + statement_process_error_code: Mapped[str] = mapped_column( + String, nullable=True, default=None + ) + statement_process_attempts: Mapped[int] = mapped_column(Integer, default=0) + + +class AccountStatementLob(BaseORMModelWithTimes): + __tablename__ = "account_statement_lobs" + statement_id: Mapped[str] = mapped_column(String, unique=True, index=True) + statement_lob: Mapped[str] = mapped_column(Text) + + +class DisbursementRecon(BaseORMModelWithTimes): + __tablename__ = "disbursement_recons" + bank_disbursement_batch_id: Mapped[str] = mapped_column(String, index=True) + disbursement_id: Mapped[str] = mapped_column(String, index=True, unique=True) + beneficiary_name_from_bank: Mapped[str] = mapped_column(String, nullable=True) + + remittance_reference_number: Mapped[str] = mapped_column( + String, nullable=True, unique=True + ) + remittance_statement_id: Mapped[str] = mapped_column(String, nullable=True) + remittance_statement_number: Mapped[str] = mapped_column(String, nullable=True) + remittance_statement_sequence: Mapped[str] = mapped_column(String, nullable=True) + remittance_entry_sequence: Mapped[str] = mapped_column(String, nullable=True) + remittance_entry_date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + remittance_value_date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + + reversal_found: Mapped[bool] = mapped_column(Boolean, default=False) + reversal_statement_id: Mapped[str] = mapped_column(String, nullable=True) + reversal_statement_number: Mapped[str] = mapped_column(String, nullable=True) + reversal_statement_sequence: Mapped[str] = mapped_column(String, nullable=True) + reversal_entry_sequence: Mapped[str] = mapped_column(String, nullable=True) + reversal_entry_date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + reversal_value_date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + reversal_reason: Mapped[str] = mapped_column(String, nullable=True) + + +class DisbursementErrorRecon(BaseORMModelWithTimes): + __tablename__ = "disbursement_error_recons" + + statement_id: Mapped[str] = mapped_column(String, nullable=True, index=True) + statement_number: Mapped[str] = mapped_column(String, nullable=True) + statement_sequence: Mapped[str] = mapped_column(String, nullable=True) + entry_sequence: Mapped[str] = mapped_column(String, nullable=True) + entry_date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + value_date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + error_reason: Mapped[G2PBridgeErrorCodes] = mapped_column( + SqlEnum(G2PBridgeErrorCodes), nullable=True + ) + disbursement_id: Mapped[str] = mapped_column(String, index=True) + bank_reference_number: Mapped[str] = mapped_column( + String, nullable=True + ) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/common_enums.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/common_enums.py new file mode 100644 index 0000000..71a42da --- /dev/null +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/common_enums.py @@ -0,0 +1,7 @@ +import enum + + +class ProcessStatus(enum.Enum): + PENDING = "PENDING" + PROCESSED = "PROCESSED" + ERROR = "ERROR" diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py index 655ee5e..f6ffe14 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement.py @@ -6,6 +6,8 @@ from sqlalchemy import Enum as SqlEnum from sqlalchemy.orm import Mapped, mapped_column +from .common_enums import ProcessStatus + class DisbursementCancellationStatus(Enum): NOT_CANCELLED = "NOT_CANCELLED" @@ -18,11 +20,6 @@ class MapperResolvedFaType(Enum): EMAIL_WALLET = "EMAIL_WALLET" -class ProcessStatus(Enum): - PENDING = "PENDING" - PROCESSED = "PROCESSED" - - class Disbursement(BaseORMModelWithTimes): __tablename__ = "disbursements" disbursement_id: Mapped[str] = mapped_column( diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py index 6afda90..52daf11 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py @@ -1,5 +1,6 @@ from datetime import datetime from enum import Enum +from sqlalchemy import Enum as SqlEnum from openg2p_fastapi_common.models import BaseORMModelWithTimes from sqlalchemy import Boolean, Date, DateTime, Integer, String @@ -41,7 +42,7 @@ class DisbursementEnvelope(BaseORMModelWithTimes): __tablename__ = "disbursement_envelopes" disbursement_envelope_id: Mapped[str] = mapped_column(String, unique=True) benefit_program_mnemonic: Mapped[str] = mapped_column(String) - disbursement_frequency: Mapped[DisbursementFrequency] = mapped_column(String) + disbursement_frequency: Mapped[DisbursementFrequency] = mapped_column(SqlEnum(DisbursementFrequency)) cycle_code_mnemonic: Mapped[str] = mapped_column(String) number_of_beneficiaries: Mapped[int] = mapped_column(Integer) number_of_disbursements: Mapped[int] = mapped_column(Integer) From 2f1baa14400b1e3ac7a2fdec1efb56a6246ee859 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 2 Aug 2024 16:13:24 +0530 Subject: [PATCH 29/39] Add logger to bridge API --- .../services/account_statement.py | 5 +- .../services/disbursement.py | 53 ++++++++++++++++--- .../services/disbursement_envelope.py | 37 +++++++++++-- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py index 5cc1bc6..f72dfae 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py @@ -18,6 +18,7 @@ class AccountStatementService(BaseService): async def upload_mt940(self, statement_file: UploadFile) -> str: + _logger.info(f"Uploading statement file") try: statement_file = await statement_file.read() except Exception as e: @@ -44,12 +45,13 @@ async def upload_mt940(self, statement_file: UploadFile) -> str: session.add(statement_lob) await session.commit() - + _logger.info(f"Statement file uploaded successfully") return statement_id async def construct_account_statement_success_response( self, statement_id: str ) -> AccountStatementResponse: + _logger.info(f"Constructing account statement success response") return AccountStatementResponse( response_status=ResponseStatus.SUCCESS, statement_id=statement_id, @@ -59,6 +61,7 @@ async def construct_account_statement_success_response( async def construct_account_statement_error_response( self, code: G2PBridgeErrorCodes ) -> AccountStatementResponse: + _logger.error(f"Constructing account statement error response") return AccountStatementResponse( response_status=ResponseStatus.FAILURE, statement_id="", diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py index 542b5f7..b17ed81 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement.py @@ -38,6 +38,7 @@ class DisbursementService(BaseService): async def create_disbursements( self, disbursement_request: DisbursementRequest ) -> List[DisbursementPayload]: + _logger.info("Creating Disbursements") session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: try: @@ -46,12 +47,14 @@ async def create_disbursements( disbursement_payloads=disbursement_request.request_payload, ) except DisbursementException as e: + _logger.error(f"Error validating disbursement envelope: {str(e)}") raise e is_error_free = await self.validate_disbursement_request( disbursement_payloads=disbursement_request.request_payload ) if not is_error_free: + _logger.error("Error validating disbursement request") raise DisbursementException( code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, disbursement_payloads=disbursement_request.request_payload, @@ -105,10 +108,11 @@ async def create_disbursements( session.add(bank_disbursement_batch_status) await session.commit() - + _logger.info("Disbursements Created Successfully!") return disbursement_request.request_payload async def update_disbursement_envelope_batch_status(self, disbursements, session): + _logger.info("Updating Disbursement Envelope Batch Status") disbursement_envelope_batch_status = ( ( await session.execute( @@ -127,11 +131,13 @@ async def update_disbursement_envelope_batch_status(self, disbursements, session disbursement_envelope_batch_status.total_disbursement_amount_received += sum( [disbursement.disbursement_amount for disbursement in disbursements] ) + _logger.info("Disbursement Envelope Batch Status Updated!") return disbursement_envelope_batch_status async def construct_disbursements( self, disbursement_payloads: List[DisbursementPayload] ) -> List[Disbursement]: + _logger.info("Constructing Disbursements") disbursements: List[Disbursement] = [] for disbursement_payload in disbursement_payloads: disbursement = Disbursement( @@ -148,11 +154,13 @@ async def construct_disbursements( disbursement_payload.id = disbursement.id disbursement_payload.disbursement_id = disbursement.disbursement_id disbursements.append(disbursement) + _logger.info("Disbursements Constructed!") return disbursements async def construct_disbursement_batch_controls( self, disbursements: List[Disbursement] ): + _logger.info("Constructing Disbursement Batch Controls") disbursement_batch_controls = [] mapper_resolution_batch_id = str(uuid.uuid4()) bank_disbursement_batch_id = str(uuid.uuid4()) @@ -166,11 +174,13 @@ async def construct_disbursement_batch_controls( active=True, ) disbursement_batch_controls.append(disbursement_batch_control) + _logger.info("Disbursement Batch Controls Constructed!") return disbursement_batch_controls async def validate_disbursement_request( self, disbursement_payloads: List[DisbursementPayload] ): + _logger.info("Validating Disbursement Request") absolutely_no_error = True for disbursement_payload in disbursement_payloads: @@ -204,12 +214,13 @@ async def validate_disbursement_request( if len(disbursement_payload.response_error_codes) > 0: absolutely_no_error = False - + _logger.info("Disbursement Request Validated!") return absolutely_no_error async def validate_disbursement_envelope( self, session, disbursement_payloads: List[DisbursementPayload] ): + _logger.info("Validating Disbursement Envelope") disbursement_envelope_id = disbursement_payloads[0].disbursement_envelope_id if not all( disbursement_payload.disbursement_envelope_id == disbursement_envelope_id @@ -232,12 +243,14 @@ async def validate_disbursement_envelope( .first() ) if not disbursement_envelope: + _logger.error("Disbursement Envelope Not Found!") raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, disbursement_payloads, ) if disbursement_envelope.cancellation_status == CancellationStatus.Cancelled: + _logger.error("Disbursement Envelope Already Canceled!") raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, disbursement_payloads, @@ -274,6 +287,7 @@ async def validate_disbursement_envelope( no_of_disbursements_after_this_request > disbursement_envelope.number_of_disbursements ): + _logger.error("Number of Disbursements Exceeds Declared!") raise DisbursementException( G2PBridgeErrorCodes.NO_OF_DISBURSEMENTS_EXCEEDS_DECLARED, disbursement_payloads, @@ -287,7 +301,7 @@ async def validate_disbursement_envelope( G2PBridgeErrorCodes.TOTAL_DISBURSEMENT_AMOUNT_EXCEEDS_DECLARED, disbursement_payloads, ) - + _logger.info("Disbursement Envelope Validated!") return True async def construct_disbursement_error_response( @@ -295,27 +309,30 @@ async def construct_disbursement_error_response( code: G2PBridgeErrorCodes, disbursement_payloads: List[DisbursementPayload], ) -> DisbursementResponse: + _logger.info("Constructing Disbursement Error Response") disbursement_response: DisbursementResponse = DisbursementResponse( response_status=ResponseStatus.FAILURE, response_payload=disbursement_payloads, response_error_code=code.value, ) - + _logger.info("Disbursement Error Response Constructed!") return disbursement_response async def construct_disbursement_success_response( self, disbursement_payloads: List[DisbursementPayload] ) -> DisbursementResponse: + _logger.info("Constructing Disbursement Success Response") disbursement_response: DisbursementResponse = DisbursementResponse( response_status=ResponseStatus.SUCCESS, response_payload=disbursement_payloads, ) - + _logger.info("Disbursement Success Response Constructed!") return disbursement_response async def cancel_disbursements( self, disbursement_request: DisbursementRequest ) -> List[DisbursementPayload]: + _logger.info("Cancelling Disbursements") session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: is_payload_valid = await self.validate_request_payload( @@ -323,6 +340,7 @@ async def cancel_disbursements( ) if not is_payload_valid: + _logger.error("Error validating disbursement request") raise DisbursementException( code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, disbursement_payloads=disbursement_request.request_payload, @@ -332,6 +350,7 @@ async def cancel_disbursements( Disbursement ] = await self.fetch_disbursements_from_db(disbursement_request, session) if not disbursements_in_db: + _logger.error("Disbursements not found in DB") raise DisbursementException( code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID, disbursement_payloads=disbursement_request.request_payload, @@ -342,6 +361,7 @@ async def cancel_disbursements( disbursements_in_db, disbursement_request.request_payload ) except DisbursementException as e: + _logger.error(f"Error checking for single envelope: {str(e)}") raise e try: @@ -351,6 +371,9 @@ async def cancel_disbursements( session=session, ) except DisbursementException as e: + _logger.error( + f"Error validating envelope for disbursement cancellation: {str(e)}" + ) raise e invalid_disbursements_exist = await self.check_for_invalid_disbursements( @@ -398,26 +421,30 @@ async def cancel_disbursements( session.add_all(disbursements_in_db) session.add(disbursement_envelope_batch_status) await session.commit() - + _logger.info("Disbursements Cancelled Successfully!") return disbursement_request.request_payload async def check_for_single_envelope( self, disbursements_in_db, disbursement_payloads ): + _logger.info("Checking for Single Envelope") disbursement_envelope_ids = { disbursement.disbursement_envelope_id for disbursement in disbursements_in_db } if len(disbursement_envelope_ids) > 1: + _logger.error("Multiple Envelopes Found!") raise DisbursementException( G2PBridgeErrorCodes.MULTIPLE_ENVELOPES_FOUND, disbursement_payloads, ) + _logger.info("Single Envelope Found!") return True async def check_for_invalid_disbursements( self, disbursement_request, disbursements_in_db ) -> bool: + _logger.info("Checking for Invalid Disbursements") invalid_disbursements_exist = False for disbursement_payload in disbursement_request.request_payload: if disbursement_payload.disbursement_id not in [ @@ -437,11 +464,13 @@ async def check_for_invalid_disbursements( disbursement_payload.response_error_codes.append( G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED.value ) + _logger.info("Invalid Disbursements Checked!") return invalid_disbursements_exist async def fetch_disbursements_from_db( self, disbursement_request, session ) -> List[Disbursement]: + _logger.info("Fetching Disbursements from DB") disbursements_in_db = ( ( await session.execute( @@ -458,6 +487,7 @@ async def fetch_disbursements_from_db( .scalars() .all() ) + _logger.info("Disbursements Fetched from DB!") return disbursements_in_db async def validate_envelope_for_disbursement_cancellation( @@ -466,6 +496,7 @@ async def validate_envelope_for_disbursement_cancellation( disbursement_payloads: List[DisbursementPayload], session, ): + _logger.info("Validating Envelope for Disbursement Cancellation") disbursement_envelope = ( ( await session.execute( @@ -479,18 +510,21 @@ async def validate_envelope_for_disbursement_cancellation( .first() ) if not disbursement_envelope: + _logger.error("Disbursement Envelope Not Found!") raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, disbursement_payloads, ) if disbursement_envelope.cancellation_status == CancellationStatus.Cancelled: + _logger.error("Disbursement Envelope Already Canceled!") raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, disbursement_payloads, ) if disbursement_envelope.disbursement_schedule_date <= datetime.now().date(): + _logger.error("Disbursement Envelope Schedule Date Reached!") raise DisbursementException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_SCHEDULE_DATE_REACHED, disbursement_payloads, @@ -524,22 +558,25 @@ async def validate_envelope_for_disbursement_cancellation( ) if no_of_disbursements_after_this_request < 0: + _logger.error("Number of Disbursements Less Than Zero!") raise DisbursementException( G2PBridgeErrorCodes.NO_OF_DISBURSEMENTS_LESS_THAN_ZERO, disbursement_payloads, ) if total_disbursement_amount_after_this_request < 0: + _logger.error("Total Disbursement Amount Less Than Zero!") raise DisbursementException( G2PBridgeErrorCodes.TOTAL_DISBURSEMENT_AMOUNT_LESS_THAN_ZERO, disbursement_payloads, ) - + _logger.info("Envelope Validated for Disbursement Cancellation!") return True async def validate_request_payload( self, disbursement_payloads: List[DisbursementPayload] ): + _logger.info("Validating Request Payload") absolutely_no_error = True for disbursement_payload in disbursement_payloads: @@ -554,5 +591,5 @@ async def validate_request_payload( if len(disbursement_payload.response_error_codes) > 0: absolutely_no_error = False - + _logger.info("Request Payload Validated!") return absolutely_no_error diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py index 83e9d4f..fb0857e 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py @@ -1,5 +1,6 @@ import time from datetime import datetime +import logging from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService @@ -23,11 +24,17 @@ from sqlalchemy.ext.asyncio import async_sessionmaker from sqlalchemy.future import select +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) + class DisbursementEnvelopeService(BaseService): async def create_disbursement_envelope( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> DisbursementEnvelopePayload: + _logger.info(f"Creating disbursement envelope") session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: try: @@ -56,12 +63,13 @@ async def create_disbursement_envelope( disbursement_envelope_payload.disbursement_envelope_id = ( disbursement_envelope.disbursement_envelope_id ) - + _logger.info(f"Disbursement envelope created successfully") return disbursement_envelope_payload async def cancel_disbursement_envelope( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> DisbursementEnvelopePayload: + _logger.info(f"Cancelling disbursement envelope") session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: disbursement_envelope_payload: DisbursementEnvelopePayload = ( @@ -81,6 +89,9 @@ async def cancel_disbursement_envelope( ).scalar() if disbursement_envelope is None: + _logger.error( + f"Disbursement envelope with ID {disbursement_envelope_id} not found" + ) raise DisbursementEnvelopeException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND ) @@ -89,6 +100,9 @@ async def cancel_disbursement_envelope( disbursement_envelope.cancellation_status == CancellationStatus.Cancelled.value ): + _logger.error( + f"Disbursement envelope with ID {disbursement_envelope_id} already cancelled" + ) raise DisbursementEnvelopeException( G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED ) @@ -99,36 +113,40 @@ async def cancel_disbursement_envelope( disbursement_envelope.cancellation_timestamp = datetime.utcnow() await session.commit() - + _logger.info(f"Disbursement envelope cancelled successfully") return disbursement_envelope_payload async def construct_disbursement_envelope_success_response( self, disbursement_envelope_payload: DisbursementEnvelopePayload ) -> DisbursementEnvelopeResponse: + _logger.info(f"Constructing disbursement envelope success response") disbursement_envelope_response: DisbursementEnvelopeResponse = ( DisbursementEnvelopeResponse( response_status=ResponseStatus.SUCCESS, response_payload=disbursement_envelope_payload, ) ) + _logger.info(f"Disbursement envelope success response constructed") return disbursement_envelope_response async def construct_disbursement_envelope_error_response( self, error_code: G2PBridgeErrorCodes ) -> DisbursementEnvelopeResponse: + _logger.error(f"Constructing disbursement envelope error response") disbursement_envelope_response: DisbursementEnvelopeResponse = ( DisbursementEnvelopeResponse( response_status=ResponseStatus.FAILURE, response_error_code=error_code.value, ) ) - + _logger.error(f"Disbursement envelope error response constructed") return disbursement_envelope_response # noinspection PyMethodMayBeStatic async def validate_envelope_request( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> bool: + _logger.info(f"Validating disbursement envelope request") disbursement_envelope_payload: DisbursementEnvelopePayload = ( disbursement_envelope_request.request_payload ) @@ -136,6 +154,7 @@ async def validate_envelope_request( disbursement_envelope_payload.benefit_program_mnemonic is None or disbursement_envelope_payload.benefit_program_mnemonic == "" ): + _logger.error(f"Invalid benefit program mnemonic") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC ) @@ -143,6 +162,7 @@ async def validate_envelope_request( disbursement_envelope_payload.disbursement_frequency not in DisbursementFrequency ): + _logger.error(f"Invalid disbursement frequency") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_DISBURSEMENT_FREQUENCY ) @@ -150,6 +170,7 @@ async def validate_envelope_request( disbursement_envelope_payload.cycle_code_mnemonic is None or disbursement_envelope_payload.cycle_code_mnemonic == "" ): + _logger.error(f"Invalid cycle code mnemonic") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_CYCLE_CODE_MNEMONIC ) @@ -157,6 +178,7 @@ async def validate_envelope_request( disbursement_envelope_payload.number_of_beneficiaries is None or disbursement_envelope_payload.number_of_beneficiaries < 1 ): + _logger.error(f"Invalid number of beneficiaries") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_NO_OF_BENEFICIARIES ) @@ -164,6 +186,7 @@ async def validate_envelope_request( disbursement_envelope_payload.number_of_disbursements is None or disbursement_envelope_payload.number_of_disbursements < 1 ): + _logger.error(f"Invalid number of disbursements") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_NO_OF_DISBURSEMENTS ) @@ -171,6 +194,7 @@ async def validate_envelope_request( disbursement_envelope_payload.total_disbursement_amount is None or disbursement_envelope_payload.total_disbursement_amount < 0 ): + _logger.error(f"Invalid total disbursement amount") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_TOTAL_DISBURSEMENT_AMOUNT ) @@ -179,16 +203,18 @@ async def validate_envelope_request( or disbursement_envelope_payload.disbursement_schedule_date < datetime.date(datetime.utcnow()) # TODO: Add a delta of x days ): + _logger.error(f"Invalid disbursement schedule date") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_DISBURSEMENT_SCHEDULE_DATE ) - + _logger.info(f"Disbursement envelope request validated successfully") return True # noinspection PyMethodMayBeStatic async def construct_disbursement_envelope( self, disbursement_envelope_payload: DisbursementEnvelopePayload ) -> DisbursementEnvelope: + _logger.info(f"Constructing disbursement envelope") disbursement_envelope: DisbursementEnvelope = DisbursementEnvelope( disbursement_envelope_id=str(int(time.time() * 1000)), benefit_program_mnemonic=disbursement_envelope_payload.benefit_program_mnemonic, @@ -207,12 +233,14 @@ async def construct_disbursement_envelope( disbursement_envelope_payload.disbursement_envelope_id = ( disbursement_envelope.disbursement_envelope_id ) + _logger.info(f"Disbursement envelope constructed successfully") return disbursement_envelope # noinspection PyMethodMayBeStatic async def construct_disbursement_envelope_batch_status( self, disbursement_envelope: DisbursementEnvelope, session ) -> DisbursementEnvelopeBatchStatus: + _logger.info(f"Constructing disbursement envelope batch status") benefit_program_configuration: BenefitProgramConfiguration = ( ( await session.execute( @@ -240,4 +268,5 @@ async def construct_disbursement_envelope_batch_status( active=True, id_mapper_resolution_required=benefit_program_configuration.id_mapper_resolution_required, ) + _logger.info(f"Disbursement envelope batch status constructed successfully") return disbursement_envelope_batch_status From 3b5c9d5aa7d01a6545705142e0aa83c01452abb6 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 2 Aug 2024 16:13:44 +0530 Subject: [PATCH 30/39] Add G2P Bridge Celery Tasks --- .../openg2p_g2p_bridge_celery_tasks/config.py | 12 +- .../helpers/resolve_helper.py | 1 - .../tasks/__init__.py | 1 + .../tasks/disburse_funds_from_bank.py | 7 +- .../tasks/mt940_processor.py | 377 ++++++++++++++++++ 5 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py index b084fb4..a6ebba1 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py @@ -26,12 +26,14 @@ class Settings(BaseSettings): funds_available_check_attempts: int = 3 funds_blocked_attempts: int = 3 funds_disbursement_attempts: int = 3 + statement_process_attempts: int = 3 - mapper_resolve_frequency: int = 3600 - funds_available_check_frequency: int = 3600 - funds_blocked_frequency: int = 3600 - funds_disbursement_frequency: int = 3600 + mapper_resolve_frequency: int = 10 + funds_available_check_frequency: int = 10 + funds_blocked_frequency: int = 10 + funds_disbursement_frequency: int = 10 + statement_process_frequency: int = 3600 - bank_deconstruct_strategy: str = "" + bank_fa_deconstruct_strategy: str = "" mobile_wallet_deconstruct_strategy: str = "" email_wallet_deconstruct_strategy: str = "" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py index f502f83..006864b 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py @@ -90,7 +90,6 @@ def deconstruct_fa(self, fa: str) -> dict: return deconstructed_fa return {} - # TODO: Update this method to return the correct deconstruct strategy based on the FA type KEY Val pair def get_deconstruct_strategy(self, fa: str) -> str: if fa.endswith(MapperResolvedFaType.BANK_ACCOUNT.value): return _config.bank_fa_deconstruct_strategy diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py index a14d7f6..88d571f 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py @@ -14,3 +14,4 @@ mapper_resolution_beat_producer, mapper_resolution_worker, ) +from .mt940_processor import mt940_processor_beat_producer, mt940_processor_worker diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py index eb0027f..d32228f 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py @@ -4,7 +4,7 @@ from openg2p_g2p_bridge_bank_connectors.bank_connectors import BankConnectorFactory from openg2p_g2p_bridge_bank_connectors.bank_interface.bank_connector_interface import ( BankConnectorInterface, - PaymentPayload, + DisbursementPaymentPayload, PaymentStatus, ) from openg2p_g2p_bridge_models.models import ( @@ -163,7 +163,7 @@ def disburse_funds_from_bank_worker(bank_disbursement_batch_id: str): ) payment_payloads.append( - PaymentPayload( + DisbursementPaymentPayload( remitting_account=benefit_program_configuration.sponsor_bank_account_number, remitting_account_currency=benefit_program_configuration.sponsor_bank_account_currency, payment_amount=disbursement.disbursement_amount, @@ -178,7 +178,7 @@ def disburse_funds_from_bank_worker(bank_disbursement_batch_id: str): beneficiary_branch_code=mapper_details.branch_code if mapper_details else None, - payment_date=datetime.utcnow(), + payment_date=str(datetime.date(datetime.utcnow())), beneficiary_id=disbursement.beneficiary_id, beneficiary_name=disbursement.beneficiary_name, beneficiary_account_type=mapper_details.mapper_resolved_fa_type, @@ -194,6 +194,7 @@ def disburse_funds_from_bank_worker(bank_disbursement_batch_id: str): beneficiary_email=mapper_details.email_address if mapper_details else None, + disbursement_narrative=disbursement.narrative, benefit_program_mnemonic=envelope.benefit_program_mnemonic, cycle_code_mnemonic=envelope.cycle_code_mnemonic, ) diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py new file mode 100644 index 0000000..74e549e --- /dev/null +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py @@ -0,0 +1,377 @@ +import logging +from datetime import datetime + +import mt940 +from openg2p_g2p_bridge_bank_connectors.bank_connectors import BankConnectorFactory +from openg2p_g2p_bridge_bank_connectors.bank_interface.bank_connector_interface import ( + BankConnectorInterface, +) +from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes +from openg2p_g2p_bridge_models.models import ( + AccountStatement, + AccountStatementLob, + BenefitProgramConfiguration, + DisbursementBatchControl, + DisbursementErrorRecon, + DisbursementRecon, + ProcessStatus, +) +from sqlalchemy import and_, select +from sqlalchemy.orm import sessionmaker + +from ..app import celery_app, get_engine +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) +_engine = get_engine() + + +@celery_app.task(name="mt940_processor_beat_producer") +def mt940_processor_beat_producer(): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + with session_maker() as session: + account_statements = ( + session.execute( + select(AccountStatement).filter( + and_( + AccountStatement.statement_process_status + == ProcessStatus.PENDING, + AccountStatement.statement_process_attempts + < _config.statement_process_attempts, + ) + ) + ) + .scalars() + .all() + ) + + for statement in account_statements: + mt940_processor_worker.delay(statement.statement_id) + + +@celery_app.task(name="mt940_processor_worker") +def mt940_processor_worker(statement_id: str): + session_maker = sessionmaker(bind=_engine, expire_on_commit=False) + + with session_maker() as session: + account_statement = ( + session.query(AccountStatement) + .filter(AccountStatement.statement_id == statement_id) + .first() + ) + + if not account_statement: + return + + lob = ( + session.query(AccountStatementLob) + .filter(AccountStatementLob.statement_id == statement_id) + .first() + ) + + if not lob: + return + + try: + # Set BalanceBase scope to Transaction + mt940.tags.BalanceBase.scope = mt940.models.Transaction + + # Parsing header section + account_number_parser = mt940.tags.AccountIdentification() + statement_number_parser = mt940.tags.StatementNumber() + transaction_reference_parser = mt940.tags.TransactionReferenceNumber() + + statement_parser = mt940.tags.Statement() + + mt940_statement = mt940.models.Transactions( + processors={ + "pre_statement": [mt940.processors.add_currency_pre_processor("")], + }, + tags={ + account_number_parser.id: account_number_parser, + statement_number_parser.id: statement_number_parser, + transaction_reference_parser.id: transaction_reference_parser, + statement_parser.id: statement_parser, + }, + ) + + mt940_statement.parse(lob.statement_lob) + + account_statement.account_number = mt940_statement.data.get( + "account_number", "" + ) + account_statement.reference_number = mt940_statement.data.get( + "reference", "" + ) + statement_number_and_sequence = mt940_statement.data.get( + "number", "" + ).split("/") + account_statement.statement_number = ( + statement_number_and_sequence[0] + if statement_number_and_sequence + else "" + ) + account_statement.sequence_number = ( + statement_number_and_sequence[1] + if len(statement_number_and_sequence) > 1 + else "" + ) + + # TODO: Refactor code + + # Get the benefit program configuration + benefit_program_configuration = ( + session.query(BenefitProgramConfiguration) + .filter( + BenefitProgramConfiguration.sponsor_bank_account_number + == account_statement.account_number + ) + .first() + ) + + if not benefit_program_configuration: + account_statement.statement_process_status = ProcessStatus.ERROR + account_statement.statement_process_error_code = ( + G2PBridgeErrorCodes.INVALID_ACCOUNT_NUMBER + ) + account_statement.statement_process_timestamp = datetime.utcnow() + account_statement.statement_process_attempts += 1 + session.commit() + return + + bank_connector: BankConnectorInterface = ( + BankConnectorFactory.get_component().get_bank_connector( + benefit_program_configuration.sponsor_bank_code + ) + ) + + # Parsing transactions + parsed_transactions = [] + entry_sequence = 0 + for transaction in mt940_statement.transactions: + entry_sequence += 1 + debit_credit_indicator = transaction.data["status"] + + if debit_credit_indicator in ["D", "RD"]: + parsed_transaction = construct_parsed_transaction( + bank_connector, + debit_credit_indicator, + entry_sequence, + transaction, + ) + parsed_transactions.append(parsed_transaction) + + # End of for loop of mt940 statement transactions + + disbursement_error_recons = [] + disbursement_recons = [] + for parsed_transaction in parsed_transactions: + bank_disbursement_batch_id = ( + session.query(DisbursementBatchControl) + .filter( + DisbursementBatchControl.disbursement_id + == parsed_transaction["disbursement_id"] + ) + .first() + .bank_disbursement_batch_id + ) + + if not bank_disbursement_batch_id: + disbursement_error_recons.append( + construct_disbursement_error_recon( + parsed_transaction, + G2PBridgeErrorCodes.INVALID_DISBURSEMENT_ID, + ) + ) + continue + + disbursement_recon = ( + session.query(DisbursementRecon) + .filter( + DisbursementRecon.disbursement_id + == parsed_transaction["disbursement_id"] + ) + .first() + ) + + if ( + disbursement_recon + and parsed_transaction["debit_credit_indicator"] == "D" + ): + disbursement_error_recons.append( + construct_disbursement_error_recon( + parsed_transaction, + G2PBridgeErrorCodes.DUPLICATE_DISBURSEMENT, + ) + ) + continue + + if ( + not disbursement_recon + and parsed_transaction["debit_credit_indicator"] == "RD" + ): + disbursement_error_recons.append( + construct_disbursement_error_recon( + parsed_transaction, G2PBridgeErrorCodes.INVALID_REVERSAL + ) + ) + continue + + if parsed_transaction["debit_credit_indicator"] == "D": + disbursement_recon = construct_new_disbursement_recon( + bank_disbursement_batch_id, + parsed_transaction, + ) + disbursement_recons.append(disbursement_recon) + elif parsed_transaction["debit_credit_indicator"] == "RD": + update_existing_disbursement_recon( + disbursement_recon, parsed_transaction + ) + disbursement_recons.append(disbursement_recon) + + # End of for loop for parsed transactions + + # Update account statement with parsed data + account_statement.statement_process_status = ProcessStatus.PROCESSED + account_statement.statement_process_error_code = None + account_statement.statement_process_timestamp = datetime.utcnow() + account_statement.statement_process_attempts += 1 + + session.add(account_statement) + session.add_all(disbursement_recons) + session.add_all(disbursement_error_recons) + session.commit() + + except Exception as e: + account_statement.statement_process_status = ProcessStatus.PENDING + account_statement.statement_process_error_code = str(e) + account_statement.statement_process_timestamp = datetime.utcnow() + account_statement.statement_process_attempts += 1 + session.commit() + + +def construct_disbursement_error_recon(parsed_transaction, g2p_bridge_error_code): + return DisbursementErrorRecon( + disbursement_id="", + bank_reference_number=parsed_transaction["remittance_reference_number"], + statement_id=parsed_transaction["remittance_statement_number"], + statement_number=parsed_transaction["remittance_statement_number"], + statement_sequence=parsed_transaction["remittance_statement_sequence"], + entry_sequence=parsed_transaction["remittance_entry_sequence"], + entry_date=parsed_transaction["remittance_entry_date"], + value_date=parsed_transaction["remittance_value_date"], + error_reason=g2p_bridge_error_code, + ) + + +def update_existing_disbursement_recon(disbursement_recon, parsed_transaction): + disbursement_recon.reversal_found = True + disbursement_recon.reversal_statement_id = parsed_transaction[ + "reversal_statement_number" + ] + disbursement_recon.reversal_statement_number = parsed_transaction[ + "reversal_statement_number" + ] + disbursement_recon.reversal_statement_sequence = parsed_transaction[ + "reversal_statement_sequence" + ] + disbursement_recon.reversal_entry_sequence = parsed_transaction[ + "reversal_entry_sequence" + ] + disbursement_recon.reversal_entry_date = parsed_transaction["reversal_entry_date"] + disbursement_recon.reversal_value_date = parsed_transaction["reversal_value_date"] + disbursement_recon.reversal_reason = parsed_transaction["reversal_reason"] + + +def construct_new_disbursement_recon( + bank_disbursement_batch_id, parsed_transaction +): + disbursement_recon = DisbursementRecon( + bank_disbursement_batch_id=bank_disbursement_batch_id, + disbursement_id=parsed_transaction["disbursement_id"], + beneficiary_name_from_bank=parsed_transaction["beneficiary_name_from_bank"], + remittance_reference_number=parsed_transaction["remittance_reference_number"], + remittance_statement_id=parsed_transaction["remittance_statement_number"], + remittance_statement_number=parsed_transaction["remittance_statement_number"], + remittance_statement_sequence=parsed_transaction[ + "remittance_statement_sequence" + ], + remittance_entry_sequence=parsed_transaction["remittance_entry_sequence"], + remittance_entry_date=parsed_transaction["remittance_entry_date"], + remittance_value_date=parsed_transaction["remittance_value_date"], + ) + return disbursement_recon + + +def construct_parsed_transaction( + bank_connector, + debit_credit_indicator, + entry_sequence, + transaction, +) -> dict: + parsed_transaction = {} + transaction_amount = transaction.data["amount"].amount + customer_reference = transaction.data["customer_reference"] + remittance_reference_number = transaction.data["bank_reference"] + narratives = transaction.data["transaction_details"].split("\n") + disbursement_id = bank_connector.retrieve_disbursement_id( + remittance_reference_number, customer_reference, narratives + ) + beneficiary_name_from_bank = None + remittance_statement_number = None + remittance_statement_sequence = None + remittance_entry_sequence = None + remittance_entry_date = None + remittance_value_date = None + + reversal_found = False + reversal_statement_number = None + reversal_statement_sequence = None + reversal_entry_sequence = None + reversal_entry_date = None + reversal_value_date = None + reversal_reason = None + + if debit_credit_indicator == "D": + reversal_found = False + beneficiary_name_from_bank = bank_connector.retrieve_beneficiary_name( + narratives + ) + remittance_statement_number = transaction.data["statement_number"] + remittance_statement_sequence = transaction.data["sequence_number"] + remittance_entry_sequence = entry_sequence + remittance_entry_date = transaction.data["entry_date"] + remittance_value_date = transaction.data["date"] + + if debit_credit_indicator == "RD": + reversal_found = True + reversal_statement_number = transaction.data["statement_number"] + reversal_statement_sequence = transaction.data["sequence_number"] + reversal_entry_sequence = entry_sequence + reversal_entry_date = transaction.data["entry_date"] + reversal_value_date = transaction.data["date"] + reversal_reason = bank_connector.retrieve_reversal_reason(narratives) + + parsed_transaction.update( + { + disbursement_id: disbursement_id, + transaction_amount: transaction_amount, + debit_credit_indicator: debit_credit_indicator, + beneficiary_name_from_bank: beneficiary_name_from_bank, + remittance_reference_number: remittance_reference_number, + remittance_statement_number: remittance_statement_number, + remittance_statement_sequence: remittance_statement_sequence, + remittance_entry_sequence: remittance_entry_sequence, + remittance_entry_date: remittance_entry_date, + remittance_value_date: remittance_value_date, + reversal_found: reversal_found, + reversal_statement_number: reversal_statement_number, + reversal_statement_sequence: reversal_statement_sequence, + reversal_entry_sequence: reversal_entry_sequence, + reversal_entry_date: reversal_entry_date, + reversal_value_date: reversal_value_date, + reversal_reason: reversal_reason, + } + ) + return parsed_transaction From fe3861e4dfb5b7445d23b6e3c0fae2f3251e47cd Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 2 Aug 2024 16:15:01 +0530 Subject: [PATCH 31/39] Add G2P Bridge Bank Connectors --- .../bank_connectors/example_bank_connector.py | 90 ++++++++++++++++--- .../bank_interface/__init__.py | 2 +- .../bank_connector_interface.py | 23 +++-- .../config.py | 6 +- .../tests/test_bank_connector.py | 58 +++++++----- 5 files changed, 137 insertions(+), 42 deletions(-) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py index e402fa2..33fd60b 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_connectors/example_bank_connector.py @@ -1,16 +1,17 @@ -from typing import List +from typing import List, Optional import httpx from openg2p_g2p_bridge_models.models import ( FundsAvailableWithBankEnum, FundsBlockedWithBankEnum, ) +from pydantic import BaseModel from ..bank_interface.bank_connector_interface import ( BankConnectorInterface, BlockFundsResponse, CheckFundsResponse, - PaymentPayload, + DisbursementPaymentPayload, PaymentResponse, PaymentStatus, ) @@ -19,6 +20,36 @@ _config = Settings.get_config() +class BankPaymentPayload(BaseModel): + payment_reference_number: str + remitting_account: str + remitting_account_currency: str + payment_amount: float + funds_blocked_reference_number: str + beneficiary_name: str + + beneficiary_account: str + beneficiary_account_currency: str + beneficiary_account_type: str + beneficiary_bank_code: str + beneficiary_branch_code: str + + beneficiary_mobile_wallet_provider: Optional[str] = None + beneficiary_phone_no: Optional[str] = None + + beneficiary_email: Optional[str] = None + beneficiary_email_wallet_provider: Optional[str] = None + + narrative_1: Optional[str] = None + narrative_2: Optional[str] = None + narrative_3: Optional[str] = None + narrative_4: Optional[str] = None + narrative_5: Optional[str] = None + narrative_6: Optional[str] = None + + payment_date: str + + class ExampleBankConnector(BankConnectorInterface): def check_funds(self, account_no, currency, amount) -> CheckFundsResponse: try: @@ -50,9 +81,9 @@ def block_funds(self, account_no, currency, amount) -> BlockFundsResponse: try: with httpx.Client() as client: request_data = { - "account_number": account_no, - "account_currency": currency, - "total_funds_needed": amount, + "account_no": account_no, + "currency": currency, + "amount": amount, } response = client.post( _config.funds_block_url_example_bank, json=request_data @@ -79,15 +110,41 @@ def block_funds(self, account_no, currency, amount) -> BlockFundsResponse: ) def initiate_payment( - self, payment_payloads: List[PaymentPayload] + self, disbursement_payment_payloads: List[DisbursementPaymentPayload] ) -> PaymentResponse: try: with httpx.Client() as client: - request_data = { - "payment_payloads": [ - payload.model_dump() for payload in payment_payloads - ] - } + bank_payment_payloads = [] + for disbursement_payment_payload in disbursement_payment_payloads: + bank_payment_payload: BankPaymentPayload = BankPaymentPayload( + payment_reference_number=disbursement_payment_payload.disbursement_id, + remitting_account=disbursement_payment_payload.remitting_account, + remitting_account_currency=disbursement_payment_payload.remitting_account_currency, + payment_amount=disbursement_payment_payload.payment_amount, + funds_blocked_reference_number=disbursement_payment_payload.funds_blocked_reference_number, + beneficiary_name=disbursement_payment_payload.beneficiary_name, + beneficiary_account=disbursement_payment_payload.beneficiary_account, + beneficiary_account_currency=disbursement_payment_payload.beneficiary_account_currency, + beneficiary_account_type=disbursement_payment_payload.beneficiary_account_type, + beneficiary_bank_code=disbursement_payment_payload.beneficiary_bank_code, + beneficiary_branch_code=disbursement_payment_payload.beneficiary_branch_code, + beneficiary_mobile_wallet_provider=disbursement_payment_payload.beneficiary_mobile_wallet_provider, + beneficiary_phone_no=disbursement_payment_payload.beneficiary_phone_no, + beneficiary_email=disbursement_payment_payload.beneficiary_email, + beneficiary_email_wallet_provider=disbursement_payment_payload.beneficiary_email_wallet_provider, + payment_date=disbursement_payment_payload.payment_date, + narrative_1=disbursement_payment_payload.disbursement_narrative, + narrative_2=disbursement_payment_payload.benefit_program_mnemonic, + narrative_3=disbursement_payment_payload.cycle_code_mnemonic, + narrative_4=disbursement_payment_payload.beneficiary_id, + narrative_5="", + narrative_6="", + active=True, + ) + bank_payment_payloads.append(bank_payment_payload.model_dump()) + + request_data = {"initiate_payment_payloads": bank_payment_payloads} + response = client.post( _config.funds_disbursement_url_example_bank, json=request_data ) @@ -101,3 +158,14 @@ def initiate_payment( ) except httpx.HTTPStatusError as e: return PaymentResponse(status=PaymentStatus.ERROR, error_code=str(e)) + + def retrieve_disbursement_id( + self, bank_reference: str, customer_reference: str, narratives: str + ) -> str: + return customer_reference + + def retrieve_beneficiary_name(self, narratives: str) -> str: + return narratives[0] + + def retrieve_reversal_reason(self, narratives: str) -> str: + return narratives[1] diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py index 8210d48..a7b9cab 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/__init__.py @@ -2,6 +2,6 @@ BankConnectorInterface, BlockFundsResponse, CheckFundsResponse, - PaymentPayload, + DisbursementPaymentPayload, PaymentStatus, ) diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py index 22c3d1c..34e7064 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/bank_interface/bank_connector_interface.py @@ -1,11 +1,11 @@ import enum -from datetime import datetime from typing import List, Optional from openg2p_fastapi_common.service import BaseService from openg2p_g2p_bridge_models.models import ( FundsAvailableWithBankEnum, FundsBlockedWithBankEnum, + MapperResolvedFaType, ) from pydantic import BaseModel @@ -21,7 +21,8 @@ class BlockFundsResponse(BaseModel): error_code: str -class PaymentPayload(BaseModel): +class DisbursementPaymentPayload(BaseModel): + disbursement_id: str remitting_account: str remitting_account_currency: str payment_amount: float @@ -32,7 +33,7 @@ class PaymentPayload(BaseModel): beneficiary_account: Optional[str] = None beneficiary_account_currency: Optional[str] = None - beneficiary_account_type: Optional[str] = None + beneficiary_account_type: Optional[MapperResolvedFaType] = None beneficiary_bank_code: Optional[str] = None beneficiary_branch_code: Optional[str] = None @@ -42,9 +43,10 @@ class PaymentPayload(BaseModel): beneficiary_email: Optional[str] = None beneficiary_email_wallet_provider: Optional[str] = None + disbursement_narrative: Optional[str] = None benefit_program_mnemonic: Optional[str] = None cycle_code_mnemonic: Optional[str] = None - payment_date: datetime + payment_date: str class PaymentStatus(enum.Enum): @@ -65,6 +67,17 @@ def block_funds(self, account_no, currency, amount) -> BlockFundsResponse: raise NotImplementedError() def initiate_payment( - self, payment_payloads: List[PaymentPayload] + self, payment_payloads: List[DisbursementPaymentPayload] ) -> PaymentResponse: raise NotImplementedError() + + def retrieve_disbursement_id( + self, bank_reference: str, customer_reference: str, narratives: str + ) -> str: + raise NotImplementedError() + + def retrieve_beneficiary_name(self, narratives: str) -> str: + raise NotImplementedError() + + def retrieve_reversal_reason(self, narratives: str) -> str: + raise NotImplementedError() diff --git a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py index 4cb6752..6f969b0 100644 --- a/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py +++ b/openg2p-g2p-bridge-bank-connectors/src/openg2p_g2p_bridge_bank_connectors/config.py @@ -9,6 +9,6 @@ class Settings(BaseSettings): db_dbname: str = "openg2p_g2p_bridge_db" - funds_available_check_url_example_bank: int = 3 - funds_block_url_example_bank: int = 3 - funds_disbursement_url_example_bank: int = 3 + funds_available_check_url_example_bank: str = "" + funds_block_url_example_bank: str = "" + funds_disbursement_url_example_bank: str = "" diff --git a/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py b/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py index e2ce670..bc84d45 100644 --- a/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py +++ b/openg2p-g2p-bridge-bank-connectors/tests/test_bank_connector.py @@ -1,10 +1,11 @@ +from datetime import datetime +from unittest.mock import Mock, patch + import pytest -from unittest.mock import patch, Mock from httpx import HTTPStatusError -from datetime import datetime from openg2p_g2p_bridge_bank_connectors.bank_connectors import ExampleBankConnector from openg2p_g2p_bridge_bank_connectors.bank_interface.bank_connector_interface import ( - PaymentPayload, + DisbursementPaymentPayload, PaymentStatus, ) from openg2p_g2p_bridge_models.models import ( @@ -13,15 +14,13 @@ ) - - @pytest.fixture def example_bank_connector(): return ExampleBankConnector() def test_check_funds_success(example_bank_connector): - with patch('httpx.Client.post') as mock_post: + with patch("httpx.Client.post") as mock_post: mock_response = Mock() mock_response.json.return_value = {"status": "success"} mock_post.return_value = mock_response @@ -33,7 +32,7 @@ def test_check_funds_success(example_bank_connector): def test_check_funds_failure(example_bank_connector): - with patch('httpx.Client.post') as mock_post: + with patch("httpx.Client.post") as mock_post: mock_response = Mock() mock_response.json.return_value = {"status": "failure"} mock_post.return_value = mock_response @@ -45,7 +44,10 @@ def test_check_funds_failure(example_bank_connector): def test_check_funds_http_error(example_bank_connector): - with patch('httpx.Client.post', side_effect=HTTPStatusError("Error", request=Mock(), response=Mock())) as mock_post: + with patch( + "httpx.Client.post", + side_effect=HTTPStatusError("Error", request=Mock(), response=Mock()), + ): response = example_bank_connector.check_funds("123456", "USD", 100.0) assert response.status == FundsAvailableWithBankEnum.PENDING_CHECK @@ -53,9 +55,12 @@ def test_check_funds_http_error(example_bank_connector): def test_block_funds_success(example_bank_connector): - with patch('httpx.Client.post') as mock_post: + with patch("httpx.Client.post") as mock_post: mock_response = Mock() - mock_response.json.return_value = {"status": "success", "block_reference_no": "BR123"} + mock_response.json.return_value = { + "status": "success", + "block_reference_no": "BR123", + } mock_post.return_value = mock_response response = example_bank_connector.block_funds("123456", "USD", 100.0) @@ -66,7 +71,7 @@ def test_block_funds_success(example_bank_connector): def test_block_funds_failure(example_bank_connector): - with patch('httpx.Client.post') as mock_post: + with patch("httpx.Client.post") as mock_post: mock_response = Mock() mock_response.json.return_value = {"status": "failure", "error_code": "ERR123"} mock_post.return_value = mock_response @@ -79,7 +84,10 @@ def test_block_funds_failure(example_bank_connector): def test_block_funds_http_error(example_bank_connector): - with patch('httpx.Client.post', side_effect=HTTPStatusError("Error", request=Mock(), response=Mock())) as mock_post: + with patch( + "httpx.Client.post", + side_effect=HTTPStatusError("Error", request=Mock(), response=Mock()), + ): response = example_bank_connector.block_funds("123456", "USD", 100.0) assert response.status == FundsBlockedWithBankEnum.FUNDS_BLOCK_FAILURE @@ -88,18 +96,18 @@ def test_block_funds_http_error(example_bank_connector): def test_initiate_payment_success(example_bank_connector): - with patch('httpx.Client.post') as mock_post: + with patch("httpx.Client.post") as mock_post: mock_response = Mock() mock_response.json.return_value = {"status": "success"} mock_post.return_value = mock_response - payment_payload = PaymentPayload( + payment_payload = DisbursementPaymentPayload( remitting_account="123456", remitting_account_currency="USD", payment_amount=100.0, funds_blocked_reference_number="BR123", beneficiary_id="BID123", - payment_date=datetime.now() + payment_date=datetime.now(), ) response = example_bank_connector.initiate_payment([payment_payload]) @@ -109,18 +117,21 @@ def test_initiate_payment_success(example_bank_connector): def test_initiate_payment_failure(example_bank_connector): - with patch('httpx.Client.post') as mock_post: + with patch("httpx.Client.post") as mock_post: mock_response = Mock() - mock_response.json.return_value = {"status": "failure", "error_message": "Payment error"} + mock_response.json.return_value = { + "status": "failure", + "error_message": "Payment error", + } mock_post.return_value = mock_response - payment_payload = PaymentPayload( + payment_payload = DisbursementPaymentPayload( remitting_account="123456", remitting_account_currency="USD", payment_amount=100.0, funds_blocked_reference_number="BR123", beneficiary_id="BID123", - payment_date=datetime.now() + payment_date=datetime.now(), ) response = example_bank_connector.initiate_payment([payment_payload]) @@ -130,14 +141,17 @@ def test_initiate_payment_failure(example_bank_connector): def test_initiate_payment_http_error(example_bank_connector): - with patch('httpx.Client.post', side_effect=HTTPStatusError("Error", request=Mock(), response=Mock())) as mock_post: - payment_payload = PaymentPayload( + with patch( + "httpx.Client.post", + side_effect=HTTPStatusError("Error", request=Mock(), response=Mock()), + ): + payment_payload = DisbursementPaymentPayload( remitting_account="123456", remitting_account_currency="USD", payment_amount=100.0, funds_blocked_reference_number="BR123", beneficiary_id="BID123", - payment_date=datetime.now() + payment_date=datetime.now(), ) response = example_bank_connector.initiate_payment([payment_payload]) From 21187163dc6ba5769b4d8f1e491ec6298bc07731 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 2 Aug 2024 16:15:33 +0530 Subject: [PATCH 32/39] Pre-commit fixes --- openg2p-g2p-bridge-api/.env.example | 2 +- .../src/openg2p_g2p_bridge_api/app.py | 5 +-- .../services/account_statement.py | 8 ++-- .../services/disbursement_envelope.py | 44 +++++++++---------- openg2p-g2p-bridge-celery-tasks/.env.example | 2 +- .../helpers/resolve_helper.py | 4 +- .../tasks/mt940_processor.py | 4 +- 7 files changed, 33 insertions(+), 36 deletions(-) diff --git a/openg2p-g2p-bridge-api/.env.example b/openg2p-g2p-bridge-api/.env.example index 3258cab..09d4fbd 100644 --- a/openg2p-g2p-bridge-api/.env.example +++ b/openg2p-g2p-bridge-api/.env.example @@ -2,4 +2,4 @@ G2P_BRIDGE_DB_DBNAME=openg2p_g2p_bridge_db G2P_BRIDGE_WORKER_TYPE=gunicorn G2P_BRIDGE_HOST=0.0.0.0 G2P_BRIDGE_PORT=8000 -G2P_BRIDGE_NO_OF_WORKERS=1 \ No newline at end of file +G2P_BRIDGE_NO_OF_WORKERS=1 diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py index e04df1e..7c0327c 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/app.py @@ -8,12 +8,9 @@ from openg2p_fastapi_common.app import Initializer as BaseInitializer from openg2p_g2p_bridge_models.models import ( + AccountStatement, DisbursementEnvelope, DisbursementEnvelopeBatchStatus, - AccountStatement, - AccountStatementLob, - DisbursementRecon, - DisbursementErrorRecon, ) from .controllers import ( diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py index f72dfae..6869345 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/account_statement.py @@ -18,7 +18,7 @@ class AccountStatementService(BaseService): async def upload_mt940(self, statement_file: UploadFile) -> str: - _logger.info(f"Uploading statement file") + _logger.info("Uploading statement file") try: statement_file = await statement_file.read() except Exception as e: @@ -45,13 +45,13 @@ async def upload_mt940(self, statement_file: UploadFile) -> str: session.add(statement_lob) await session.commit() - _logger.info(f"Statement file uploaded successfully") + _logger.info("Statement file uploaded successfully") return statement_id async def construct_account_statement_success_response( self, statement_id: str ) -> AccountStatementResponse: - _logger.info(f"Constructing account statement success response") + _logger.info("Constructing account statement success response") return AccountStatementResponse( response_status=ResponseStatus.SUCCESS, statement_id=statement_id, @@ -61,7 +61,7 @@ async def construct_account_statement_success_response( async def construct_account_statement_error_response( self, code: G2PBridgeErrorCodes ) -> AccountStatementResponse: - _logger.error(f"Constructing account statement error response") + _logger.error("Constructing account statement error response") return AccountStatementResponse( response_status=ResponseStatus.FAILURE, statement_id="", diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py index fb0857e..5cdebfa 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/services/disbursement_envelope.py @@ -1,6 +1,6 @@ +import logging import time from datetime import datetime -import logging from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService @@ -34,7 +34,7 @@ class DisbursementEnvelopeService(BaseService): async def create_disbursement_envelope( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> DisbursementEnvelopePayload: - _logger.info(f"Creating disbursement envelope") + _logger.info("Creating disbursement envelope") session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: try: @@ -63,13 +63,13 @@ async def create_disbursement_envelope( disbursement_envelope_payload.disbursement_envelope_id = ( disbursement_envelope.disbursement_envelope_id ) - _logger.info(f"Disbursement envelope created successfully") + _logger.info("Disbursement envelope created successfully") return disbursement_envelope_payload async def cancel_disbursement_envelope( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> DisbursementEnvelopePayload: - _logger.info(f"Cancelling disbursement envelope") + _logger.info("Cancelling disbursement envelope") session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) async with session_maker() as session: disbursement_envelope_payload: DisbursementEnvelopePayload = ( @@ -113,40 +113,40 @@ async def cancel_disbursement_envelope( disbursement_envelope.cancellation_timestamp = datetime.utcnow() await session.commit() - _logger.info(f"Disbursement envelope cancelled successfully") + _logger.info("Disbursement envelope cancelled successfully") return disbursement_envelope_payload async def construct_disbursement_envelope_success_response( self, disbursement_envelope_payload: DisbursementEnvelopePayload ) -> DisbursementEnvelopeResponse: - _logger.info(f"Constructing disbursement envelope success response") + _logger.info("Constructing disbursement envelope success response") disbursement_envelope_response: DisbursementEnvelopeResponse = ( DisbursementEnvelopeResponse( response_status=ResponseStatus.SUCCESS, response_payload=disbursement_envelope_payload, ) ) - _logger.info(f"Disbursement envelope success response constructed") + _logger.info("Disbursement envelope success response constructed") return disbursement_envelope_response async def construct_disbursement_envelope_error_response( self, error_code: G2PBridgeErrorCodes ) -> DisbursementEnvelopeResponse: - _logger.error(f"Constructing disbursement envelope error response") + _logger.error("Constructing disbursement envelope error response") disbursement_envelope_response: DisbursementEnvelopeResponse = ( DisbursementEnvelopeResponse( response_status=ResponseStatus.FAILURE, response_error_code=error_code.value, ) ) - _logger.error(f"Disbursement envelope error response constructed") + _logger.error("Disbursement envelope error response constructed") return disbursement_envelope_response # noinspection PyMethodMayBeStatic async def validate_envelope_request( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> bool: - _logger.info(f"Validating disbursement envelope request") + _logger.info("Validating disbursement envelope request") disbursement_envelope_payload: DisbursementEnvelopePayload = ( disbursement_envelope_request.request_payload ) @@ -154,7 +154,7 @@ async def validate_envelope_request( disbursement_envelope_payload.benefit_program_mnemonic is None or disbursement_envelope_payload.benefit_program_mnemonic == "" ): - _logger.error(f"Invalid benefit program mnemonic") + _logger.error("Invalid benefit program mnemonic") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_PROGRAM_MNEMONIC ) @@ -162,7 +162,7 @@ async def validate_envelope_request( disbursement_envelope_payload.disbursement_frequency not in DisbursementFrequency ): - _logger.error(f"Invalid disbursement frequency") + _logger.error("Invalid disbursement frequency") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_DISBURSEMENT_FREQUENCY ) @@ -170,7 +170,7 @@ async def validate_envelope_request( disbursement_envelope_payload.cycle_code_mnemonic is None or disbursement_envelope_payload.cycle_code_mnemonic == "" ): - _logger.error(f"Invalid cycle code mnemonic") + _logger.error("Invalid cycle code mnemonic") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_CYCLE_CODE_MNEMONIC ) @@ -178,7 +178,7 @@ async def validate_envelope_request( disbursement_envelope_payload.number_of_beneficiaries is None or disbursement_envelope_payload.number_of_beneficiaries < 1 ): - _logger.error(f"Invalid number of beneficiaries") + _logger.error("Invalid number of beneficiaries") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_NO_OF_BENEFICIARIES ) @@ -186,7 +186,7 @@ async def validate_envelope_request( disbursement_envelope_payload.number_of_disbursements is None or disbursement_envelope_payload.number_of_disbursements < 1 ): - _logger.error(f"Invalid number of disbursements") + _logger.error("Invalid number of disbursements") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_NO_OF_DISBURSEMENTS ) @@ -194,7 +194,7 @@ async def validate_envelope_request( disbursement_envelope_payload.total_disbursement_amount is None or disbursement_envelope_payload.total_disbursement_amount < 0 ): - _logger.error(f"Invalid total disbursement amount") + _logger.error("Invalid total disbursement amount") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_TOTAL_DISBURSEMENT_AMOUNT ) @@ -203,18 +203,18 @@ async def validate_envelope_request( or disbursement_envelope_payload.disbursement_schedule_date < datetime.date(datetime.utcnow()) # TODO: Add a delta of x days ): - _logger.error(f"Invalid disbursement schedule date") + _logger.error("Invalid disbursement schedule date") raise DisbursementEnvelopeException( G2PBridgeErrorCodes.INVALID_DISBURSEMENT_SCHEDULE_DATE ) - _logger.info(f"Disbursement envelope request validated successfully") + _logger.info("Disbursement envelope request validated successfully") return True # noinspection PyMethodMayBeStatic async def construct_disbursement_envelope( self, disbursement_envelope_payload: DisbursementEnvelopePayload ) -> DisbursementEnvelope: - _logger.info(f"Constructing disbursement envelope") + _logger.info("Constructing disbursement envelope") disbursement_envelope: DisbursementEnvelope = DisbursementEnvelope( disbursement_envelope_id=str(int(time.time() * 1000)), benefit_program_mnemonic=disbursement_envelope_payload.benefit_program_mnemonic, @@ -233,14 +233,14 @@ async def construct_disbursement_envelope( disbursement_envelope_payload.disbursement_envelope_id = ( disbursement_envelope.disbursement_envelope_id ) - _logger.info(f"Disbursement envelope constructed successfully") + _logger.info("Disbursement envelope constructed successfully") return disbursement_envelope # noinspection PyMethodMayBeStatic async def construct_disbursement_envelope_batch_status( self, disbursement_envelope: DisbursementEnvelope, session ) -> DisbursementEnvelopeBatchStatus: - _logger.info(f"Constructing disbursement envelope batch status") + _logger.info("Constructing disbursement envelope batch status") benefit_program_configuration: BenefitProgramConfiguration = ( ( await session.execute( @@ -268,5 +268,5 @@ async def construct_disbursement_envelope_batch_status( active=True, id_mapper_resolution_required=benefit_program_configuration.id_mapper_resolution_required, ) - _logger.info(f"Disbursement envelope batch status constructed successfully") + _logger.info("Disbursement envelope batch status constructed successfully") return disbursement_envelope_batch_status diff --git a/openg2p-g2p-bridge-celery-tasks/.env.example b/openg2p-g2p-bridge-celery-tasks/.env.example index ff59a1d..6d3466b 100644 --- a/openg2p-g2p-bridge-celery-tasks/.env.example +++ b/openg2p-g2p-bridge-celery-tasks/.env.example @@ -7,4 +7,4 @@ G2P_BRIDGE_CELERY_TASKS_NO_OF_WORKERS=1 G2P_BRIDGE_CELERY_TASKS_DB_DBNAME=openg2p_g2p_bridge_db G2P_BRIDGE_BANK_DECONSTRUCT_STRATEGY="bank_(?P\d+)_(?P\d+)_(?P\d+)_(?P\w+)" G2P_BRIDGE_MOBILE_WALLET_DECONSTRUCT_STRATEGY="mobile_(?P\d+)_(?P\w+)" -G2P_BRIDGE_EMAIL_WALLET_DECONSTRUCT_STRATEGY="email_(?P\w+)_(?P\w+)" \ No newline at end of file +G2P_BRIDGE_EMAIL_WALLET_DECONSTRUCT_STRATEGY="email_(?P\w+)_(?P\w+)" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py index 006864b..4c4bf2e 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py @@ -86,7 +86,9 @@ def deconstruct_fa(self, fa: str) -> dict: deconstruct_strategy = self.get_deconstruct_strategy(fa) if deconstruct_strategy: deconstructed_pairs = self._deconstruct(fa, deconstruct_strategy) - deconstructed_fa = {pair.key.value: pair.value for pair in deconstructed_pairs} + deconstructed_fa = { + pair.key.value: pair.value for pair in deconstructed_pairs + } return deconstructed_fa return {} diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py index 74e549e..16ecc3b 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py +++ b/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py @@ -284,9 +284,7 @@ def update_existing_disbursement_recon(disbursement_recon, parsed_transaction): disbursement_recon.reversal_reason = parsed_transaction["reversal_reason"] -def construct_new_disbursement_recon( - bank_disbursement_batch_id, parsed_transaction -): +def construct_new_disbursement_recon(bank_disbursement_batch_id, parsed_transaction): disbursement_recon = DisbursementRecon( bank_disbursement_batch_id=bank_disbursement_batch_id, disbursement_id=parsed_transaction["disbursement_id"], From 1431244054c5dff5d17920641f437875d87b6152 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 2 Aug 2024 16:15:51 +0530 Subject: [PATCH 33/39] Pre-commit fixes --- .../openg2p_g2p_bridge_models/models/account_statement.py | 4 +--- .../models/disbursement_envelope.py | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py index 29c7b51..fe1022f 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/account_statement.py @@ -77,6 +77,4 @@ class DisbursementErrorRecon(BaseORMModelWithTimes): SqlEnum(G2PBridgeErrorCodes), nullable=True ) disbursement_id: Mapped[str] = mapped_column(String, index=True) - bank_reference_number: Mapped[str] = mapped_column( - String, nullable=True - ) + bank_reference_number: Mapped[str] = mapped_column(String, nullable=True) diff --git a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py index 52daf11..312eb84 100644 --- a/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py +++ b/openg2p-g2p-bridge-models/src/openg2p_g2p_bridge_models/models/disbursement_envelope.py @@ -1,9 +1,9 @@ from datetime import datetime from enum import Enum -from sqlalchemy import Enum as SqlEnum from openg2p_fastapi_common.models import BaseORMModelWithTimes from sqlalchemy import Boolean, Date, DateTime, Integer, String +from sqlalchemy import Enum as SqlEnum from sqlalchemy.orm import Mapped, mapped_column @@ -42,7 +42,9 @@ class DisbursementEnvelope(BaseORMModelWithTimes): __tablename__ = "disbursement_envelopes" disbursement_envelope_id: Mapped[str] = mapped_column(String, unique=True) benefit_program_mnemonic: Mapped[str] = mapped_column(String) - disbursement_frequency: Mapped[DisbursementFrequency] = mapped_column(SqlEnum(DisbursementFrequency)) + disbursement_frequency: Mapped[DisbursementFrequency] = mapped_column( + SqlEnum(DisbursementFrequency) + ) cycle_code_mnemonic: Mapped[str] = mapped_column(String) number_of_beneficiaries: Mapped[int] = mapped_column(Integer) number_of_disbursements: Mapped[int] = mapped_column(Integer) From 2708e0a755643e517dfe6daeaf83e89af87adae8 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Fri, 2 Aug 2024 16:16:08 +0530 Subject: [PATCH 34/39] Example Bank API --- .../celery_app.py | 111 ++++++++++++++---- .../config.py | 4 +- .../controllers/initiate_payment.py | 14 ++- .../models/account.py | 33 ++++-- .../schemas/fund_schemas.py | 11 +- 5 files changed, 133 insertions(+), 40 deletions(-) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py index 22fffc9..62cd7cc 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py @@ -1,5 +1,7 @@ +import random import uuid from datetime import datetime +from typing import List from celery import Celery from sqlalchemy import select @@ -53,11 +55,13 @@ def process_payments(): .all() ) - for payment_request in initiate_payment_requests: + failure_logs = [] + for initiate_payment_request in initiate_payment_requests: account = ( session.execute( select(Account).where( - Account.account_number == payment_request.remitting_account + Account.account_number + == initiate_payment_request.remitting_account ) ) .scalars() @@ -68,38 +72,95 @@ def process_payments(): session.execute( select(FundBlock).where( FundBlock.block_reference_no - == payment_request.funds_blocked_reference_number + == initiate_payment_request.funds_blocked_reference_number ) ) .scalars() .first() ) - log = AccountingLog( - reference_no=str(uuid.uuid4()), - debit_credit=DebitCreditTypes.DEBIT, - account_number=payment_request.remitting_account, - transaction_amount=payment_request.payment_amount, - transaction_date=datetime.utcnow(), - transaction_currency=payment_request.remitting_account_currency, - transaction_code="DBT", - narrative_1=payment_request.narrative_1, - narrative_2=payment_request.narrative_2, - narrative_3=payment_request.narrative_3, - narrative_4=payment_request.narrative_4, - narrative_5=payment_request.narrative_5, - narrative_6=payment_request.narrative_6, - active=True, + accounting_log: AccountingLog = construct_accounting_log( + initiate_payment_request ) - session.add(log) + update_account(account, initiate_payment_request.payment_amount) + update_fund_block(fund_block, initiate_payment_request.payment_amount) + initiate_payment_request.payment_status = PaymentStatus.SUCCESS + initiate_payment_request.payment_initiate_attempts += 1 - fund_block.amount_released += payment_request.payment_amount + failure_random_number = random.randint(1, 100) + if failure_random_number <= 30: + failure_logs.append(accounting_log) - account.book_balance -= payment_request.payment_amount - account.blocked_amount -= payment_request.payment_amount - account.available_balance = account.book_balance - account.blocked_amount - payment_request.payment_status = PaymentStatus.SUCCESS - payment_request.payment_initiate_attempts += 1 + session.add(accounting_log) + session.add(fund_block) + session.add(account) + + # End of loop + + generate_failures(account, failure_logs, fund_block, session) session.commit() + + +def construct_accounting_log(initiate_payment_request: InitiatePaymentRequest): + return AccountingLog( + reference_no=str(uuid.uuid4()), + customer_reference_no=initiate_payment_request.payment_reference_number, + debit_credit=DebitCreditTypes.DEBIT, + account_number=initiate_payment_request.remitting_account, + transaction_amount=initiate_payment_request.payment_amount, + transaction_date=datetime.utcnow(), + transaction_currency=initiate_payment_request.remitting_account_currency, + transaction_code="DBT", + narrative_1=initiate_payment_request.narrative_1, + narrative_2=initiate_payment_request.narrative_2, + narrative_3=initiate_payment_request.narrative_3, + narrative_4=initiate_payment_request.narrative_4, + narrative_5=initiate_payment_request.narrative_5, + narrative_6=initiate_payment_request.narrative_6, + active=True, + ) + + +def generate_failures( + account: Account, failure_logs: List[AccountingLog], fund_block: FundBlock, session +): + failure_reasons = [ + "ACCOUNT_CLOSED", + "ACCOUNT_NOT_FOUND", + "ACCOUNT_DORMANT", + "ACCOUNT_DECEASED", + ] + for failure_log in failure_logs: + account_log: AccountingLog = AccountingLog( + reference_no=str(uuid.uuid4()), + customer_reference_no=failure_log.customer_reference_no, + debit_credit=failure_log.debit_credit, + account_number=failure_log.account_number, + transaction_amount=-failure_log.transaction_amount, + transaction_date=failure_log.transaction_date, + transaction_currency=failure_log.transaction_currency, + transaction_code=failure_log.transaction_code, + narrative_1=failure_log.narrative_1, + narrative_2=failure_log.narrative_2, + narrative_3=failure_log.narrative_3, + narrative_4=failure_log.narrative_4, + narrative_5=failure_log.narrative_5, + narrative_6=random.choice(failure_reasons), + active=True, + ) + session.add(account_log) + + update_account(account, account_log.transaction_amount) + update_fund_block(fund_block, account_log.transaction_amount) + + +def update_account(account, payment_amount): + account.book_balance -= payment_amount + account.blocked_amount -= payment_amount + account.available_balance = account.book_balance - account.blocked_amount + + +def update_fund_block(fund_block, payment_amount): + fund_block.amount_released += payment_amount diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py index 2d6cfef..825cb72 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py @@ -17,7 +17,7 @@ class Settings(BaseSettings): """ openapi_version: str = __version__ - db_dbname: str = "openg2p_g2p_bridge_db" - db_driver: str = "postgresql" + db_dbname: str = "example_bank_db" + # db_driver: str = "postgresql" payment_initiate_attempts: int = 3 diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py index 5598c4d..bbcbadc 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py @@ -1,8 +1,9 @@ +from typing import List + from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.controller import BaseController from sqlalchemy.ext.asyncio import async_sessionmaker from sqlalchemy.future import select -from typing import List from ..models import FundBlock, InitiatePaymentRequest from ..schemas import InitiatePaymentPayload, InitiatorPaymentResponse @@ -46,6 +47,7 @@ async def initiate_payment( ) payment = InitiatePaymentRequest( + payment_reference_number=initiate_payment_payload.payment_reference_number, remitting_account=initiate_payment_payload.remitting_account, remitting_account_currency=initiate_payment_payload.remitting_account_currency, payment_amount=initiate_payment_payload.payment_amount, @@ -56,7 +58,17 @@ async def initiate_payment( beneficiary_account_type=initiate_payment_payload.beneficiary_account_type, beneficiary_bank_code=initiate_payment_payload.beneficiary_bank_code, beneficiary_branch_code=initiate_payment_payload.beneficiary_branch_code, + beneficiary_mobile_wallet_provider=initiate_payment_payload.beneficiary_mobile_wallet_provider, + beneficiary_phone_no=initiate_payment_payload.beneficiary_phone_no, + beneficiary_email=initiate_payment_payload.beneficiary_email, + beneficiary_email_wallet_provider=initiate_payment_payload.beneficiary_email_wallet_provider, payment_date=initiate_payment_payload.payment_date, + narrative_1=initiate_payment_payload.narrative_1, + narrative_2=initiate_payment_payload.narrative_2, + narrative_3=initiate_payment_payload.narrative_3, + narrative_4=initiate_payment_payload.narrative_4, + narrative_5=initiate_payment_payload.narrative_5, + narrative_6=initiate_payment_payload.narrative_6, active=True, ) session.add(payment) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py index 9a1b0e6..5e94088 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py @@ -39,10 +39,14 @@ class FundBlock(BaseORMModelWithTimes): class InitiatePaymentRequest(BaseORMModelWithTimes): __tablename__ = "initiate_payment_requests" + payment_reference_number = mapped_column( + String, index=True, unique=True + ) # disbursement id + remitting_account: Mapped[str] = mapped_column(String, nullable=False) remitting_account_currency: Mapped[str] = mapped_column(String, nullable=False) payment_amount: Mapped[float] = mapped_column(Float, nullable=False) - payment_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False) + payment_date: Mapped[str] = mapped_column(String, nullable=False) funds_blocked_reference_number: Mapped[str] = mapped_column(String, nullable=False) beneficiary_name: Mapped[str] = mapped_column(String) @@ -52,16 +56,26 @@ class InitiatePaymentRequest(BaseORMModelWithTimes): beneficiary_bank_code: Mapped[str] = mapped_column(String) beneficiary_branch_code: Mapped[str] = mapped_column(String) - narrative_1: Mapped[str] = mapped_column(String, nullable=True) # disbursement id - narrative_2: Mapped[str] = mapped_column(String, nullable=True) # beneficiary id - narrative_3: Mapped[str] = mapped_column(String, nullable=True) # program pneumonic - narrative_4: Mapped[str] = mapped_column( + beneficiary_mobile_wallet_provider: Mapped[str] = mapped_column( String, nullable=True - ) # cycle code pneumonic - narrative_5: Mapped[str] = mapped_column(String, nullable=True) # beneficiary email - narrative_6: Mapped[str] = mapped_column( + ) + beneficiary_phone_no: Mapped[str] = mapped_column(String, nullable=True) + + beneficiary_email: Mapped[str] = mapped_column(String, nullable=True) + beneficiary_email_wallet_provider: Mapped[str] = mapped_column( String, nullable=True - ) # beneficiary phone number + ) + + narrative_1: Mapped[str] = mapped_column( + String, nullable=True + ) # disbursement narrative + narrative_2: Mapped[str] = mapped_column(String, nullable=True) # program pneumonic + narrative_3: Mapped[str] = mapped_column( + String, nullable=True + ) # cycle code pneumonic + narrative_4: Mapped[str] = mapped_column(String, nullable=True) # beneficiary id + narrative_5: Mapped[str] = mapped_column(String, nullable=True) + narrative_6: Mapped[str] = mapped_column(String, nullable=True) payment_initiate_attempts: Mapped[int] = mapped_column(Integer, default=0) payment_status: Mapped[PaymentStatus] = mapped_column( @@ -72,6 +86,7 @@ class InitiatePaymentRequest(BaseORMModelWithTimes): class AccountingLog(BaseORMModelWithTimes): __tablename__ = "accounting_logs" reference_no: Mapped[str] = mapped_column(String, index=True, unique=True) + customer_reference_no: Mapped[str] = mapped_column(String, index=True) debit_credit: Mapped[DebitCreditTypes] = mapped_column(SqlEnum(DebitCreditTypes)) account_number: Mapped[str] = mapped_column(String, index=True) transaction_amount: Mapped[float] = mapped_column(Float) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py index d6c24ea..c623d9c 100644 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py +++ b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py @@ -1,4 +1,3 @@ -from datetime import datetime from typing import Optional from pydantic import BaseModel @@ -30,18 +29,24 @@ class BlockFundsResponse(BaseModel): class InitiatePaymentPayload(BaseModel): + payment_reference_number: str remitting_account: str remitting_account_currency: str payment_amount: float funds_blocked_reference_number: str beneficiary_name: str + beneficiary_account: str beneficiary_account_currency: str beneficiary_account_type: str beneficiary_bank_code: str beneficiary_branch_code: str - benefit_program_mnemonic: str + beneficiary_mobile_wallet_provider: Optional[str] = None + beneficiary_phone_no: Optional[str] = None + + beneficiary_email: Optional[str] = None + beneficiary_email_wallet_provider: Optional[str] = None narrative_1: Optional[str] = None narrative_2: Optional[str] = None @@ -50,7 +55,7 @@ class InitiatePaymentPayload(BaseModel): narrative_5: Optional[str] = None narrative_6: Optional[str] = None - payment_date: datetime + payment_date: str class InitiatorPaymentResponse(BaseModel): From 770e5e9f9ab2cf802d5d75d69611438cdf47eec0 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Sun, 4 Aug 2024 13:53:57 +0530 Subject: [PATCH 35/39] G2P Bridge Celery Beat Producers --- .../.env.example | 0 .../.gitignore | 0 .../.pre-commit-config.yaml | 0 .../.ruff.toml | 0 .../CODE-OF-CONDUCT.md | 0 .../CONTRIBUTING.md | 0 .../LICENSE | 0 .../README.md | 0 .../__init__.py | 0 .../celerybeat-schedule.db | Bin 0 -> 16384 bytes .../main.py | 2 +- .../pyproject.toml | 10 +-- .../__init__.py | 0 .../app.py | 58 ++++++++++++++++++ .../config.py | 0 .../tests/__init__.py | 0 .../tests/test_mapper_resolve_task.py | 0 17 files changed, 64 insertions(+), 6 deletions(-) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/.env.example (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/.gitignore (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/.pre-commit-config.yaml (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/.ruff.toml (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/CODE-OF-CONDUCT.md (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/CONTRIBUTING.md (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/LICENSE (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/README.md (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/__init__.py (100%) create mode 100644 openg2p-g2p-bridge-celery-beat-producers/celerybeat-schedule.db rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/main.py (75%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/pyproject.toml (67%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers}/__init__.py (100%) create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/app.py rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers}/config.py (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/tests/__init__.py (100%) rename {openg2p-g2p-bridge-celery-tasks => openg2p-g2p-bridge-celery-beat-producers}/tests/test_mapper_resolve_task.py (100%) diff --git a/openg2p-g2p-bridge-celery-tasks/.env.example b/openg2p-g2p-bridge-celery-beat-producers/.env.example similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/.env.example rename to openg2p-g2p-bridge-celery-beat-producers/.env.example diff --git a/openg2p-g2p-bridge-celery-tasks/.gitignore b/openg2p-g2p-bridge-celery-beat-producers/.gitignore similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/.gitignore rename to openg2p-g2p-bridge-celery-beat-producers/.gitignore diff --git a/openg2p-g2p-bridge-celery-tasks/.pre-commit-config.yaml b/openg2p-g2p-bridge-celery-beat-producers/.pre-commit-config.yaml similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/.pre-commit-config.yaml rename to openg2p-g2p-bridge-celery-beat-producers/.pre-commit-config.yaml diff --git a/openg2p-g2p-bridge-celery-tasks/.ruff.toml b/openg2p-g2p-bridge-celery-beat-producers/.ruff.toml similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/.ruff.toml rename to openg2p-g2p-bridge-celery-beat-producers/.ruff.toml diff --git a/openg2p-g2p-bridge-celery-tasks/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-celery-beat-producers/CODE-OF-CONDUCT.md similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/CODE-OF-CONDUCT.md rename to openg2p-g2p-bridge-celery-beat-producers/CODE-OF-CONDUCT.md diff --git a/openg2p-g2p-bridge-celery-tasks/CONTRIBUTING.md b/openg2p-g2p-bridge-celery-beat-producers/CONTRIBUTING.md similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/CONTRIBUTING.md rename to openg2p-g2p-bridge-celery-beat-producers/CONTRIBUTING.md diff --git a/openg2p-g2p-bridge-celery-tasks/LICENSE b/openg2p-g2p-bridge-celery-beat-producers/LICENSE similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/LICENSE rename to openg2p-g2p-bridge-celery-beat-producers/LICENSE diff --git a/openg2p-g2p-bridge-celery-tasks/README.md b/openg2p-g2p-bridge-celery-beat-producers/README.md similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/README.md rename to openg2p-g2p-bridge-celery-beat-producers/README.md diff --git a/openg2p-g2p-bridge-celery-tasks/__init__.py b/openg2p-g2p-bridge-celery-beat-producers/__init__.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/__init__.py rename to openg2p-g2p-bridge-celery-beat-producers/__init__.py diff --git a/openg2p-g2p-bridge-celery-beat-producers/celerybeat-schedule.db b/openg2p-g2p-bridge-celery-beat-producers/celerybeat-schedule.db new file mode 100644 index 0000000000000000000000000000000000000000..eabaa5e16ea3318102da12678b3ca639a58bb0f1 GIT binary patch literal 16384 zcmeI%J#W)M7zgkRbsMJyxeQbYgcvHJ!eB9!i7{e85eyWDu$=sI!B!kQI-fzU6sZ$$ zx&f)zfltE5hvr0PU!sXz*0SK(&*UyAfJAu(T1(D+p&i`YxUS4iw8mg0uX=z1Rwwb2tWV= z5P$##{u=?8ef2(jAH4V8q4(B%;|;pK?tp#)KtKQj5P$##AOHafKmY;|Xe6*_!#UWt z{}xhA&8FUn5)qB~u*&0-KS@=>!yq5=Py~ukWD!@9kVc=5L?+~pZx0)NW}7aGt4!R= zmE1A=jc?QJVQw5Kq0+H1`d7MddaLbEuH&BVF2B1;XTmDYRGODYw;u@=C?%;8?d}%2 zNb_Mq=|f6y+mw!Xc$H7mXp{-_)Z8B0S;>9=fdw>z&|daZuPvVJ+bvpdh}7D-!o zY*VqwR6zTE)?e+jXLRw4yY}8rKo4;P!{7q z%}(27)7r*`M_axi<}LUC-E!p%S`n!zX_0k)tTCpSZ}?mOb>nmXSjf^E^F8OeU#WOLqGrm5P$##AOHaf QKmY;|fB*y_aMA_71H~g@9RL6T literal 0 HcmV?d00001 diff --git a/openg2p-g2p-bridge-celery-tasks/main.py b/openg2p-g2p-bridge-celery-beat-producers/main.py similarity index 75% rename from openg2p-g2p-bridge-celery-tasks/main.py rename to openg2p-g2p-bridge-celery-beat-producers/main.py index 8aaeb2a..fef5390 100644 --- a/openg2p-g2p-bridge-celery-tasks/main.py +++ b/openg2p-g2p-bridge-celery-beat-producers/main.py @@ -2,7 +2,7 @@ # ruff: noqa: I001 -from openg2p_g2p_bridge_celery_tasks.app import Initializer, celery_app +from openg2p_g2p_bridge_celery_beat_producers.app import Initializer, celery_app from openg2p_fastapi_common.ping import PingInitializer initializer = Initializer() diff --git a/openg2p-g2p-bridge-celery-tasks/pyproject.toml b/openg2p-g2p-bridge-celery-beat-producers/pyproject.toml similarity index 67% rename from openg2p-g2p-bridge-celery-tasks/pyproject.toml rename to openg2p-g2p-bridge-celery-beat-producers/pyproject.toml index a122697..5640622 100644 --- a/openg2p-g2p-bridge-celery-tasks/pyproject.toml +++ b/openg2p-g2p-bridge-celery-beat-producers/pyproject.toml @@ -3,11 +3,11 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "openg2p-g2p-bridge-celery-tasks" +name = "openg2p-g2p-bridge-celery-beat-producers" authors = [ { name="OpenG2P", email="info@openg2p.org" }, ] -description = "OpenG2P G2P Bridge Celery Tasks" +description = "OpenG2P G2P Bridge Celery Beat Producers" readme = "README.md" requires-python = ">=3.7" classifiers = [ @@ -26,8 +26,8 @@ dynamic = ["version"] [project.urls] Homepage = "https://openg2p.org" Documentation = "https://docs.openg2p.org/" -Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-celery-tasks" -Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-celery-tasks" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge" [tool.hatch.version] -path = "src/openg2p_g2p_bridge_celery_tasks/__init__.py" +path = "src/openg2p_g2p_bridge_celery_beat_producers/__init__.py" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/__init__.py b/openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/__init__.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/__init__.py rename to openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/__init__.py diff --git a/openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/app.py b/openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/app.py new file mode 100644 index 0000000..0a65822 --- /dev/null +++ b/openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/app.py @@ -0,0 +1,58 @@ +# ruff: noqa: E402 + +from .config import Settings + +_config = Settings.get_config() + +from celery import Celery +from openg2p_fastapi_common.app import Initializer as BaseInitializer +from openg2p_fastapi_common.exception import BaseExceptionHandler +from openg2p_g2p_bridge_bank_connectors.app import ( + Initializer as BankConnectorInitializer, +) +from openg2p_g2pconnect_mapper_lib.app import Initializer as MapperInitializer +from sqlalchemy import create_engine + + +class Initializer(BaseInitializer): + def initialize(self, **kwargs): + super().init_logger() + super().init_app() + BaseExceptionHandler() + + BankConnectorInitializer() + MapperInitializer() + + +def get_engine(): + if _config.db_datasource: + db_engine = create_engine(_config.db_datasource) + return db_engine + + +celery_app = Celery( + "g2p_bridge_celery_tasks", + broker="redis://localhost:6379/0", + backend="redis://localhost:6379/0", + include=["openg2p_g2p_bridge_celery_workers.tasks"], +) + +celery_app.conf.beat_schedule = { + "mapper_resolution_beat_producer": { + "task": "mapper_resolution_beat_producer", + "schedule": _config.mapper_resolve_frequency, + }, + "check_funds_with_bank_beat_producer": { + "task": "check_funds_with_bank_beat_producer", + "schedule": _config.funds_available_check_frequency, + }, + "block_funds_with_bank_beat_producer": { + "task": "block_funds_with_bank_beat_producer", + "schedule": _config.funds_blocked_frequency, + }, + "disburse_funds_from_bank_beat_producer": { + "task": "disburse_funds_from_bank_beat_producer", + "schedule": _config.funds_disbursement_frequency, + }, +} +celery_app.conf.timezone = "UTC" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py b/openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/config.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/config.py rename to openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/config.py diff --git a/openg2p-g2p-bridge-celery-tasks/tests/__init__.py b/openg2p-g2p-bridge-celery-beat-producers/tests/__init__.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/tests/__init__.py rename to openg2p-g2p-bridge-celery-beat-producers/tests/__init__.py diff --git a/openg2p-g2p-bridge-celery-tasks/tests/test_mapper_resolve_task.py b/openg2p-g2p-bridge-celery-beat-producers/tests/test_mapper_resolve_task.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/tests/test_mapper_resolve_task.py rename to openg2p-g2p-bridge-celery-beat-producers/tests/test_mapper_resolve_task.py From 3c67e2349a6a9b1ea463f762764c292ea015c686 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Sun, 4 Aug 2024 13:54:10 +0530 Subject: [PATCH 36/39] G2P Bridge Celery Workers --- .../.env.example | 10 + .../.gitignore | 2 +- .../.pre-commit-config.yaml | 0 .../.ruff.toml | 0 .../CODE-OF-CONDUCT.md | 0 .../CONTRIBUTING.md | 0 .../LICENSE | 0 .../README.md | 2 +- .../__init__.py | 0 .../main.py | 3 +- .../pyproject.toml | 12 +- .../__init__.py | 0 .../openg2p_g2p_bridge_celery_workers}/app.py | 20 +- .../config.py | 39 ++++ .../helpers/__init__.py | 0 .../helpers/resolve_helper.py | 0 .../tasks/__init__.py | 0 .../tasks/block_funds_with_bank.py | 0 .../tasks/check_funds_with_bank_task.py | 0 .../tasks/disburse_funds_from_bank.py | 0 .../tasks/mapper_resolution_task.py | 0 .../tasks/mt940_processor.py | 0 .../tests}/__init__.py | 0 .../tests/test_mapper_resolve_task.py | 176 ++++++++++++++++++ 24 files changed, 237 insertions(+), 27 deletions(-) create mode 100644 openg2p-g2p-bridge-celery-workers/.env.example rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/.gitignore (99%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/.pre-commit-config.yaml (100%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/.ruff.toml (100%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/CODE-OF-CONDUCT.md (100%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/CONTRIBUTING.md (100%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/LICENSE (100%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/README.md (96%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/__init__.py (100%) rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/main.py (70%) mode change 100755 => 100644 rename {openg2p-g2p-bridge-example-bank-api => openg2p-g2p-bridge-celery-workers}/pyproject.toml (64%) rename {openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/__init__.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/app.py (57%) create mode 100644 openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/config.py rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/helpers/__init__.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/helpers/resolve_helper.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/tasks/__init__.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/tasks/block_funds_with_bank.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/tasks/check_funds_with_bank_task.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/tasks/disburse_funds_from_bank.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/tasks/mapper_resolution_task.py (100%) rename {openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks => openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers}/tasks/mt940_processor.py (100%) rename {openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/utils => openg2p-g2p-bridge-celery-workers/tests}/__init__.py (100%) create mode 100644 openg2p-g2p-bridge-celery-workers/tests/test_mapper_resolve_task.py diff --git a/openg2p-g2p-bridge-celery-workers/.env.example b/openg2p-g2p-bridge-celery-workers/.env.example new file mode 100644 index 0000000..6d3466b --- /dev/null +++ b/openg2p-g2p-bridge-celery-workers/.env.example @@ -0,0 +1,10 @@ +G2P_BRIDGE_CELERY_TASKS_MAPPER_RESOLVE_API_URL="http://127.0.0.1:8003/sync/resolve" +G2P_BRIDGE_CELERY_TASKS_MAPPER_RESOLVE_RETRIES=3 +G2P_BRIDGE_CELERY_TASKS_MAPPER_RESOLVE_RETRY_DELAY=5 +G2P_BRIDGE_CELERY_TASKS_PORT=8001 +G2P_BRIDGE_CELERY_TASKS_WORKER_TYPE=gunicorn +G2P_BRIDGE_CELERY_TASKS_NO_OF_WORKERS=1 +G2P_BRIDGE_CELERY_TASKS_DB_DBNAME=openg2p_g2p_bridge_db +G2P_BRIDGE_BANK_DECONSTRUCT_STRATEGY="bank_(?P\d+)_(?P\d+)_(?P\d+)_(?P\w+)" +G2P_BRIDGE_MOBILE_WALLET_DECONSTRUCT_STRATEGY="mobile_(?P\d+)_(?P\w+)" +G2P_BRIDGE_EMAIL_WALLET_DECONSTRUCT_STRATEGY="email_(?P\w+)_(?P\w+)" diff --git a/openg2p-g2p-bridge-example-bank-api/.gitignore b/openg2p-g2p-bridge-celery-workers/.gitignore similarity index 99% rename from openg2p-g2p-bridge-example-bank-api/.gitignore rename to openg2p-g2p-bridge-celery-workers/.gitignore index c633cec..f5e0368 100644 --- a/openg2p-g2p-bridge-example-bank-api/.gitignore +++ b/openg2p-g2p-bridge-celery-workers/.gitignore @@ -78,4 +78,4 @@ docs/_build/ # Ignore secret files and env .secrets.* -../.env +.env diff --git a/openg2p-g2p-bridge-example-bank-api/.pre-commit-config.yaml b/openg2p-g2p-bridge-celery-workers/.pre-commit-config.yaml similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/.pre-commit-config.yaml rename to openg2p-g2p-bridge-celery-workers/.pre-commit-config.yaml diff --git a/openg2p-g2p-bridge-example-bank-api/.ruff.toml b/openg2p-g2p-bridge-celery-workers/.ruff.toml similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/.ruff.toml rename to openg2p-g2p-bridge-celery-workers/.ruff.toml diff --git a/openg2p-g2p-bridge-example-bank-api/CODE-OF-CONDUCT.md b/openg2p-g2p-bridge-celery-workers/CODE-OF-CONDUCT.md similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/CODE-OF-CONDUCT.md rename to openg2p-g2p-bridge-celery-workers/CODE-OF-CONDUCT.md diff --git a/openg2p-g2p-bridge-example-bank-api/CONTRIBUTING.md b/openg2p-g2p-bridge-celery-workers/CONTRIBUTING.md similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/CONTRIBUTING.md rename to openg2p-g2p-bridge-celery-workers/CONTRIBUTING.md diff --git a/openg2p-g2p-bridge-example-bank-api/LICENSE b/openg2p-g2p-bridge-celery-workers/LICENSE similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/LICENSE rename to openg2p-g2p-bridge-celery-workers/LICENSE diff --git a/openg2p-g2p-bridge-example-bank-api/README.md b/openg2p-g2p-bridge-celery-workers/README.md similarity index 96% rename from openg2p-g2p-bridge-example-bank-api/README.md rename to openg2p-g2p-bridge-celery-workers/README.md index 15fc796..17c08f0 100644 --- a/openg2p-g2p-bridge-example-bank-api/README.md +++ b/openg2p-g2p-bridge-celery-workers/README.md @@ -1,4 +1,4 @@ -# openg2p-g2p-bridge-api +# openg2p-g2p-bridge-celery-tasks [![Pre-commit Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/pre-commit.yml?query=branch%3Adevelop) [![Build Status](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/OpenG2P/openg2p-g2p-bridge-api/actions/workflows/test.yml?query=branch%3Adevelop) diff --git a/openg2p-g2p-bridge-example-bank-api/__init__.py b/openg2p-g2p-bridge-celery-workers/__init__.py similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/__init__.py rename to openg2p-g2p-bridge-celery-workers/__init__.py diff --git a/openg2p-g2p-bridge-example-bank-api/main.py b/openg2p-g2p-bridge-celery-workers/main.py old mode 100755 new mode 100644 similarity index 70% rename from openg2p-g2p-bridge-example-bank-api/main.py rename to openg2p-g2p-bridge-celery-workers/main.py index f97a162..3745f67 --- a/openg2p-g2p-bridge-example-bank-api/main.py +++ b/openg2p-g2p-bridge-celery-workers/main.py @@ -2,13 +2,14 @@ # ruff: noqa: I001 -from openg2p_g2p_bridge_example_bank_api.app import Initializer +from openg2p_g2p_bridge_celery_workers.app import Initializer, celery_app from openg2p_fastapi_common.ping import PingInitializer initializer = Initializer() PingInitializer() app = initializer.return_app() +celery_app = celery_app if __name__ == "__main__": initializer.main() diff --git a/openg2p-g2p-bridge-example-bank-api/pyproject.toml b/openg2p-g2p-bridge-celery-workers/pyproject.toml similarity index 64% rename from openg2p-g2p-bridge-example-bank-api/pyproject.toml rename to openg2p-g2p-bridge-celery-workers/pyproject.toml index afe215a..0614b78 100644 --- a/openg2p-g2p-bridge-example-bank-api/pyproject.toml +++ b/openg2p-g2p-bridge-celery-workers/pyproject.toml @@ -3,11 +3,11 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "openg2p-g2p-bridge-example-bank-api" +name = "openg2p-g2p-bridge-celery-workers" authors = [ { name="OpenG2P", email="info@openg2p.org" }, ] -description = "OpenG2P G2P Bridge API" +description = "OpenG2P G2P Bridge Celery Workers" readme = "README.md" requires-python = ">=3.7" classifiers = [ @@ -18,14 +18,16 @@ classifiers = [ dependencies = [ "openg2p-fastapi-common", "openg2p-fastapi-auth", + "openg2p-g2pconnect-mapper-lib", + "celery" ] dynamic = ["version"] [project.urls] Homepage = "https://openg2p.org" Documentation = "https://docs.openg2p.org/" -Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge-example-bank-api" -Source = "https://github.com/OpenG2P/openg2p-g2p-bridge-example-bank-api" +Repository = "https://github.com/OpenG2P/openg2p-g2p-bridge" +Source = "https://github.com/OpenG2P/openg2p-g2p-bridge" [tool.hatch.version] -path = "src/openg2p_g2p_bridge_example_bank_api/__init__.py" +path = "src/openg2p_g2p_bridge_celery_workers/__init__.py" diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/__init__.py similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/__init__.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/__init__.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/app.py similarity index 57% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/app.py index 9dd28a4..68c218e 100644 --- a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/app.py +++ b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/app.py @@ -37,25 +37,7 @@ def get_engine(): "g2p_bridge_celery_tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0", - include=["openg2p_g2p_bridge_celery_tasks.tasks.mapper_resolution_task"], + include=["openg2p_g2p_bridge_celery_workers.tasks.mapper_resolution_task"], ) -celery_app.conf.beat_schedule = { - "mapper_resolution_beat_producer": { - "task": "mapper_resolution_beat_producer", - "schedule": _config.mapper_resolve_frequency, - }, - "check_funds_with_bank_beat_producer": { - "task": "check_funds_with_bank_beat_producer", - "schedule": _config.funds_available_check_frequency, - }, - "block_funds_with_bank_beat_producer": { - "task": "block_funds_with_bank_beat_producer", - "schedule": _config.funds_blocked_frequency, - }, - "disburse_funds_from_bank_beat_producer": { - "task": "disburse_funds_from_bank_beat_producer", - "schedule": _config.funds_disbursement_frequency, - }, -} celery_app.conf.timezone = "UTC" diff --git a/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/config.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/config.py new file mode 100644 index 0000000..a6ebba1 --- /dev/null +++ b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/config.py @@ -0,0 +1,39 @@ +from openg2p_fastapi_common.config import Settings as BaseSettings +from pydantic_settings import SettingsConfigDict + +from . import __version__ + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="g2p_bridge_celery_tasks_", env_file=".env", extra="allow" + ) + openapi_title: str = "OpenG2P G2P Bridge Celery Tasks" + openapi_description: str = """ + Celery tasks for OpenG2P G2P Bridge API + *********************************** + Further details goes here + *********************************** + """ + openapi_version: str = __version__ + + db_dbname: str = "openg2p_g2p_bridge_db" + db_driver: str = "postgresql" + + mapper_resolve_api_url: str = "" + + mapper_resolve_attempts: int = 3 + funds_available_check_attempts: int = 3 + funds_blocked_attempts: int = 3 + funds_disbursement_attempts: int = 3 + statement_process_attempts: int = 3 + + mapper_resolve_frequency: int = 10 + funds_available_check_frequency: int = 10 + funds_blocked_frequency: int = 10 + funds_disbursement_frequency: int = 10 + statement_process_frequency: int = 3600 + + bank_fa_deconstruct_strategy: str = "" + mobile_wallet_deconstruct_strategy: str = "" + email_wallet_deconstruct_strategy: str = "" diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/__init__.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/helpers/__init__.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/__init__.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/helpers/__init__.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/helpers/resolve_helper.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/helpers/resolve_helper.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/helpers/resolve_helper.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/__init__.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/__init__.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/__init__.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/block_funds_with_bank.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/block_funds_with_bank.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/block_funds_with_bank.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/check_funds_with_bank_task.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/check_funds_with_bank_task.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/check_funds_with_bank_task.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/disburse_funds_from_bank.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/disburse_funds_from_bank.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/disburse_funds_from_bank.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/mapper_resolution_task.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mapper_resolution_task.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/mapper_resolution_task.py diff --git a/openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py b/openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/mt940_processor.py similarity index 100% rename from openg2p-g2p-bridge-celery-tasks/src/openg2p_g2p_bridge_celery_tasks/tasks/mt940_processor.py rename to openg2p-g2p-bridge-celery-workers/src/openg2p_g2p_bridge_celery_workers/tasks/mt940_processor.py diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py b/openg2p-g2p-bridge-celery-workers/tests/__init__.py similarity index 100% rename from openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/utils/__init__.py rename to openg2p-g2p-bridge-celery-workers/tests/__init__.py diff --git a/openg2p-g2p-bridge-celery-workers/tests/test_mapper_resolve_task.py b/openg2p-g2p-bridge-celery-workers/tests/test_mapper_resolve_task.py new file mode 100644 index 0000000..bf5478c --- /dev/null +++ b/openg2p-g2p-bridge-celery-workers/tests/test_mapper_resolve_task.py @@ -0,0 +1,176 @@ +import uuid +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from openg2p_g2p_bridge_celery_workers.tasks import mapper_resolution_worker +from openg2p_g2p_bridge_models.models import DisbursementBatchControl +from openg2p_g2pconnect_common_lib.schemas import ( + RequestHeader, + StatusEnum, + SyncResponseHeader, +) +from openg2p_g2pconnect_mapper_lib.client import MapperResolveClient +from openg2p_g2pconnect_mapper_lib.schemas import ( + ResolveRequest, + ResolveRequestMessage, + ResolveResponse, + ResolveResponseMessage, + SingleResolveRequest, + SingleResolveResponse, +) + + +@pytest.fixture +def mock_session_maker(): + session_mock = MagicMock() + session_mock.configure_mock( + **{ + "execute.return_value.scalars.return_value.all.return_value": [ + DisbursementBatchControl(beneficiary_id="1", disbursement_id="101") + ], + "__enter__.return_value": session_mock, + "__exit__.return_value": None, + } + ) + return session_mock + + +@pytest.fixture +def mock_resolve_helper(): + helper_mock = MagicMock() + single_resolve_request = SingleResolveRequest( + reference_id=str(uuid.uuid4()), + timestamp=datetime.now(), + id="1", + scope="details", + ) + helper_mock.construct_single_resolve_request.return_value = single_resolve_request + helper_mock.construct_resolve_request.return_value = ResolveRequest( + signature="", + header=RequestHeader( + message_id=str(uuid.uuid4()), + message_ts=str(datetime.now()), + action="resolve", + sender_id="", + sender_uri="", + total_count=1, + ), + message=ResolveRequestMessage( + transaction_id=str(uuid.uuid4()), + resolve_request=[single_resolve_request], + ), + ) + return helper_mock + + +@pytest.fixture +def mock_resolve_client(): + client_mock = MagicMock(spec=MapperResolveClient) + single_response = SingleResolveResponse( + reference_id="ref123", + timestamp=datetime.now(), + fa="FA123", + id="1", + account_provider_info=None, + status=StatusEnum.succ, # Assuming you have an Enum for status + status_reason_message="No issues.", + ) + resolve_response_message = ResolveResponseMessage( + transaction_id="trans123", + correlation_id="corr123", + resolve_response=[single_response], + ) + resolve_response = ResolveResponse( + header=SyncResponseHeader( + version="1.0.0", + message_id="", + message_ts="", + action="resolve", + status=StatusEnum.succ, + ), + message=resolve_response_message, + ) + client_mock.resolve_request.return_value = resolve_response + return client_mock + + +@patch("openg2p_g2p_bridge_celery_tasks.app.get_engine") +@patch("openg2p_g2p_bridge_celery_tasks.helpers.ResolveHelper.get_component") +@patch("openg2p_g2pconnect_mapper_lib.client.MapperResolveClient") +@patch("sqlalchemy.orm.sessionmaker") +def test_mapper_resolve_task_success( + mock_session_maker_func, + mock_resolve_client_cls, + mock_resolve_helper_func, + mock_engine, + mock_session_maker, + mock_resolve_helper, + mock_resolve_client, +): + print("Starting test...") + mock_session_maker_func.return_value = mock_session_maker + mock_resolve_helper_func.return_value = mock_resolve_helper + mock_resolve_client_cls.return_value = mock_resolve_client + + mock_resolve_client.resolve_request.return_value = MagicMock( + message=MagicMock( + resolve_response=[ + MagicMock( + id="1", + fa="Test FA", + account_provider_info=MagicMock(name="Test Provider"), + ) + ] + ) + ) + + valid_uuid = str(uuid.uuid4()) + print("UUID for testing:", valid_uuid) + + try: + mapper_resolution_worker(valid_uuid) + except Exception as e: + print("Error during task execution:", str(e)) + + session_mock = mock_session_maker.__enter__.return_value + print("Session mock add called:", session_mock.add.called) + print("Session mock commit called:", session_mock.commit.called) + print("API Call details:", mock_resolve_client.resolve_request.call_args_list) + + assert ( + mock_resolve_client.resolve_request.called + ), "Resolve request was not called as expected." + assert ( + session_mock.add.called + ), "The session.add method was not called, which suggests the task exited early or failed." + assert session_mock.commit.called, "The session.commit method was not called." + + +@patch("openg2p_g2p_bridge_celery_tasks.app.get_engine") +@patch("openg2p_g2p_bridge_celery_tasks.helpers.ResolveHelper.get_component") +@patch("openg2p_g2pconnect_mapper_lib.client.MapperResolveClient") +@patch("sqlalchemy.orm.sessionmaker") +def test_mapper_resolve_task_failure( + mock_session_maker_func, + mock_resolve_client_cls, + mock_resolve_helper_func, + mock_engine, + mock_session_maker, + mock_resolve_helper, + mock_resolve_client, +): + mock_session_maker_func.return_value = mock_session_maker + mock_resolve_helper_func.return_value = mock_resolve_helper + mock_resolve_client_cls.return_value = mock_resolve_client + + mock_resolve_client.resolve_request.side_effect = Exception("API failure") + valid_uuid = str(uuid.uuid4()) # Generate a valid UUID + mapper_resolution_worker(valid_uuid) + + session_mock = mock_session_maker.__enter__.return_value + assert session_mock.add.called + assert session_mock.commit.called + assert ( + mock_resolve_client.resolve_request.called + ), "Resolve request was not called as expected" From 3875ab2b8a85c15f03c54745527ef369e4472889 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Sun, 4 Aug 2024 13:55:26 +0530 Subject: [PATCH 37/39] Move Example Bank APIs --- .../.copier-answers.yml | 17 -- .../.dockerignore | 89 -------- .../.editorconfig | 20 -- .../celery_main.py | 3 - .../app.py | 45 ---- .../celery_app.py | 166 --------------- .../config.py | 23 -- .../controllers/__init__.py | 3 - .../controllers/block_funds.py | 74 ------- .../controllers/check_available_funds.py | 55 ----- .../controllers/initiate_payment.py | 78 ------- .../models/__init__.py | 8 - .../models/account.py | 106 ---------- .../schemas/__init__.py | 8 - .../schemas/fund_schemas.py | 63 ------ .../test-requirements.txt | 3 - .../tests/__init__.py | 0 .../tests/test_disbursement.py | 175 ---------------- .../tests/test_disbursement_envelope.py | 197 ------------------ 19 files changed, 1133 deletions(-) delete mode 100644 openg2p-g2p-bridge-example-bank-api/.copier-answers.yml delete mode 100644 openg2p-g2p-bridge-example-bank-api/.dockerignore delete mode 100644 openg2p-g2p-bridge-example-bank-api/.editorconfig delete mode 100644 openg2p-g2p-bridge-example-bank-api/celery_main.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/test-requirements.txt delete mode 100644 openg2p-g2p-bridge-example-bank-api/tests/__init__.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py delete mode 100644 openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py diff --git a/openg2p-g2p-bridge-example-bank-api/.copier-answers.yml b/openg2p-g2p-bridge-example-bank-api/.copier-answers.yml deleted file mode 100644 index 60cfca7..0000000 --- a/openg2p-g2p-bridge-example-bank-api/.copier-answers.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Do NOT update manually; changes here will be overwritten by Copier -_commit: af08ec1 -_src_path: https://github.com/openg2p/openg2p-fastapi-template -github_ci_docker_build: true -github_ci_openapi_publish: true -github_ci_precommit: true -github_ci_pypi_publish: true -github_ci_tests: true -github_ci_tests_codecov: true -module_name: openg2p_g2p_bridge_api -org_name: OpenG2P -org_slug: OpenG2P -package_name: openg2p-g2p-bridge-api -repo_name: ' openg2p-g2p-bridge-api - - ' -repo_slug: openg2p-g2p-bridge-api diff --git a/openg2p-g2p-bridge-example-bank-api/.dockerignore b/openg2p-g2p-bridge-example-bank-api/.dockerignore deleted file mode 100644 index d47f7ee..0000000 --- a/openg2p-g2p-bridge-example-bank-api/.dockerignore +++ /dev/null @@ -1,89 +0,0 @@ -# Git -.git -.gitignore -.gitattributes - - -# CI -.codeclimate.yml -.travis.yml -.taskcluster.yml - -# Docker -docker-compose.yml -Dockerfile -.docker -.dockerignore - -# Byte-compiled / optimized / DLL files -**/__pycache__/ -**/*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -nosetests.xml -coverage.xml - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Virtual environment -../.env -.venv/ -venv/ - -# PyCharm -.idea - -# Python mode for VIM -.ropeproject -**/.ropeproject - -# Vim swap files -**/*.swp - -# VS Code -.vscode/ diff --git a/openg2p-g2p-bridge-example-bank-api/.editorconfig b/openg2p-g2p-bridge-example-bank-api/.editorconfig deleted file mode 100644 index 7d8f3a5..0000000 --- a/openg2p-g2p-bridge-example-bank-api/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -# Configuration for known file extensions -[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml,toml,jinja}] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{yml,yaml,rst,md,jinja}] -indent_size = 2 - -# Do not configure editor for libs and autogenerated content -[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] -charset = unset -end_of_line = unset -indent_size = unset -indent_style = unset -insert_final_newline = false -trim_trailing_whitespace = false diff --git a/openg2p-g2p-bridge-example-bank-api/celery_main.py b/openg2p-g2p-bridge-example-bank-api/celery_main.py deleted file mode 100644 index 3bc4a3c..0000000 --- a/openg2p-g2p-bridge-example-bank-api/celery_main.py +++ /dev/null @@ -1,3 +0,0 @@ -from openg2p_g2p_bridge_example_bank_api.celery_app import celery_app - -celery_app = celery_app diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py deleted file mode 100644 index 15e55da..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/app.py +++ /dev/null @@ -1,45 +0,0 @@ -# ruff: noqa: E402 -import asyncio -import logging - -from .config import Settings - -_config = Settings.get_config() - -from openg2p_fastapi_common.app import Initializer as BaseInitializer -from sqlalchemy import create_engine - -from .controllers import ( - BlockFundsController, - FundAvailabilityController, - PaymentController, -) -from .models import Account, FundBlock, InitiatePaymentRequest - -_logger = logging.getLogger(_config.logging_default_logger_name) - - -class Initializer(BaseInitializer): - def initialize(self, **kwargs): - super().initialize() - - BlockFundsController().post_init() - FundAvailabilityController().post_init() - PaymentController().post_init() - - def migrate_database(self, args): - super().migrate_database(args) - - async def migrate(): - _logger.info("Migrating database") - await Account.create_migrate() - await FundBlock.create_migrate() - await InitiatePaymentRequest.create_migrate() - - asyncio.run(migrate()) - - -def get_engine(): - if _config.db_datasource: - db_engine = create_engine(_config.db_datasource) - return db_engine diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py deleted file mode 100644 index 62cd7cc..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/celery_app.py +++ /dev/null @@ -1,166 +0,0 @@ -import random -import uuid -from datetime import datetime -from typing import List - -from celery import Celery -from sqlalchemy import select -from sqlalchemy.orm import sessionmaker - -from .app import get_engine -from .config import Settings -from .models import ( - Account, - AccountingLog, - DebitCreditTypes, - FundBlock, - InitiatePaymentRequest, - PaymentStatus, -) - -_config = Settings.get_config() - -celery_app = Celery( - "example_bank_celery_tasks", - broker="redis://localhost:6379/0", - backend="redis://localhost:6379/0", -) - -celery_app.conf.beat_schedule = { - "process_payments": { - "task": "process_payments", - "schedule": 10, - } -} - -celery_app.conf.timezone = "UTC" -_engine = get_engine() - - -@celery_app.task(name="process_payments") -def process_payments(): - session_maker = sessionmaker(bind=_engine, expire_on_commit=False) - with session_maker() as session: - initiate_payment_requests = ( - session.execute( - select(InitiatePaymentRequest).where( - (InitiatePaymentRequest.payment_status.in_(["PENDING", "FAILED"])) - & ( - InitiatePaymentRequest.payment_initiate_attempts - < _config.payment_initiate_attempts - ) - ) - ) - .scalars() - .all() - ) - - failure_logs = [] - for initiate_payment_request in initiate_payment_requests: - account = ( - session.execute( - select(Account).where( - Account.account_number - == initiate_payment_request.remitting_account - ) - ) - .scalars() - .first() - ) - - fund_block = ( - session.execute( - select(FundBlock).where( - FundBlock.block_reference_no - == initiate_payment_request.funds_blocked_reference_number - ) - ) - .scalars() - .first() - ) - - accounting_log: AccountingLog = construct_accounting_log( - initiate_payment_request - ) - - update_account(account, initiate_payment_request.payment_amount) - update_fund_block(fund_block, initiate_payment_request.payment_amount) - initiate_payment_request.payment_status = PaymentStatus.SUCCESS - initiate_payment_request.payment_initiate_attempts += 1 - - failure_random_number = random.randint(1, 100) - if failure_random_number <= 30: - failure_logs.append(accounting_log) - - session.add(accounting_log) - session.add(fund_block) - session.add(account) - - # End of loop - - generate_failures(account, failure_logs, fund_block, session) - - session.commit() - - -def construct_accounting_log(initiate_payment_request: InitiatePaymentRequest): - return AccountingLog( - reference_no=str(uuid.uuid4()), - customer_reference_no=initiate_payment_request.payment_reference_number, - debit_credit=DebitCreditTypes.DEBIT, - account_number=initiate_payment_request.remitting_account, - transaction_amount=initiate_payment_request.payment_amount, - transaction_date=datetime.utcnow(), - transaction_currency=initiate_payment_request.remitting_account_currency, - transaction_code="DBT", - narrative_1=initiate_payment_request.narrative_1, - narrative_2=initiate_payment_request.narrative_2, - narrative_3=initiate_payment_request.narrative_3, - narrative_4=initiate_payment_request.narrative_4, - narrative_5=initiate_payment_request.narrative_5, - narrative_6=initiate_payment_request.narrative_6, - active=True, - ) - - -def generate_failures( - account: Account, failure_logs: List[AccountingLog], fund_block: FundBlock, session -): - failure_reasons = [ - "ACCOUNT_CLOSED", - "ACCOUNT_NOT_FOUND", - "ACCOUNT_DORMANT", - "ACCOUNT_DECEASED", - ] - for failure_log in failure_logs: - account_log: AccountingLog = AccountingLog( - reference_no=str(uuid.uuid4()), - customer_reference_no=failure_log.customer_reference_no, - debit_credit=failure_log.debit_credit, - account_number=failure_log.account_number, - transaction_amount=-failure_log.transaction_amount, - transaction_date=failure_log.transaction_date, - transaction_currency=failure_log.transaction_currency, - transaction_code=failure_log.transaction_code, - narrative_1=failure_log.narrative_1, - narrative_2=failure_log.narrative_2, - narrative_3=failure_log.narrative_3, - narrative_4=failure_log.narrative_4, - narrative_5=failure_log.narrative_5, - narrative_6=random.choice(failure_reasons), - active=True, - ) - session.add(account_log) - - update_account(account, account_log.transaction_amount) - update_fund_block(fund_block, account_log.transaction_amount) - - -def update_account(account, payment_amount): - account.book_balance -= payment_amount - account.blocked_amount -= payment_amount - account.available_balance = account.book_balance - account.blocked_amount - - -def update_fund_block(fund_block, payment_amount): - fund_block.amount_released += payment_amount diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py deleted file mode 100644 index 825cb72..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/config.py +++ /dev/null @@ -1,23 +0,0 @@ -from openg2p_fastapi_common.config import Settings as BaseSettings -from pydantic_settings import SettingsConfigDict - -from . import __version__ - - -class Settings(BaseSettings): - model_config = SettingsConfigDict( - env_prefix="example_bank_", env_file=".env", extra="allow" - ) - - openapi_title: str = "Example Bank APIs for Cash Transfer" - openapi_description: str = """ - *********************************** - Further details goes here - *********************************** - """ - openapi_version: str = __version__ - - db_dbname: str = "example_bank_db" - # db_driver: str = "postgresql" - - payment_initiate_attempts: int = 3 diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py deleted file mode 100644 index 41dd4d4..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .block_funds import BlockFundsController -from .check_available_funds import FundAvailabilityController -from .initiate_payment import PaymentController diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py deleted file mode 100644 index 4b11683..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/block_funds.py +++ /dev/null @@ -1,74 +0,0 @@ -import uuid - -from openg2p_fastapi_common.context import dbengine -from openg2p_fastapi_common.controller import BaseController -from sqlalchemy import update -from sqlalchemy.ext.asyncio import async_sessionmaker -from sqlalchemy.future import select - -from ..models import Account, FundBlock -from ..schemas import BlockFundsRequest, BlockFundsResponse - - -class BlockFundsController(BaseController): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.router.tags += ["Funds Management"] - - self.router.add_api_route( - "/block_funds", - self.block_funds, - response_model=BlockFundsResponse, - methods=["POST"], - ) - - async def block_funds(self, request: BlockFundsRequest) -> BlockFundsResponse: - session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) - async with session_maker() as session: - stmt = select(Account).where( - (Account.account_number == request.account_no) - & (Account.account_currency == request.currency) - ) - result = await session.execute(stmt) - account = result.scalars().first() - - if not account: - return BlockFundsResponse( - status="failed", - block_reference_no="", - error_message="Account not found", - ) - if account.available_balance < request.amount: - return BlockFundsResponse( - status="failed", - block_reference_no="", - error_message="Insufficient funds", - ) - - await session.execute( - update(Account) - .where(Account.account_number == request.account_no) - .values( - available_balance=account.book_balance - - (account.blocked_amount + request.amount), - blocked_amount=account.blocked_amount + request.amount, - ) - ) - - block_reference_no = str(uuid.uuid4()) - fund_block = FundBlock( - block_reference_no=block_reference_no, - account_no=request.account_no, - amount=request.amount, - currency=request.currency, - active=True, - ) - session.add(fund_block) - - await session.commit() - return BlockFundsResponse( - status="success", - block_reference_no=block_reference_no, - error_message="", - ) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py deleted file mode 100644 index 2b68632..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/check_available_funds.py +++ /dev/null @@ -1,55 +0,0 @@ -from openg2p_fastapi_common.context import dbengine -from openg2p_fastapi_common.controller import BaseController -from sqlalchemy.ext.asyncio import async_sessionmaker -from sqlalchemy.future import select - -from ..models import Account -from ..schemas import CheckFundRequest, CheckFundResponse - - -class FundAvailabilityController(BaseController): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.router.tags += ["Fund Availability"] - - self.router.add_api_route( - "/check_funds", - self.check_available_funds, - response_model=CheckFundResponse, - methods=["POST"], - ) - - async def check_available_funds( - self, request: CheckFundRequest - ) -> CheckFundResponse: - session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) - async with session_maker() as session: - stmt = select(Account).where( - Account.account_number == request.account_number - ) - result = await session.execute(stmt) - account = result.scalars().first() - - if not account: - return CheckFundResponse( - status="failed", - account_number=request.account_number, - has_sufficient_funds=False, - error_message="Account not found", - ) - - if account.available_balance >= request.total_funds_needed: - return CheckFundResponse( - status="success", - account_number=account.account_number, - has_sufficient_funds=True, - error_message="", - ) - else: - return CheckFundResponse( - status="failed", - account_number=account.account_number, - has_sufficient_funds=False, - error_message="Insufficient funds", - ) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py deleted file mode 100644 index bbcbadc..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/controllers/initiate_payment.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import List - -from openg2p_fastapi_common.context import dbengine -from openg2p_fastapi_common.controller import BaseController -from sqlalchemy.ext.asyncio import async_sessionmaker -from sqlalchemy.future import select - -from ..models import FundBlock, InitiatePaymentRequest -from ..schemas import InitiatePaymentPayload, InitiatorPaymentResponse - - -class PaymentController(BaseController): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.router.tags += ["Payments Management"] - - self.router.add_api_route( - "/initiate_payment", - self.initiate_payment, - response_model=InitiatorPaymentResponse, - methods=["POST"], - ) - - async def initiate_payment( - self, initiate_payment_payloads: List[InitiatePaymentPayload] - ) -> InitiatorPaymentResponse: - session_maker = async_sessionmaker(dbengine.get(), expire_on_commit=False) - async with session_maker() as session: - for initiate_payment_payload in initiate_payment_payloads: - fund_block_stmt = select(FundBlock).where( - FundBlock.block_reference_no - == initiate_payment_payload.funds_blocked_reference_number - ) - fund_block_result = await session.execute(fund_block_stmt) - fund_block = fund_block_result.scalars().first() - - if ( - not fund_block - or initiate_payment_payload.payment_amount > fund_block.amount - or fund_block.currency - != initiate_payment_payload.remitting_account_currency - ): - return InitiatorPaymentResponse( - status="failed", - error_message="Invalid funds block reference or mismatch in details", - ) - - payment = InitiatePaymentRequest( - payment_reference_number=initiate_payment_payload.payment_reference_number, - remitting_account=initiate_payment_payload.remitting_account, - remitting_account_currency=initiate_payment_payload.remitting_account_currency, - payment_amount=initiate_payment_payload.payment_amount, - funds_blocked_reference_number=initiate_payment_payload.funds_blocked_reference_number, - beneficiary_name=initiate_payment_payload.beneficiary_name, - beneficiary_account=initiate_payment_payload.beneficiary_account, - beneficiary_account_currency=initiate_payment_payload.beneficiary_account_currency, - beneficiary_account_type=initiate_payment_payload.beneficiary_account_type, - beneficiary_bank_code=initiate_payment_payload.beneficiary_bank_code, - beneficiary_branch_code=initiate_payment_payload.beneficiary_branch_code, - beneficiary_mobile_wallet_provider=initiate_payment_payload.beneficiary_mobile_wallet_provider, - beneficiary_phone_no=initiate_payment_payload.beneficiary_phone_no, - beneficiary_email=initiate_payment_payload.beneficiary_email, - beneficiary_email_wallet_provider=initiate_payment_payload.beneficiary_email_wallet_provider, - payment_date=initiate_payment_payload.payment_date, - narrative_1=initiate_payment_payload.narrative_1, - narrative_2=initiate_payment_payload.narrative_2, - narrative_3=initiate_payment_payload.narrative_3, - narrative_4=initiate_payment_payload.narrative_4, - narrative_5=initiate_payment_payload.narrative_5, - narrative_6=initiate_payment_payload.narrative_6, - active=True, - ) - session.add(payment) - - await session.commit() - - return InitiatorPaymentResponse(status="success", error_message="") diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py deleted file mode 100644 index 497521c..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .account import ( - Account, - AccountingLog, - DebitCreditTypes, - FundBlock, - InitiatePaymentRequest, - PaymentStatus, -) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py deleted file mode 100644 index 5e94088..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/models/account.py +++ /dev/null @@ -1,106 +0,0 @@ -from datetime import datetime -from enum import Enum - -from openg2p_fastapi_common.models import BaseORMModelWithTimes -from sqlalchemy import DateTime, Float, Integer, String -from sqlalchemy import Enum as SqlEnum -from sqlalchemy.orm import Mapped, mapped_column - - -class PaymentStatus(Enum): - PENDING = "PENDING" - SUCCESS = "SUCCESS" - FAILED = "FAILED" - - -class DebitCreditTypes(Enum): - DEBIT = "debit" - CREDIT = "credit" - - -class Account(BaseORMModelWithTimes): - __tablename__ = "accounts" - account_holder_name: Mapped[str] = mapped_column(String) - account_number: Mapped[str] = mapped_column(String) - account_currency: Mapped[str] = mapped_column(String) - book_balance: Mapped[float] = mapped_column(Float) - available_balance: Mapped[float] = mapped_column(Float) - blocked_amount: Mapped[float] = mapped_column(Float, default=0) - - -class FundBlock(BaseORMModelWithTimes): - __tablename__ = "fund_blocks" - block_reference_no: Mapped[str] = mapped_column(String, index=True, unique=True) - account_no: Mapped[str] = mapped_column(String) - currency: Mapped[str] = mapped_column(String) - amount: Mapped[float] = mapped_column(Float) - amount_released: Mapped[float] = mapped_column(Float, default=0) - - -class InitiatePaymentRequest(BaseORMModelWithTimes): - __tablename__ = "initiate_payment_requests" - payment_reference_number = mapped_column( - String, index=True, unique=True - ) # disbursement id - - remitting_account: Mapped[str] = mapped_column(String, nullable=False) - remitting_account_currency: Mapped[str] = mapped_column(String, nullable=False) - payment_amount: Mapped[float] = mapped_column(Float, nullable=False) - payment_date: Mapped[str] = mapped_column(String, nullable=False) - funds_blocked_reference_number: Mapped[str] = mapped_column(String, nullable=False) - - beneficiary_name: Mapped[str] = mapped_column(String) - beneficiary_account: Mapped[str] = mapped_column(String) - beneficiary_account_currency: Mapped[str] = mapped_column(String) - beneficiary_account_type: Mapped[str] = mapped_column(String) - beneficiary_bank_code: Mapped[str] = mapped_column(String) - beneficiary_branch_code: Mapped[str] = mapped_column(String) - - beneficiary_mobile_wallet_provider: Mapped[str] = mapped_column( - String, nullable=True - ) - beneficiary_phone_no: Mapped[str] = mapped_column(String, nullable=True) - - beneficiary_email: Mapped[str] = mapped_column(String, nullable=True) - beneficiary_email_wallet_provider: Mapped[str] = mapped_column( - String, nullable=True - ) - - narrative_1: Mapped[str] = mapped_column( - String, nullable=True - ) # disbursement narrative - narrative_2: Mapped[str] = mapped_column(String, nullable=True) # program pneumonic - narrative_3: Mapped[str] = mapped_column( - String, nullable=True - ) # cycle code pneumonic - narrative_4: Mapped[str] = mapped_column(String, nullable=True) # beneficiary id - narrative_5: Mapped[str] = mapped_column(String, nullable=True) - narrative_6: Mapped[str] = mapped_column(String, nullable=True) - - payment_initiate_attempts: Mapped[int] = mapped_column(Integer, default=0) - payment_status: Mapped[PaymentStatus] = mapped_column( - SqlEnum(PaymentStatus), default=PaymentStatus.PENDING - ) - - -class AccountingLog(BaseORMModelWithTimes): - __tablename__ = "accounting_logs" - reference_no: Mapped[str] = mapped_column(String, index=True, unique=True) - customer_reference_no: Mapped[str] = mapped_column(String, index=True) - debit_credit: Mapped[DebitCreditTypes] = mapped_column(SqlEnum(DebitCreditTypes)) - account_number: Mapped[str] = mapped_column(String, index=True) - transaction_amount: Mapped[float] = mapped_column(Float) - transaction_date: Mapped[datetime] = mapped_column(DateTime) - transaction_currency: Mapped[str] = mapped_column(String) - transaction_code: Mapped[str] = mapped_column(String, nullable=True) - - narrative_1: Mapped[str] = mapped_column(String, nullable=True) # disbursement id - narrative_2: Mapped[str] = mapped_column(String, nullable=True) # beneficiary id - narrative_3: Mapped[str] = mapped_column(String, nullable=True) # program pneumonic - narrative_4: Mapped[str] = mapped_column( - String, nullable=True - ) # cycle code pneumonic - narrative_5: Mapped[str] = mapped_column(String, nullable=True) # beneficiary email - narrative_6: Mapped[str] = mapped_column( - String, nullable=True - ) # beneficiary phone number diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py deleted file mode 100644 index 3b47a75..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .fund_schemas import ( - BlockFundsRequest, - BlockFundsResponse, - CheckFundRequest, - CheckFundResponse, - InitiatePaymentPayload, - InitiatorPaymentResponse, -) diff --git a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py b/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py deleted file mode 100644 index c623d9c..0000000 --- a/openg2p-g2p-bridge-example-bank-api/src/openg2p_g2p_bridge_example_bank_api/schemas/fund_schemas.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel - - -class CheckFundRequest(BaseModel): - account_number: str - account_currency: str - total_funds_needed: float - - -class CheckFundResponse(BaseModel): - status: str - account_number: str - has_sufficient_funds: bool - error_message: Optional[str] = None - - -class BlockFundsRequest(BaseModel): - account_no: str - currency: str - amount: float - - -class BlockFundsResponse(BaseModel): - status: str - block_reference_no: str - error_message: Optional[str] = None - - -class InitiatePaymentPayload(BaseModel): - payment_reference_number: str - remitting_account: str - remitting_account_currency: str - payment_amount: float - funds_blocked_reference_number: str - beneficiary_name: str - - beneficiary_account: str - beneficiary_account_currency: str - beneficiary_account_type: str - beneficiary_bank_code: str - beneficiary_branch_code: str - - beneficiary_mobile_wallet_provider: Optional[str] = None - beneficiary_phone_no: Optional[str] = None - - beneficiary_email: Optional[str] = None - beneficiary_email_wallet_provider: Optional[str] = None - - narrative_1: Optional[str] = None - narrative_2: Optional[str] = None - narrative_3: Optional[str] = None - narrative_4: Optional[str] = None - narrative_5: Optional[str] = None - narrative_6: Optional[str] = None - - payment_date: str - - -class InitiatorPaymentResponse(BaseModel): - status: str - error_message: Optional[str] = None diff --git a/openg2p-g2p-bridge-example-bank-api/test-requirements.txt b/openg2p-g2p-bridge-example-bank-api/test-requirements.txt deleted file mode 100644 index 4f53afa..0000000 --- a/openg2p-g2p-bridge-example-bank-api/test-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest-cov -git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-common -git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-auth diff --git a/openg2p-g2p-bridge-example-bank-api/tests/__init__.py b/openg2p-g2p-bridge-example-bank-api/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py b/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py deleted file mode 100644 index 8171c3e..0000000 --- a/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement.py +++ /dev/null @@ -1,175 +0,0 @@ -import datetime -from unittest.mock import AsyncMock, patch - -import pytest -from openg2p_g2p_bridge_api.controllers import DisbursementController -from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes -from openg2p_g2p_bridge_models.errors.exceptions import DisbursementException -from openg2p_g2p_bridge_models.models import CancellationStatus -from openg2p_g2p_bridge_models.schemas import ( - DisbursementPayload, - DisbursementRequest, - DisbursementResponse, - ResponseStatus, -) - - -def mock_create_disbursements(is_valid, disbursement_payloads): - if not is_valid: - raise DisbursementException( - code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, - disbursement_payloads=disbursement_payloads, - ) - return disbursement_payloads - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") -async def test_create_disbursements_success(mock_service_get_component): - mock_service_instance = AsyncMock() - disbursement_payloads = [ - DisbursementPayload( - disbursement_envelope_id="env123", - beneficiary_id="123AB", - disbursement_amount=1000, - ) - ] - mock_service_instance.create_disbursements = AsyncMock( - return_value=mock_create_disbursements(True, disbursement_payloads) - ) - mock_service_instance.construct_disbursement_success_response = AsyncMock( - return_value=DisbursementResponse( - response_status=ResponseStatus.SUCCESS, - response_payload=disbursement_payloads, - ) - ) - - mock_service_get_component.return_value = mock_service_instance - - controller = DisbursementController() - request_payload = DisbursementRequest(request_payload=disbursement_payloads) - - response = await controller.create_disbursements(request_payload) - - assert response.response_status == ResponseStatus.SUCCESS - assert response.response_payload == disbursement_payloads - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") -async def test_create_disbursements_failure(mock_service_get_component): - mock_service_instance = AsyncMock() - disbursement_payloads = [ - DisbursementPayload( - disbursement_envelope_id="env123", - beneficiary_id="123AB", - disbursement_amount=1000, - ) - ] - mock_service_instance.create_disbursements = AsyncMock( - side_effect=lambda req: mock_create_disbursements(False, req.request_payload) - ) - mock_service_instance.construct_disbursement_error_response = AsyncMock( - return_value=DisbursementResponse( - response_status=ResponseStatus.FAILURE, - response_error_code=G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD, - response_payload=disbursement_payloads, - ) - ) - - mock_service_get_component.return_value = mock_service_instance - - controller = DisbursementController() - request_payload = DisbursementRequest(request_payload=disbursement_payloads) - - response = await controller.create_disbursements(request_payload) - - assert response.response_status == ResponseStatus.FAILURE - assert ( - response.response_error_code == G2PBridgeErrorCodes.INVALID_DISBURSEMENT_PAYLOAD - ) - - -def mock_cancel_disbursements(is_valid, disbursement_payloads): - if not is_valid: - raise DisbursementException( - code=G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED, - disbursement_payloads=disbursement_payloads, - ) - for payload in disbursement_payloads: - payload.cancellation_status = CancellationStatus.Cancelled - payload.cancellation_time_stamp = datetime.datetime.utcnow() - return disbursement_payloads - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") -async def test_cancel_disbursements_success(mock_service_get_component): - mock_service_instance = AsyncMock() - disbursement_payloads = [ - DisbursementPayload( - disbursement_id="123", - beneficiary_id="123AB", - disbursement_amount=1000, - cancellation_status=None, - ) - ] - mock_service_instance.cancel_disbursements = AsyncMock( - return_value=mock_cancel_disbursements(True, disbursement_payloads) - ) - mock_service_instance.construct_disbursement_success_response = AsyncMock( - return_value=DisbursementResponse( - response_status=ResponseStatus.SUCCESS, - response_payload=disbursement_payloads, - ) - ) - - mock_service_get_component.return_value = mock_service_instance - - controller = DisbursementController() - request_payload = DisbursementRequest(request_payload=disbursement_payloads) - - response = await controller.cancel_disbursements(request_payload) - - assert response.response_status == ResponseStatus.SUCCESS - assert all( - payload.cancellation_status == CancellationStatus.Cancelled - for payload in response.response_payload - ) - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementService.get_component") -async def test_cancel_disbursements_failure(mock_service_get_component): - mock_service_instance = AsyncMock() - disbursement_payloads = [ - DisbursementPayload( - disbursement_id="123", - beneficiary_id="123AB", - disbursement_amount=1000, - cancellation_status=None, - ) - ] - mock_service_instance.cancel_disbursements = AsyncMock( - side_effect=lambda req: mock_cancel_disbursements(False, req.request_payload) - ) - mock_service_instance.construct_disbursement_error_response = AsyncMock( - return_value=DisbursementResponse( - response_status=ResponseStatus.FAILURE, - response_error_code=G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED, - response_payload=disbursement_payloads, - ) - ) - - mock_service_get_component.return_value = mock_service_instance - - controller = DisbursementController() - request_payload = DisbursementRequest(request_payload=disbursement_payloads) - - response = await controller.cancel_disbursements(request_payload) - - assert response.response_status == ResponseStatus.FAILURE - assert ( - response.response_error_code - == G2PBridgeErrorCodes.DISBURSEMENT_ALREADY_CANCELED - ) diff --git a/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py b/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py deleted file mode 100644 index 4ccfd8a..0000000 --- a/openg2p-g2p-bridge-example-bank-api/tests/test_disbursement_envelope.py +++ /dev/null @@ -1,197 +0,0 @@ -from datetime import datetime -from unittest.mock import AsyncMock, patch - -import pytest -from openg2p_g2p_bridge_api.controllers import DisbursementEnvelopeController -from openg2p_g2p_bridge_models.errors.codes import G2PBridgeErrorCodes -from openg2p_g2p_bridge_models.errors.exceptions import DisbursementEnvelopeException -from openg2p_g2p_bridge_models.schemas import ( - DisbursementEnvelopePayload, - DisbursementEnvelopeRequest, - DisbursementEnvelopeResponse, - ResponseStatus, -) - - -def mock_create_disbursement_envelope(is_valid, error_code=None): - if not is_valid: - raise DisbursementEnvelopeException( - code=error_code, message=f"{error_code} error." - ) - return DisbursementEnvelopePayload( - disbursement_envelope_id="env123", - benefit_program_mnemonic="TEST123", - disbursement_frequency="Monthly", - cycle_code_mnemonic="CYCLE42", - number_of_beneficiaries=100, - number_of_disbursements=100, - total_disbursement_amount=5000.00, - disbursement_schedule_date=datetime.date(datetime.utcnow()), - ) - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") -async def test_create_disbursement_envelope_success(mock_service_get_component): - mock_service_instance = AsyncMock() - mock_service_instance.create_disbursement_envelope = AsyncMock( - return_value=mock_create_disbursement_envelope(True) - ) - mock_service_instance.construct_disbursement_envelope_success_response = AsyncMock() - - mock_service_get_component.return_value = mock_service_instance - - expected_payload = mock_create_disbursement_envelope(True) - expected_response = DisbursementEnvelopeResponse( - response_status=ResponseStatus.SUCCESS, response_payload=expected_payload - ) - mock_service_instance.construct_disbursement_envelope_success_response.return_value = ( - expected_response - ) - controller = DisbursementEnvelopeController() - request_payload = DisbursementEnvelopeRequest( - request_payload=DisbursementEnvelopePayload( - benefit_program_mnemonic="TEST123", - disbursement_frequency="Monthly", - cycle_code_mnemonic="CYCLE42", - number_of_beneficiaries=100, - number_of_disbursements=100, - total_disbursement_amount=5000.00, - disbursement_schedule_date=datetime.date(datetime.utcnow()), - ) - ) - - actual_response = await controller.create_disbursement_envelope(request_payload) - - assert actual_response == expected_response - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") -@pytest.mark.parametrize("error_code", list(G2PBridgeErrorCodes)) -async def test_create_disbursement_envelope_errors( - mock_service_get_component, error_code -): - mock_service_instance = AsyncMock() - mock_service_instance.create_disbursement_envelope.side_effect = ( - lambda request: mock_create_disbursement_envelope(False, error_code) - ) - mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() - - mock_service_get_component.return_value = mock_service_instance - - error_response = DisbursementEnvelopeResponse( - response_status=ResponseStatus.FAILURE, - response_error_code=error_code, - ) - - mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( - error_response - ) - - controller = DisbursementEnvelopeController() - - request_payload = DisbursementEnvelopeRequest( - request_payload=DisbursementEnvelopePayload( - benefit_program_mnemonic="", # Trigger the error - disbursement_frequency="Monthly", - cycle_code_mnemonic="CYCLE42", - number_of_beneficiaries=100, - number_of_disbursements=100, - total_disbursement_amount=5000.00, - disbursement_schedule_date=datetime.date(datetime.utcnow()), - ) - ) - - actual_response = await controller.create_disbursement_envelope(request_payload) - - assert ( - actual_response == error_response - ), f"The response did not match the expected error response for {error_code}." - - -def mock_cancel_disbursement_envelope(is_valid, error_code=None): - if not is_valid: - raise DisbursementEnvelopeException( - code=error_code, message=f"{error_code} error." - ) - - return DisbursementEnvelopePayload( - disbursement_envelope_id="env123", - benefit_program_mnemonic="TEST123", - disbursement_frequency="Monthly", - cycle_code_mnemonic="CYCLE42", - number_of_beneficiaries=100, - number_of_disbursements=100, - total_disbursement_amount=5000.00, - disbursement_schedule_date=datetime.date(datetime.utcnow()), - ) - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") -async def test_cancel_disbursement_envelope_success(mock_service_get_component): - mock_service_instance = AsyncMock() - mock_service_instance.cancel_disbursement_envelope = AsyncMock( - return_value=mock_cancel_disbursement_envelope(True) - ) - mock_service_instance.construct_disbursement_envelope_success_response = AsyncMock() - - mock_service_get_component.return_value = mock_service_instance - - successful_payload = mock_cancel_disbursement_envelope(True) - expected_response = DisbursementEnvelopeResponse( - response_status=ResponseStatus.SUCCESS, response_payload=successful_payload - ) - mock_service_instance.construct_disbursement_envelope_success_response.return_value = ( - expected_response - ) - - controller = DisbursementEnvelopeController() - request_payload = DisbursementEnvelopeRequest( - request_payload=DisbursementEnvelopePayload(disbursement_envelope_id="env123") - ) - - actual_response = await controller.cancel_disbursement_envelope(request_payload) - assert actual_response == expected_response - - -@pytest.mark.asyncio -@patch("openg2p_g2p_bridge_api.services.DisbursementEnvelopeService.get_component") -@pytest.mark.parametrize( - "error_code", - [ - G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_NOT_FOUND, - G2PBridgeErrorCodes.DISBURSEMENT_ENVELOPE_ALREADY_CANCELED, - ], -) -async def test_cancel_disbursement_envelope_failure( - mock_service_get_component, error_code -): - mock_service_instance = AsyncMock() - mock_service_instance.cancel_disbursement_envelope.side_effect = ( - lambda request: mock_cancel_disbursement_envelope(False, error_code) - ) - mock_service_instance.construct_disbursement_envelope_error_response = AsyncMock() - - mock_service_get_component.return_value = mock_service_instance - - error_response = DisbursementEnvelopeResponse( - response_status=ResponseStatus.FAILURE, - response_error_code=error_code.value, - ) - mock_service_instance.construct_disbursement_envelope_error_response.return_value = ( - error_response - ) - - controller = DisbursementEnvelopeController() - request_payload = DisbursementEnvelopeRequest( - request_payload=DisbursementEnvelopePayload( - disbursement_envelope_id="env123" # Assuming this ID triggers the error - ) - ) - - actual_response = await controller.cancel_disbursement_envelope(request_payload) - assert ( - actual_response == error_response - ), f"The response for {error_code} did not match the expected error response." From 8e20d0e08aa6e846dabb1ed9d58178904bee2bc4 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Sun, 4 Aug 2024 14:01:50 +0530 Subject: [PATCH 38/39] Add logging --- .../controllers/account_statement.py | 12 +++++++++++- .../controllers/disbursement.py | 12 +++++++++++- .../controllers/disbursement_envelope.py | 14 ++++++++++++-- .../src/openg2p_g2p_bridge_api/utils/__init__.py | 1 - .../utils/model_serializer.py | 6 ------ 5 files changed, 34 insertions(+), 11 deletions(-) delete mode 100644 openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/model_serializer.py diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py index 66e2261..18b5bae 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/account_statement.py @@ -1,3 +1,5 @@ +import logging + from fastapi import File, UploadFile from openg2p_fastapi_common.controller import BaseController from openg2p_g2p_bridge_models.errors.codes import ( @@ -12,6 +14,11 @@ from openg2p_g2p_bridge_api.services import AccountStatementService +from ..config import Settings + +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) + class AccountStatementController(BaseController): def __init__(self, **kwargs): @@ -31,6 +38,7 @@ async def upload_mt940( self, statement_file: UploadFile = File(...), ) -> AccountStatementResponse: + _logger.info("Uploading statement file") try: account_statement_id: str = ( await self.account_statement_service.upload_mt940(statement_file) @@ -39,8 +47,10 @@ async def upload_mt940( account_statement_id ) except AccountStatementException: + _logger.error("Error uploading statement file") account_statement_response: AccountStatementResponse = await self.account_statement_service.construct_account_statement_error_response( G2PBridgeErrorCodes.STATEMENT_UPLOAD_ERROR ) - + return account_statement_response + _logger.info("Statement file uploaded successfully") return account_statement_response diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement.py index 6562f80..984d3c3 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement.py @@ -1,3 +1,4 @@ +import logging from typing import List from openg2p_fastapi_common.controller import BaseController @@ -8,8 +9,12 @@ DisbursementResponse, ) +from ..config import Settings from ..services import DisbursementService +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) + class DisbursementController(BaseController): def __init__(self, **kwargs): @@ -34,6 +39,7 @@ def __init__(self, **kwargs): async def create_disbursements( self, disbursement_request: DisbursementRequest ) -> DisbursementResponse: + _logger.info("Creating disbursements") try: disbursement_payloads: List[ DisbursementPayload @@ -41,6 +47,7 @@ async def create_disbursements( disbursement_request ) except DisbursementException as e: + _logger.error("Error creating disbursements") error_response: DisbursementResponse = ( await self.disbursement_service.construct_disbursement_error_response( e.code, e.disbursement_payloads @@ -53,12 +60,14 @@ async def create_disbursements( disbursement_payloads ) ) + _logger.info("Disbursements created successfully") return disbursement_response async def cancel_disbursements( self, disbursement_request: DisbursementRequest ) -> DisbursementResponse: + _logger.info("Cancelling disbursements") try: disbursement_payloads: List[ DisbursementPayload @@ -66,6 +75,7 @@ async def cancel_disbursements( disbursement_request ) except DisbursementException as e: + _logger.error("Error cancelling disbursements") error_response: DisbursementResponse = ( await self.disbursement_service.construct_disbursement_error_response( e.code, e.disbursement_payloads @@ -78,5 +88,5 @@ async def cancel_disbursements( disbursement_payloads ) ) - + _logger.info("Disbursements cancelled successfully") return disbursement_response diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py index fc98a48..92256d7 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/controllers/disbursement_envelope.py @@ -1,3 +1,5 @@ +import logging + from openg2p_fastapi_common.controller import BaseController from openg2p_g2p_bridge_models.errors.exceptions import DisbursementEnvelopeException from openg2p_g2p_bridge_models.schemas import ( @@ -6,8 +8,12 @@ DisbursementEnvelopeResponse, ) +from ..config import Settings from ..services import DisbursementEnvelopeService +_config = Settings.get_config() +_logger = logging.getLogger(_config.logging_default_logger_name) + class DisbursementEnvelopeController(BaseController): def __init__(self, **kwargs): @@ -32,6 +38,7 @@ def __init__(self, **kwargs): async def create_disbursement_envelope( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> DisbursementEnvelopeResponse: + _logger.info("Creating disbursement envelope") try: disbursement_envelope_payload: DisbursementEnvelopePayload = ( await self.disbursement_envelope_service.create_disbursement_envelope( @@ -39,6 +46,7 @@ async def create_disbursement_envelope( ) ) except DisbursementEnvelopeException as e: + _logger.error("Error creating disbursement envelope") error_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_error_response( e.code ) @@ -47,12 +55,13 @@ async def create_disbursement_envelope( disbursement_envelope_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_success_response( disbursement_envelope_payload ) - + _logger.info("Disbursement envelope created successfully") return disbursement_envelope_response async def cancel_disbursement_envelope( self, disbursement_envelope_request: DisbursementEnvelopeRequest ) -> DisbursementEnvelopeResponse: + _logger.info("Cancelling disbursement envelope") try: disbursement_envelope_payload: DisbursementEnvelopePayload = ( await self.disbursement_envelope_service.cancel_disbursement_envelope( @@ -60,6 +69,7 @@ async def cancel_disbursement_envelope( ) ) except DisbursementEnvelopeException as e: + _logger.error("Error cancelling disbursement envelope") error_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_error_response( e.code ) @@ -68,5 +78,5 @@ async def cancel_disbursement_envelope( disbursement_envelope_response: DisbursementEnvelopeResponse = await self.disbursement_envelope_service.construct_disbursement_envelope_success_response( disbursement_envelope_payload ) - + _logger.info("Disbursement envelope cancelled successfully") return disbursement_envelope_response diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/__init__.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/__init__.py index 224d44a..e69de29 100644 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/__init__.py +++ b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/__init__.py @@ -1 +0,0 @@ -from .model_serializer import serialize_model diff --git a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/model_serializer.py b/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/model_serializer.py deleted file mode 100644 index 97a93e2..0000000 --- a/openg2p-g2p-bridge-api/src/openg2p_g2p_bridge_api/utils/model_serializer.py +++ /dev/null @@ -1,6 +0,0 @@ -from sqlalchemy.inspection import inspect - - -def serialize_model(obj): - """Converts SQLAlchemy model instance into a JSON-compliant dictionary.""" - return {c.key: getattr(obj, c.key) for c in inspect(obj).mapper.column_attrs} From 7edfe3d9decb00854c6361b8daa239b2bf560004 Mon Sep 17 00:00:00 2001 From: PSNAppZ Date: Sun, 4 Aug 2024 14:32:00 +0530 Subject: [PATCH 39/39] Separate Beat and Worker Tasks and Create Separate Queue --- .../celerybeat-schedule.db | Bin 16384 -> 16384 bytes .../app.py | 2 +- .../tasks/__init__.py | 13 +++ .../tasks/block_funds_with_bank.py | 76 ++++++++++++++++++ .../tasks/check_funds_with_bank_task.py | 69 ++++++++++++++++ .../tasks/disburse_funds_from_bank.py | 75 +++++++++++++++++ .../tasks/mapper_resolution_task.py | 43 ++++++++++ .../tasks/mt940_processor.py | 42 ++++++++++ .../openg2p_g2p_bridge_celery_workers/app.py | 4 +- .../config.py | 6 -- .../tasks/__init__.py | 6 +- .../tasks/block_funds_with_bank.py | 55 +------------ .../tasks/check_funds_with_bank_task.py | 49 ----------- .../tasks/disburse_funds_from_bank.py | 54 ------------- .../tasks/mapper_resolution_task.py | 28 +------ .../tasks/mt940_processor.py | 24 ------ 16 files changed, 327 insertions(+), 219 deletions(-) create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/tasks/__init__.py create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/tasks/block_funds_with_bank.py create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/tasks/check_funds_with_bank_task.py create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/tasks/disburse_funds_from_bank.py create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/tasks/mapper_resolution_task.py create mode 100644 openg2p-g2p-bridge-celery-beat-producers/src/openg2p_g2p_bridge_celery_beat_producers/tasks/mt940_processor.py diff --git a/openg2p-g2p-bridge-celery-beat-producers/celerybeat-schedule.db b/openg2p-g2p-bridge-celery-beat-producers/celerybeat-schedule.db index eabaa5e16ea3318102da12678b3ca639a58bb0f1..a092bf0a7d3b1f0e0b4cc6c392af1089a609089e 100644 GIT binary patch delta 186 zcmZo@U~Fh$+#n#p7RKkm7sj{oqCF2s0|>Bm=uN&WuRM95yduj~e`W?4hj}VHGmOnM z^#BtC7}QSDn7m6?ZE~-S^5i|Ts*J3Yf62(R8VayBoti8s8^Gv2xj IllM{p0Qq+~AOHXW delta 457 zcmZo@U~Fh$+#n#p7Rlqx6UnpjqCJmz0|>B84Q6HlgW4$?J<7=$sgoULU6>)X5(`v7 zY4UeDIaWa)*4x&T&&vid`A(RuAg46BS6*ZC1vxe53>FZrKKY`Ys!Rr_GZ*^{4i*kU z9yT$pDH&pIQ-Y@Wb}?p1XUI;TBQIIsBcGdCP>@;_UzA#$pHo_rnV%P*l$ux)Ur>~v zQktAvGzDlJVMT;hAiG2&66O-NnMh7iOUW!wDlIBbjZZ7hODT>|E6UG}PfE