From e42df62a5bbd707ce05d0465549f463128a52f27 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:10:55 -0700 Subject: [PATCH 01/10] Use black / isort --- jobs/ftp-poller/Makefile | 18 +- jobs/ftp-poller/config.py | 206 +++++++++--------- jobs/ftp-poller/invoke_jobs.py | 38 ++-- jobs/ftp-poller/poetry.lock | 84 ++++++- jobs/ftp-poller/pyproject.toml | 124 +++++++++++ jobs/ftp-poller/services/sftp.py | 28 +-- jobs/ftp-poller/setup.cfg | 105 --------- jobs/ftp-poller/setup.py | 5 +- jobs/ftp-poller/tasks/cas_poller_ftp.py | 16 +- .../tasks/cgi_feeder_poller_task.py | 44 ++-- jobs/ftp-poller/tasks/eft_poller_ftp.py | 27 ++- jobs/ftp-poller/tests/jobs/__init__.py | 3 +- jobs/ftp-poller/tests/jobs/conftest.py | 22 +- jobs/ftp-poller/tests/jobs/test_sftp.py | 25 ++- jobs/ftp-poller/utils/logger.py | 4 +- jobs/ftp-poller/utils/minio.py | 17 +- jobs/ftp-poller/utils/utils.py | 29 +-- 17 files changed, 457 insertions(+), 338 deletions(-) delete mode 100755 jobs/ftp-poller/setup.cfg diff --git a/jobs/ftp-poller/Makefile b/jobs/ftp-poller/Makefile index df6aca8fd..a4f94ccbb 100644 --- a/jobs/ftp-poller/Makefile +++ b/jobs/ftp-poller/Makefile @@ -45,15 +45,27 @@ install: clean ################################################################################# # COMMANDS - CI # ################################################################################# -ci: lint flake8 test ## CI flow +ci: isort-ci black-ci lint flake8 test ## CI flow + +isort: + poetry run isort . + +isort-ci: + poetry run isort --check . + +black: ## Linting with black + poetry run black . + +black-ci: + poetry run black --check . pylint: ## Linting with pylint - poetry run pylint --rcfile=setup.cfg tasks tests utils + poetry run pylint tasks tests utils flake8: ## Linting with flake8 poetry run flake8 tasks tests utils -lint: pylint flake8 ## run all lint type scripts +lint: isort black pylint flake8 ## run all lint type scripts test: ## Unit testing poetry run pytest diff --git a/jobs/ftp-poller/config.py b/jobs/ftp-poller/config.py index f2c162b0e..9a1fc1381 100644 --- a/jobs/ftp-poller/config.py +++ b/jobs/ftp-poller/config.py @@ -23,23 +23,23 @@ load_dotenv(find_dotenv()) CONFIGURATION = { - 'development': 'config.DevConfig', - 'testing': 'config.TestConfig', - 'production': 'config.ProdConfig', - 'default': 'config.ProdConfig' + "development": "config.DevConfig", + "testing": "config.TestConfig", + "production": "config.ProdConfig", + "default": "config.ProdConfig", } -def get_named_config(config_name: str = 'production'): +def get_named_config(config_name: str = "production"): """Return the configuration object based on the name :raise: KeyError: if an unknown configuration is requested """ - if config_name in ['production', 'staging', 'default']: + if config_name in ["production", "staging", "default"]: config = ProdConfig() - elif config_name == 'testing': + elif config_name == "testing": config = TestConfig() - elif config_name == 'development': + elif config_name == "development": config = DevConfig() else: raise KeyError(f"Unknown configuration '{config_name}'") @@ -47,101 +47,105 @@ def get_named_config(config_name: str = 'production'): class _Config(object): # pylint: disable=too-few-public-methods - """Base class configuration that should set reasonable defaults for all the other configurations. """ + """Base class configuration that should set reasonable defaults for all the other configurations.""" + PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - SECRET_KEY = 'a secret' + SECRET_KEY = "a secret" # FTP CONFIG - CAS_SFTP_HOST = os.getenv('CAS_SFTP_HOST', 'localhost') - CAS_SFTP_USER_NAME = os.getenv('CAS_SFTP_USER_NAME', 'foo') - CAS_SFTP_PASSWORD = os.getenv('CAS_SFTP_PASSWORD', '') - CAS_SFTP_DIRECTORY = os.getenv('CAS_SFTP_DIRECTORY', '/upload') - CAS_SFTP_BACKUP_DIRECTORY = os.getenv('CAS_SFTP_BACKUP_DIRECTORY', '/backup') - SFTP_VERIFY_HOST = os.getenv('SFTP_VERIFY_HOST', 'True') - CAS_SFTP_PORT = os.getenv('CAS_SFTP_PORT', 22) - CAS_SFTP_HOST_KEY = os.getenv('CAS_SFTP_HOST_KEY', '') - BCREG_FTP_PRIVATE_KEY_LOCATION = os.getenv('BCREG_FTP_PRIVATE_KEY_LOCATION', - '/ftp-poller/key/sftp_priv_key') # full path to the privatey key - BCREG_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv('BCREG_FTP_PRIVATE_KEY_PASSPHRASE', '') + CAS_SFTP_HOST = os.getenv("CAS_SFTP_HOST", "localhost") + CAS_SFTP_USER_NAME = os.getenv("CAS_SFTP_USER_NAME", "foo") + CAS_SFTP_PASSWORD = os.getenv("CAS_SFTP_PASSWORD", "") + CAS_SFTP_DIRECTORY = os.getenv("CAS_SFTP_DIRECTORY", "/upload") + CAS_SFTP_BACKUP_DIRECTORY = os.getenv("CAS_SFTP_BACKUP_DIRECTORY", "/backup") + SFTP_VERIFY_HOST = os.getenv("SFTP_VERIFY_HOST", "True") + CAS_SFTP_PORT = os.getenv("CAS_SFTP_PORT", 22) + CAS_SFTP_HOST_KEY = os.getenv("CAS_SFTP_HOST_KEY", "") + BCREG_FTP_PRIVATE_KEY_LOCATION = os.getenv( + "BCREG_FTP_PRIVATE_KEY_LOCATION", "/ftp-poller/key/sftp_priv_key" + ) # full path to the privatey key + BCREG_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv("BCREG_FTP_PRIVATE_KEY_PASSPHRASE", "") # CGI FTP CONFIG - BCREG_CGI_FTP_PRIVATE_KEY_LOCATION = os.getenv('BCREG_CGI_FTP_PRIVATE_KEY_LOCATION', - '/ftp-poller/key/cgi_sftp_priv_key') # full path to the privatey key - BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv('BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE', '') - CGI_SFTP_USER_NAME = os.getenv('CAS_SFTP_USER_NAME', 'foo') - CGI_SFTP_BACKUP_DIRECTORY = os.getenv('CGI_SFTP_BACKUP_DIRECTORY', '/backup') - CGI_SFTP_DIRECTORY = os.getenv('CGI_SFTP_DIRECTORY', '/data') + BCREG_CGI_FTP_PRIVATE_KEY_LOCATION = os.getenv( + "BCREG_CGI_FTP_PRIVATE_KEY_LOCATION", "/ftp-poller/key/cgi_sftp_priv_key" + ) # full path to the privatey key + BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv("BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE", "") + CGI_SFTP_USER_NAME = os.getenv("CAS_SFTP_USER_NAME", "foo") + CGI_SFTP_BACKUP_DIRECTORY = os.getenv("CGI_SFTP_BACKUP_DIRECTORY", "/backup") + CGI_SFTP_DIRECTORY = os.getenv("CGI_SFTP_DIRECTORY", "/data") # EFT FTP CONFIG - BCREG_EFT_FTP_PRIVATE_KEY_LOCATION = os.getenv('BCREG_EFT_FTP_PRIVATE_KEY_LOCATION', - '/ftp-poller/key/eft_sftp_priv_key') - EFT_SFTP_HOST = os.getenv('EFT_SFTP_HOST', 'localhost') - EFT_SFTP_USER_NAME = os.getenv('EFT_SFTP_USER_NAME', 'foo') - EFT_SFTP_PASSWORD = os.getenv('EFT_SFTP_PASSWORD', '') - EFT_SFTP_DIRECTORY = os.getenv('EFT_SFTP_DIRECTORY', '/outgoing') - EFT_SFTP_BACKUP_DIRECTORY = os.getenv('EFT_SFTP_BACKUP_DIRECTORY', '/outgoing-backup') - EFT_SFTP_VERIFY_HOST = os.getenv('EFT_SFTP_VERIFY_HOST', 'True') - EFT_SFTP_PORT = os.getenv('EFT_SFTP_PORT', 22) - EFT_SFTP_HOST_KEY = os.getenv('EFT_SFTP_HOST_KEY', '') - BCREG_EFT_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv('BCREG_EFT_FTP_PRIVATE_KEY_PASSPHRASE', '') + BCREG_EFT_FTP_PRIVATE_KEY_LOCATION = os.getenv( + "BCREG_EFT_FTP_PRIVATE_KEY_LOCATION", "/ftp-poller/key/eft_sftp_priv_key" + ) + EFT_SFTP_HOST = os.getenv("EFT_SFTP_HOST", "localhost") + EFT_SFTP_USER_NAME = os.getenv("EFT_SFTP_USER_NAME", "foo") + EFT_SFTP_PASSWORD = os.getenv("EFT_SFTP_PASSWORD", "") + EFT_SFTP_DIRECTORY = os.getenv("EFT_SFTP_DIRECTORY", "/outgoing") + EFT_SFTP_BACKUP_DIRECTORY = os.getenv("EFT_SFTP_BACKUP_DIRECTORY", "/outgoing-backup") + EFT_SFTP_VERIFY_HOST = os.getenv("EFT_SFTP_VERIFY_HOST", "True") + EFT_SFTP_PORT = os.getenv("EFT_SFTP_PORT", 22) + EFT_SFTP_HOST_KEY = os.getenv("EFT_SFTP_HOST_KEY", "") + BCREG_EFT_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv("BCREG_EFT_FTP_PRIVATE_KEY_PASSPHRASE", "") # CGI File specific configs - CGI_TRIGGER_FILE_SUFFIX = os.getenv('CGI_TRIGGER_FILE_SUFFIX', '.TRG') - CGI_ACK_FILE_PREFIX = os.getenv('CGI_ACK_FILE_PREFIX', 'ACK') - CGI_FEEDBACK_FILE_PREFIX = os.getenv('CGI_FEEDBACK_FILE_PREFIX', 'FEEDBACK') - CGI_INBOX_FILE_PREFIX = os.getenv('CGI_FEEDBACK_FILE_PREFIX', 'INBOX') + CGI_TRIGGER_FILE_SUFFIX = os.getenv("CGI_TRIGGER_FILE_SUFFIX", ".TRG") + CGI_ACK_FILE_PREFIX = os.getenv("CGI_ACK_FILE_PREFIX", "ACK") + CGI_FEEDBACK_FILE_PREFIX = os.getenv("CGI_FEEDBACK_FILE_PREFIX", "FEEDBACK") + CGI_INBOX_FILE_PREFIX = os.getenv("CGI_FEEDBACK_FILE_PREFIX", "INBOX") SFTP_CONFIGS = { - 'CAS': { - 'SFTP_HOST': CAS_SFTP_HOST, - 'SFTP_USERNAME': CAS_SFTP_USER_NAME, - 'SFTP_PASSWORD': CAS_SFTP_PASSWORD, - 'SFTP_VERIFY_HOST': SFTP_VERIFY_HOST, - 'SFTP_HOST_KEY': CAS_SFTP_HOST_KEY, - 'SFTP_PORT': CAS_SFTP_PORT, - 'FTP_PRIVATE_KEY_LOCATION': BCREG_FTP_PRIVATE_KEY_LOCATION, - 'BCREG_FTP_PRIVATE_KEY_PASSPHRASE': BCREG_FTP_PRIVATE_KEY_PASSPHRASE + "CAS": { + "SFTP_HOST": CAS_SFTP_HOST, + "SFTP_USERNAME": CAS_SFTP_USER_NAME, + "SFTP_PASSWORD": CAS_SFTP_PASSWORD, + "SFTP_VERIFY_HOST": SFTP_VERIFY_HOST, + "SFTP_HOST_KEY": CAS_SFTP_HOST_KEY, + "SFTP_PORT": CAS_SFTP_PORT, + "FTP_PRIVATE_KEY_LOCATION": BCREG_FTP_PRIVATE_KEY_LOCATION, + "BCREG_FTP_PRIVATE_KEY_PASSPHRASE": BCREG_FTP_PRIVATE_KEY_PASSPHRASE, }, # between CGI and CAS , only account name and private key changes.So reusing most of the information. - 'CGI': { - 'SFTP_HOST': os.getenv('CAS_SFTP_HOST', 'localhost'), # same as CAS - 'SFTP_USERNAME': os.getenv('CGI_SFTP_USER_NAME', 'foo'), # different user.so not same as CAS - 'SFTP_PASSWORD': os.getenv('CAS_SFTP_PASSWORD', ''), # same as CAS - 'SFTP_VERIFY_HOST': os.getenv('SFTP_VERIFY_HOST', 'True'), # same as CAS - 'SFTP_HOST_KEY': os.getenv('CAS_SFTP_HOST_KEY', ''), # same as CAS - 'SFTP_PORT': CAS_SFTP_PORT, # same as CAS - 'FTP_PRIVATE_KEY_LOCATION': BCREG_CGI_FTP_PRIVATE_KEY_LOCATION, # different user.so not same as CAS - 'BCREG_FTP_PRIVATE_KEY_PASSPHRASE': BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE + "CGI": { + "SFTP_HOST": os.getenv("CAS_SFTP_HOST", "localhost"), # same as CAS + "SFTP_USERNAME": os.getenv("CGI_SFTP_USER_NAME", "foo"), # different user.so not same as CAS + "SFTP_PASSWORD": os.getenv("CAS_SFTP_PASSWORD", ""), # same as CAS + "SFTP_VERIFY_HOST": os.getenv("SFTP_VERIFY_HOST", "True"), # same as CAS + "SFTP_HOST_KEY": os.getenv("CAS_SFTP_HOST_KEY", ""), # same as CAS + "SFTP_PORT": CAS_SFTP_PORT, # same as CAS + "FTP_PRIVATE_KEY_LOCATION": BCREG_CGI_FTP_PRIVATE_KEY_LOCATION, # different user.so not same as CAS + "BCREG_FTP_PRIVATE_KEY_PASSPHRASE": BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE, + }, + "EFT": { + "SFTP_HOST": EFT_SFTP_HOST, + "SFTP_USERNAME": EFT_SFTP_USER_NAME, + "SFTP_PASSWORD": EFT_SFTP_PASSWORD, + "SFTP_VERIFY_HOST": EFT_SFTP_VERIFY_HOST, + "SFTP_HOST_KEY": EFT_SFTP_HOST_KEY, + "SFTP_PORT": EFT_SFTP_PORT, + "FTP_PRIVATE_KEY_LOCATION": BCREG_EFT_FTP_PRIVATE_KEY_LOCATION, + "BCREG_FTP_PRIVATE_KEY_PASSPHRASE": BCREG_EFT_FTP_PRIVATE_KEY_PASSPHRASE, }, - 'EFT': { - 'SFTP_HOST': EFT_SFTP_HOST, - 'SFTP_USERNAME': EFT_SFTP_USER_NAME, - 'SFTP_PASSWORD': EFT_SFTP_PASSWORD, - 'SFTP_VERIFY_HOST': EFT_SFTP_VERIFY_HOST, - 'SFTP_HOST_KEY': EFT_SFTP_HOST_KEY, - 'SFTP_PORT': EFT_SFTP_PORT, - 'FTP_PRIVATE_KEY_LOCATION': BCREG_EFT_FTP_PRIVATE_KEY_LOCATION, - 'BCREG_FTP_PRIVATE_KEY_PASSPHRASE': BCREG_EFT_FTP_PRIVATE_KEY_PASSPHRASE - } } # Minio configuration values - MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT') - MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY') - MINIO_ACCESS_SECRET = os.getenv('MINIO_ACCESS_SECRET') - MINIO_BUCKET_NAME = os.getenv('MINIO_BUCKET_NAME', 'payment-sftp') - MINIO_CGI_BUCKET_NAME = os.getenv('MINIO_CGI_BUCKET_NAME', 'cgi-ejv') - MINIO_EFT_BUCKET_NAME = os.getenv('MINIO_EFT_BUCKET_NAME', 'eft-sftp') + MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT") + MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") + MINIO_ACCESS_SECRET = os.getenv("MINIO_ACCESS_SECRET") + MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME", "payment-sftp") + MINIO_CGI_BUCKET_NAME = os.getenv("MINIO_CGI_BUCKET_NAME", "cgi-ejv") + MINIO_EFT_BUCKET_NAME = os.getenv("MINIO_EFT_BUCKET_NAME", "eft-sftp") MINIO_SECURE = True - SENTRY_ENABLE = os.getenv('SENTRY_ENABLE', 'False') - SENTRY_DSN = os.getenv('SENTRY_DSN', None) + SENTRY_ENABLE = os.getenv("SENTRY_ENABLE", "False") + SENTRY_DSN = os.getenv("SENTRY_DSN", None) # PUB/SUB - PUB: ftp-poller-payment-reconciliation-dev - FTP_POLLER_TOPIC = os.getenv('FTP_POLLER_TOPIC', 'ftp-poller-payment-reconciliation-dev') - GCP_AUTH_KEY = os.getenv('AUTHPAY_GCP_AUTH_KEY', None) - PUB_ENABLE_MESSAGE_ORDERING = os.getenv('PUB_ENABLE_MESSAGE_ORDERING', 'True') + FTP_POLLER_TOPIC = os.getenv("FTP_POLLER_TOPIC", "ftp-poller-payment-reconciliation-dev") + GCP_AUTH_KEY = os.getenv("AUTHPAY_GCP_AUTH_KEY", None) + PUB_ENABLE_MESSAGE_ORDERING = os.getenv("PUB_ENABLE_MESSAGE_ORDERING", "True") TESTING = False DEBUG = True @@ -159,30 +163,30 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods TESTING = True # POSTGRESQL - SERVER_NAME = 'localhost:5001' + SERVER_NAME = "localhost:5001" - AUTH_API_ENDPOINT = 'http://localhost:8080/auth-api/' + AUTH_API_ENDPOINT = "http://localhost:8080/auth-api/" - CFS_BASE_URL = 'http://localhost:8080/paybc-api' - CFS_CLIENT_ID = 'TEST' - CFS_CLIENT_SECRET = 'TEST' - USE_DOCKER_MOCK = os.getenv('USE_DOCKER_MOCK', None) + CFS_BASE_URL = "http://localhost:8080/paybc-api" + CFS_CLIENT_ID = "TEST" + CFS_CLIENT_SECRET = "TEST" + USE_DOCKER_MOCK = os.getenv("USE_DOCKER_MOCK", None) - CAS_SFTP_HOST = 'localhost' - CAS_SFTP_USER_NAME = 'ftp_user' - CAS_SFTP_PASSWORD = 'ftp_pass' - CAS_SFTP_DIRECTORY = 'paymentfolder' - CAS_SFTP_BACKUP_DIRECTORY = 'backup' - SFTP_VERIFY_HOST = 'False' + CAS_SFTP_HOST = "localhost" + CAS_SFTP_USER_NAME = "ftp_user" + CAS_SFTP_PASSWORD = "ftp_pass" + CAS_SFTP_DIRECTORY = "paymentfolder" + CAS_SFTP_BACKUP_DIRECTORY = "backup" + SFTP_VERIFY_HOST = "False" CAS_SFTP_PORT = 2222 SFTP_CONFIGS = { - 'CAS': { - 'SFTP_HOST': CAS_SFTP_HOST, - 'SFTP_USERNAME': CAS_SFTP_USER_NAME, - 'SFTP_PASSWORD': CAS_SFTP_PASSWORD, - 'SFTP_VERIFY_HOST': SFTP_VERIFY_HOST, - 'SFTP_PORT': CAS_SFTP_PORT + "CAS": { + "SFTP_HOST": CAS_SFTP_HOST, + "SFTP_USERNAME": CAS_SFTP_USER_NAME, + "SFTP_PASSWORD": CAS_SFTP_PASSWORD, + "SFTP_VERIFY_HOST": SFTP_VERIFY_HOST, + "SFTP_PORT": CAS_SFTP_PORT, } } @@ -190,11 +194,11 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods class ProdConfig(_Config): # pylint: disable=too-few-public-methods """Production environment configuration.""" - SECRET_KEY = os.getenv('SECRET_KEY', None) + SECRET_KEY = os.getenv("SECRET_KEY", None) if not SECRET_KEY: SECRET_KEY = os.urandom(24) - print('WARNING: SECRET_KEY being set as a one-shot', file=sys.stderr) + print("WARNING: SECRET_KEY being set as a one-shot", file=sys.stderr) TESTING = False DEBUG = False diff --git a/jobs/ftp-poller/invoke_jobs.py b/jobs/ftp-poller/invoke_jobs.py index 875ff01e8..8f95bc209 100755 --- a/jobs/ftp-poller/invoke_jobs.py +++ b/jobs/ftp-poller/invoke_jobs.py @@ -20,17 +20,16 @@ import sentry_sdk from flask import Flask -from sentry_sdk.integrations.flask import FlaskIntegration from pay_api.services.gcp_queue import queue +from sentry_sdk.integrations.flask import FlaskIntegration import config from utils.logger import setup_logging - -setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first +setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): +def create_app(run_mode=os.getenv("FLASK_ENV", "production")): """Return a configured Flask App using the Factory method.""" from pay_api.models import db, ma @@ -38,13 +37,10 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): app.config.from_object(config.CONFIGURATION[run_mode]) # Configure Sentry - if str(app.config.get('SENTRY_ENABLE')).lower() == 'true': - if app.config.get('SENTRY_DSN', None): - sentry_sdk.init( - dsn=app.config.get('SENTRY_DSN'), - integrations=[FlaskIntegration()] - ) - app.logger.info('<<<< Starting Ftp Poller Job >>>>') + if str(app.config.get("SENTRY_ENABLE")).lower() == "true": + if app.config.get("SENTRY_DSN", None): + sentry_sdk.init(dsn=app.config.get("SENTRY_DSN"), integrations=[FlaskIntegration()]) + app.logger.info("<<<< Starting Ftp Poller Job >>>>") queue.init_app(app) ma.init_app(app) @@ -58,9 +54,7 @@ def register_shellcontext(app): def shell_context(): """Shell context objects.""" - return { - 'app': app - } # pragma: no cover + return {"app": app} # pragma: no cover app.shell_context_processor(shell_context) @@ -73,19 +67,19 @@ def run(job_name): application = create_app() application.app_context().push() - if job_name == 'CAS_FTP_POLLER': + if job_name == "CAS_FTP_POLLER": CASPollerFtpTask.poll_ftp() - application.logger.info(f'<<<< Completed Polling CAS FTP >>>>') - elif job_name == 'CGI_FTP_POLLER': + application.logger.info(f"<<<< Completed Polling CAS FTP >>>>") + elif job_name == "CGI_FTP_POLLER": CGIFeederPollerTask.poll_ftp() - application.logger.info(f'<<<< Completed Polling CGI FTP >>>>') - elif job_name == 'EFT_FTP_POLLER': + application.logger.info(f"<<<< Completed Polling CGI FTP >>>>") + elif job_name == "EFT_FTP_POLLER": EFTPollerFtpTask.poll_ftp() - application.logger.info(f'<<<< Completed Polling EFT FTP >>>>') + application.logger.info(f"<<<< Completed Polling EFT FTP >>>>") else: - application.logger.debug('No valid args passed.Exiting job without running any ***************') + application.logger.debug("No valid args passed.Exiting job without running any ***************") if __name__ == "__main__": - print('----------------------------Scheduler Ran With Argument--', sys.argv[1]) + print("----------------------------Scheduler Ran With Argument--", sys.argv[1]) run(sys.argv[1]) diff --git a/jobs/ftp-poller/poetry.lock b/jobs/ftp-poller/poetry.lock index c945e7b47..1a1af6bc5 100644 --- a/jobs/ftp-poller/poetry.lock +++ b/jobs/ftp-poller/poetry.lock @@ -321,6 +321,50 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "blinker" version = "1.7.0" @@ -836,6 +880,22 @@ isort = ">=5.0.0,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + [[package]] name = "flake8-quotes" version = "3.4.0" @@ -1843,6 +1903,17 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "opentracing" version = "2.4.0" @@ -1888,6 +1959,17 @@ all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1 gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] invoke = ["invoke (>=2.0)"] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pay-api" version = "0.1.0" @@ -3142,4 +3224,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "defec2f44fc170daa5b97f71d5b2ff379adc3431d9e9c547e020c5145306baf1" +content-hash = "597c3c0c00986d37821fece9db35c48a2beb4fb6b19591ba6b034280eddbcc57" diff --git a/jobs/ftp-poller/pyproject.toml b/jobs/ftp-poller/pyproject.toml index 2a32dad39..b8eb339bf 100644 --- a/jobs/ftp-poller/pyproject.toml +++ b/jobs/ftp-poller/pyproject.toml @@ -45,6 +45,130 @@ pylint = "^3.1.0" pylint-flask = "^0.6" lovely-pytest-docker = "^0.3.1" pytest-asyncio = "^0.23.5.post1" +black = "^24.10.0" +isort = "^5.13.2" +flake8-pyproject = "^1.2.3" + +[tool.flake8] +ignore = ["F401","E402", "Q000", "E203", "W503"] +exclude = [ + ".venv", + "./venv", + ".git", + ".history", + "devops", + "*migrations*", +] +per-file-ignores = [ + "__init__.py:F401", + "*.py:B902" +] +max-line-length = 120 +docstring-min-length=10 +count = true + +[tool.zimports] +black-line-length = 120 +keep-unused-type-checking = true + +[tool.black] +target-version = ["py310", "py311", "py312"] +line-length = 120 +include = '\.pyi?$' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + migrations + | devops + | .history +)/ +''' + +[tool.isort] +atomic = true +profile = "black" +line_length = 120 +skip_gitignore = true +skip_glob = ["migrations", "devops"] + +[tool.pylint.main] +fail-under = 10 +max-line-length = 120 +ignore = [ "migrations", "devops", "tests"] +ignore-patterns = ["^\\.#"] +ignored-modules= ["flask_sqlalchemy", "sqlalchemy", "SQLAlchemy" , "alembic", "scoped_session"] +ignored-classes= "scoped_session" +ignore-long-lines = "^\\s*(# )??$" +extension-pkg-whitelist = "pydantic" +notes = ["FIXME","XXX","TODO"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] +disable = "C0209,C0301,W0511,W0613,W0703,W1514,W1203,R0801,R0902,R0903,R0911,R0401,R1705,R1718,W3101" +argument-naming-style = "snake_case" +attr-naming-style = "snake_case" +class-attribute-naming-style = "any" +class-const-naming-style = "UPPER_CASE" +class-naming-style = "PascalCase" +const-naming-style = "UPPER_CASE" +function-naming-style = "snake_case" +inlinevar-naming-style = "any" +method-naming-style = "snake_case" +module-naming-style = "any" +variable-naming-style = "snake_case" +docstring-min-length = -1 +good-names = ["i", "j", "k", "ex", "Run", "_"] +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] +valid-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/auth_api", +] +omit = [ + "wsgi.py", + "gunicorn_config.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core"] diff --git a/jobs/ftp-poller/services/sftp.py b/jobs/ftp-poller/services/sftp.py index 06fe984e0..d980f919f 100644 --- a/jobs/ftp-poller/services/sftp.py +++ b/jobs/ftp-poller/services/sftp.py @@ -18,13 +18,13 @@ import paramiko from flask import current_app -from pysftp import Connection, CnOpts +from pysftp import CnOpts, Connection class SFTPService: # pylint: disable=too-few-public-methods """SFTP Service class.""" - DEFAULT_CONNECT_SERVER = 'CAS' + DEFAULT_CONNECT_SERVER = "CAS" @staticmethod def get_connection(server_name: str = DEFAULT_CONNECT_SERVER) -> Connection: @@ -35,35 +35,35 @@ def get_connection(server_name: str = DEFAULT_CONNECT_SERVER) -> Connection: @staticmethod def _connect(server_name: str) -> Connection: - sftp_configs = current_app.config.get('SFTP_CONFIGS') + sftp_configs = current_app.config.get("SFTP_CONFIGS") # if not passed , connect to CAS server always. to make the existing code work if not server_name or server_name not in sftp_configs.keys(): server_name = SFTPService.DEFAULT_CONNECT_SERVER connect_configs = sftp_configs.get(server_name) - sftp_host: str = connect_configs.get('SFTP_HOST') + sftp_host: str = connect_configs.get("SFTP_HOST") cnopts = CnOpts() # only for local development set this to false . - if connect_configs.get('SFTP_VERIFY_HOST').lower() == 'false': + if connect_configs.get("SFTP_VERIFY_HOST").lower() == "false": cnopts.hostkeys = None else: - ftp_host_key_data = connect_configs.get('SFTP_HOST_KEY').encode() + ftp_host_key_data = connect_configs.get("SFTP_HOST_KEY").encode() key = paramiko.RSAKey(data=decodebytes(ftp_host_key_data)) - cnopts.hostkeys.add(sftp_host, 'ssh-rsa', key) + cnopts.hostkeys.add(sftp_host, "ssh-rsa", key) - sftp_port: int = connect_configs.get('SFTP_PORT') + sftp_port: int = connect_configs.get("SFTP_PORT") sft_credentials = { - 'username': connect_configs.get('SFTP_USERNAME'), + "username": connect_configs.get("SFTP_USERNAME"), # private_key should be the absolute path to where private key file lies since sftp - 'private_key': connect_configs.get('FTP_PRIVATE_KEY_LOCATION'), - 'private_key_pass': connect_configs.get('BCREG_FTP_PRIVATE_KEY_PASSPHRASE') + "private_key": connect_configs.get("FTP_PRIVATE_KEY_LOCATION"), + "private_key_pass": connect_configs.get("BCREG_FTP_PRIVATE_KEY_PASSPHRASE"), } # to support local testing. SFTP CAS server should run in private key mode - if password := connect_configs.get('SFTP_PASSWORD'): - sft_credentials['password'] = password + if password := connect_configs.get("SFTP_PASSWORD"): + sft_credentials["password"] = password sftp_connection = Connection(host=sftp_host, **sft_credentials, cnopts=cnopts, port=sftp_port) - current_app.logger.debug('sftp_connection successful') + current_app.logger.debug("sftp_connection successful") return sftp_connection diff --git a/jobs/ftp-poller/setup.cfg b/jobs/ftp-poller/setup.cfg deleted file mode 100755 index 78c859cfa..000000000 --- a/jobs/ftp-poller/setup.cfg +++ /dev/null @@ -1,105 +0,0 @@ -[metadata] -name = ftp_poller -url = https://github.com/bcgov/sbc-pay/ -author = Pay Team -author_email = -classifiers = - Development Status :: Beta - Intended Audience :: Developers / QA - Topic :: Payment - License :: OSI Approved :: Apache Software License - Natural Language :: English - Programming Language :: Python :: 3.7 -license = Apache Software License Version 2.0 -description = A short description of the project -long_description = file: README.md -keywords = - -[options] -zip_safe = True -python_requires = >=3.12 -include_package_data = True -packages = find: - -[options.package_data] -pay_api = - -[wheel] -universal = 1 - -[bdist_wheel] -universal = 1 - -[aliases] -test = pytest - -[flake8] -ignore = I001, I003, I004, E126, W504 -exclude = .git,*migrations* -max-line-length = 120 -docstring-min-length=10 -per-file-ignores = - */__init__.py:F401 - -[pycodestyle] -max-line-length = 120 -ignore = E501 -docstring-min-length=10 -notes=FIXME,XXX -match_dir = tasks -ignored-modules=flask_sqlalchemy - sqlalchemy -per-file-ignores = - */__init__.py:F401 -good-names= - b, - d, - i, - e, - f, - k, - q, - u, - v, - ar, - id, - rv, - logger, - -[pylint] -ignore=migrations,test -max-line-length=120 -notes=FIXME,XXX,TODO -ignored-modules=flask_sqlalchemy,sqlalchemy,SQLAlchemy,alembic,scoped_session -ignored-classes=scoped_session -min-similarity-lines=8 -disable=C0301,W0511 - -good-names= - b, - d, - i, - e, - f, - k, - q, - u, - v, - ar, - id, - rv, - logger, - - -[isort] -line_length = 120 -indent = 4 -multi_line_output = 4 -lines_after_imports = 2 - -[tool:pytest] -addopts = --cov=tasks --cov-report html:htmlcov --cov-report xml:coverage.xml -testpaths = tests/jobs -filterwarnings = - ignore::UserWarning - diff --git a/jobs/ftp-poller/setup.py b/jobs/ftp-poller/setup.py index a707f29b0..d03f939ec 100755 --- a/jobs/ftp-poller/setup.py +++ b/jobs/ftp-poller/setup.py @@ -16,7 +16,4 @@ from setuptools import find_packages, setup -setup( - name="ftp_poller", - packages=find_packages() -) +setup(name="ftp_poller", packages=find_packages()) diff --git a/jobs/ftp-poller/tasks/cas_poller_ftp.py b/jobs/ftp-poller/tasks/cas_poller_ftp.py index 3101e0e05..c08ad9a5b 100644 --- a/jobs/ftp-poller/tasks/cas_poller_ftp.py +++ b/jobs/ftp-poller/tasks/cas_poller_ftp.py @@ -38,15 +38,15 @@ def poll_ftp(cls): payment_file_list: List[str] = [] with SFTPService.get_connection() as sftp_client: try: - ftp_dir: str = current_app.config.get('CAS_SFTP_DIRECTORY') + ftp_dir: str = current_app.config.get("CAS_SFTP_DIRECTORY") file_list: List[SFTPAttributes] = sftp_client.listdir_attr(ftp_dir) - current_app.logger.info(f'Found {len(file_list)} to be copied.') + current_app.logger.info(f"Found {len(file_list)} to be copied.") for file in file_list: file_name = file.filename - file_full_name = ftp_dir + '/' + file_name - current_app.logger.info(f'Processing file {file_full_name} started-----.') + file_full_name = ftp_dir + "/" + file_name + current_app.logger.info(f"Processing file {file_full_name} started-----.") if CASPollerFtpTask._is_valid_payment_file(sftp_client, file_full_name): - upload_to_minio(file, file_full_name, sftp_client, current_app.config['MINIO_BUCKET_NAME']) + upload_to_minio(file, file_full_name, sftp_client, current_app.config["MINIO_BUCKET_NAME"]) payment_file_list.append(file_name) if len(payment_file_list) > 0: @@ -69,10 +69,10 @@ def _post_process(cls, sftp_client, payment_file_list: List[str]): @classmethod def _move_file_to_backup(cls, sftp_client, payment_file_list): - ftp_backup_dir: str = current_app.config.get('CAS_SFTP_BACKUP_DIRECTORY') - ftp_dir: str = current_app.config.get('CAS_SFTP_DIRECTORY') + ftp_backup_dir: str = current_app.config.get("CAS_SFTP_BACKUP_DIRECTORY") + ftp_dir: str = current_app.config.get("CAS_SFTP_DIRECTORY") for file_name in payment_file_list: - sftp_client.rename(ftp_dir + '/' + file_name, ftp_backup_dir + '/' + file_name) + sftp_client.rename(ftp_dir + "/" + file_name, ftp_backup_dir + "/" + file_name) @classmethod def _is_valid_payment_file(cls, sftp_client, file_name): diff --git a/jobs/ftp-poller/tasks/cgi_feeder_poller_task.py b/jobs/ftp-poller/tasks/cgi_feeder_poller_task.py index ba79e2347..4dbb4bcd3 100644 --- a/jobs/ftp-poller/tasks/cgi_feeder_poller_task.py +++ b/jobs/ftp-poller/tasks/cgi_feeder_poller_task.py @@ -33,61 +33,63 @@ def poll_ftp(cls): 1. List Files. 2. If TRG , find its associated data file and do the operations. """ - with SFTPService.get_connection('CGI') as sftp_client: + with SFTPService.get_connection("CGI") as sftp_client: try: - ftp_dir: str = current_app.config.get('CGI_SFTP_DIRECTORY') + ftp_dir: str = current_app.config.get("CGI_SFTP_DIRECTORY") file_list: List[SFTPAttributes] = sftp_client.listdir_attr(ftp_dir) current_app.logger.info( - f'Found {len(file_list)} to be processed.This includes all files in the folder.') + f"Found {len(file_list)} to be processed.This includes all files in the folder." + ) for file in file_list: file_name = file.filename - file_full_name = ftp_dir + '/' + file_name + file_full_name = ftp_dir + "/" + file_name if not sftp_client.isfile(file_full_name): # skip directories - current_app.logger.info( - f'Skipping directory {file_name}.') + current_app.logger.info(f"Skipping directory {file_name}.") continue if cls._is_ack_file(file_name): utils.publish_to_queue([file_name], QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) cls._move_file_to_backup(sftp_client, [file_name]) elif cls._is_feedback_file(file_name): - bucket_name = current_app.config.get('MINIO_CGI_BUCKET_NAME') + bucket_name = current_app.config.get("MINIO_CGI_BUCKET_NAME") utils.upload_to_minio(file, file_full_name, sftp_client, bucket_name) - utils.publish_to_queue([file_name], QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value, - location=bucket_name) + utils.publish_to_queue( + [file_name], QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value, location=bucket_name + ) cls._move_file_to_backup(sftp_client, [file_name]) elif cls._is_a_trigger_file(file_name): cls._remove_file(sftp_client, file_name) else: current_app.logger.warning( - f'Ignoring file found which is not trigger ACK or feedback {file_name}.') + f"Ignoring file found which is not trigger ACK or feedback {file_name}." + ) except Exception as e: # NOQA # pylint: disable=broad-except current_app.logger.error(e) @classmethod def _move_file_to_backup(cls, sftp_client, backup_file_list): - ftp_backup_dir: str = current_app.config.get('CGI_SFTP_BACKUP_DIRECTORY') - ftp_dir: str = current_app.config.get('CGI_SFTP_DIRECTORY') + ftp_backup_dir: str = current_app.config.get("CGI_SFTP_BACKUP_DIRECTORY") + ftp_dir: str = current_app.config.get("CGI_SFTP_DIRECTORY") for file_name in backup_file_list: - sftp_client.rename(ftp_dir + '/' + file_name, ftp_backup_dir + '/' + file_name) + sftp_client.rename(ftp_dir + "/" + file_name, ftp_backup_dir + "/" + file_name) @classmethod def _remove_file(cls, sftp_client, file_name: str): - ftp_dir: str = current_app.config.get('CGI_SFTP_DIRECTORY') - current_app.logger.info(f'Removing file: {ftp_dir}/{file_name}') - sftp_client.remove(ftp_dir + '/' + file_name) + ftp_dir: str = current_app.config.get("CGI_SFTP_DIRECTORY") + current_app.logger.info(f"Removing file: {ftp_dir}/{file_name}") + sftp_client.remove(ftp_dir + "/" + file_name) @classmethod def _is_a_trigger_file(cls, file_name: str): - return file_name.endswith(current_app.config.get('CGI_TRIGGER_FILE_SUFFIX')) and \ - not file_name.startswith( - current_app.config.get('CGI_INBOX_FILE_PREFIX')) # INBOX TRG is for them to listen + return file_name.endswith(current_app.config.get("CGI_TRIGGER_FILE_SUFFIX")) and not file_name.startswith( + current_app.config.get("CGI_INBOX_FILE_PREFIX") + ) # INBOX TRG is for them to listen @classmethod def _is_ack_file(cls, file_name: str): - return file_name.startswith(current_app.config.get('CGI_ACK_FILE_PREFIX')) + return file_name.startswith(current_app.config.get("CGI_ACK_FILE_PREFIX")) @classmethod def _is_feedback_file(cls, file_name: str): - return file_name.startswith(current_app.config.get('CGI_FEEDBACK_FILE_PREFIX')) + return file_name.startswith(current_app.config.get("CGI_FEEDBACK_FILE_PREFIX")) diff --git a/jobs/ftp-poller/tasks/eft_poller_ftp.py b/jobs/ftp-poller/tasks/eft_poller_ftp.py index ff6b3dc52..485ee745b 100644 --- a/jobs/ftp-poller/tasks/eft_poller_ftp.py +++ b/jobs/ftp-poller/tasks/eft_poller_ftp.py @@ -16,8 +16,8 @@ from flask import current_app from paramiko.sftp_attr import SFTPAttributes - from sbc_common_components.utils.enums import QueueMessageTypes + from services.sftp import SFTPService from utils.utils import publish_to_queue, upload_to_minio @@ -37,17 +37,17 @@ def poll_ftp(cls): send jms message """ payment_file_list: List[str] = [] - with SFTPService.get_connection('EFT') as sftp_client: + with SFTPService.get_connection("EFT") as sftp_client: try: - ftp_dir: str = current_app.config.get('EFT_SFTP_DIRECTORY') + ftp_dir: str = current_app.config.get("EFT_SFTP_DIRECTORY") file_list: List[SFTPAttributes] = sftp_client.listdir_attr(ftp_dir) - current_app.logger.info(f'Found {len(file_list)} to be copied.') + current_app.logger.info(f"Found {len(file_list)} to be copied.") for file in file_list: file_name = file.filename - file_full_name = ftp_dir + '/' + file_name - current_app.logger.info(f'Processing file {file_full_name} started-----.') + file_full_name = ftp_dir + "/" + file_name + current_app.logger.info(f"Processing file {file_full_name} started-----.") if EFTPollerFtpTask._is_valid_payment_file(sftp_client, file_full_name): - upload_to_minio(file, file_full_name, sftp_client, current_app.config['MINIO_EFT_BUCKET_NAME']) + upload_to_minio(file, file_full_name, sftp_client, current_app.config["MINIO_EFT_BUCKET_NAME"]) payment_file_list.append(file_name) if len(payment_file_list) > 0: @@ -66,15 +66,18 @@ def _post_process(cls, sftp_client, payment_file_list: List[str]): 2.Send a message to queue """ cls._move_file_to_backup(sftp_client, payment_file_list) - publish_to_queue(payment_file_list, QueueMessageTypes.EFT_FILE_UPLOADED.value, - location=current_app.config.get('MINIO_EFT_BUCKET_NAME')) + publish_to_queue( + payment_file_list, + QueueMessageTypes.EFT_FILE_UPLOADED.value, + location=current_app.config.get("MINIO_EFT_BUCKET_NAME"), + ) @classmethod def _move_file_to_backup(cls, sftp_client, payment_file_list): - ftp_backup_dir: str = current_app.config.get('EFT_SFTP_BACKUP_DIRECTORY') - ftp_dir: str = current_app.config.get('EFT_SFTP_DIRECTORY') + ftp_backup_dir: str = current_app.config.get("EFT_SFTP_BACKUP_DIRECTORY") + ftp_dir: str = current_app.config.get("EFT_SFTP_DIRECTORY") for file_name in payment_file_list: - sftp_client.rename(ftp_dir + '/' + file_name, ftp_backup_dir + '/' + file_name) + sftp_client.rename(ftp_dir + "/" + file_name, ftp_backup_dir + "/" + file_name) @classmethod def _is_valid_payment_file(cls, sftp_client, file_name): diff --git a/jobs/ftp-poller/tests/jobs/__init__.py b/jobs/ftp-poller/tests/jobs/__init__.py index 21e84332a..00b0f1fb9 100644 --- a/jobs/ftp-poller/tests/jobs/__init__.py +++ b/jobs/ftp-poller/tests/jobs/__init__.py @@ -15,6 +15,5 @@ import os import sys - my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + '/../') +sys.path.insert(0, my_path + "/../") diff --git a/jobs/ftp-poller/tests/jobs/conftest.py b/jobs/ftp-poller/tests/jobs/conftest.py index fd4717049..9abe7b6a2 100644 --- a/jobs/ftp-poller/tests/jobs/conftest.py +++ b/jobs/ftp-poller/tests/jobs/conftest.py @@ -17,35 +17,35 @@ import time import pytest + from invoke_jobs import create_app -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def app(): """Return a session-wide application configured in TEST mode.""" - return create_app('testing') + return create_app("testing") -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def session(app): # pylint: disable=redefined-outer-name, invalid-name """Return a function-scoped session.""" with app.app_context(): yield app -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def auto(docker_services, app): # pylint: disable=redefined-outer-name """Spin up docker instances.""" - if app.config['USE_DOCKER_MOCK']: - docker_services.start('proxy') - docker_services.start('sftp') + if app.config["USE_DOCKER_MOCK"]: + docker_services.start("proxy") + docker_services.start("sftp") time.sleep(2) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def docker_compose_files(pytestconfig): """Get the docker-compose.yml absolute path.""" import os # pylint: disable=import-outside-toplevel - return [ - os.path.join(str(pytestconfig.rootdir), 'tests/docker', 'docker-compose.yml') - ] + + return [os.path.join(str(pytestconfig.rootdir), "tests/docker", "docker-compose.yml")] diff --git a/jobs/ftp-poller/tests/jobs/test_sftp.py b/jobs/ftp-poller/tests/jobs/test_sftp.py index 07dc79d5b..7bfc34c60 100644 --- a/jobs/ftp-poller/tests/jobs/test_sftp.py +++ b/jobs/ftp-poller/tests/jobs/test_sftp.py @@ -18,7 +18,6 @@ """ import pytest from flask import current_app - from sbc_common_components.utils.enums import QueueMessageTypes from services.sftp import SFTPService @@ -35,19 +34,25 @@ def test_poll_ftp_task(): """Test Poll.""" con = SFTPService.get_connection() - ftp_dir: str = current_app.config.get('CAS_SFTP_DIRECTORY') + ftp_dir: str = current_app.config.get("CAS_SFTP_DIRECTORY") files = con.listdir(ftp_dir) - assert len(files) == 1, 'Files exist in FTP folder' + assert len(files) == 1, "Files exist in FTP folder" -@pytest.mark.skip(reason='leave this to manually verify pubsub connection;' - 'needs env vars, disable def mock_queue_publish(monkeypatch):') +@pytest.mark.skip( + reason="leave this to manually verify pubsub connection;" + "needs env vars, disable def mock_queue_publish(monkeypatch):" +) def test_queue_message(session): # pylint:disable=unused-argument """Test publishing to topic.""" - file_name = 'file1.csv' + file_name = "file1.csv" publish_to_queue([file_name]) publish_to_queue([file_name], QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) - publish_to_queue([file_name], QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value, - location=current_app.config.get('MINIO_CGI_BUCKET_NAME')) - publish_to_queue([file_name], QueueMessageTypes.EFT_FILE_UPLOADED.value, - location=current_app.config.get('MINIO_EFT_BUCKET_NAME')) + publish_to_queue( + [file_name], + QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value, + location=current_app.config.get("MINIO_CGI_BUCKET_NAME"), + ) + publish_to_queue( + [file_name], QueueMessageTypes.EFT_FILE_UPLOADED.value, location=current_app.config.get("MINIO_EFT_BUCKET_NAME") + ) diff --git a/jobs/ftp-poller/utils/logger.py b/jobs/ftp-poller/utils/logger.py index afa83de96..fb7dacecb 100755 --- a/jobs/ftp-poller/utils/logger.py +++ b/jobs/ftp-poller/utils/logger.py @@ -21,6 +21,6 @@ def setup_logging(conf): """Create the services logger.""" if conf and path.isfile(conf): logging.config.fileConfig(conf) - print(f'Configure logging, from conf: {conf}', file=sys.stdout) + print(f"Configure logging, from conf: {conf}", file=sys.stdout) else: - print(f'Unable to configure logging, attempted conf: {conf}', file=sys.stderr) + print(f"Unable to configure logging, attempted conf: {conf}", file=sys.stderr) diff --git a/jobs/ftp-poller/utils/minio.py b/jobs/ftp-poller/utils/minio.py index f5e8acbe2..a7294b5f1 100644 --- a/jobs/ftp-poller/utils/minio.py +++ b/jobs/ftp-poller/utils/minio.py @@ -21,7 +21,7 @@ def put_object(value_as_bytes, file_name: str, bucket_name, file_size: int = 0): """Return a pre-signed URL for new doc upload.""" - current_app.logger.debug(f'Creating pre-signed URL for {file_name}') + current_app.logger.debug(f"Creating pre-signed URL for {file_name}") minio_client: Minio = _get_client() value_as_stream = io.BytesIO(value_as_bytes) minio_client.put_object(bucket_name, file_name, value_as_stream, file_size) @@ -29,15 +29,16 @@ def put_object(value_as_bytes, file_name: str, bucket_name, file_size: int = 0): def get_object(file_name: str) -> HTTPResponse: """Return a pre-signed URL for new doc upload.""" - current_app.logger.debug(f'Creating pre-signed URL for {file_name}') + current_app.logger.debug(f"Creating pre-signed URL for {file_name}") minio_client: Minio = _get_client() - return minio_client.get_object(current_app.config['MINIO_BUCKET_NAME'], file_name) + return minio_client.get_object(current_app.config["MINIO_BUCKET_NAME"], file_name) def _get_client() -> Minio: """Return a minio client.""" - minio_endpoint = current_app.config['MINIO_ENDPOINT'] - minio_key = current_app.config['MINIO_ACCESS_KEY'] - minio_secret = current_app.config['MINIO_ACCESS_SECRET'] - return Minio(minio_endpoint, access_key=minio_key, secret_key=minio_secret, - secure=current_app.config['MINIO_SECURE']) + minio_endpoint = current_app.config["MINIO_ENDPOINT"] + minio_key = current_app.config["MINIO_ACCESS_KEY"] + minio_secret = current_app.config["MINIO_ACCESS_SECRET"] + return Minio( + minio_endpoint, access_key=minio_key, secret_key=minio_secret, secure=current_app.config["MINIO_SECURE"] + ) diff --git a/jobs/ftp-poller/utils/utils.py b/jobs/ftp-poller/utils/utils.py index afc22828a..5538c66ec 100644 --- a/jobs/ftp-poller/utils/utils.py +++ b/jobs/ftp-poller/utils/utils.py @@ -25,15 +25,13 @@ from utils.minio import put_object -def publish_to_queue(payment_file_list: List[str], message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, - location: str = ''): +def publish_to_queue( + payment_file_list: List[str], message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, location: str = "" +): """Publish message to the Queue, saying file has been uploaded. Using the event spec.""" - queue_data = { - 'fileSource': 'MINIO', - 'location': location or current_app.config['MINIO_BUCKET_NAME'] - } + queue_data = {"fileSource": "MINIO", "location": location or current_app.config["MINIO_BUCKET_NAME"]} for file_name in payment_file_list: - queue_data['fileName'] = file_name + queue_data["fileName"] = file_name try: gcp_queue_publisher.publish_to_queue( @@ -41,14 +39,12 @@ def publish_to_queue(payment_file_list: List[str], message_type=QueueMessageType source=QueueSources.FTP_POLLER.value, message_type=message_type, payload=queue_data, - topic=current_app.config.get('FTP_POLLER_TOPIC'), - ordering_key=str(time()) + topic=current_app.config.get("FTP_POLLER_TOPIC"), + ordering_key=str(time()), ) ) except Exception as e: # NOQA # pylint: disable=broad-except - current_app.logger.warning( - f'Notification to Queue failed for the file {file_name}', - e) + current_app.logger.warning(f"Notification to Queue failed for the file {file_name}", e) raise @@ -59,7 +55,12 @@ def upload_to_minio(file, file_full_name, sftp_client, bucket_name): f.prefetch() value_as_bytes = f.read() try: - put_object(value_as_bytes, file.filename, bucket_name, file.st_size, ) + put_object( + value_as_bytes, + file.filename, + bucket_name, + file.st_size, + ) except Exception: # NOQA # pylint: disable=broad-except - current_app.logger.error(f'upload to minio failed for the file: {file_full_name}') + current_app.logger.error(f"upload to minio failed for the file: {file_full_name}") raise From a98fcf6a904b3df16d110f52d2c5f3a8eff62857 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:14:07 -0700 Subject: [PATCH 02/10] bcol-api changes --- bcol-api/Makefile | 18 +- bcol-api/gunicorn_config.py | 9 +- bcol-api/manage.py | 7 +- bcol-api/poetry.lock | 84 ++++++++- bcol-api/pyproject.toml | 123 +++++++++++++ bcol-api/setup.py | 18 +- bcol-api/src/bcol_api/__init__.py | 24 ++- bcol-api/src/bcol_api/config.py | 118 ++++++------ bcol-api/src/bcol_api/exceptions/__init__.py | 29 ++- bcol-api/src/bcol_api/resources/__init__.py | 33 ++-- bcol-api/src/bcol_api/resources/apihelper.py | 4 +- .../src/bcol_api/resources/bcol_payment.py | 30 +-- .../src/bcol_api/resources/bcol_profile.py | 35 ++-- bcol-api/src/bcol_api/resources/meta.py | 7 +- bcol-api/src/bcol_api/resources/ops.py | 11 +- bcol-api/src/bcol_api/schemas/utils.py | 45 ++--- .../src/bcol_api/services/bcol_payment.py | 77 ++++---- .../src/bcol_api/services/bcol_profile.py | 100 +++++----- bcol-api/src/bcol_api/services/bcol_soap.py | 12 +- bcol-api/src/bcol_api/utils/auth.py | 1 - bcol-api/src/bcol_api/utils/constants.py | 24 +-- bcol-api/src/bcol_api/utils/errors.py | 30 ++- bcol-api/src/bcol_api/utils/logging.py | 4 +- bcol-api/src/bcol_api/utils/run_version.py | 4 +- bcol-api/src/bcol_api/utils/util.py | 18 +- bcol-api/src/bcol_api/version.py | 2 +- bcol-api/tests/conftest.py | 174 +++++++++--------- bcol-api/tests/unit/api/test_bcol_payment.py | 78 ++++---- bcol-api/tests/unit/api/test_bcol_profile.py | 48 +++-- bcol-api/tests/unit/api/test_meta.py | 12 +- bcol-api/tests/unit/api/test_ops.py | 8 +- .../tests/unit/conf/test_configuration.py | 22 +-- .../tests/unit/services/test_bcol_payment.py | 13 +- .../tests/unit/services/test_bcol_profile.py | 18 +- bcol-api/tests/unit/utils/test_logging.py | 6 +- bcol-api/tests/unit/utils/test_util_cors.py | 24 +-- bcol-api/tests/utilities/base_test.py | 43 ++--- bcol-api/tests/utilities/decorators.py | 3 +- bcol-api/tests/utilities/ldap_mock.py | 12 +- bcol-api/tests/utilities/schema_assertions.py | 2 +- bcol-api/wsgi.py | 1 - jobs/payment-jobs/setup.cfg | 98 ---------- 42 files changed, 784 insertions(+), 645 deletions(-) delete mode 100755 jobs/payment-jobs/setup.cfg diff --git a/bcol-api/Makefile b/bcol-api/Makefile index 158db1ba7..b50fc3bfb 100755 --- a/bcol-api/Makefile +++ b/bcol-api/Makefile @@ -45,15 +45,27 @@ install: clean ################################################################################# # COMMANDS - CI # ################################################################################# -ci: lint flake8 test ## CI flow +ci: isort-ci black-ci lint flake8 test ## CI flow + +isort: + poetry run isort . + +isort-ci: + poetry run isort --check . + +black: ## Linting with black + poetry run black . + +black-ci: + poetry run black --check . pylint: ## Linting with pylint - poetry run pylint --rcfile=setup.cfg src/$(PROJECT_NAME) + poetry run pylint src/$(PROJECT_NAME) flake8: ## Linting with flake8 poetry run flake8 src/$(PROJECT_NAME) tests -lint: pylint flake8 ## run all lint type scripts +lint: isort black pylint flake8 ## run all lint type scripts test: ## Unit testing poetry run pytest diff --git a/bcol-api/gunicorn_config.py b/bcol-api/gunicorn_config.py index 9cb75dd26..ef109bd97 100755 --- a/bcol-api/gunicorn_config.py +++ b/bcol-api/gunicorn_config.py @@ -17,9 +17,8 @@ import os +workers = int(os.environ.get("GUNICORN_PROCESSES", "1")) # pylint: disable=invalid-name +threads = int(os.environ.get("GUNICORN_THREADS", "1")) # pylint: disable=invalid-name -workers = int(os.environ.get('GUNICORN_PROCESSES', '1')) # pylint: disable=invalid-name -threads = int(os.environ.get('GUNICORN_THREADS', '1')) # pylint: disable=invalid-name - -forwarded_allow_ips = '*' # pylint: disable=invalid-name -secure_scheme_headers = {'X-Forwarded-Proto': 'https'} # pylint: disable=invalid-name +forwarded_allow_ips = "*" # pylint: disable=invalid-name +secure_scheme_headers = {"X-Forwarded-Proto": "https"} # pylint: disable=invalid-name diff --git a/bcol-api/manage.py b/bcol-api/manage.py index fd9bc8b8f..594d34dc2 100755 --- a/bcol-api/manage.py +++ b/bcol-api/manage.py @@ -24,13 +24,12 @@ from bcol_api import create_app from bcol_api.models import db - APP = create_app() MIGRATE = Migrate(APP, db) MANAGER = Manager(APP) -MANAGER.add_command('db', MigrateCommand) +MANAGER.add_command("db", MigrateCommand) -if __name__ == '__main__': - logging.log(logging.INFO, 'Running the Manager') +if __name__ == "__main__": + logging.log(logging.INFO, "Running the Manager") MANAGER.run() diff --git a/bcol-api/poetry.lock b/bcol-api/poetry.lock index 33749ca13..53cae3dbf 100644 --- a/bcol-api/poetry.lock +++ b/bcol-api/poetry.lock @@ -58,6 +58,50 @@ files = [ [package.dependencies] pycodestyle = ">=2.12.0" +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "blinker" version = "1.8.2" @@ -406,6 +450,22 @@ isort = ">=5.0.0,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + [[package]] name = "flake8-quotes" version = "3.4.0" @@ -1025,6 +1085,17 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "opentracing" version = "2.4.0" @@ -1049,6 +1120,17 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pep8-naming" version = "0.13.3" @@ -1881,4 +1963,4 @@ xmlsec = ["xmlsec (>=0.6.1)"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "374eb1432cd78d908b2026a53d9f3ca94e0cc0e5bc0eb7a803c2d0d64368139c" +content-hash = "984ba8697f043e9062d12c6a513db14eacdffa1f43d8da30df9492cefb5cd74f" diff --git a/bcol-api/pyproject.toml b/bcol-api/pyproject.toml index 6c102d737..9132d7b38 100644 --- a/bcol-api/pyproject.toml +++ b/bcol-api/pyproject.toml @@ -49,6 +49,129 @@ pylint-flask = "^0.6" pydocstyle = "^6.3.0" lovely-pytest-docker = "^0.3.1" isort = "^5.13.2" +black = "^24.10.0" +flake8-pyproject = "^1.2.3" + +[tool.flake8] +ignore = ["F401","E402", "Q000", "E203", "W503"] +exclude = [ + ".venv", + "./venv", + ".git", + ".history", + "devops", + "*migrations*", +] +per-file-ignores = [ + "__init__.py:F401", + "*.py:B902" +] +max-line-length = 120 +docstring-min-length=10 +count = true + +[tool.zimports] +black-line-length = 120 +keep-unused-type-checking = true + +[tool.black] +target-version = ["py310", "py311", "py312"] +line-length = 120 +include = '\.pyi?$' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + migrations + | devops + | .history +)/ +''' + +[tool.isort] +atomic = true +profile = "black" +line_length = 120 +skip_gitignore = true +skip_glob = ["migrations", "devops"] + +[tool.pylint.main] +fail-under = 10 +max-line-length = 120 +ignore = [ "migrations", "devops", "tests"] +ignore-patterns = ["^\\.#"] +ignored-modules= ["flask_sqlalchemy", "sqlalchemy", "SQLAlchemy" , "alembic", "scoped_session"] +ignored-classes= "scoped_session" +ignore-long-lines = "^\\s*(# )??$" +extension-pkg-whitelist = "pydantic" +notes = ["FIXME","XXX","TODO"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] +disable = "C0209,C0301,W0511,W0613,W0703,W1514,W1203,R0801,R0902,R0903,R0911,R0401,R1705,R1718,W3101" +argument-naming-style = "snake_case" +attr-naming-style = "snake_case" +class-attribute-naming-style = "any" +class-const-naming-style = "UPPER_CASE" +class-naming-style = "PascalCase" +const-naming-style = "UPPER_CASE" +function-naming-style = "snake_case" +inlinevar-naming-style = "any" +method-naming-style = "snake_case" +module-naming-style = "any" +variable-naming-style = "snake_case" +docstring-min-length = -1 +good-names = ["i", "j", "k", "ex", "Run", "_"] +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] +valid-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/auth_api", +] +omit = [ + "wsgi.py", + "gunicorn_config.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core"] diff --git a/bcol-api/setup.py b/bcol-api/setup.py index f15130d12..b65147616 100755 --- a/bcol-api/setup.py +++ b/bcol-api/setup.py @@ -26,9 +26,9 @@ def read_requirements(filename): :return: Python requirements :rtype: list """ - with open(filename, 'r') as req: + with open(filename, "r") as req: requirements = req.readlines() - install_requires = [r.strip() for r in requirements if r.find('git+') != 0] + install_requires = [r.strip() for r in requirements if r.find("git+") != 0] return install_requires @@ -39,21 +39,21 @@ def read(filepath): :return: file contents :rtype: str """ - with open(filepath, 'r') as file_handle: + with open(filepath, "r") as file_handle: content = file_handle.read() return content -REQUIREMENTS = read_requirements('requirements/prod.txt') +REQUIREMENTS = read_requirements("requirements/prod.txt") setup( name="bcol_api", - packages=find_packages('src'), - package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + packages=find_packages("src"), + package_dir={"": "src"}, + py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], include_package_data=True, - license=read('LICENSE'), - long_description=read('README.md'), + license=read("LICENSE"), + long_description=read("README.md"), zip_safe=False, install_requires=REQUIREMENTS, setup_requires=["pytest-runner"], diff --git a/bcol-api/src/bcol_api/__init__.py b/bcol-api/src/bcol_api/__init__.py index b603d95e3..ea0dd2e70 100755 --- a/bcol-api/src/bcol_api/__init__.py +++ b/bcol-api/src/bcol_api/__init__.py @@ -31,22 +31,18 @@ from bcol_api.utils.logging import setup_logging from bcol_api.utils.run_version import get_run_version +setup_logging(os.path.join(_Config.PROJECT_ROOT, "logging.conf")) # important to do this first -setup_logging(os.path.join(_Config.PROJECT_ROOT, 'logging.conf')) # important to do this first - -def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): +def create_app(run_mode=os.getenv("FLASK_ENV", "production")): """Return a configured Flask App using the Factory method.""" app = Flask(__name__) app.config.from_object(config.CONFIGURATION[run_mode]) # Configure Sentry - if str(app.config.get('SENTRY_ENABLE')).lower() == 'true': - if app.config.get('SENTRY_DSN', None): - sentry_sdk.init( - dsn=app.config.get('SENTRY_DSN'), - integrations=[FlaskIntegration()] - ) + if str(app.config.get("SENTRY_ENABLE")).lower() == "true": + if app.config.get("SENTRY_DSN", None): + sentry_sdk.init(dsn=app.config.get("SENTRY_DSN"), integrations=[FlaskIntegration()]) app.register_blueprint(API_BLUEPRINT) app.register_blueprint(OPS_BLUEPRINT) @@ -59,7 +55,7 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): @app.after_request def add_version(response): # pylint: disable=unused-variable version = get_run_version() - response.headers['API'] = f'bcol_api/{version}' + response.headers["API"] = f"bcol_api/{version}" return response register_shellcontext(app) @@ -69,18 +65,20 @@ def add_version(response): # pylint: disable=unused-variable def setup_jwt_manager(app, jwt_manager): """Use flask app to configure the JWTManager to work for a particular Realm.""" + def get_roles(a_dict): - return a_dict['realm_access']['roles'] # pragma: no cover + return a_dict["realm_access"]["roles"] # pragma: no cover - app.config['JWT_ROLE_CALLBACK'] = get_roles + app.config["JWT_ROLE_CALLBACK"] = get_roles jwt_manager.init_app(app) def register_shellcontext(app): """Register shell context objects.""" + def shell_context(): """Shell context objects.""" - return {'app': app, 'jwt': jwt} # pragma: no cover + return {"app": app, "jwt": jwt} # pragma: no cover app.shell_context_processor(shell_context) diff --git a/bcol-api/src/bcol_api/config.py b/bcol-api/src/bcol_api/config.py index cf7ba09ec..b91cafd90 100755 --- a/bcol-api/src/bcol_api/config.py +++ b/bcol-api/src/bcol_api/config.py @@ -28,24 +28,24 @@ load_dotenv(find_dotenv()) CONFIGURATION = { - 'development': 'bcol_api.config.DevConfig', - 'testing': 'bcol_api.config.TestConfig', - 'production': 'bcol_api.config.ProdConfig', - 'default': 'bcol_api.config.ProdConfig', - 'migration': 'bcol_api.config.MigrationConfig', + "development": "bcol_api.config.DevConfig", + "testing": "bcol_api.config.TestConfig", + "production": "bcol_api.config.ProdConfig", + "default": "bcol_api.config.ProdConfig", + "migration": "bcol_api.config.MigrationConfig", } -def get_named_config(config_name: str = 'production'): +def get_named_config(config_name: str = "production"): """Return the configuration object based on the name. :raise: KeyError: if an unknown configuration is requested """ - if config_name in ['production', 'staging', 'default']: + if config_name in ["production", "staging", "default"]: config = ProdConfig() - elif config_name == 'testing': + elif config_name == "testing": config = TestConfig() - elif config_name == 'development': + elif config_name == "development": config = DevConfig() else: raise KeyError(f"Unknown configuration '{config_name}'") @@ -54,48 +54,48 @@ def get_named_config(config_name: str = 'production'): def _get_config(config_key: str, **kwargs): """Get the config from environment, and throw error if there are no default values and if the value is None.""" - if 'default' in kwargs: - value = os.getenv(config_key, kwargs.get('default')) + if "default" in kwargs: + value = os.getenv(config_key, kwargs.get("default")) else: value = os.getenv(config_key) assert value return value -class _Config(): # pylint: disable=too-few-public-methods +class _Config: # pylint: disable=too-few-public-methods """Base class configuration that should set reasonable defaults for all the other configurations.""" PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - SECRET_KEY = 'a secret' + SECRET_KEY = "a secret" SQLALCHEMY_TRACK_MODIFICATIONS = False - ALEMBIC_INI = 'migrations/alembic.ini' + ALEMBIC_INI = "migrations/alembic.ini" # JWT_OIDC Settings - JWT_OIDC_WELL_KNOWN_CONFIG = _get_config('JWT_OIDC_WELL_KNOWN_CONFIG') - JWT_OIDC_ALGORITHMS = _get_config('JWT_OIDC_ALGORITHMS') - JWT_OIDC_ISSUER = _get_config('JWT_OIDC_ISSUER') - JWT_OIDC_AUDIENCE = _get_config('JWT_OIDC_AUDIENCE') - JWT_OIDC_CLIENT_SECRET = _get_config('JWT_OIDC_CLIENT_SECRET') - JWT_OIDC_CACHING_ENABLED = _get_config('JWT_OIDC_CACHING_ENABLED', default=False) - JWT_OIDC_JWKS_CACHE_TIMEOUT = int(_get_config('JWT_OIDC_JWKS_CACHE_TIMEOUT', default=300)) + JWT_OIDC_WELL_KNOWN_CONFIG = _get_config("JWT_OIDC_WELL_KNOWN_CONFIG") + JWT_OIDC_ALGORITHMS = _get_config("JWT_OIDC_ALGORITHMS") + JWT_OIDC_ISSUER = _get_config("JWT_OIDC_ISSUER") + JWT_OIDC_AUDIENCE = _get_config("JWT_OIDC_AUDIENCE") + JWT_OIDC_CLIENT_SECRET = _get_config("JWT_OIDC_CLIENT_SECRET") + JWT_OIDC_CACHING_ENABLED = _get_config("JWT_OIDC_CACHING_ENABLED", default=False) + JWT_OIDC_JWKS_CACHE_TIMEOUT = int(_get_config("JWT_OIDC_JWKS_CACHE_TIMEOUT", default=300)) # BCOL PROFILE - BCOL_QUERY_PROFILE_WSDL_URL = _get_config('BCOL_QUERY_PROFILE_WSDL_URL') - BCOL_LDAP_SERVER = _get_config('BCOL_LDAP_SERVER') - BCOL_LDAP_USER_DN_PATTERN = _get_config('BCOL_LDAP_USER_DN_PATTERN') - BCOL_DEBIT_ACCOUNT_VERSION = _get_config('BCOL_DEBIT_ACCOUNT_VERSION') - BCOL_LINK_CODE = _get_config('BCOL_LINK_CODE') + BCOL_QUERY_PROFILE_WSDL_URL = _get_config("BCOL_QUERY_PROFILE_WSDL_URL") + BCOL_LDAP_SERVER = _get_config("BCOL_LDAP_SERVER") + BCOL_LDAP_USER_DN_PATTERN = _get_config("BCOL_LDAP_USER_DN_PATTERN") + BCOL_DEBIT_ACCOUNT_VERSION = _get_config("BCOL_DEBIT_ACCOUNT_VERSION") + BCOL_LINK_CODE = _get_config("BCOL_LINK_CODE") # BCOL PAYMENT - BCOL_PAYMENTS_WSDL_URL = _get_config('BCOL_PAYMENTS_WSDL_URL') - BCOL_APPLIED_CHARGE_WSDL_URL = _get_config('BCOL_APPLIED_CHARGE_WSDL_URL') + BCOL_PAYMENTS_WSDL_URL = _get_config("BCOL_PAYMENTS_WSDL_URL") + BCOL_APPLIED_CHARGE_WSDL_URL = _get_config("BCOL_APPLIED_CHARGE_WSDL_URL") # Sentry Config - SENTRY_ENABLE = _get_config('SENTRY_ENABLE', default=False) - SENTRY_DSN = _get_config('SENTRY_DSN', default=None) + SENTRY_ENABLE = _get_config("SENTRY_ENABLE", default=False) + SENTRY_DSN = _get_config("SENTRY_DSN", default=None) TESTING = False DEBUG = True @@ -113,43 +113,43 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods DEBUG = True TESTING = True - USE_TEST_KEYCLOAK_DOCKER = 'YES' + USE_TEST_KEYCLOAK_DOCKER = "YES" JWT_OIDC_TEST_MODE = True - JWT_OIDC_TEST_AUDIENCE = os.getenv('JWT_OIDC_AUDIENCE') - JWT_OIDC_TEST_CLIENT_SECRET = os.getenv('JWT_OIDC_CLIENT_SECRET') - JWT_OIDC_TEST_ISSUER = os.getenv('JWT_OIDC_ISSUER') + JWT_OIDC_TEST_AUDIENCE = os.getenv("JWT_OIDC_AUDIENCE") + JWT_OIDC_TEST_CLIENT_SECRET = os.getenv("JWT_OIDC_CLIENT_SECRET") + JWT_OIDC_TEST_ISSUER = os.getenv("JWT_OIDC_ISSUER") JWT_OIDC_TEST_KEYS = { - 'keys': [ + "keys": [ { - 'kid': 'sbc-auth-cron-job', - 'kty': 'RSA', - 'alg': 'RS256', - 'use': 'sig', - 'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-' - 'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR', - 'e': 'AQAB', + "kid": "sbc-auth-cron-job", + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": "AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-" + "TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR", + "e": "AQAB", } ] } JWT_OIDC_TEST_PRIVATE_KEY_JWKS = { - 'keys': [ + "keys": [ { - 'kid': 'sbc-auth-cron-job', - 'kty': 'RSA', - 'alg': 'RS256', - 'use': 'sig', - 'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-' - 'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR', - 'e': 'AQAB', - 'd': 'C0G3QGI6OQ6tvbCNYGCqq043YI_8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhskURaDwk4-8VBW9SlvcfSJJrnZhgFMjOY' - 'SSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh_xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0', - 'p': 'APXcusFMQNHjh6KVD_hOUIw87lvK13WkDEeeuqAydai9Ig9JKEAAfV94W6Aftka7tGgE7ulg1vo3eJoLWJ1zvKM', - 'q': 'AOjX3OnPJnk0ZFUQBwhduCweRi37I6DAdLTnhDvcPTrrNWuKPg9uGwHjzFCJgKd8KBaDQ0X1rZTZLTqi3peT43s', - 'dp': 'AN9kBoA5o6_Rl9zeqdsIdWFmv4DB5lEqlEnC7HlAP-3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhc', - 'dq': 'ANtbSY6njfpPploQsF9sU26U0s7MsuLljM1E8uml8bVJE1mNsiu9MgpUvg39jEu9BtM2tDD7Y51AAIEmIQex1nM', - 'qi': 'XLE5O360x-MhsdFXx8Vwz4304-MJg-oGSJXCK_ZWYOB_FGXFRTfebxCsSYi0YwJo-oNu96bvZCuMplzRI1liZw', + "kid": "sbc-auth-cron-job", + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": "AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-" + "TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR", + "e": "AQAB", + "d": "C0G3QGI6OQ6tvbCNYGCqq043YI_8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhskURaDwk4-8VBW9SlvcfSJJrnZhgFMjOY" + "SSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh_xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0", + "p": "APXcusFMQNHjh6KVD_hOUIw87lvK13WkDEeeuqAydai9Ig9JKEAAfV94W6Aftka7tGgE7ulg1vo3eJoLWJ1zvKM", + "q": "AOjX3OnPJnk0ZFUQBwhduCweRi37I6DAdLTnhDvcPTrrNWuKPg9uGwHjzFCJgKd8KBaDQ0X1rZTZLTqi3peT43s", + "dp": "AN9kBoA5o6_Rl9zeqdsIdWFmv4DB5lEqlEnC7HlAP-3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhc", + "dq": "ANtbSY6njfpPploQsF9sU26U0s7MsuLljM1E8uml8bVJE1mNsiu9MgpUvg39jEu9BtM2tDD7Y51AAIEmIQex1nM", + "qi": "XLE5O360x-MhsdFXx8Vwz4304-MJg-oGSJXCK_ZWYOB_FGXFRTfebxCsSYi0YwJo-oNu96bvZCuMplzRI1liZw", } ] } @@ -175,11 +175,11 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods class ProdConfig(_Config): # pylint: disable=too-few-public-methods """Production environment configuration.""" - SECRET_KEY = _get_config('SECRET_KEY', default=None) + SECRET_KEY = _get_config("SECRET_KEY", default=None) if not SECRET_KEY: SECRET_KEY = os.urandom(24) - print('WARNING: SECRET_KEY being set as a one-shot', file=sys.stderr) + print("WARNING: SECRET_KEY being set as a one-shot", file=sys.stderr) TESTING = False DEBUG = False diff --git a/bcol-api/src/bcol_api/exceptions/__init__.py b/bcol-api/src/bcol_api/exceptions/__init__.py index f65fe5677..7390e876b 100755 --- a/bcol-api/src/bcol_api/exceptions/__init__.py +++ b/bcol-api/src/bcol_api/exceptions/__init__.py @@ -30,17 +30,20 @@ def convert_to_response(body: Dict, status: int = HTTPStatus.BAD_REQUEST): """Convert json error to problem response.""" - return Response(response=json.dumps(body), mimetype='application/problem+json', status=status) + return Response(response=json.dumps(body), mimetype="application/problem+json", status=status) def error_to_response(error: Error, invalid_params=None): """Convert Error enum to response.""" - return convert_to_response(body={ - 'type': error.name, - 'title': error.title, - 'detail': error.details, - 'invalidParams': invalid_params - }, status=error.status) + return convert_to_response( + body={ + "type": error.name, + "title": error.title, + "detail": error.details, + "invalidParams": invalid_params, + }, + status=error.status, + ) class BusinessException(Exception): # noqa @@ -56,11 +59,7 @@ def __init__(self, error: Error, *args, **kwargs): def as_problem_json(self): """Return problem+json of error message.""" - return { - 'type': self.code, - 'title': self.message, - 'detail': self.details - } + return {"type": self.code, "title": self.message, "detail": self.details} def response(self): """Response attributes.""" @@ -80,11 +79,7 @@ def __init__(self, code: str, message: str, *args, **kwargs): def as_problem_json(self): """Return problem+json of error message.""" - return { - 'type': self.code, - 'title': self.message, - 'detail': self.details - } + return {"type": self.code, "title": self.message, "detail": self.details} def response(self): """Response attributes.""" diff --git a/bcol-api/src/bcol_api/resources/__init__.py b/bcol-api/src/bcol_api/resources/__init__.py index 28f382357..6d6626695 100755 --- a/bcol-api/src/bcol_api/resources/__init__.py +++ b/bcol-api/src/bcol_api/resources/__init__.py @@ -29,39 +29,38 @@ from .meta import API as META_API from .ops import API as OPS_API - -__all__ = ('API_BLUEPRINT', 'OPS_BLUEPRINT') +__all__ = ("API_BLUEPRINT", "OPS_BLUEPRINT") # This will add the Authorize button to the swagger docs -AUTHORIZATIONS = {'apikey': {'type': 'apiKey', 'in': 'header', 'name': 'Authorization'}} +AUTHORIZATIONS = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}} -OPS_BLUEPRINT = Blueprint('API_OPS', __name__, url_prefix='/ops') +OPS_BLUEPRINT = Blueprint("API_OPS", __name__, url_prefix="/ops") API_OPS = Api( OPS_BLUEPRINT, - title='Service OPS API', - version='1.0', - description='Microservice for BC Online SOAP Services', - security=['apikey'], + title="Service OPS API", + version="1.0", + description="Microservice for BC Online SOAP Services", + security=["apikey"], authorizations=AUTHORIZATIONS, ) -API_OPS.add_namespace(OPS_API, path='/') +API_OPS.add_namespace(OPS_API, path="/") -API_BLUEPRINT = Blueprint('API', __name__, url_prefix='/api/v1') +API_BLUEPRINT = Blueprint("API", __name__, url_prefix="/api/v1") API = Api( API_BLUEPRINT, - title='BCOL API', - version='1.0', - description='Microservice for BC Online SOAP Services', - security=['apikey'], + title="BCOL API", + version="1.0", + description="Microservice for BC Online SOAP Services", + security=["apikey"], authorizations=AUTHORIZATIONS, ) HANDLER = ExceptionHandler(API) -API.add_namespace(META_API, path='/meta') +API.add_namespace(META_API, path="/meta") -API.add_namespace(BCOL_PROFILE_API, path='/profiles') -API.add_namespace(BCOL_PAYMENTS_API, path='/payments') +API.add_namespace(BCOL_PROFILE_API, path="/profiles") +API.add_namespace(BCOL_PAYMENTS_API, path="/payments") diff --git a/bcol-api/src/bcol_api/resources/apihelper.py b/bcol-api/src/bcol_api/resources/apihelper.py index 873eb1b62..027904e7c 100644 --- a/bcol-api/src/bcol_api/resources/apihelper.py +++ b/bcol-api/src/bcol_api/resources/apihelper.py @@ -25,5 +25,5 @@ class Api(BaseApi): # pragma: no cover @property def specs_url(self): """Return URL for endpoint.""" - scheme = 'http' if '5000' in self.base_url else 'https' - return url_for(self.endpoint('specs'), _external=True, _scheme=scheme) + scheme = "http" if "5000" in self.base_url else "https" + return url_for(self.endpoint("specs"), _external=True, _scheme=scheme) diff --git a/bcol-api/src/bcol_api/resources/bcol_payment.py b/bcol-api/src/bcol_api/resources/bcol_payment.py index 4323e90f8..b9ffae0cf 100755 --- a/bcol-api/src/bcol_api/resources/bcol_payment.py +++ b/bcol-api/src/bcol_api/resources/bcol_payment.py @@ -26,18 +26,17 @@ from bcol_api.utils.errors import Error from bcol_api.utils.util import cors_preflight +API = Namespace("bcol accounts", description="Payment System - BCOL Accounts") -API = Namespace('bcol accounts', description='Payment System - BCOL Accounts') - -@cors_preflight(['POST', 'OPTIONS']) -@API.route('', methods=['POST', 'OPTIONS']) +@cors_preflight(["POST", "OPTIONS"]) +@API.route("", methods=["POST", "OPTIONS"]) class AccountPayment(Resource): """Endpoint resource to manage BCOL Payments.""" @staticmethod @_jwt.requires_auth - @cors.crossdomain(origin='*') + @cors.crossdomain(origin="*") def post(): """Create a payment record in BCOL.""" try: @@ -52,19 +51,26 @@ def post(): req_json = request.get_json() current_app.logger.debug(req_json) # Validate the input request - valid_format = schema_utils.validate(req_json, 'payment_request') + valid_format = schema_utils.validate(req_json, "payment_request") if not valid_format[0]: - current_app.logger.info('Validation Error with incoming request %s', - schema_utils.serialize(valid_format[1])) - return error_to_response(Error.INVALID_REQUEST, - invalid_params=schema_utils.serialize(valid_format[1])) + current_app.logger.info( + "Validation Error with incoming request %s", + schema_utils.serialize(valid_format[1]), + ) + return error_to_response( + Error.INVALID_REQUEST, + invalid_params=schema_utils.serialize(valid_format[1]), + ) # Override for apply charge, allows for service fees, this is used by CSO's service account. - if _jwt.validate_roles([Role.SYSTEM.value]) and req_json.get('forceUseDebitAccount') is True: + if _jwt.validate_roles([Role.SYSTEM.value]) and req_json.get("forceUseDebitAccount") is True: is_apply_charge = False - response, status = BcolPayment().create_payment(req_json, is_apply_charge), HTTPStatus.OK + response, status = ( + BcolPayment().create_payment(req_json, is_apply_charge), + HTTPStatus.OK, + ) except BusinessException as exception: return exception.response() diff --git a/bcol-api/src/bcol_api/resources/bcol_profile.py b/bcol-api/src/bcol_api/resources/bcol_profile.py index 5b1c565d9..bb3b8b1c7 100755 --- a/bcol-api/src/bcol_api/resources/bcol_profile.py +++ b/bcol-api/src/bcol_api/resources/bcol_profile.py @@ -25,46 +25,53 @@ from bcol_api.utils.errors import Error from bcol_api.utils.util import cors_preflight +API = Namespace("bcol profile", description="Payment System - BCOL Profiles") -API = Namespace('bcol profile', description='Payment System - BCOL Profiles') - -@cors_preflight(['POST', 'OPTIONS']) -@API.route('', methods=['POST', 'OPTIONS']) +@cors_preflight(["POST", "OPTIONS"]) +@API.route("", methods=["POST", "OPTIONS"]) class BcolProfiles(Resource): """Endpoint query bcol profile using user id and password.""" @staticmethod @_jwt.requires_auth - @cors.crossdomain(origin='*') + @cors.crossdomain(origin="*") def post(): """Return the account details.""" try: req_json = request.get_json() # Validate the input request - valid_format = schema_utils.validate(req_json, 'accounts_request') + valid_format = schema_utils.validate(req_json, "accounts_request") if not valid_format[0]: - return error_to_response(Error.INVALID_REQUEST, invalid_params=schema_utils.serialize(valid_format[1])) + return error_to_response( + Error.INVALID_REQUEST, + invalid_params=schema_utils.serialize(valid_format[1]), + ) - response, status = BcolProfileService().query_profile(req_json.get('userId'), - req_json.get('password')), HTTPStatus.OK + response, status = ( + BcolProfileService().query_profile(req_json.get("userId"), req_json.get("password")), + HTTPStatus.OK, + ) except BusinessException as exception: return exception.response() return response, status -@cors_preflight(['GET', 'OPTIONS']) -@API.route('/', methods=['GET', 'OPTIONS']) +@cors_preflight(["GET", "OPTIONS"]) +@API.route("/", methods=["GET", "OPTIONS"]) class BcolProfile(Resource): """Endpoint resource to get bcol profile by user id.""" @staticmethod - @_jwt.has_one_of_roles(['system']) - @cors.crossdomain(origin='*') + @_jwt.has_one_of_roles(["system"]) + @cors.crossdomain(origin="*") def get(bcol_user_id: str): """Return the bcol profile.""" try: - response, status = BcolProfileService().get_profile(bcol_user_id), HTTPStatus.OK + response, status = ( + BcolProfileService().get_profile(bcol_user_id), + HTTPStatus.OK, + ) except BusinessException as exception: return exception.response() return response, status diff --git a/bcol-api/src/bcol_api/resources/meta.py b/bcol-api/src/bcol_api/resources/meta.py index bce91026d..c1aab14cd 100755 --- a/bcol-api/src/bcol_api/resources/meta.py +++ b/bcol-api/src/bcol_api/resources/meta.py @@ -20,11 +20,10 @@ from bcol_api.utils.run_version import get_run_version +API = Namespace("Meta", description="Metadata") -API = Namespace('Meta', description='Metadata') - -@API.route('/info') +@API.route("/info") class Info(Resource): """Meta information about the overall service.""" @@ -32,4 +31,4 @@ class Info(Resource): def get(): """Return a JSON object with meta information about the Service.""" version = get_run_version() - return jsonify(API=f'bcol_api/{version}') + return jsonify(API=f"bcol_api/{version}") diff --git a/bcol-api/src/bcol_api/resources/ops.py b/bcol-api/src/bcol_api/resources/ops.py index c97f64e9a..50eb70d95 100755 --- a/bcol-api/src/bcol_api/resources/ops.py +++ b/bcol-api/src/bcol_api/resources/ops.py @@ -14,11 +14,10 @@ """Endpoints to check and manage the health of the service.""" from flask_restx import Namespace, Resource +API = Namespace("OPS", description="Service - OPS checks") -API = Namespace('OPS', description='Service - OPS checks') - -@API.route('healthz') +@API.route("healthz") class Healthz(Resource): """Determines if the service and required dependencies are still working. @@ -29,14 +28,14 @@ class Healthz(Resource): def get(): """Return a JSON object stating the health of the Service and dependencies.""" # made it here, so all checks passed - return {'message': 'api is healthy'}, 200 + return {"message": "api is healthy"}, 200 -@API.route('readyz') +@API.route("readyz") class Readyz(Resource): """Determines if the service is ready to respond.""" @staticmethod def get(): """Return a JSON object that identifies if the service is setupAnd ready to work.""" - return {'message': 'api is ready'}, 200 + return {"message": "api is ready"}, 200 diff --git a/bcol-api/src/bcol_api/schemas/utils.py b/bcol-api/src/bcol_api/schemas/utils.py index e8f8160da..0b66f8ddd 100644 --- a/bcol-api/src/bcol_api/schemas/utils.py +++ b/bcol-api/src/bcol_api/schemas/utils.py @@ -21,8 +21,7 @@ from jsonschema import Draft7Validator, RefResolver, SchemaError - -BASE_URI = 'https://bcrs.gov.bc.ca/.well_known/schemas' +BASE_URI = "https://bcrs.gov.bc.ca/.well_known/schemas" def get_schema_store(validate_schema: bool = False, schema_search_path: str = None) -> dict: @@ -32,16 +31,16 @@ def get_schema_store(validate_schema: bool = False, schema_search_path: str = No """ try: if not schema_search_path: - schema_search_path = path.join(path.dirname(__file__), 'schemas') + schema_search_path = path.join(path.dirname(__file__), "schemas") schemastore = {} fnames = listdir(schema_search_path) for fname in fnames: fpath = path.join(schema_search_path, fname) - if fpath[-5:] == '.json': - with open(fpath, 'r', encoding='utf-8') as schema_fd: + if fpath[-5:] == ".json": + with open(fpath, "r", encoding="utf-8") as schema_fd: schema = json.load(schema_fd) - if '$id' in schema: - schemastore[schema['$id']] = schema + if "$id" in schema: + schemastore[schema["$id"]] = schema if validate_schema: for _, schema in schemastore.items(): @@ -53,37 +52,33 @@ def get_schema_store(validate_schema: bool = False, schema_search_path: str = No raise error -def validate(json_data: json, - schema_id: str, - schema_store: dict = None, - validate_schema: bool = False, - schema_search_path: str = None - ) -> Tuple[bool, iter]: +def validate( + json_data: json, + schema_id: str, + schema_store: dict = None, + validate_schema: bool = False, + schema_search_path: str = None, +) -> Tuple[bool, iter]: """Load the json file and validate against loaded schema.""" try: if not schema_search_path: - schema_search_path = path.join(path.dirname(__file__), 'schemas') + schema_search_path = path.join(path.dirname(__file__), "schemas") if not schema_store: schema_store = get_schema_store(validate_schema, schema_search_path) - schema = schema_store.get(f'{BASE_URI}/{schema_id}') + schema = schema_store.get(f"{BASE_URI}/{schema_id}") if validate_schema: Draft7Validator.check_schema(schema) schema_file_path = path.join(schema_search_path, schema_id) - resolver = RefResolver(f'file://{schema_file_path}.json', schema, schema_store) - - draft_7_validator = Draft7Validator(schema, - format_checker=Draft7Validator.FORMAT_CHECKER, - resolver=resolver - ) - if draft_7_validator \ - .is_valid(json_data): + resolver = RefResolver(f"file://{schema_file_path}.json", schema, schema_store) + + draft_7_validator = Draft7Validator(schema, format_checker=Draft7Validator.FORMAT_CHECKER, resolver=resolver) + if draft_7_validator.is_valid(json_data): return True, None - errors = draft_7_validator \ - .iter_errors(json_data) + errors = draft_7_validator.iter_errors(json_data) return False, errors except SchemaError as error: diff --git a/bcol-api/src/bcol_api/services/bcol_payment.py b/bcol-api/src/bcol_api/services/bcol_payment.py index 8b3412dfc..19ea99c57 100644 --- a/bcol-api/src/bcol_api/services/bcol_payment.py +++ b/bcol-api/src/bcol_api/services/bcol_payment.py @@ -29,51 +29,51 @@ class BcolPayment: # pylint:disable=too-few-public-methods def create_payment(self, pay_request: Dict, is_apply_charge: bool): """Create payment record in BCOL.""" current_app.logger.debug(f'create_payment') + current_app.logger.debug(">create_payment") return pay_response def __get(self, value: object, key: object) -> str: # pragma: no cover @@ -110,7 +112,7 @@ def apply_charge(self, data: Dict): # pragma: no cover client = BcolSoap().get_applied_chg_client() return zeep.helpers.serialize_object(client.service.appliedCharge(req=data)) - def _pad_zeros(self, amount: str = '0'): + def _pad_zeros(self, amount: str = "0"): """Pad the amount with Zeroes to make sure the string is 10 chars.""" if not amount: return None @@ -121,5 +123,6 @@ def _check_service_fees_match(self, ts_fee, invoice_service_fees): """Check to see if BCOL return matches passed in service fees.""" ts_fee = -float(ts_fee) / 100 if ts_fee else 0 if ts_fee != float(invoice_service_fees): - current_app.logger.error(f"TSFee {ts_fee} from BCOL doesn\'t match" - f' SBC-PAY invoice service fees: {invoice_service_fees}') + current_app.logger.error( + f"TSFee {ts_fee} from BCOL doesn't match" f" SBC-PAY invoice service fees: {invoice_service_fees}" + ) diff --git a/bcol-api/src/bcol_api/services/bcol_profile.py b/bcol-api/src/bcol_api/services/bcol_profile.py index 9a39953cf..45b6a1e9c 100644 --- a/bcol-api/src/bcol_api/services/bcol_profile.py +++ b/bcol-api/src/bcol_api/services/bcol_profile.py @@ -31,81 +31,75 @@ class BcolProfile: # pylint:disable=too-few-public-methods def query_profile(self, bcol_user_id: str, password: str): """Query for profile and return the results.""" - current_app.logger.debug('query_profile') + current_app.logger.debug(">query_profile") return response def get_profile(self, bcol_user_id): """Return bcol profile by user id.""" # Call the query profile service to fetch profile data = { - 'Version': current_app.config.get('BCOL_DEBIT_ACCOUNT_VERSION'), - 'Userid': bcol_user_id, # 'pc25020', - 'linkcode': current_app.config.get('BCOL_LINK_CODE'), + "Version": current_app.config.get("BCOL_DEBIT_ACCOUNT_VERSION"), + "Userid": bcol_user_id, # 'pc25020', + "linkcode": current_app.config.get("BCOL_LINK_CODE"), } try: profile_resp = self.get_profile_response(data) current_app.logger.debug(profile_resp) - auth_code = self.__get(profile_resp, 'AuthCode') - if auth_code != 'P': + auth_code = self.__get(profile_resp, "AuthCode") + if auth_code != "P": raise BusinessException(Error.NOT_A_PRIME_USER) response = { - 'userId': self.__get(profile_resp, 'Userid'), - 'accountNumber': self.__get(profile_resp, 'AccountNumber'), - 'authCode': auth_code, - 'authCodeDesc': auth_code_mapping()[ - self.__get(profile_resp, 'AuthCode') - ], - 'accountType': self.__get(profile_resp, 'AccountType'), - 'accountTypeDesc': account_type_mapping()[ - self.__get(profile_resp, 'AccountType') - ], - 'gstStatus': self.__get(profile_resp, 'GSTStatus'), - 'gstStatusDesc': tax_status_mapping()[ - self.__get(profile_resp, 'GSTStatus') - ], - 'pstStatus': self.__get(profile_resp, 'PSTStatus'), - 'pstStatusDesc': tax_status_mapping()[ - self.__get(profile_resp, 'PSTStatus') - ], - 'userName': self.__get(profile_resp, 'UserName'), - 'orgName': self.__get(profile_resp, 'org-name'), - 'orgType': self.__get(profile_resp, 'org-type'), - 'phone': self.__get(profile_resp, 'UserPhone'), - 'fax': self.__get(profile_resp, 'UserFax'), + "userId": self.__get(profile_resp, "Userid"), + "accountNumber": self.__get(profile_resp, "AccountNumber"), + "authCode": auth_code, + "authCodeDesc": auth_code_mapping()[self.__get(profile_resp, "AuthCode")], + "accountType": self.__get(profile_resp, "AccountType"), + "accountTypeDesc": account_type_mapping()[self.__get(profile_resp, "AccountType")], + "gstStatus": self.__get(profile_resp, "GSTStatus"), + "gstStatusDesc": tax_status_mapping()[self.__get(profile_resp, "GSTStatus")], + "pstStatus": self.__get(profile_resp, "PSTStatus"), + "pstStatusDesc": tax_status_mapping()[self.__get(profile_resp, "PSTStatus")], + "userName": self.__get(profile_resp, "UserName"), + "orgName": self.__get(profile_resp, "org-name"), + "orgType": self.__get(profile_resp, "org-type"), + "phone": self.__get(profile_resp, "UserPhone"), + "fax": self.__get(profile_resp, "UserFax"), } - address = profile_resp['Address'] + address = profile_resp["Address"] if address: - country = self.standardize_country(self.__get(address, 'Country')) - - response['address'] = { - 'line1': self.__get(address, 'AddressA'), - 'line2': self.__get(address, 'AddressB'), - 'city': self.__get(address, 'City'), - 'province': self.__get(address, 'Prov'), - 'country': country, - 'postalCode': self.__get(address, 'PostalCode'), + country = self.standardize_country(self.__get(address, "Country")) + + response["address"] = { + "line1": self.__get(address, "AddressA"), + "line2": self.__get(address, "AddressB"), + "city": self.__get(address, "City"), + "province": self.__get(address, "Prov"), + "country": country, + "postalCode": self.__get(address, "PostalCode"), } - query_profile_flags = profile_resp['queryProfileFlag'] + query_profile_flags = profile_resp["queryProfileFlag"] if query_profile_flags: flags: list = [] for flag in query_profile_flags: - value = dict(flag).get('_value_1') - if value and value.strip() == 'Y': - flags.append(flag['name']) - response['profile_flags'] = flags + value = dict(flag).get("_value_1") + if value and value.strip() == "Y": + flags.append(flag["name"]) + response["profile_flags"] = flags except zeep.exceptions.Fault as fault: current_app.logger.error(fault) parsed_fault_detail = BcolSoap().get_profile_client().wsdl.types.deserialize(fault.detail[0]) current_app.logger.error(parsed_fault_detail) - raise PaymentException(message=self.__get(parsed_fault_detail, 'message'), - code=self.__get(parsed_fault_detail, 'returnCode')) from fault + raise PaymentException( + message=self.__get(parsed_fault_detail, "message"), + code=self.__get(parsed_fault_detail, "returnCode"), + ) from fault except BusinessException as e: raise e from e except Exception as e: # NOQA @@ -115,22 +109,18 @@ def get_profile(self, bcol_user_id): def __authenticate_user(self, user_id: str, password: str) -> bool: """Validate the user by ldap lookup.""" - current_app.logger.debug('<<< _validate_user') + current_app.logger.debug("<<< _validate_user") ldap_conn = None try: ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) # pylint: disable=no-member - ldap_conn = ldap.initialize( - current_app.config.get('BCOL_LDAP_SERVER'), trace_level=2 - ) + ldap_conn = ldap.initialize(current_app.config.get("BCOL_LDAP_SERVER"), trace_level=2) ldap_conn.set_option(ldap.OPT_REFERRALS, 0) # pylint: disable=no-member ldap_conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3) # pylint: disable=no-member # ldap_conn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) # pylint: disable=no-member ldap_conn.set_option(ldap.OPT_X_TLS_DEMAND, True) # pylint: disable=no-member ldap_conn.set_option(ldap.OPT_DEBUG_LEVEL, 255) # pylint: disable=no-member - username = current_app.config.get('BCOL_LDAP_USER_DN_PATTERN').format( - user_id - ) + username = current_app.config.get("BCOL_LDAP_USER_DN_PATTERN").format(user_id) ldap_conn.simple_bind_s(username, password) except Exception as error: # NOQA current_app.logger.warning(error) @@ -139,7 +129,7 @@ def __authenticate_user(self, user_id: str, password: str) -> bool: if ldap_conn: ldap_conn.unbind_s() - current_app.logger.debug('>>> _validate_user') + current_app.logger.debug(">>> _validate_user") def __get(self, value: object, key: object) -> str: """Get the value from dict and strip.""" diff --git a/bcol-api/src/bcol_api/services/bcol_soap.py b/bcol-api/src/bcol_api/services/bcol_soap.py index 818973d1b..e1381bae1 100644 --- a/bcol-api/src/bcol_api/services/bcol_soap.py +++ b/bcol-api/src/bcol_api/services/bcol_soap.py @@ -50,14 +50,8 @@ def get_applied_chg_client(self): def __init__(self): """Private constructor.""" - self.__profile_client = zeep.Client( - current_app.config.get('BCOL_QUERY_PROFILE_WSDL_URL') - ) + self.__profile_client = zeep.Client(current_app.config.get("BCOL_QUERY_PROFILE_WSDL_URL")) - self.__payment_client = zeep.Client( - current_app.config.get('BCOL_PAYMENTS_WSDL_URL') - ) + self.__payment_client = zeep.Client(current_app.config.get("BCOL_PAYMENTS_WSDL_URL")) - self.__applied_chg_client = zeep.Client( - current_app.config.get('BCOL_APPLIED_CHARGE_WSDL_URL') - ) + self.__applied_chg_client = zeep.Client(current_app.config.get("BCOL_APPLIED_CHARGE_WSDL_URL")) diff --git a/bcol-api/src/bcol_api/utils/auth.py b/bcol-api/src/bcol_api/utils/auth.py index c900a8ab7..bcaf94c2c 100644 --- a/bcol-api/src/bcol_api/utils/auth.py +++ b/bcol-api/src/bcol_api/utils/auth.py @@ -14,6 +14,5 @@ """Bring in the common JWT Manager.""" from flask_jwt_oidc import JwtManager - # lower case name as used by convention in most Flask apps jwt = JwtManager() # pylint: disable=invalid-name diff --git a/bcol-api/src/bcol_api/utils/constants.py b/bcol-api/src/bcol_api/utils/constants.py index 601db0dc6..e0ec187e4 100644 --- a/bcol-api/src/bcol_api/utils/constants.py +++ b/bcol-api/src/bcol_api/utils/constants.py @@ -19,29 +19,29 @@ class Role(Enum): """Role enum.""" - STAFF = 'staff' - EDIT = 'edit' - ACCOUNT_HOLDER = 'account_holder' - SYSTEM = 'system' + STAFF = "staff" + EDIT = "edit" + ACCOUNT_HOLDER = "account_holder" + SYSTEM = "system" def auth_code_mapping() -> Dict: """Return Auth code mapping from BCOL.""" return { - 'G': 'GDSA', - 'M': 'Master', - 'O': 'Office', - 'P': 'Prime', - 'C': 'Contact', - '': 'Ordinary', + "G": "GDSA", + "M": "Master", + "O": "Office", + "P": "Prime", + "C": "Contact", + "": "Ordinary", } def account_type_mapping() -> Dict: """Return Account type mapping from BCOL.""" - return {'B': 'Billable', 'N': 'Non-Billable', 'I': 'Internal'} + return {"B": "Billable", "N": "Non-Billable", "I": "Internal"} def tax_status_mapping() -> Dict: """Return Tax status mapping from BCOL.""" - return {'E': 'Exempt', 'Z': 'Zero-rate', '': 'Must-Pay'} + return {"E": "Exempt", "Z": "Zero-rate", "": "Must-Pay"} diff --git a/bcol-api/src/bcol_api/utils/errors.py b/bcol-api/src/bcol_api/utils/errors.py index 594f4c6b2..6fcdc5e75 100644 --- a/bcol-api/src/bcol_api/utils/errors.py +++ b/bcol-api/src/bcol_api/utils/errors.py @@ -19,16 +19,28 @@ class Error(Enum): """Error Codes.""" - INVALID_CREDENTIALS = 'Invalid Credentials', 'Invalid User ID or Password', HTTPStatus.BAD_REQUEST - NOT_A_PRIME_USER = 'Not a prime user.', \ - 'You must enter the PRIME CONTACT User ID and password for your BC Online account.', \ - HTTPStatus.BAD_REQUEST - SYSTEM_ERROR = 'BC Online is currently not available.', \ - 'BC Online is currently not available. Please try again later.', \ - HTTPStatus.BAD_REQUEST - PAYMENT_ERROR = 'Cannot create payment', 'Error occurred during payment', HTTPStatus.BAD_REQUEST + INVALID_CREDENTIALS = ( + "Invalid Credentials", + "Invalid User ID or Password", + HTTPStatus.BAD_REQUEST, + ) + NOT_A_PRIME_USER = ( + "Not a prime user.", + "You must enter the PRIME CONTACT User ID and password for your BC Online account.", + HTTPStatus.BAD_REQUEST, + ) + SYSTEM_ERROR = ( + "BC Online is currently not available.", + "BC Online is currently not available. Please try again later.", + HTTPStatus.BAD_REQUEST, + ) + PAYMENT_ERROR = ( + "Cannot create payment", + "Error occurred during payment", + HTTPStatus.BAD_REQUEST, + ) - INVALID_REQUEST = 'Invalid Request', 'Invalid Request', HTTPStatus.BAD_REQUEST + INVALID_REQUEST = "Invalid Request", "Invalid Request", HTTPStatus.BAD_REQUEST def __new__(cls, title, details, status): """Attributes for the enum.""" diff --git a/bcol-api/src/bcol_api/utils/logging.py b/bcol-api/src/bcol_api/utils/logging.py index 8568f87dd..d593fae51 100755 --- a/bcol-api/src/bcol_api/utils/logging.py +++ b/bcol-api/src/bcol_api/utils/logging.py @@ -21,6 +21,6 @@ def setup_logging(conf): """Create the services logger.""" if conf and path.isfile(conf): logging.config.fileConfig(conf) - print(f'Configure logging, from conf:{conf}', file=sys.stdout) + print(f"Configure logging, from conf:{conf}", file=sys.stdout) else: - print(f'Unable to configure logging, attempted conf:{conf}', file=sys.stderr) + print(f"Unable to configure logging, attempted conf:{conf}", file=sys.stderr) diff --git a/bcol-api/src/bcol_api/utils/run_version.py b/bcol-api/src/bcol_api/utils/run_version.py index c389e5d8e..4693ccf52 100755 --- a/bcol-api/src/bcol_api/utils/run_version.py +++ b/bcol-api/src/bcol_api/utils/run_version.py @@ -18,12 +18,12 @@ def _get_build_openshift_commit_hash(): - return os.getenv('OPENSHIFT_BUILD_COMMIT', None) + return os.getenv("OPENSHIFT_BUILD_COMMIT", None) def get_run_version(): """Return a formatted version string for this service.""" commit_hash = _get_build_openshift_commit_hash() if commit_hash: - return f'{__version__}-{commit_hash}' + return f"{__version__}-{commit_hash}" return __version__ diff --git a/bcol-api/src/bcol_api/utils/util.py b/bcol-api/src/bcol_api/utils/util.py index 0d111ed72..b7af6e133 100755 --- a/bcol-api/src/bcol_api/utils/util.py +++ b/bcol-api/src/bcol_api/utils/util.py @@ -18,16 +18,22 @@ """ -def cors_preflight(methods: str = 'GET'): +def cors_preflight(methods: str = "GET"): """Render an option method on the class.""" + def wrapper(f): def options(self, *args, **kwargs): # pylint: disable=unused-argument - return {'Allow': methods}, 200, \ - {'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': methods, - 'Access-Control-Allow-Headers': 'Authorization, Content-Type, registries-trace-id'} + return ( + {"Allow": methods}, + 200, + { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": methods, + "Access-Control-Allow-Headers": "Authorization, Content-Type, registries-trace-id", + }, + ) - setattr(f, 'options', options) + setattr(f, "options", options) return f return wrapper diff --git a/bcol-api/src/bcol_api/version.py b/bcol-api/src/bcol_api/version.py index 6f57debc9..eadba756b 100755 --- a/bcol-api/src/bcol_api/version.py +++ b/bcol-api/src/bcol_api/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = '1.0.0' # pylint: disable=invalid-name +__version__ = "1.0.0" # pylint: disable=invalid-name diff --git a/bcol-api/tests/conftest.py b/bcol-api/tests/conftest.py index c6bc09d99..dd43053a7 100755 --- a/bcol-api/tests/conftest.py +++ b/bcol-api/tests/conftest.py @@ -25,54 +25,54 @@ from tests.utilities.ldap_mock import MockLDAP -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def app(): """Return a session-wide application configured in TEST mode.""" - _app = create_app('testing') + _app = create_app("testing") return _app -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def app_request(): """Return a session-wide application configured in TEST mode.""" - _app = create_app('testing') + _app = create_app("testing") return _app -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client(app): # pylint: disable=redefined-outer-name """Return a session-wide Flask test client.""" return app.test_client() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def jwt(app): """Return session-wide jwt manager.""" return _jwt -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client_ctx(app): """Return session-wide Flask test client.""" with app.test_client() as _client: yield _client -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def client_id(): """Return a unique client_id that can be used in tests.""" _id = random.SystemRandom().getrandbits(0x58) # _id = (base64.urlsafe_b64encode(uuid.uuid4().bytes)).replace('=', '') - return f'client-{_id}' + return f"client-{_id}" @pytest.fixture() def ldap_mock(): """Mock ldap.""" - ldap_patcher = patch('bcol_api.services.bcol_profile.ldap.initialize') + ldap_patcher = patch("bcol_api.services.bcol_profile.ldap.initialize") _mock_ldap = MockLDAP() mock_ldap = ldap_patcher.start() mock_ldap.return_value = _mock_ldap @@ -83,7 +83,10 @@ def ldap_mock(): @pytest.fixture() def ldap_mock_error(): """Mock ldap error.""" - ldap_patcher = patch('bcol_api.services.bcol_profile.ldap.initialize', side_effect=Exception('Mocked Error')) + ldap_patcher = patch( + "bcol_api.services.bcol_profile.ldap.initialize", + side_effect=Exception("Mocked Error"), + ) _mock_ldap = MockLDAP() mock_ldap = ldap_patcher.start() mock_ldap.return_value = _mock_ldap @@ -94,32 +97,30 @@ def ldap_mock_error(): @pytest.fixture() def query_profile_mock(): """Mock Query Profile SOAP.""" - mock_query_profile_patcher = patch( - 'bcol_api.services.bcol_profile.BcolProfile.get_profile_response' - ) + mock_query_profile_patcher = patch("bcol_api.services.bcol_profile.BcolProfile.get_profile_response") mock_query_profile = mock_query_profile_patcher.start() mock_query_profile.return_value = { - 'Userid': 'PB25020', - 'AccountNumber': '1234567890', - 'AuthCode': 'P', - 'AccountType': 'B', - 'GSTStatus': ' ', - 'PSTStatus': ' ', - 'UserName': 'Test, Test', - 'Address': { - 'AddressA': '#400A - 4000 SEYMOUR PLACE', - 'AddressB': 'PENTHOUSE', - 'City': 'AB1', - 'Prov': 'BC', - 'Country': 'CANADA', - 'PostalCode': 'V8X 5J8', + "Userid": "PB25020", + "AccountNumber": "1234567890", + "AuthCode": "P", + "AccountType": "B", + "GSTStatus": " ", + "PSTStatus": " ", + "UserName": "Test, Test", + "Address": { + "AddressA": "#400A - 4000 SEYMOUR PLACE", + "AddressB": "PENTHOUSE", + "City": "AB1", + "Prov": "BC", + "Country": "CANADA", + "PostalCode": "V8X 5J8", }, - 'UserPhone': '(250)953-8271 EX1999', - 'UserFax': '(250)953-8212', - 'Status': 'Y', - 'org-name': 'BC ONLINE TECHNICAL TEAM DEVL', - 'org-type': 'LAW', - 'queryProfileFlag': [{'name': 'TEST'}], + "UserPhone": "(250)953-8271 EX1999", + "UserFax": "(250)953-8212", + "Status": "Y", + "org-name": "BC ONLINE TECHNICAL TEAM DEVL", + "org-type": "LAW", + "queryProfileFlag": [{"name": "TEST"}], } yield @@ -129,32 +130,30 @@ def query_profile_mock(): @pytest.fixture() def query_profile_contact_mock(): """Mock Query Profile SOAP for Master user.""" - mock_query_profile_patcher = patch( - 'bcol_api.services.bcol_profile.BcolProfile.get_profile_response' - ) + mock_query_profile_patcher = patch("bcol_api.services.bcol_profile.BcolProfile.get_profile_response") mock_query_profile = mock_query_profile_patcher.start() mock_query_profile.return_value = { - 'Userid': 'PB25020', - 'AccountNumber': '1234567890', - 'AuthCode': 'C', - 'AccountType': 'B', - 'GSTStatus': ' ', - 'PSTStatus': ' ', - 'UserName': 'Test, Test', - 'Address': { - 'AddressA': '#400A - 4000 SEYMOUR PLACE', - 'AddressB': 'PENTHOUSE', - 'City': 'AB1', - 'Prov': 'BC', - 'Country': 'CANADA', - 'PostalCode': 'V8X 5J8', + "Userid": "PB25020", + "AccountNumber": "1234567890", + "AuthCode": "C", + "AccountType": "B", + "GSTStatus": " ", + "PSTStatus": " ", + "UserName": "Test, Test", + "Address": { + "AddressA": "#400A - 4000 SEYMOUR PLACE", + "AddressB": "PENTHOUSE", + "City": "AB1", + "Prov": "BC", + "Country": "CANADA", + "PostalCode": "V8X 5J8", }, - 'UserPhone': '(250)953-8271 EX1999', - 'UserFax': '(250)953-8212', - 'Status': 'Y', - 'org-name': 'BC ONLINE TECHNICAL TEAM DEVL', - 'org-type': 'LAW', - 'queryProfileFlag': [{'name': 'TEST'}], + "UserPhone": "(250)953-8271 EX1999", + "UserFax": "(250)953-8212", + "Status": "Y", + "org-name": "BC ONLINE TECHNICAL TEAM DEVL", + "org-type": "LAW", + "queryProfileFlag": [{"name": "TEST"}], } yield @@ -165,7 +164,8 @@ def query_profile_contact_mock(): def query_profile_mock_error(): """Mock Query Profile SOAP.""" mock_query_profile_patcher = patch( - 'bcol_api.services.bcol_profile.BcolProfile.get_profile_response', side_effect=Exception('Mocked Error') + "bcol_api.services.bcol_profile.BcolProfile.get_profile_response", + side_effect=Exception("Mocked Error"), ) mock_query_profile_patcher.start() @@ -176,29 +176,27 @@ def query_profile_mock_error(): @pytest.fixture() def payment_mock(): """Mock Query Profile SOAP.""" - mock_payment_patcher = patch( - 'bcol_api.services.bcol_payment.BcolPayment.debit_account' - ) + mock_payment_patcher = patch("bcol_api.services.bcol_payment.BcolPayment.debit_account") mock_payment = mock_payment_patcher.start() mock_payment.return_value = { - 'RespType': 'RESPONSE', - 'ReturnCode': '0000', - 'ReturnMsg': 'LOOKS OK TO ME', - 'Uniqueid': '', - 'StatFee': '-700', - 'Totamt': '-850', - 'TSFee': '-150', - 'Totgst': '+00', - 'Totpst': '+00', - 'TranID': { - 'Account': '180670', - 'UserID': 'PB25020 ', - 'AppliedDate': '20191108', - 'AppliedTime': '113405428', - 'FeeCode': 'BSH105 ', - 'Key': 'TEST12345678901', - 'SequenceNo': '0001' - } + "RespType": "RESPONSE", + "ReturnCode": "0000", + "ReturnMsg": "LOOKS OK TO ME", + "Uniqueid": "", + "StatFee": "-700", + "Totamt": "-850", + "TSFee": "-150", + "Totgst": "+00", + "Totpst": "+00", + "TranID": { + "Account": "180670", + "UserID": "PB25020 ", + "AppliedDate": "20191108", + "AppliedTime": "113405428", + "FeeCode": "BSH105 ", + "Key": "TEST12345678901", + "SequenceNo": "0001", + }, } yield @@ -209,7 +207,8 @@ def payment_mock(): def payment_mock_error(): """Mock Payment SOAP.""" mock_query_profile_patcher = patch( - 'bcol_api.services.bcol_payment.BcolPayment.debit_account', side_effect=Exception('Mocked Error') + "bcol_api.services.bcol_payment.BcolPayment.debit_account", + side_effect=Exception("Mocked Error"), ) mock_query_profile_patcher.start() @@ -217,20 +216,19 @@ def payment_mock_error(): mock_query_profile_patcher.stop() -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def keycloak(docker_services, app): """Spin up a keycloak instance and initialize jwt.""" - if 'USE_TEST_KEYCLOAK_DOCKER' in app.config and app.config['USE_TEST_KEYCLOAK_DOCKER']: - docker_services.start('keycloak') - docker_services.wait_for_service('keycloak', 8081) + if "USE_TEST_KEYCLOAK_DOCKER" in app.config and app.config["USE_TEST_KEYCLOAK_DOCKER"]: + docker_services.start("keycloak") + docker_services.wait_for_service("keycloak", 8081) setup_jwt_manager(app, _jwt) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def docker_compose_files(pytestconfig): """Get the docker-compose.yml absolute path.""" import os - return [ - os.path.join(str(pytestconfig.rootdir), 'tests/docker', 'docker-compose.yml') - ] + + return [os.path.join(str(pytestconfig.rootdir), "tests/docker", "docker-compose.yml")] diff --git a/bcol-api/tests/unit/api/test_bcol_payment.py b/bcol-api/tests/unit/api/test_bcol_payment.py index 5728cafce..00df6b037 100755 --- a/bcol-api/tests/unit/api/test_bcol_payment.py +++ b/bcol-api/tests/unit/api/test_bcol_payment.py @@ -24,48 +24,62 @@ def test_post_payments(client, jwt, app, payment_mock): """Assert that the endpoint returns 200.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/payments', data=json.dumps({ - 'feeCode': 'BSH105', - 'userId': 'PB25020', - 'invoiceNumber': 'TEST12345678901', - 'folioNumber': 'TEST1234567890', - 'formNumber': '', - 'quantity': '', - 'rate': '', - 'amount': '', - 'remarks': 'TEST', - 'reduntantFlag': ' ', - 'serviceFees': '1.50' - }), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/payments", + data=json.dumps( + { + "feeCode": "BSH105", + "userId": "PB25020", + "invoiceNumber": "TEST12345678901", + "folioNumber": "TEST1234567890", + "formNumber": "", + "quantity": "", + "rate": "", + "amount": "", + "remarks": "TEST", + "reduntantFlag": " ", + "serviceFees": "1.50", + } + ), + headers=headers, + ) assert rv.status_code == 200 def test_post_payments_invalid_request(client, jwt, app, payment_mock): """Assert that the endpoint returns 400.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/payments', data=json.dumps({ - 'feeCode': 'BSH105', - 'userId': 'PB25020'}), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/payments", + data=json.dumps({"feeCode": "BSH105", "userId": "PB25020"}), + headers=headers, + ) assert rv.status_code == 400 def test_post_payments_error(client, jwt, app, payment_mock_error): """Assert that the endpoint returns 200.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/payments', data=json.dumps({ - 'feeCode': 'BSH105', - 'userId': 'PB25020', - 'invoiceNumber': 'TEST12345678901', - 'folioNumber': 'TEST1234567890', - 'formNumber': '', - 'quantity': '', - 'rate': '', - 'amount': '', - 'remarks': 'TEST', - 'reduntantFlag': ' ', - 'serviceFees': '1.50' - }), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/payments", + data=json.dumps( + { + "feeCode": "BSH105", + "userId": "PB25020", + "invoiceNumber": "TEST12345678901", + "folioNumber": "TEST1234567890", + "formNumber": "", + "quantity": "", + "rate": "", + "amount": "", + "remarks": "TEST", + "reduntantFlag": " ", + "serviceFees": "1.50", + } + ), + headers=headers, + ) assert rv.status_code == 400 diff --git a/bcol-api/tests/unit/api/test_bcol_profile.py b/bcol-api/tests/unit/api/test_bcol_profile.py index 28a010c64..e4dffea59 100755 --- a/bcol-api/tests/unit/api/test_bcol_profile.py +++ b/bcol-api/tests/unit/api/test_bcol_profile.py @@ -25,47 +25,67 @@ def test_post_accounts(client, jwt, app, ldap_mock, query_profile_mock): """Assert that the endpoint returns 200.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/profiles', data=json.dumps({'userId': 'TEST', 'password': 'TEST'}), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/profiles", + data=json.dumps({"userId": "TEST", "password": "TEST"}), + headers=headers, + ) assert rv.status_code == 200 def test_post_accounts_invalid_request(client, jwt, app, ldap_mock, query_profile_mock): """Assert that the endpoint returns 400.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/profiles', data=json.dumps({'user': 'TEST', 'password': 'TEST'}), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/profiles", + data=json.dumps({"user": "TEST", "password": "TEST"}), + headers=headers, + ) assert rv.status_code == 400 def test_post_accounts_auth_error(client, jwt, app, ldap_mock_error, query_profile_mock): """Assert that the endpoint returns 400.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/profiles', data=json.dumps({'userId': 'TEST', 'password': 'TEST'}), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/profiles", + data=json.dumps({"userId": "TEST", "password": "TEST"}), + headers=headers, + ) assert rv.status_code == 400 def test_post_accounts_query_error(client, jwt, app, ldap_mock, query_profile_mock_error): """Assert that the endpoint returns 400.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/profiles', data=json.dumps({'userId': 'TEST', 'password': 'TEST'}), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/profiles", + data=json.dumps({"userId": "TEST", "password": "TEST"}), + headers=headers, + ) assert rv.status_code == 400 def test_post_accounts_not_prime_error(client, jwt, app, ldap_mock, query_profile_contact_mock): """Assert that the endpoint returns 400.""" token = jwt.create_jwt(get_claims(), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.post('/api/v1/profiles', data=json.dumps({'userId': 'TEST', 'password': 'TEST'}), headers=headers) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.post( + "/api/v1/profiles", + data=json.dumps({"userId": "TEST", "password": "TEST"}), + headers=headers, + ) assert rv.status_code == 400 - assert rv.json.get('type') == Error.NOT_A_PRIME_USER.name + assert rv.json.get("type") == Error.NOT_A_PRIME_USER.name def test_get_profile(client, jwt, app, query_profile_mock): """Assert that the endpoint returns 200.""" - token = jwt.create_jwt(get_claims(role='system'), get_token_header()) - headers = {'content-type': 'application/json', 'Authorization': f'Bearer {token}'} - rv = client.get('/api/v1/profiles/PB25020', headers=headers) + token = jwt.create_jwt(get_claims(role="system"), get_token_header()) + headers = {"content-type": "application/json", "Authorization": f"Bearer {token}"} + rv = client.get("/api/v1/profiles/PB25020", headers=headers) assert rv.status_code == 200 diff --git a/bcol-api/tests/unit/api/test_meta.py b/bcol-api/tests/unit/api/test_meta.py index b24d8e9ca..80187749c 100755 --- a/bcol-api/tests/unit/api/test_meta.py +++ b/bcol-api/tests/unit/api/test_meta.py @@ -24,19 +24,19 @@ def test_meta_no_commit_hash(client): """Assert that the endpoint returns just the services __version__.""" from bcol_api.version import __version__ - rv = client.get('/api/v1/meta/info') + rv = client.get("/api/v1/meta/info") assert rv.status_code == 200 - assert rv.json == {'API': f'bcol_api/{__version__}'} + assert rv.json == {"API": f"bcol_api/{__version__}"} def test_meta_with_commit_hash(monkeypatch, client): """Assert that the endpoint return __version__ and the last git hash used to build the services image.""" from bcol_api.version import __version__ - commit_hash = 'deadbeef_ha' - monkeypatch.setenv('OPENSHIFT_BUILD_COMMIT', commit_hash) + commit_hash = "deadbeef_ha" + monkeypatch.setenv("OPENSHIFT_BUILD_COMMIT", commit_hash) - rv = client.get('/api/v1/meta/info') + rv = client.get("/api/v1/meta/info") assert rv.status_code == 200 - assert rv.json == {'API': f'bcol_api/{__version__}-{commit_hash}'} + assert rv.json == {"API": f"bcol_api/{__version__}-{commit_hash}"} diff --git a/bcol-api/tests/unit/api/test_ops.py b/bcol-api/tests/unit/api/test_ops.py index a1aa7b28d..c5e46eeef 100755 --- a/bcol-api/tests/unit/api/test_ops.py +++ b/bcol-api/tests/unit/api/test_ops.py @@ -21,15 +21,15 @@ def test_ops_healthz_success(client): """Assert that the service is healthy if it can successfully access the database.""" - rv = client.get('/ops/healthz') + rv = client.get("/ops/healthz") assert rv.status_code == 200 - assert rv.json == {'message': 'api is healthy'} + assert rv.json == {"message": "api is healthy"} def test_ops_readyz(client): """Asserts that the service is ready to serve.""" - rv = client.get('/ops/readyz') + rv = client.get("/ops/readyz") assert rv.status_code == 200 - assert rv.json == {'message': 'api is ready'} + assert rv.json == {"message": "api is ready"} diff --git a/bcol-api/tests/unit/conf/test_configuration.py b/bcol-api/tests/unit/conf/test_configuration.py index 136286bc1..4f2ee661f 100755 --- a/bcol-api/tests/unit/conf/test_configuration.py +++ b/bcol-api/tests/unit/conf/test_configuration.py @@ -23,22 +23,22 @@ # testdata pattern is ({str: environment}, {expected return value}) TEST_ENVIRONMENT_DATA = [ - ('valid', 'development', config.DevConfig), - ('valid', 'testing', config.TestConfig), - ('valid', 'default', config.ProdConfig), - ('valid', 'staging', config.ProdConfig), - ('valid', 'production', config.ProdConfig), - ('error', None, KeyError) + ("valid", "development", config.DevConfig), + ("valid", "testing", config.TestConfig), + ("valid", "default", config.ProdConfig), + ("valid", "staging", config.ProdConfig), + ("valid", "production", config.ProdConfig), + ("error", None, KeyError), ] -@pytest.mark.parametrize('test_type,environment,expected', TEST_ENVIRONMENT_DATA) +@pytest.mark.parametrize("test_type,environment,expected", TEST_ENVIRONMENT_DATA) def test_get_named_config(test_type, environment, expected): """Assert that the named configurations can be loaded. Or that a KeyError is returned for missing config types. """ - if test_type == 'valid': + if test_type == "valid": assert isinstance(config.get_named_config(environment), expected) else: with pytest.raises(KeyError): @@ -51,7 +51,7 @@ def test_prod_config_secret_key(monkeypatch): # pylint: disable=missing-docstri The object either uses the SECRET_KEY from the environment, or creates the SECRET_KEY on the fly. """ - key = 'SECRET_KEY' + key = "SECRET_KEY" # Assert that secret key will default to some value # even if missed in the environment setup @@ -60,6 +60,6 @@ def test_prod_config_secret_key(monkeypatch): # pylint: disable=missing-docstri assert config.ProdConfig().SECRET_KEY is not None # Assert that the secret_key is set to the assigned environment value - monkeypatch.setenv(key, 'SECRET_KEY') + monkeypatch.setenv(key, "SECRET_KEY") reload(config) - assert config.ProdConfig().SECRET_KEY == 'SECRET_KEY' + assert config.ProdConfig().SECRET_KEY == "SECRET_KEY" diff --git a/bcol-api/tests/unit/services/test_bcol_payment.py b/bcol-api/tests/unit/services/test_bcol_payment.py index 07ec69e06..d4e4767c9 100644 --- a/bcol-api/tests/unit/services/test_bcol_payment.py +++ b/bcol-api/tests/unit/services/test_bcol_payment.py @@ -18,6 +18,7 @@ """ import unittest + from bcol_api.services.bcol_payment import BcolPayment @@ -25,16 +26,16 @@ def test_payment(app, payment_mock): """Test payment service.""" test_case = unittest.TestCase() with app.app_context(): - with test_case.assertLogs(level='ERROR') as log: + with test_case.assertLogs(level="ERROR") as log: payment_response = BcolPayment().create_payment({}, False) - assert payment_response.get('userId') == 'PB25020' + assert payment_response.get("userId") == "PB25020" # Non matching fee case. - BcolPayment().create_payment({'serviceFees': 50.00}, False) + BcolPayment().create_payment({"serviceFees": 50.00}, False) has_non_match_error = False for message in log.output: - if "from BCOL doesn\'t match" in message: + if "from BCOL doesn't match" in message: has_non_match_error = True assert has_non_match_error # Success case. - with test_case.assertNoLogs(level='ERROR'): - BcolPayment().create_payment({'serviceFees': 1.50}, False) + with test_case.assertNoLogs(level="ERROR"): + BcolPayment().create_payment({"serviceFees": 1.50}, False) diff --git a/bcol-api/tests/unit/services/test_bcol_profile.py b/bcol-api/tests/unit/services/test_bcol_profile.py index 5ccf4bb14..7b260771f 100644 --- a/bcol-api/tests/unit/services/test_bcol_profile.py +++ b/bcol-api/tests/unit/services/test_bcol_profile.py @@ -23,18 +23,18 @@ def test_query_profile(app, ldap_mock, query_profile_mock): """Test query profile service.""" with app.app_context(): - query_profile_response = BcolProfile().query_profile('TEST', 'TEST') - assert query_profile_response.get('userId') == 'PB25020' - assert query_profile_response.get('address').get('country') == 'CA' + query_profile_response = BcolProfile().query_profile("TEST", "TEST") + assert query_profile_response.get("userId") == "PB25020" + assert query_profile_response.get("address").get("country") == "CA" def test_standardize_country(): """Test standardize country to code.""" - code = BcolProfile().standardize_country('CANADA') - assert code == 'CA' + code = BcolProfile().standardize_country("CANADA") + assert code == "CA" - code = BcolProfile().standardize_country('CA') - assert code == 'CA' + code = BcolProfile().standardize_country("CA") + assert code == "CA" - code = BcolProfile().standardize_country('Test') - assert code == 'Test' + code = BcolProfile().standardize_country("Test") + assert code == "Test" diff --git a/bcol-api/tests/unit/utils/test_logging.py b/bcol-api/tests/unit/utils/test_logging.py index 6283e0dfc..247ff1d1e 100755 --- a/bcol-api/tests/unit/utils/test_logging.py +++ b/bcol-api/tests/unit/utils/test_logging.py @@ -24,12 +24,12 @@ def test_logging_with_file(capsys): """Assert that logging is setup with the configuration file.""" - file_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf') + file_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf") setup_logging(file_path) # important to do this first captured = capsys.readouterr() - assert captured.out.startswith('Configure logging, from conf') + assert captured.out.startswith("Configure logging, from conf") def test_logging_with_missing_file(capsys): @@ -39,4 +39,4 @@ def test_logging_with_missing_file(capsys): captured = capsys.readouterr() - assert captured.err.startswith('Unable to configure logging') + assert captured.err.startswith("Unable to configure logging") diff --git a/bcol-api/tests/unit/utils/test_util_cors.py b/bcol-api/tests/unit/utils/test_util_cors.py index 210ce8bfa..b67b23be9 100755 --- a/bcol-api/tests/unit/utils/test_util_cors.py +++ b/bcol-api/tests/unit/utils/test_util_cors.py @@ -20,26 +20,26 @@ from bcol_api.utils.util import cors_preflight - TEST_CORS_METHODS_DATA = [ - ('GET'), - ('PUT'), - ('POST'), - ('GET,PUT'), - ('GET,POST'), - ('PUT,POST'), - ('GET,PUT,POST'), + ("GET"), + ("PUT"), + ("POST"), + ("GET,PUT"), + ("GET,POST"), + ("PUT,POST"), + ("GET,PUT,POST"), ] -@pytest.mark.parametrize('methods', TEST_CORS_METHODS_DATA) +@pytest.mark.parametrize("methods", TEST_CORS_METHODS_DATA) def test_cors_preflight_post(methods): """Assert that the options methos is added to the class and that the correct access controls are set.""" + @cors_preflight(methods) # pylint: disable=too-few-public-methods - class TestCors(): + class TestCors: pass rv = TestCors().options() # pylint: disable=no-member - assert rv[2]['Access-Control-Allow-Origin'] == '*' - assert rv[2]['Access-Control-Allow-Methods'] == methods + assert rv[2]["Access-Control-Allow-Origin"] == "*" + assert rv[2]["Access-Control-Allow-Methods"] == methods diff --git a/bcol-api/tests/utilities/base_test.py b/bcol-api/tests/utilities/base_test.py index 13f3e59eb..330b624ae 100644 --- a/bcol-api/tests/utilities/base_test.py +++ b/bcol-api/tests/utilities/base_test.py @@ -16,34 +16,27 @@ def get_token_header(): """Get the token header json.""" - return { - 'alg': 'RS256', - 'typ': 'JWT', - 'kid': 'sbc-auth-cron-job' - } + return {"alg": "RS256", "typ": "JWT", "kid": "sbc-auth-cron-job"} -def get_claims(app_request=None, role: str = 'account_holder', username: str = 'CP0001234', - login_source: str = 'PASSCODE'): +def get_claims( + app_request=None, + role: str = "account_holder", + username: str = "CP0001234", + login_source: str = "PASSCODE", +): """Return the claim with the role param.""" claim = { - 'jti': 'a50fafa4-c4d6-4a9b-9e51-1e5e0d102878', - 'exp': 31531718745, - 'iat': 1531718745, - 'iss': app_request.config[ - 'JWT_OIDC_ISSUER'] if app_request else 'http://localhost:8081/auth/realms/demo', - 'aud': 'sbc-auth-web', - 'sub': '15099883-3c3f-4b4c-a124-a1824d6cba84', - 'typ': 'Bearer', - 'realm_access': - { - 'roles': - [ - '{}'.format(role) - ] - }, - 'preferred_username': username, - 'username': username, - 'loginSource': login_source + "jti": "a50fafa4-c4d6-4a9b-9e51-1e5e0d102878", + "exp": 31531718745, + "iat": 1531718745, + "iss": (app_request.config["JWT_OIDC_ISSUER"] if app_request else "http://localhost:8081/auth/realms/demo"), + "aud": "sbc-auth-web", + "sub": "15099883-3c3f-4b4c-a124-a1824d6cba84", + "typ": "Bearer", + "realm_access": {"roles": ["{}".format(role)]}, + "preferred_username": username, + "username": username, + "loginSource": login_source, } return claim diff --git a/bcol-api/tests/utilities/decorators.py b/bcol-api/tests/utilities/decorators.py index 88deec93f..4cc940905 100644 --- a/bcol-api/tests/utilities/decorators.py +++ b/bcol-api/tests/utilities/decorators.py @@ -17,8 +17,7 @@ import pytest from dotenv import find_dotenv, load_dotenv - # this will load all the envars from a .env file located in the project root (api) load_dotenv(find_dotenv()) -skip_in_pod = pytest.mark.skipif(os.getenv('POD_TESTING', False), reason='Skip test when running in pod') +skip_in_pod = pytest.mark.skipif(os.getenv("POD_TESTING", False), reason="Skip test when running in pod") diff --git a/bcol-api/tests/utilities/ldap_mock.py b/bcol-api/tests/utilities/ldap_mock.py index 64dd97dd6..100581d3e 100644 --- a/bcol-api/tests/utilities/ldap_mock.py +++ b/bcol-api/tests/utilities/ldap_mock.py @@ -27,19 +27,15 @@ def __init__(self): def set_option(self, option, invalue): """Set option value.""" - def initialize( - self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None - ): + def initialize(self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None): """Initialize ldap.""" - def simple_bind_s(self, who='', cred=''): + def simple_bind_s(self, who="", cred=""): """Bind.""" def unbind_s(self): """Unbind.""" - def search_s( - self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0 - ): + def search_s(self, base, scope, filterstr="(objectClass=*)", attrlist=None, attrsonly=0): """Search.""" - return 'TEST_USER' + return "TEST_USER" diff --git a/bcol-api/tests/utilities/schema_assertions.py b/bcol-api/tests/utilities/schema_assertions.py index 24d3d1f8e..9eb11b7d0 100755 --- a/bcol-api/tests/utilities/schema_assertions.py +++ b/bcol-api/tests/utilities/schema_assertions.py @@ -30,7 +30,7 @@ def assert_valid_schema(data: dict, schema_file: dict): def _load_json_schema(filename: str): """Return the given schema file identified by filename.""" - relative_path = join('schemas', filename) + relative_path = join("schemas", filename) absolute_path = join(dirname(__file__), relative_path) with open(absolute_path) as schema_file: diff --git a/bcol-api/wsgi.py b/bcol-api/wsgi.py index 3b4baca98..86a4b005d 100755 --- a/bcol-api/wsgi.py +++ b/bcol-api/wsgi.py @@ -15,7 +15,6 @@ """ from bcol_api import create_app - # Openshift s2i expects a lower case name of application application = create_app() # pylint: disable=invalid-name diff --git a/jobs/payment-jobs/setup.cfg b/jobs/payment-jobs/setup.cfg deleted file mode 100755 index 33f93bc87..000000000 --- a/jobs/payment-jobs/setup.cfg +++ /dev/null @@ -1,98 +0,0 @@ -[metadata] -name = payment_jobs -url = https://github.com/bcgov/sbc-pay/ -author = Pay Team -author_email = -classifiers = - Development Status :: Beta - Intended Audience :: Developers / QA - Topic :: Payment - License :: OSI Approved :: Apache Software License - Natural Language :: English - Programming Language :: Python :: 3.7 -license = Apache Software License Version 2.0 -description = A short description of the project -long_description = file: README.md -keywords = - -[options] -zip_safe = True -python_requires = >=3.12 -include_package_data = True -packages = find: - -[options.package_data] -pay_api = - -[wheel] -universal = 1 - -[bdist_wheel] -universal = 1 - -[aliases] -test = pytest - -[flake8] -ignore = I001, I003, I004, E126, W504 -exclude = .git,*migrations* -max-line-length = 120 -docstring-min-length=10 -per-file-ignores = - */__init__.py:F401 - -[pycodestyle] -max_line_length = 120 -ignore = E501 -docstring-min-length=10 -notes=FIXME,XXX -match_dir = tasks -ignored-modules=flask_sqlalchemy - sqlalchemy -per-file-ignores = - */__init__.py:F401 -good-names= - b, - d, - i, - e, - f, - u, - rv, - logger, - id, - p, - rs, - -[pylint] -ignore=migrations,test -notes=FIXME,XXX,TODO -ignored-modules=flask_sqlalchemy,sqlalchemy,SQLAlchemy,alembic,scoped_session -ignored-classes=scoped_session -min-similarity-lines=8 -disable=C0301,W0511 -good-names= - b, - d, - i, - e, - f, - u, - rv, - logger, - id, - p, - rs, - -[isort] -line_length = 120 -indent = 4 -multi_line_output = 4 -lines_after_imports = 2 - - -[tool:pytest] -addopts = --cov=tasks --cov-report html:htmlcov --cov-report xml:coverage.xml -testpaths = tests/jobs -filterwarnings = - ignore::UserWarning From 7ca864ff127531cc7c9d7448cff8e8b68adcaf16 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:14:45 -0700 Subject: [PATCH 03/10] pay-jobs --- jobs/payment-jobs/Makefile | 18 +- jobs/payment-jobs/config.py | 273 ++++---- jobs/payment-jobs/invoke_jobs.py | 133 ++-- jobs/payment-jobs/poetry.lock | 84 ++- jobs/payment-jobs/pyproject.toml | 124 ++++ jobs/payment-jobs/services/email_service.py | 35 +- jobs/payment-jobs/services/oracle.py | 39 +- jobs/payment-jobs/services/routing_slip.py | 29 +- jobs/payment-jobs/setup.py | 5 +- .../tasks/activate_pad_account_task.py | 16 +- jobs/payment-jobs/tasks/ap_task.py | 197 +++--- .../tasks/bcol_refund_confirmation_task.py | 41 +- .../tasks/cfs_bank_name_updater.py | 139 +++-- .../tasks/cfs_create_account_task.py | 146 +++-- .../tasks/cfs_create_invoice_task.py | 429 +++++++------ jobs/payment-jobs/tasks/common/cgi_ap.py | 144 +++-- jobs/payment-jobs/tasks/common/cgi_ejv.py | 99 +-- jobs/payment-jobs/tasks/common/dataclasses.py | 31 +- jobs/payment-jobs/tasks/common/enums.py | 10 +- .../tasks/direct_pay_automated_refund_task.py | 86 ++- jobs/payment-jobs/tasks/distribution_task.py | 94 +-- .../eft_overpayment_notification_task.py | 90 +-- .../tasks/eft_statement_due_task.py | 202 +++--- jobs/payment-jobs/tasks/eft_task.py | 369 +++++++---- .../tasks/ejv_partner_distribution_task.py | 293 +++++---- jobs/payment-jobs/tasks/ejv_payment_task.py | 183 ++++-- jobs/payment-jobs/tasks/routing_slip_task.py | 317 ++++++---- jobs/payment-jobs/tasks/stale_payment_task.py | 65 +- .../tasks/statement_notification_task.py | 73 +-- jobs/payment-jobs/tasks/statement_task.py | 203 +++--- .../tasks/unpaid_invoice_notify_task.py | 77 ++- jobs/payment-jobs/tests/jobs/__init__.py | 3 +- jobs/payment-jobs/tests/jobs/conftest.py | 99 +-- jobs/payment-jobs/tests/jobs/factory.py | 583 +++++++++++------- jobs/payment-jobs/tests/jobs/mocks.py | 77 ++- .../jobs/test_activate_pad_account_task.py | 32 +- jobs/payment-jobs/tests/jobs/test_ap_task.py | 87 +-- .../test_bcol_refund_confirmation_task.py | 107 +++- .../jobs/test_cfs_create_account_task.py | 44 +- .../jobs/test_cfs_create_invoice_task.py | 305 ++++++--- ...st_cfs_create_routing_slip_account_task.py | 1 + .../test_direct_pay_automated_refund_task.py | 115 ++-- .../tests/jobs/test_distribution_task.py | 103 +++- .../test_eft_overpayment_notification_task.py | 123 ++-- .../tests/jobs/test_eft_statement_due_task.py | 179 +++--- jobs/payment-jobs/tests/jobs/test_eft_task.py | 565 +++++++++++------ .../test_ejv_partner_distribution_task.py | 108 ++-- ...artner_partial_refund_distribution_task.py | 70 ++- .../tests/jobs/test_ejv_payment_task.py | 78 ++- .../tests/jobs/test_routing_slip_task.py | 152 +++-- .../jobs/test_statement_notification_task.py | 175 +++--- .../tests/jobs/test_statements_task.py | 341 +++++----- .../jobs/test_unpaid_invoice_notifytask.py | 152 +++-- .../payment-jobs/tests/services/test_flags.py | 63 +- jobs/payment-jobs/utils/auth.py | 29 +- jobs/payment-jobs/utils/auth_event.py | 51 +- jobs/payment-jobs/utils/enums.py | 6 +- jobs/payment-jobs/utils/logger.py | 4 +- jobs/payment-jobs/utils/mailer.py | 107 ++-- jobs/payment-jobs/utils/minio.py | 23 +- jobs/payment-jobs/utils/sftp.py | 28 +- 61 files changed, 4815 insertions(+), 3039 deletions(-) diff --git a/jobs/payment-jobs/Makefile b/jobs/payment-jobs/Makefile index 6946c4b6f..50a03a17e 100644 --- a/jobs/payment-jobs/Makefile +++ b/jobs/payment-jobs/Makefile @@ -44,15 +44,27 @@ install: clean ################################################################################# # COMMANDS - CI # ################################################################################# -ci: lint flake8 test ## CI flow +ci: isort-ci black-ci lint flake8 test ## CI flow + +isort: + poetry run isort . + +isort-ci: + poetry run isort --check . + +black: ## Linting with black + poetry run black . + +black-ci: + poetry run black --check . pylint: ## Linting with pylint - poetry run pylint --rcfile=setup.cfg tasks + poetry run pylint tasks flake8: ## Linting with flake8 poetry run flake8 tasks tests -lint: pylint flake8 ## run all lint type scripts +lint: isort black pylint flake8 ## run all lint type scripts test: ## Unit testing poetry run pytest diff --git a/jobs/payment-jobs/config.py b/jobs/payment-jobs/config.py index ca590ddef..88a0b0850 100644 --- a/jobs/payment-jobs/config.py +++ b/jobs/payment-jobs/config.py @@ -23,23 +23,23 @@ load_dotenv(find_dotenv()) CONFIGURATION = { - 'development': 'config.DevConfig', - 'testing': 'config.TestConfig', - 'production': 'config.ProdConfig', - 'default': 'config.ProdConfig' + "development": "config.DevConfig", + "testing": "config.TestConfig", + "production": "config.ProdConfig", + "default": "config.ProdConfig", } -def get_named_config(config_name: str = 'production'): +def get_named_config(config_name: str = "production"): """Return the configuration object based on the name :raise: KeyError: if an unknown configuration is requested """ - if config_name in ['production', 'staging', 'default']: + if config_name in ["production", "staging", "default"]: config = ProdConfig() - elif config_name == 'testing': + elif config_name == "testing": config = TestConfig() - elif config_name == 'development': + elif config_name == "development": config = DevConfig() else: raise KeyError(f"Unknown configuration '{config_name}'") @@ -47,157 +47,157 @@ def get_named_config(config_name: str = 'production'): class _Config(object): # pylint: disable=too-few-public-methods - """Base class configuration that should set reasonable defaults for all the other configurations. """ + """Base class configuration that should set reasonable defaults for all the other configurations.""" + PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - SECRET_KEY = 'a secret' + SECRET_KEY = "a secret" SQLALCHEMY_TRACK_MODIFICATIONS = False - ALEMBIC_INI = 'migrations/alembic.ini' + ALEMBIC_INI = "migrations/alembic.ini" - PAY_LD_SDK_KEY = os.getenv('PAY_LD_SDK_KEY', None) + PAY_LD_SDK_KEY = os.getenv("PAY_LD_SDK_KEY", None) # POSTGRESQL - DB_USER = os.getenv('DATABASE_USERNAME', '') - DB_PASSWORD = os.getenv('DATABASE_PASSWORD', '') - DB_NAME = os.getenv('DATABASE_NAME', '') - DB_HOST = os.getenv('DATABASE_HOST', '') - DB_PORT = os.getenv('DATABASE_PORT', '5432') - if DB_UNIX_SOCKET := os.getenv('DATABASE_UNIX_SOCKET', None): + DB_USER = os.getenv("DATABASE_USERNAME", "") + DB_PASSWORD = os.getenv("DATABASE_PASSWORD", "") + DB_NAME = os.getenv("DATABASE_NAME", "") + DB_HOST = os.getenv("DATABASE_HOST", "") + DB_PORT = os.getenv("DATABASE_PORT", "5432") + if DB_UNIX_SOCKET := os.getenv("DATABASE_UNIX_SOCKET", None): SQLALCHEMY_DATABASE_URI = ( - f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?unix_sock={DB_UNIX_SOCKET}/.s.PGSQL.5432' + f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?unix_sock={DB_UNIX_SOCKET}/.s.PGSQL.5432" ) else: - SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}" SQLALCHEMY_ECHO = False - ORACLE_USER = os.getenv('ORACLE_USER', '') - ORACLE_PASSWORD = os.getenv('ORACLE_PASSWORD', '') - ORACLE_DB_NAME = os.getenv('ORACLE_DB_NAME', '') - ORACLE_HOST = os.getenv('ORACLE_HOST', '') - ORACLE_PORT = int(os.getenv('ORACLE_PORT', '1521')) + ORACLE_USER = os.getenv("ORACLE_USER", "") + ORACLE_PASSWORD = os.getenv("ORACLE_PASSWORD", "") + ORACLE_DB_NAME = os.getenv("ORACLE_DB_NAME", "") + ORACLE_HOST = os.getenv("ORACLE_HOST", "") + ORACLE_PORT = int(os.getenv("ORACLE_PORT", "1521")) # PAYBC Direct Pay Settings - PAYBC_DIRECT_PAY_REF_NUMBER = os.getenv('PAYBC_DIRECT_PAY_REF_NUMBER') - PAYBC_DIRECT_PAY_API_KEY = os.getenv('PAYBC_DIRECT_PAY_API_KEY') - PAYBC_DIRECT_PAY_BASE_URL = os.getenv('PAYBC_DIRECT_PAY_BASE_URL') - PAYBC_DIRECT_PAY_CLIENT_ID = os.getenv('PAYBC_DIRECT_PAY_CLIENT_ID') - PAYBC_DIRECT_PAY_CLIENT_SECRET = os.getenv('PAYBC_DIRECT_PAY_CLIENT_SECRET') + PAYBC_DIRECT_PAY_REF_NUMBER = os.getenv("PAYBC_DIRECT_PAY_REF_NUMBER") + PAYBC_DIRECT_PAY_API_KEY = os.getenv("PAYBC_DIRECT_PAY_API_KEY") + PAYBC_DIRECT_PAY_BASE_URL = os.getenv("PAYBC_DIRECT_PAY_BASE_URL") + PAYBC_DIRECT_PAY_CLIENT_ID = os.getenv("PAYBC_DIRECT_PAY_CLIENT_ID") + PAYBC_DIRECT_PAY_CLIENT_SECRET = os.getenv("PAYBC_DIRECT_PAY_CLIENT_SECRET") # CFS API Settings - CFS_BASE_URL = os.getenv('CFS_BASE_URL') - CFS_CLIENT_ID = os.getenv('CFS_CLIENT_ID') - CFS_CLIENT_SECRET = os.getenv('CFS_CLIENT_SECRET') - CONNECT_TIMEOUT = int(os.getenv('CONNECT_TIMEOUT', 10)) - GENERATE_RANDOM_INVOICE_NUMBER = os.getenv('CFS_GENERATE_RANDOM_INVOICE_NUMBER', 'False') + CFS_BASE_URL = os.getenv("CFS_BASE_URL") + CFS_CLIENT_ID = os.getenv("CFS_CLIENT_ID") + CFS_CLIENT_SECRET = os.getenv("CFS_CLIENT_SECRET") + CONNECT_TIMEOUT = int(os.getenv("CONNECT_TIMEOUT", 10)) + GENERATE_RANDOM_INVOICE_NUMBER = os.getenv("CFS_GENERATE_RANDOM_INVOICE_NUMBER", "False") # legislative timezone for future effective dating - LEGISLATIVE_TIMEZONE = os.getenv('LEGISLATIVE_TIMEZONE', 'America/Vancouver') + LEGISLATIVE_TIMEZONE = os.getenv("LEGISLATIVE_TIMEZONE", "America/Vancouver") # API Endpoints - AUTH_API_URL = os.getenv('AUTH_API_URL', '') - AUTH_API_VERSION = os.getenv('AUTH_API_VERSION', '') - NOTIFY_API_URL = os.getenv('NOTIFY_API_URL', '') - NOTIFY_API_VERSION = os.getenv('NOTIFY_API_VERSION', '') + AUTH_API_URL = os.getenv("AUTH_API_URL", "") + AUTH_API_VERSION = os.getenv("AUTH_API_VERSION", "") + NOTIFY_API_URL = os.getenv("NOTIFY_API_URL", "") + NOTIFY_API_VERSION = os.getenv("NOTIFY_API_VERSION", "") - AUTH_API_ENDPOINT = f'{AUTH_API_URL + AUTH_API_VERSION}/' - NOTIFY_API_ENDPOINT = f'{NOTIFY_API_URL + NOTIFY_API_VERSION}/' + AUTH_API_ENDPOINT = f"{AUTH_API_URL + AUTH_API_VERSION}/" + NOTIFY_API_ENDPOINT = f"{NOTIFY_API_URL + NOTIFY_API_VERSION}/" # Service account details - KEYCLOAK_SERVICE_ACCOUNT_ID = os.getenv('SBC_AUTH_ADMIN_CLIENT_ID') - KEYCLOAK_SERVICE_ACCOUNT_SECRET = os.getenv('SBC_AUTH_ADMIN_CLIENT_SECRET') + KEYCLOAK_SERVICE_ACCOUNT_ID = os.getenv("SBC_AUTH_ADMIN_CLIENT_ID") + KEYCLOAK_SERVICE_ACCOUNT_SECRET = os.getenv("SBC_AUTH_ADMIN_CLIENT_SECRET") # JWT_OIDC Settings - JWT_OIDC_ISSUER = os.getenv('JWT_OIDC_ISSUER') + JWT_OIDC_ISSUER = os.getenv("JWT_OIDC_ISSUER") # Front end url - AUTH_WEB_URL = os.getenv('AUTH_WEB_PAY_TRANSACTION_URL', '') - AUTH_WEB_STATEMENT_URL = os.getenv('AUTH_WEB_STATEMENT_URL', 'account/orgId/settings/statements') - REGISTRIES_LOGO_IMAGE_NAME = os.getenv('REGISTRIES_LOGO_IMAGE_NAME', 'bc_logo_for_email.png') + AUTH_WEB_URL = os.getenv("AUTH_WEB_PAY_TRANSACTION_URL", "") + AUTH_WEB_STATEMENT_URL = os.getenv("AUTH_WEB_STATEMENT_URL", "account/orgId/settings/statements") + REGISTRIES_LOGO_IMAGE_NAME = os.getenv("REGISTRIES_LOGO_IMAGE_NAME", "bc_logo_for_email.png") # GCP PubSub - GCP_AUTH_KEY = os.getenv('AUTHPAY_GCP_AUTH_KEY', None) - ACCOUNT_MAILER_TOPIC = os.getenv('ACCOUNT_MAILER_TOPIC', None) - AUTH_EVENT_TOPIC = os.getenv('AUTH_EVENT_TOPIC', None) + GCP_AUTH_KEY = os.getenv("AUTHPAY_GCP_AUTH_KEY", None) + ACCOUNT_MAILER_TOPIC = os.getenv("ACCOUNT_MAILER_TOPIC", None) + AUTH_EVENT_TOPIC = os.getenv("AUTH_EVENT_TOPIC", None) - CFS_ACCOUNT_DESCRIPTION = os.getenv('CFS_ACCOUNT_DESCRIPTION', 'BCR') - CFS_INVOICE_PREFIX = os.getenv('CFS_INVOICE_PREFIX', 'REG') - CFS_STOP_PAD_ACCOUNT_CREATION = os.getenv('CFS_STOP_PAD_ACCOUNT_CREATION', 'false').lower() == 'true' - CFS_PARTY_PREFIX = os.getenv('CFS_PARTY_PREFIX', 'BCR-') + CFS_ACCOUNT_DESCRIPTION = os.getenv("CFS_ACCOUNT_DESCRIPTION", "BCR") + CFS_INVOICE_PREFIX = os.getenv("CFS_INVOICE_PREFIX", "REG") + CFS_STOP_PAD_ACCOUNT_CREATION = os.getenv("CFS_STOP_PAD_ACCOUNT_CREATION", "false").lower() == "true" + CFS_PARTY_PREFIX = os.getenv("CFS_PARTY_PREFIX", "BCR-") - CFS_INVOICE_CUT_OFF_HOURS_UTC = int(os.getenv('CFS_INVOICE_CUT_OFF_HOURS_UTC', '2')) - CFS_INVOICE_CUT_OFF_MINUTES_UTC = int(os.getenv('CFS_INVOICE_CUT_OFF_MINUTES_UTC', '0')) + CFS_INVOICE_CUT_OFF_HOURS_UTC = int(os.getenv("CFS_INVOICE_CUT_OFF_HOURS_UTC", "2")) + CFS_INVOICE_CUT_OFF_MINUTES_UTC = int(os.getenv("CFS_INVOICE_CUT_OFF_MINUTES_UTC", "0")) - SENTRY_ENABLE = os.getenv('SENTRY_ENABLE', 'False') - SENTRY_DSN = os.getenv('SENTRY_DSN', None) + SENTRY_ENABLE = os.getenv("SENTRY_ENABLE", "False") + SENTRY_DSN = os.getenv("SENTRY_DSN", None) # The number of characters which can be exposed to admins for a bank account number - MASK_LEN = int(os.getenv('MASK_LEN', 3)) + MASK_LEN = int(os.getenv("MASK_LEN", 3)) TESTING = False DEBUG = True - PAD_CONFIRMATION_PERIOD_IN_DAYS = int(os.getenv('PAD_CONFIRMATION_PERIOD_IN_DAYS', '3')) + PAD_CONFIRMATION_PERIOD_IN_DAYS = int(os.getenv("PAD_CONFIRMATION_PERIOD_IN_DAYS", "3")) # Secret key for encrypting bank account - ACCOUNT_SECRET_KEY = os.getenv('ACCOUNT_SECRET_KEY') + ACCOUNT_SECRET_KEY = os.getenv("ACCOUNT_SECRET_KEY") # EJV config variables - CGI_FEEDER_NUMBER = os.getenv('CGI_FEEDER_NUMBER') - CGI_MINISTRY_PREFIX = os.getenv('CGI_MINISTRY_PREFIX') - CGI_DISBURSEMENT_DESC = os.getenv('CGI_DISBURSEMENT_DESC', 'BCREGISTRIES {} {} DISBURSEMENTS') - CGI_MESSAGE_VERSION = os.getenv('CGI_MESSAGE_VERSION', '4010') - CGI_BCREG_CLIENT_CODE = os.getenv('CGI_BCREG_CLIENT_CODE', '112') - CGI_EJV_SUPPLIER_NUMBER = os.getenv('CGI_EJV_SUPPLIER_NUMBER', '') + CGI_FEEDER_NUMBER = os.getenv("CGI_FEEDER_NUMBER") + CGI_MINISTRY_PREFIX = os.getenv("CGI_MINISTRY_PREFIX") + CGI_DISBURSEMENT_DESC = os.getenv("CGI_DISBURSEMENT_DESC", "BCREGISTRIES {} {} DISBURSEMENTS") + CGI_MESSAGE_VERSION = os.getenv("CGI_MESSAGE_VERSION", "4010") + CGI_BCREG_CLIENT_CODE = os.getenv("CGI_BCREG_CLIENT_CODE", "112") + CGI_EJV_SUPPLIER_NUMBER = os.getenv("CGI_EJV_SUPPLIER_NUMBER", "") # Minio configuration values - MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT') - MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY') - MINIO_ACCESS_SECRET = os.getenv('MINIO_ACCESS_SECRET') - MINIO_BUCKET_NAME = os.getenv('MINIO_EJV_BUCKET_NAME', 'cgi-ejv') + MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT") + MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") + MINIO_ACCESS_SECRET = os.getenv("MINIO_ACCESS_SECRET") + MINIO_BUCKET_NAME = os.getenv("MINIO_EJV_BUCKET_NAME", "cgi-ejv") MINIO_SECURE = True # the day on which mail to get.put 1 to get mail next day of creation.put 2 to get mails day after tomorrow. - NOTIFY_AFTER_DAYS = int(os.getenv('NOTIFY_AFTER_DAYS', 8)) # to get full 7 days tp pass, u need to put 8. + NOTIFY_AFTER_DAYS = int(os.getenv("NOTIFY_AFTER_DAYS", 8)) # to get full 7 days tp pass, u need to put 8. # CGI FTP Configuration - CGI_SFTP_HOST = os.getenv('CAS_SFTP_HOST', 'localhost') - CGI_SFTP_USERNAME = os.getenv('CGI_SFTP_USER_NAME') - CGI_SFTP_PASSWORD = os.getenv('CGI_SFTP_PASSWORD') - CGI_SFTP_VERIFY_HOST = os.getenv('SFTP_VERIFY_HOST', 'True') - CGI_SFTP_HOST_KEY = os.getenv('CAS_SFTP_HOST_KEY', '') - CGI_SFTP_PORT = int(os.getenv('CAS_SFTP_PORT', 22)) - BCREG_CGI_FTP_PRIVATE_KEY_LOCATION = os.getenv('BCREG_CGI_FTP_PRIVATE_KEY_LOCATION', - '/payment-jobs/key/cgi_sftp_priv_key') - BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv('BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE') - CGI_SFTP_DIRECTORY = os.getenv('CGI_SFTP_DIRECTORY', '/data') + CGI_SFTP_HOST = os.getenv("CAS_SFTP_HOST", "localhost") + CGI_SFTP_USERNAME = os.getenv("CGI_SFTP_USER_NAME") + CGI_SFTP_PASSWORD = os.getenv("CGI_SFTP_PASSWORD") + CGI_SFTP_VERIFY_HOST = os.getenv("SFTP_VERIFY_HOST", "True") + CGI_SFTP_HOST_KEY = os.getenv("CAS_SFTP_HOST_KEY", "") + CGI_SFTP_PORT = int(os.getenv("CAS_SFTP_PORT", 22)) + BCREG_CGI_FTP_PRIVATE_KEY_LOCATION = os.getenv( + "BCREG_CGI_FTP_PRIVATE_KEY_LOCATION", "/payment-jobs/key/cgi_sftp_priv_key" + ) + BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE = os.getenv("BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE") + CGI_SFTP_DIRECTORY = os.getenv("CGI_SFTP_DIRECTORY", "/data") # CGI File specific configs - CGI_TRIGGER_FILE_SUFFIX = os.getenv('CGI_TRIGGER_FILE_SUFFIX', 'TRG') + CGI_TRIGGER_FILE_SUFFIX = os.getenv("CGI_TRIGGER_FILE_SUFFIX", "TRG") # disbursement delay - DISBURSEMENT_DELAY_IN_DAYS = int(os.getenv('DISBURSEMENT_DELAY', 5)) + DISBURSEMENT_DELAY_IN_DAYS = int(os.getenv("DISBURSEMENT_DELAY", 5)) # CP Job variables - CGI_AP_DISTRIBUTION = os.getenv('CGI_AP_DISTRIBUTION', '') - CGI_AP_SUPPLIER_NUMBER = os.getenv('CGI_AP_SUPPLIER_NUMBER', '') - CGI_AP_SUPPLIER_LOCATION = os.getenv('CGI_AP_SUPPLIER_LOCATION', '') - CGI_AP_REMITTANCE_CODE = os.getenv('CGI_AP_REMITTANCE_CODE', '78') - BCA_SUPPLIER_NUMBER = os.getenv('BCA_SUPPLIER_NUMBER', '') - BCA_SUPPLIER_LOCATION = os.getenv('BCA_SUPPLIER_LOCATION', '') - EFT_AP_DISTRIBUTION = os.getenv('EFT_AP_DISTRIBUTION', '') - EFT_AP_SUPPLIER_LOCATION = os.getenv('EFT_AP_SUPPLIER_LOCATION', '') + CGI_AP_DISTRIBUTION = os.getenv("CGI_AP_DISTRIBUTION", "") + CGI_AP_SUPPLIER_NUMBER = os.getenv("CGI_AP_SUPPLIER_NUMBER", "") + CGI_AP_SUPPLIER_LOCATION = os.getenv("CGI_AP_SUPPLIER_LOCATION", "") + CGI_AP_REMITTANCE_CODE = os.getenv("CGI_AP_REMITTANCE_CODE", "78") + BCA_SUPPLIER_NUMBER = os.getenv("BCA_SUPPLIER_NUMBER", "") + BCA_SUPPLIER_LOCATION = os.getenv("BCA_SUPPLIER_LOCATION", "") + EFT_AP_DISTRIBUTION = os.getenv("EFT_AP_DISTRIBUTION", "") + EFT_AP_SUPPLIER_LOCATION = os.getenv("EFT_AP_SUPPLIER_LOCATION", "") # FAS Client and secret - CFS_FAS_CLIENT_ID = os.getenv('CFS_FAS_CLIENT_ID', '') - CFS_FAS_CLIENT_SECRET = os.getenv('CFS_FAS_CLIENT_SECRET', '') + CFS_FAS_CLIENT_ID = os.getenv("CFS_FAS_CLIENT_ID", "") + CFS_FAS_CLIENT_SECRET = os.getenv("CFS_FAS_CLIENT_SECRET", "") # EFT variables - EFT_HOLDING_GL = os.getenv('EFT_HOLDING_GL', '') - EFT_TRANSFER_DESC = os.getenv('EFT_TRANSFER_DESC', 'BCREGISTRIES {} {} EFT TRANSFER') - EFT_OVERDUE_NOTIFY_EMAILS = os.getenv('EFT_OVERDUE_NOTIFY_EMAILS', '') - - + EFT_HOLDING_GL = os.getenv("EFT_HOLDING_GL", "") + EFT_TRANSFER_DESC = os.getenv("EFT_TRANSFER_DESC", "BCREGISTRIES {} {} EFT TRANSFER") + EFT_OVERDUE_NOTIFY_EMAILS = os.getenv("EFT_OVERDUE_NOTIFY_EMAILS", "") class DevConfig(_Config): # pylint: disable=too-few-public-methods @@ -207,58 +207,61 @@ class DevConfig(_Config): # pylint: disable=too-few-public-methods class TestConfig(_Config): # pylint: disable=too-few-public-methods """In support of testing only used by the py.test suite.""" + DEBUG = True TESTING = True # POSTGRESQL - DB_USER = os.getenv('DATABASE_TEST_USERNAME', '') - DB_PASSWORD = os.getenv('DATABASE_TEST_PASSWORD', '') - DB_NAME = os.getenv('DATABASE_TEST_NAME', '') - DB_HOST = os.getenv('DATABASE_TEST_HOST', '') - DB_PORT = os.getenv('DATABASE_TEST_PORT', '5432') - SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_TEST_URL', - 'postgresql+pg8000://{user}:{password}@{host}:{port}/{name}'.format( - user=DB_USER, - password=DB_PASSWORD, - host=DB_HOST, - port=int(DB_PORT), - name=DB_NAME, - )) - - SERVER_NAME = 'localhost:5001' - - AUTH_API_ENDPOINT = 'http://localhost:8080/auth-api/' - - CFS_BASE_URL = 'http://localhost:8080/paybc-api' - CFS_CLIENT_ID = 'TEST' - CFS_CLIENT_SECRET = 'TEST' - USE_DOCKER_MOCK = os.getenv('USE_DOCKER_MOCK', None) - - PAYBC_DIRECT_PAY_CLIENT_ID = 'abc' - PAYBC_DIRECT_PAY_CLIENT_SECRET = '123' - PAYBC_DIRECT_PAY_BASE_URL = 'http://localhost:8080/paybc-api' - PAYBC_DIRECT_PAY_REF_NUMBER = '123' + DB_USER = os.getenv("DATABASE_TEST_USERNAME", "") + DB_PASSWORD = os.getenv("DATABASE_TEST_PASSWORD", "") + DB_NAME = os.getenv("DATABASE_TEST_NAME", "") + DB_HOST = os.getenv("DATABASE_TEST_HOST", "") + DB_PORT = os.getenv("DATABASE_TEST_PORT", "5432") + SQLALCHEMY_DATABASE_URI = os.getenv( + "DATABASE_TEST_URL", + "postgresql+pg8000://{user}:{password}@{host}:{port}/{name}".format( + user=DB_USER, + password=DB_PASSWORD, + host=DB_HOST, + port=int(DB_PORT), + name=DB_NAME, + ), + ) + + SERVER_NAME = "localhost:5001" + + AUTH_API_ENDPOINT = "http://localhost:8080/auth-api/" + + CFS_BASE_URL = "http://localhost:8080/paybc-api" + CFS_CLIENT_ID = "TEST" + CFS_CLIENT_SECRET = "TEST" + USE_DOCKER_MOCK = os.getenv("USE_DOCKER_MOCK", None) + + PAYBC_DIRECT_PAY_CLIENT_ID = "abc" + PAYBC_DIRECT_PAY_CLIENT_SECRET = "123" + PAYBC_DIRECT_PAY_BASE_URL = "http://localhost:8080/paybc-api" + PAYBC_DIRECT_PAY_REF_NUMBER = "123" # Secret key for encrypting bank account - ACCOUNT_SECRET_KEY = os.getenv('ACCOUNT_SECRET_KEY', '1234') + ACCOUNT_SECRET_KEY = os.getenv("ACCOUNT_SECRET_KEY", "1234") # Setting values from the sftp docker container - CGI_SFTP_VERIFY_HOST = 'false' - CGI_SFTP_USERNAME = 'ftp_user' - CGI_SFTP_PASSWORD = 'ftp_pass' + CGI_SFTP_VERIFY_HOST = "false" + CGI_SFTP_USERNAME = "ftp_user" + CGI_SFTP_PASSWORD = "ftp_pass" CGI_SFTP_PORT = 2222 - CGI_SFTP_DIRECTORY = '/data/' - CGI_SFTP_HOST = 'localhost' + CGI_SFTP_DIRECTORY = "/data/" + CGI_SFTP_HOST = "localhost" GCP_AUTH_KEY = None class ProdConfig(_Config): # pylint: disable=too-few-public-methods """Production environment configuration.""" - SECRET_KEY = os.getenv('SECRET_KEY', None) + SECRET_KEY = os.getenv("SECRET_KEY", None) if not SECRET_KEY: SECRET_KEY = os.urandom(24) - print('WARNING: SECRET_KEY being set as a one-shot', file=sys.stderr) + print("WARNING: SECRET_KEY being set as a one-shot", file=sys.stderr) TESTING = False DEBUG = False diff --git a/jobs/payment-jobs/invoke_jobs.py b/jobs/payment-jobs/invoke_jobs.py index 15ed250e8..67c7c927c 100755 --- a/jobs/payment-jobs/invoke_jobs.py +++ b/jobs/payment-jobs/invoke_jobs.py @@ -20,23 +20,26 @@ import sentry_sdk from flask import Flask +from pay_api.services import Flags +from pay_api.services.gcp_queue import queue from sentry_sdk.integrations.flask import FlaskIntegration import config from services import oracle_db -from tasks.routing_slip_task import RoutingSlipTask from tasks.eft_overpayment_notification_task import EFTOverpaymentNotificationTask -from tasks.eft_task import EFTTask from tasks.eft_statement_due_task import EFTStatementDueTask +from tasks.eft_task import EFTTask +from tasks.routing_slip_task import RoutingSlipTask from utils.logger import setup_logging -from pay_api.services import Flags -from pay_api.services.gcp_queue import queue - -setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first +setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -def create_app(run_mode=os.getenv('DEPLOYMENT_ENV', 'production'), job_name='unknown', init_oracle=False): +def create_app( + run_mode=os.getenv("DEPLOYMENT_ENV", "production"), + job_name="unknown", + init_oracle=False, +): """Return a configured Flask App using the Factory method.""" from pay_api.models import db, ma @@ -45,14 +48,14 @@ def create_app(run_mode=os.getenv('DEPLOYMENT_ENV', 'production'), job_name='unk app.config.from_object(config.CONFIGURATION[run_mode]) # Configure Sentry - if str(app.config.get('SENTRY_ENABLE')).lower() == 'true': - if app.config.get('SENTRY_DSN', None): + if str(app.config.get("SENTRY_ENABLE")).lower() == "true": + if app.config.get("SENTRY_DSN", None): sentry_sdk.init( - dsn=app.config.get('SENTRY_DSN'), + dsn=app.config.get("SENTRY_DSN"), integrations=[FlaskIntegration()], - release=f'payment-jobs-{job_name}@-', + release=f"payment-jobs-{job_name}@-", ) - app.logger.info('<<<< Starting Payment Jobs >>>>') + app.logger.info("<<<< Starting Payment Jobs >>>>") queue.init_app(app) db.init_app(app) if init_oracle: @@ -71,107 +74,105 @@ def register_shellcontext(app): def shell_context(): """Shell context objects.""" - return { - 'app': app - } # pragma: no cover + return {"app": app} # pragma: no cover app.shell_context_processor(shell_context) def run(job_name, argument=None): + from tasks.activate_pad_account_task import ActivatePadAccountTask + from tasks.ap_task import ApTask + from tasks.bcol_refund_confirmation_task import BcolRefundConfirmationTask from tasks.cfs_create_account_task import CreateAccountTask from tasks.cfs_create_invoice_task import CreateInvoiceTask + from tasks.direct_pay_automated_refund_task import DirectPayAutomatedRefundTask from tasks.distribution_task import DistributionTask + from tasks.ejv_partner_distribution_task import EjvPartnerDistributionTask + from tasks.ejv_payment_task import EjvPaymentTask from tasks.stale_payment_task import StalePaymentTask from tasks.statement_notification_task import StatementNotificationTask from tasks.statement_task import StatementTask - from tasks.activate_pad_account_task import ActivatePadAccountTask - from tasks.ejv_partner_distribution_task import EjvPartnerDistributionTask from tasks.unpaid_invoice_notify_task import UnpaidInvoiceNotifyTask - from tasks.ejv_payment_task import EjvPaymentTask - from tasks.ap_task import ApTask - from tasks.direct_pay_automated_refund_task import DirectPayAutomatedRefundTask - from tasks.bcol_refund_confirmation_task import BcolRefundConfirmationTask - jobs_with_oracle_connections = ['BCOL_REFUND_CONFIRMATION'] + jobs_with_oracle_connections = ["BCOL_REFUND_CONFIRMATION"] application = create_app(job_name=job_name, init_oracle=job_name in jobs_with_oracle_connections) application.app_context().push() match job_name: - case 'UPDATE_GL_CODE': + case "UPDATE_GL_CODE": DistributionTask.update_failed_distributions() - application.logger.info('<<<< Completed Updating GL Codes >>>>') - case 'GENERATE_STATEMENTS': + application.logger.info("<<<< Completed Updating GL Codes >>>>") + case "GENERATE_STATEMENTS": StatementTask.generate_statements(argument) - application.logger.info('<<<< Completed Generating Statements >>>>') - case 'SEND_NOTIFICATIONS': + application.logger.info("<<<< Completed Generating Statements >>>>") + case "SEND_NOTIFICATIONS": StatementNotificationTask.send_notifications() - application.logger.info('<<<< Completed Sending notifications >>>>') - case 'UPDATE_STALE_PAYMENTS': + application.logger.info("<<<< Completed Sending notifications >>>>") + case "UPDATE_STALE_PAYMENTS": StalePaymentTask.update_stale_payments() - application.logger.info('<<<< Completed Updating stale payments >>>>') - case 'CREATE_CFS_ACCOUNTS': + application.logger.info("<<<< Completed Updating stale payments >>>>") + case "CREATE_CFS_ACCOUNTS": CreateAccountTask.create_accounts() - application.logger.info('<<<< Completed creating cfs accounts >>>>') - case 'CREATE_INVOICES': + application.logger.info("<<<< Completed creating cfs accounts >>>>") + case "CREATE_INVOICES": CreateInvoiceTask.create_invoices() - application.logger.info('<<<< Completed creating cfs invoices >>>>') - case 'ACTIVATE_PAD_ACCOUNTS': + application.logger.info("<<<< Completed creating cfs invoices >>>>") + case "ACTIVATE_PAD_ACCOUNTS": ActivatePadAccountTask.activate_pad_accounts() - application.logger.info('<<<< Completed Activating PAD accounts >>>>') - case 'EJV_PARTNER': + application.logger.info("<<<< Completed Activating PAD accounts >>>>") + case "EJV_PARTNER": EjvPartnerDistributionTask.create_ejv_file() - application.logger.info('<<<< Completed Creating EJV File for partner distribution>>>>') - case 'NOTIFY_UNPAID_INVOICE_OB': + application.logger.info("<<<< Completed Creating EJV File for partner distribution>>>>") + case "NOTIFY_UNPAID_INVOICE_OB": UnpaidInvoiceNotifyTask.notify_unpaid_invoices() - application.logger.info('<<<< Completed Sending notification for OB invoices >>>>') - case 'STATEMENTS_DUE': + application.logger.info("<<<< Completed Sending notification for OB invoices >>>>") + case "STATEMENTS_DUE": action_override = argument[0] if len(argument) >= 1 else None date_override = argument[1] if len(argument) >= 2 else None auth_account_override = argument[2] if len(argument) >= 3 else None - application.logger.info(f'{action_override} {date_override} {auth_account_override}') - EFTStatementDueTask.process_unpaid_statements(action_override=action_override, - date_override=date_override, - auth_account_override=auth_account_override) - application.logger.info( - '<<<< Completed Sending notification for unpaid statements >>>>') - case 'ROUTING_SLIP': + application.logger.info(f"{action_override} {date_override} {auth_account_override}") + EFTStatementDueTask.process_unpaid_statements( + action_override=action_override, + date_override=date_override, + auth_account_override=auth_account_override, + ) + application.logger.info("<<<< Completed Sending notification for unpaid statements >>>>") + case "ROUTING_SLIP": RoutingSlipTask.link_routing_slips() RoutingSlipTask.process_void() RoutingSlipTask.process_nsf() RoutingSlipTask.process_correction() RoutingSlipTask.adjust_routing_slips() - application.logger.info('<<<< Completed Routing Slip tasks >>>>') - case 'EFT': + application.logger.info("<<<< Completed Routing Slip tasks >>>>") + case "EFT": EFTTask.link_electronic_funds_transfers_cfs() EFTTask.reverse_electronic_funds_transfers_cfs() - application.logger.info('<<<< Completed EFT tasks >>>>') - case 'EFT_OVERPAYMENT': + application.logger.info("<<<< Completed EFT tasks >>>>") + case "EFT_OVERPAYMENT": date_override = argument[0] if len(argument) >= 1 else None EFTOverpaymentNotificationTask.process_overpayment_notification(date_override=date_override) - application.logger.info( - '<<<< Completed Sending notification for EFT Over Payment >>>>') - case 'EJV_PAYMENT': + application.logger.info("<<<< Completed Sending notification for EFT Over Payment >>>>") + case "EJV_PAYMENT": EjvPaymentTask.create_ejv_file() - application.logger.info('<<<< Completed running EJV payment >>>>') - case 'AP': + application.logger.info("<<<< Completed running EJV payment >>>>") + case "AP": ApTask.create_ap_files() - application.logger.info('<<<< Completed running AP Job for refund >>>>') - case 'DIRECT_PAY_REFUND': + application.logger.info("<<<< Completed running AP Job for refund >>>>") + case "DIRECT_PAY_REFUND": DirectPayAutomatedRefundTask.process_cc_refunds() - application.logger.info('<<<< Completed running Direct Pay Automated Refund Job >>>>') - case 'BCOL_REFUND_CONFIRMATION': + application.logger.info("<<<< Completed running Direct Pay Automated Refund Job >>>>") + case "BCOL_REFUND_CONFIRMATION": BcolRefundConfirmationTask.update_bcol_refund_invoices() - application.logger.info('<<<< Completed running BCOL Refund Confirmation Job >>>>') + application.logger.info("<<<< Completed running BCOL Refund Confirmation Job >>>>") case _: - application.logger.debug('No valid args passed. Exiting job without running any ***************') + application.logger.debug("No valid args passed. Exiting job without running any ***************") if __name__ == "__main__": - print('----------------------------Scheduler Ran With Argument--', sys.argv[1]) - if (len(sys.argv) > 2): - params = sys.argv[2:len(sys.argv)] + print("----------------------------Scheduler Ran With Argument--", sys.argv[1]) + if len(sys.argv) > 2: + params = sys.argv[2 : len(sys.argv)] run(sys.argv[1], params) else: run(sys.argv[1]) diff --git a/jobs/payment-jobs/poetry.lock b/jobs/payment-jobs/poetry.lock index a66a53fb4..544dc55fd 100644 --- a/jobs/payment-jobs/poetry.lock +++ b/jobs/payment-jobs/poetry.lock @@ -292,6 +292,50 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "blinker" version = "1.7.0" @@ -882,6 +926,22 @@ isort = ">=5.0.0,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + [[package]] name = "flake8-quotes" version = "3.4.0" @@ -1891,6 +1951,17 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "opentracing" version = "2.4.0" @@ -1936,6 +2007,17 @@ all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1 gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] invoke = ["invoke (>=2.0)"] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pay-api" version = "0.1.0" @@ -3174,4 +3256,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "87aad64db611e28e63afb6f6af6523731ddc6cdb8e2f6fae542601308ad9bbd5" +content-hash = "c2501e19a1d6786aafc63cf2f5c713817708b2aeea42dde1638af23532b31428" diff --git a/jobs/payment-jobs/pyproject.toml b/jobs/payment-jobs/pyproject.toml index 9a4e8baa8..44d53e4af 100644 --- a/jobs/payment-jobs/pyproject.toml +++ b/jobs/payment-jobs/pyproject.toml @@ -51,6 +51,130 @@ pylint-flask = "^0.6" freezegun = "^1.4.0" lovely-pytest-docker = "^0.3.1" pytest-asyncio = "^0.23.5.post1" +black = "^24.10.0" +isort = "^5.13.2" +flake8-pyproject = "^1.2.3" + +[tool.flake8] +ignore = ["F401","E402", "Q000", "E203", "W503"] +exclude = [ + ".venv", + "./venv", + ".git", + ".history", + "devops", + "*migrations*", +] +per-file-ignores = [ + "__init__.py:F401", + "*.py:B902" +] +max-line-length = 120 +docstring-min-length=10 +count = true + +[tool.zimports] +black-line-length = 120 +keep-unused-type-checking = true + +[tool.black] +target-version = ["py310", "py311", "py312"] +line-length = 120 +include = '\.pyi?$' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + migrations + | devops + | .history +)/ +''' + +[tool.isort] +atomic = true +profile = "black" +line_length = 120 +skip_gitignore = true +skip_glob = ["migrations", "devops"] + +[tool.pylint.main] +fail-under = 10 +max-line-length = 120 +ignore = [ "migrations", "devops", "tests"] +ignore-patterns = ["^\\.#"] +ignored-modules= ["flask_sqlalchemy", "sqlalchemy", "SQLAlchemy" , "alembic", "scoped_session"] +ignored-classes= "scoped_session" +ignore-long-lines = "^\\s*(# )??$" +extension-pkg-whitelist = "pydantic" +notes = ["FIXME","XXX","TODO"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] +disable = "C0209,C0301,W0511,W0613,W0703,W1514,W1203,R0801,R0902,R0903,R0911,R0401,R1705,R1718,W3101" +argument-naming-style = "snake_case" +attr-naming-style = "snake_case" +class-attribute-naming-style = "any" +class-const-naming-style = "UPPER_CASE" +class-naming-style = "PascalCase" +const-naming-style = "UPPER_CASE" +function-naming-style = "snake_case" +inlinevar-naming-style = "any" +method-naming-style = "snake_case" +module-naming-style = "any" +variable-naming-style = "snake_case" +docstring-min-length = -1 +good-names = ["i", "j", "k", "ex", "Run", "_"] +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] +valid-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/auth_api", +] +omit = [ + "wsgi.py", + "gunicorn_config.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core"] diff --git a/jobs/payment-jobs/services/email_service.py b/jobs/payment-jobs/services/email_service.py index 1e6492e21..17c49e026 100644 --- a/jobs/payment-jobs/services/email_service.py +++ b/jobs/payment-jobs/services/email_service.py @@ -14,8 +14,8 @@ """This manages all of the email notification service.""" import os -from typing import Dict from decimal import Decimal +from typing import Dict from attr import define from flask import current_app @@ -29,40 +29,41 @@ def send_email(recipients: str, subject: str, body: str): """Send the email notification.""" # Note if we send HTML in the body, we aren't sending through GCNotify, ideally we'd like to send through GCNotify. token = get_service_account_token() - current_app.logger.info(f'>send_email to recipients: {recipients}') - notify_url = current_app.config.get('NOTIFY_API_ENDPOINT') + 'notify/' + current_app.logger.info(f">send_email to recipients: {recipients}") + notify_url = current_app.config.get("NOTIFY_API_ENDPOINT") + "notify/" notify_body = { - 'recipients': recipients, - 'content': { - 'subject': subject, - 'body': body - } + "recipients": recipients, + "content": {"subject": subject, "body": body}, } try: - notify_response = OAuthService.post(notify_url, token=token, - auth_header_type=AuthHeaderType.BEARER, - content_type=ContentType.JSON, data=notify_body) - current_app.logger.info(' str: """Render eft overpayment template.""" - template = _get_template('eft_overpayment.html') + template = _get_template("eft_overpayment.html") short_name_detail_url = f"{current_app.config.get('AUTH_WEB_URL')}/pay/shortname-details/{params['shortNameId']}" - params['shortNameDetailUrl'] = short_name_detail_url + params["shortNameDetailUrl"] = short_name_detail_url return template.render(params) diff --git a/jobs/payment-jobs/services/oracle.py b/jobs/payment-jobs/services/oracle.py index 6ffc99d80..d70f4bca2 100644 --- a/jobs/payment-jobs/services/oracle.py +++ b/jobs/payment-jobs/services/oracle.py @@ -19,6 +19,7 @@ from flask import current_app from flask.globals import app_ctx + class OracleDB: """Oracle database connection object for re-use in application.""" @@ -41,7 +42,7 @@ def teardown(ctx=None): """Oracle session pool cleans up after itself.""" if not ctx: ctx = app_ctx - if hasattr(ctx, '_oracle_pool'): + if hasattr(ctx, "_oracle_pool"): ctx._oracle_pool.close() # pylint: disable=protected-access @staticmethod @@ -56,21 +57,25 @@ def init_session(conn, *args): # pylint: disable=unused-argument; Extra var bei cursor = conn.cursor() cursor.execute("alter session set TIME_ZONE = 'America/Vancouver'") - return cx_Oracle.SessionPool(user=current_app.config.get('ORACLE_USER'), # pylint:disable=c-extension-no-member - password=current_app.config.get('ORACLE_PASSWORD'), - dsn='{0}:{1}/{2}'.format(current_app.config.get('ORACLE_HOST'), - current_app.config.get('ORACLE_PORT'), - current_app.config.get('ORACLE_DB_NAME')), - min=1, - max=10, - increment=1, - threaded=True, # wraps the underlying calls in a Mutex - getmode=cx_Oracle.SPOOL_ATTRVAL_NOWAIT, # pylint:disable=c-extension-no-member - waitTimeout=1500, - timeout=3600, - sessionCallback=init_session, - encoding='UTF-8', - nencoding='UTF-8') + return cx_Oracle.SessionPool( + user=current_app.config.get("ORACLE_USER"), # pylint:disable=c-extension-no-member + password=current_app.config.get("ORACLE_PASSWORD"), + dsn="{0}:{1}/{2}".format( + current_app.config.get("ORACLE_HOST"), + current_app.config.get("ORACLE_PORT"), + current_app.config.get("ORACLE_DB_NAME"), + ), + min=1, + max=10, + increment=1, + threaded=True, # wraps the underlying calls in a Mutex + getmode=cx_Oracle.SPOOL_ATTRVAL_NOWAIT, # pylint:disable=c-extension-no-member + waitTimeout=1500, + timeout=3600, + sessionCallback=init_session, + encoding="UTF-8", + nencoding="UTF-8", + ) @property def connection(self): # pylint: disable=inconsistent-return-statements @@ -83,7 +88,7 @@ def connection(self): # pylint: disable=inconsistent-return-statements """ ctx = app_ctx if ctx is not None: - if not hasattr(ctx, '_oracle_pool'): + if not hasattr(ctx, "_oracle_pool"): ctx._oracle_pool = self._create_pool() # pylint: disable = protected-access; need this method return ctx._oracle_pool.acquire() # pylint: disable = protected-access; need this method diff --git a/jobs/payment-jobs/services/routing_slip.py b/jobs/payment-jobs/services/routing_slip.py index dc4f25c6c..5051587c2 100644 --- a/jobs/payment-jobs/services/routing_slip.py +++ b/jobs/payment-jobs/services/routing_slip.py @@ -32,26 +32,31 @@ def create_cfs_account(cfs_account: CfsAccountModel, pay_account: PaymentAccount identifier=pay_account.name, contact_info={}, site_name=routing_slip.number, - is_fas=True + is_fas=True, ) - cfs_account.cfs_account = cfs_account_details.get('account_number') - cfs_account.cfs_party = cfs_account_details.get('party_number') - cfs_account.cfs_site = cfs_account_details.get('site_number') + cfs_account.cfs_account = cfs_account_details.get("account_number") + cfs_account.cfs_party = cfs_account_details.get("party_number") + cfs_account.cfs_site = cfs_account_details.get("site_number") cfs_account.status = CfsAccountStatus.ACTIVE.value cfs_account.payment_method = PaymentMethod.INTERNAL.value - CFSService.create_cfs_receipt(cfs_account=cfs_account, - rcpt_number=routing_slip.number, - rcpt_date=routing_slip.routing_slip_date.strftime('%Y-%m-%d'), - amount=routing_slip.total, - payment_method=pay_account.payment_method, - access_token=CFSService.get_token(PaymentSystem.FAS).json().get('access_token')) + CFSService.create_cfs_receipt( + cfs_account=cfs_account, + rcpt_number=routing_slip.number, + rcpt_date=routing_slip.routing_slip_date.strftime("%Y-%m-%d"), + amount=routing_slip.total, + payment_method=pay_account.payment_method, + access_token=CFSService.get_token(PaymentSystem.FAS).json().get("access_token"), + ) cfs_account.commit() return except Exception as e: # NOQA # pylint: disable=broad-except - capture_message(f'Error on creating Routing Slip CFS Account: account id={pay_account.id}, ' - f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') + capture_message( + f"Error on creating Routing Slip CFS Account: account id={pay_account.id}, " + f"auth account : {pay_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) cfs_account.rollback() return diff --git a/jobs/payment-jobs/setup.py b/jobs/payment-jobs/setup.py index a2aada1b3..39a5fed68 100755 --- a/jobs/payment-jobs/setup.py +++ b/jobs/payment-jobs/setup.py @@ -16,7 +16,4 @@ from setuptools import find_packages, setup -setup( - name="payment_jobs", - packages=find_packages() -) +setup(name="payment_jobs", packages=find_packages()) diff --git a/jobs/payment-jobs/tasks/activate_pad_account_task.py b/jobs/payment-jobs/tasks/activate_pad_account_task.py index 798767187..a8420cf53 100644 --- a/jobs/payment-jobs/tasks/activate_pad_account_task.py +++ b/jobs/payment-jobs/tasks/activate_pad_account_task.py @@ -37,9 +37,11 @@ def activate_pad_accounts(cls): 2. Activate them. """ pending_pad_activation_accounts = CfsAccountModel.find_all_accounts_with_status( - status=CfsAccountStatus.PENDING_PAD_ACTIVATION.value) + status=CfsAccountStatus.PENDING_PAD_ACTIVATION.value + ) current_app.logger.info( - f'Found {len(pending_pad_activation_accounts)} CFS Accounts to be pending PAD activation.') + f"Found {len(pending_pad_activation_accounts)} CFS Accounts to be pending PAD activation." + ) if len(pending_pad_activation_accounts) == 0: return @@ -47,15 +49,15 @@ def activate_pad_accounts(cls): pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(pending_account.account_id) # check is still in the pad activation period - is_activation_period_over = pay_account.pad_activation_date - timedelta(hours=1) \ - < datetime.now(tz=timezone.utc).replace(tzinfo=None) - current_app.logger.info( - f'Account {pay_account.id} ready for activation:{is_activation_period_over}') + is_activation_period_over = pay_account.pad_activation_date - timedelta(hours=1) < datetime.now( + tz=timezone.utc + ).replace(tzinfo=None) + current_app.logger.info(f"Account {pay_account.id} ready for activation:{is_activation_period_over}") if is_activation_period_over: pending_account.status = CfsAccountStatus.ACTIVE.value pending_account.save() - if flags.is_on('multiple-payment-methods', default=False) is False: + if flags.is_on("multiple-payment-methods", default=False) is False: # If account was in another payment method, update it to pad if pay_account.payment_method != PaymentMethod.PAD.value: pay_account.payment_method = PaymentMethod.PAD.value diff --git a/jobs/payment-jobs/tasks/ap_task.py b/jobs/payment-jobs/tasks/ap_task.py index 0ddad3b2d..0a8454524 100644 --- a/jobs/payment-jobs/tasks/ap_task.py +++ b/jobs/payment-jobs/tasks/ap_task.py @@ -34,8 +34,14 @@ from pay_api.models import RoutingSlip as RoutingSlipModel from pay_api.models import db from pay_api.utils.enums import ( - DisbursementStatus, EFTShortnameRefundStatus, EjvFileType, EJVLinkType, InvoiceStatus, PaymentMethod, - RoutingSlipStatus) + DisbursementStatus, + EFTShortnameRefundStatus, + EjvFileType, + EJVLinkType, + InvoiceStatus, + PaymentMethod, + RoutingSlipStatus, +) from sqlalchemy import Date, cast from tasks.common.cgi_ap import CgiAP @@ -80,23 +86,34 @@ def create_ap_files(cls): def _create_eft_refund_file(cls): """Create AP file for EFT refunds and upload to CGI.""" cls.ap_type = EjvFileType.EFT_REFUND - eft_refunds_dao: List[EFTRefundModel] = db.session.query(EFTRefundModel) \ - .join(EFTShortnameLinksModel, EFTRefundModel.short_name_id == EFTShortnameLinksModel.eft_short_name_id) \ - .join(EFTCreditModel, EFTCreditModel.short_name_id == EFTShortnameLinksModel.eft_short_name_id) \ - .join(EFTCreditInvoiceLinkModel, EFTCreditModel.id == EFTCreditInvoiceLinkModel.eft_credit_id) \ - .join(InvoiceModel, EFTCreditInvoiceLinkModel.invoice_id == InvoiceModel.id) \ - .filter(EFTRefundModel.status == EFTShortnameRefundStatus.APPROVED.value) \ - .filter(EFTRefundModel.disbursement_status_code != DisbursementStatus.UPLOADED.value) \ - .filter(EFTRefundModel.refund_amount > 0) \ + eft_refunds_dao: List[EFTRefundModel] = ( + db.session.query(EFTRefundModel) + .join( + EFTShortnameLinksModel, + EFTRefundModel.short_name_id == EFTShortnameLinksModel.eft_short_name_id, + ) + .join( + EFTCreditModel, + EFTCreditModel.short_name_id == EFTShortnameLinksModel.eft_short_name_id, + ) + .join( + EFTCreditInvoiceLinkModel, + EFTCreditModel.id == EFTCreditInvoiceLinkModel.eft_credit_id, + ) + .join(InvoiceModel, EFTCreditInvoiceLinkModel.invoice_id == InvoiceModel.id) + .filter(EFTRefundModel.status == EFTShortnameRefundStatus.APPROVED.value) + .filter(EFTRefundModel.disbursement_status_code != DisbursementStatus.UPLOADED.value) + .filter(EFTRefundModel.refund_amount > 0) .all() + ) - current_app.logger.info(f'Found {len(eft_refunds_dao)} to refund.') + current_app.logger.info(f"Found {len(eft_refunds_dao)} to refund.") for refunds in list(batched(eft_refunds_dao, 250)): ejv_file_model = EjvFileModel( file_type=cls.ap_type.value, file_ref=cls.get_file_name(), - disbursement_status_code=DisbursementStatus.UPLOADED.value + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).flush() batch_number: str = cls.get_batch_number(ejv_file_model.id) ap_content: str = cls.get_batch_header(batch_number) @@ -104,37 +121,46 @@ def _create_eft_refund_file(cls): line_count_total = 0 for eft_refund in refunds: current_app.logger.info( - f'Creating refund for EFT Refund {eft_refund.id}, Amount {eft_refund.refund_amount}.') - ap_content = f'{ap_content}{cls.get_ap_header( - eft_refund.refund_amount, eft_refund.id, eft_refund.created_on, eft_refund.cas_supplier_number)}' + f"Creating refund for EFT Refund {eft_refund.id}, Amount {eft_refund.refund_amount}." + ) + ap_content = f"{ap_content}{cls.get_ap_header( + eft_refund.refund_amount, eft_refund.id, eft_refund.created_on, eft_refund.cas_supplier_number)}" ap_line = APLine( total=eft_refund.refund_amount, invoice_number=eft_refund.id, - line_number=line_count_total + 1) - ap_content = f'{ap_content}{cls.get_ap_invoice_line(ap_line, eft_refund.cas_supplier_number)}' + line_number=line_count_total + 1, + ) + ap_content = f"{ap_content}{cls.get_ap_invoice_line(ap_line, eft_refund.cas_supplier_number)}" line_count_total += 2 if ap_comment := cls.get_eft_ap_comment( - eft_refund.comment, eft_refund.id, eft_refund.short_name_id, eft_refund.cas_supplier_number + eft_refund.comment, + eft_refund.id, + eft_refund.short_name_id, + eft_refund.cas_supplier_number, ): - ap_content = f'{ap_content}{ap_comment:<40}' + ap_content = f"{ap_content}{ap_comment:<40}" line_count_total += 1 batch_total += eft_refund.refund_amount eft_refund.disbursement_status_code = DisbursementStatus.UPLOADED.value batch_trailer: str = cls.get_batch_trailer(batch_number, batch_total, control_total=line_count_total) - ap_content = f'{ap_content}{batch_trailer}' + ap_content = f"{ap_content}{batch_trailer}" cls._create_file_and_upload(ap_content) @classmethod - def _create_routing_slip_refund_file(cls): # pylint:disable=too-many-locals, too-many-statements + def _create_routing_slip_refund_file( + cls, + ): # pylint:disable=too-many-locals, too-many-statements """Create AP file for routing slip refunds (unapplied routing slip amounts) and upload to CGI.""" cls.ap_type = EjvFileType.REFUND - routing_slips_dao: List[RoutingSlipModel] = db.session.query(RoutingSlipModel) \ - .filter(RoutingSlipModel.status == RoutingSlipStatus.REFUND_AUTHORIZED.value) \ - .filter(RoutingSlipModel.refund_amount > 0) \ + routing_slips_dao: List[RoutingSlipModel] = ( + db.session.query(RoutingSlipModel) + .filter(RoutingSlipModel.status == RoutingSlipStatus.REFUND_AUTHORIZED.value) + .filter(RoutingSlipModel.refund_amount > 0) .all() + ) - current_app.logger.info(f'Found {len(routing_slips_dao)} to refund.') + current_app.logger.info(f"Found {len(routing_slips_dao)} to refund.") if not routing_slips_dao: return @@ -142,7 +168,7 @@ def _create_routing_slip_refund_file(cls): # pylint:disable=too-many-locals, to ejv_file_model: EjvFileModel = EjvFileModel( file_type=cls.ap_type.value, file_ref=cls.get_file_name(), - disbursement_status_code=DisbursementStatus.UPLOADED.value + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).flush() batch_number: str = cls.get_batch_number(ejv_file_model.id) @@ -150,40 +176,50 @@ def _create_routing_slip_refund_file(cls): # pylint:disable=too-many-locals, to batch_total = 0 total_line_count: int = 0 for rs in routing_slips: - current_app.logger.info(f'Creating refund for {rs.number}, Amount {rs.refund_amount}.') + current_app.logger.info(f"Creating refund for {rs.number}, Amount {rs.refund_amount}.") refund: RefundModel = RefundModel.find_by_routing_slip_id(rs.id) - ap_content = f'{ap_content}{cls.get_ap_header(rs.refund_amount, rs.number, - datetime.now(tz=timezone.utc))}' + ap_content = f"{ap_content}{cls.get_ap_header(rs.refund_amount, rs.number, + datetime.now(tz=timezone.utc))}" ap_line = APLine(total=rs.refund_amount, invoice_number=rs.number, line_number=1) - ap_content = f'{ap_content}{cls.get_ap_invoice_line(ap_line)}' - ap_content = f'{ap_content}{cls.get_ap_address(refund.details, rs.number)}' + ap_content = f"{ap_content}{cls.get_ap_invoice_line(ap_line)}" + ap_content = f"{ap_content}{cls.get_ap_address(refund.details, rs.number)}" total_line_count += 3 if ap_comment := cls.get_rs_ap_comment(refund.details, rs.number): - ap_content = f'{ap_content}{ap_comment:<40}' + ap_content = f"{ap_content}{ap_comment:<40}" total_line_count += 1 batch_total += rs.refund_amount rs.status = RoutingSlipStatus.REFUND_UPLOADED.value batch_trailer = cls.get_batch_trailer(batch_number, float(batch_total), control_total=total_line_count) - ap_content = f'{ap_content}{batch_trailer}' + ap_content = f"{ap_content}{batch_trailer}" cls._create_file_and_upload(ap_content) @classmethod def get_invoices_for_disbursement(cls, partner): """Return invoices for disbursement. Used by EJV and AP.""" - disbursement_date = datetime.now(tz=timezone.utc).replace(tzinfo=None) \ - - timedelta(days=current_app.config.get('DISBURSEMENT_DELAY_IN_DAYS')) - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ + disbursement_date = datetime.now(tz=timezone.utc).replace(tzinfo=None) - timedelta( + days=current_app.config.get("DISBURSEMENT_DELAY_IN_DAYS") + ) + invoices: List[InvoiceModel] = ( + db.session.query(InvoiceModel) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) .filter( - InvoiceModel.payment_method_code.notin_([PaymentMethod.INTERNAL.value, - PaymentMethod.DRAWDOWN.value, - PaymentMethod.EFT.value])) \ - .filter((InvoiceModel.disbursement_status_code.is_(None)) | - (InvoiceModel.disbursement_status_code == DisbursementStatus.ERRORED.value)) \ - .filter(~InvoiceModel.receipts.any(cast(ReceiptModel.receipt_date, Date) >= disbursement_date.date())) \ - .filter(InvoiceModel.corp_type_code == partner.code) \ + InvoiceModel.payment_method_code.notin_( + [ + PaymentMethod.INTERNAL.value, + PaymentMethod.DRAWDOWN.value, + PaymentMethod.EFT.value, + ] + ) + ) + .filter( + (InvoiceModel.disbursement_status_code.is_(None)) + | (InvoiceModel.disbursement_status_code == DisbursementStatus.ERRORED.value) + ) + .filter(~InvoiceModel.receipts.any(cast(ReceiptModel.receipt_date, Date) >= disbursement_date.date())) + .filter(InvoiceModel.corp_type_code == partner.code) .all() + ) current_app.logger.info(invoices) return invoices @@ -191,18 +227,28 @@ def get_invoices_for_disbursement(cls, partner): def get_invoices_for_refund_reversal(cls, partner): """Return invoices for refund reversal.""" # REFUND_REQUESTED for credit card payments, CREDITED for AR and REFUNDED for other payments. - refund_inv_statuses = (InvoiceStatus.REFUNDED.value, InvoiceStatus.REFUND_REQUESTED.value, - InvoiceStatus.CREDITED.value) + refund_inv_statuses = ( + InvoiceStatus.REFUNDED.value, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.CREDITED.value, + ) - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.invoice_status_code.in_(refund_inv_statuses)) \ + invoices: List[InvoiceModel] = ( + db.session.query(InvoiceModel) + .filter(InvoiceModel.invoice_status_code.in_(refund_inv_statuses)) .filter( - InvoiceModel.payment_method_code.notin_([PaymentMethod.INTERNAL.value, - PaymentMethod.DRAWDOWN.value, - PaymentMethod.EFT.value])) \ - .filter(InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value) \ - .filter(InvoiceModel.corp_type_code == partner.code) \ + InvoiceModel.payment_method_code.notin_( + [ + PaymentMethod.INTERNAL.value, + PaymentMethod.DRAWDOWN.value, + PaymentMethod.EFT.value, + ] + ) + ) + .filter(InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value) + .filter(InvoiceModel.corp_type_code == partner.code) .all() + ) current_app.logger.info(invoices) return invoices @@ -210,12 +256,13 @@ def get_invoices_for_refund_reversal(cls, partner): def _create_non_gov_disbursement_file(cls): # pylint:disable=too-many-locals """Create AP file for disbursement for non government entities without a GL code via EFT and upload to CGI.""" cls.ap_type = EjvFileType.NON_GOV_DISBURSEMENT - bca_partner = CorpTypeModel.find_by_code('BCA') + bca_partner = CorpTypeModel.find_by_code("BCA") # TODO these two functions need to be reworked when we onboard BCA again. - total_invoices: List[InvoiceModel] = cls.get_invoices_for_disbursement(bca_partner) + \ - cls.get_invoices_for_refund_reversal(bca_partner) + total_invoices: List[InvoiceModel] = cls.get_invoices_for_disbursement( + bca_partner + ) + cls.get_invoices_for_refund_reversal(bca_partner) - current_app.logger.info(f'Found {len(total_invoices)} to disburse.') + current_app.logger.info(f"Found {len(total_invoices)} to disburse.") if not total_invoices: return @@ -225,14 +272,14 @@ def _create_non_gov_disbursement_file(cls): # pylint:disable=too-many-locals ejv_file_model: EjvFileModel = EjvFileModel( file_type=cls.ap_type.value, file_ref=cls.get_file_name(), - disbursement_status_code=DisbursementStatus.UPLOADED.value + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).flush() # Create a single header record for this file, provides query ejv_file -> ejv_header -> ejv_invoice_links # Note the inbox file doesn't include ejv_header when submitting. ejv_header_model: EjvFileModel = EjvHeaderModel( - partner_code='BCA', + partner_code="BCA", disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file_model.id + ejv_file_id=ejv_file_model.id, ).flush() batch_number: str = cls.get_batch_number(ejv_file_model.id) @@ -244,24 +291,28 @@ def _create_non_gov_disbursement_file(cls): # pylint:disable=too-many-locals batch_total += disbursement_invoice_total if disbursement_invoice_total == 0: continue - ap_content = f'{ap_content}{cls.get_ap_header(disbursement_invoice_total, inv.id, inv.created_on)}' + ap_content = f"{ap_content}{cls.get_ap_header(disbursement_invoice_total, inv.id, inv.created_on)}" control_total += 1 line_number: int = 0 for line_item in inv.payment_line_items: if line_item.total == 0: continue ap_line = APLine.from_invoice_and_line_item(inv, line_item, line_number + 1, bca_distribution) - ap_content = f'{ap_content}{cls.get_ap_invoice_line(ap_line)}' + ap_content = f"{ap_content}{cls.get_ap_invoice_line(ap_line)}" line_number += 1 control_total += line_number batch_trailer: str = cls.get_batch_trailer(batch_number, batch_total, control_total=control_total) - ap_content = f'{ap_content}{batch_trailer}' + ap_content = f"{ap_content}{batch_trailer}" for inv in invoices: - db.session.add(EjvLinkModel(link_id=inv.id, - link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header_model.id, - disbursement_status_code=DisbursementStatus.UPLOADED.value)) + db.session.add( + EjvLinkModel( + link_id=inv.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header_model.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ) + ) inv.disbursement_status_code = DisbursementStatus.UPLOADED.value db.session.flush() @@ -278,9 +329,11 @@ def _create_file_and_upload(cls, ap_content): @classmethod def _get_bca_distribution_string(cls) -> str: valid_date = date.today() - d = db.session.query(DistributionCodeModel) \ - .filter(DistributionCodeModel.name == 'BC Assessment') \ - .filter(DistributionCodeModel.start_date <= valid_date) \ - .filter((DistributionCodeModel.end_date.is_(None)) | (DistributionCodeModel.end_date >= valid_date)) \ + d = ( + db.session.query(DistributionCodeModel) + .filter(DistributionCodeModel.name == "BC Assessment") + .filter(DistributionCodeModel.start_date <= valid_date) + .filter((DistributionCodeModel.end_date.is_(None)) | (DistributionCodeModel.end_date >= valid_date)) .one_or_none() - return f'{d.client}{d.responsibility_centre}{d.service_line}{d.stob}{d.project_code}' + ) + return f"{d.client}{d.responsibility_centre}{d.service_line}{d.stob}{d.project_code}" diff --git a/jobs/payment-jobs/tasks/bcol_refund_confirmation_task.py b/jobs/payment-jobs/tasks/bcol_refund_confirmation_task.py index 252c7fac8..45d00fa79 100644 --- a/jobs/payment-jobs/tasks/bcol_refund_confirmation_task.py +++ b/jobs/payment-jobs/tasks/bcol_refund_confirmation_task.py @@ -13,8 +13,8 @@ # limitations under the License. """Task to update refunded invoices that have been processed by BCOL.""" from __future__ import annotations -from datetime import datetime, timezone +from datetime import datetime, timezone from decimal import Decimal from typing import Dict, List @@ -44,42 +44,45 @@ def update_bcol_refund_invoices(cls): invoice_refs = cls._get_paydb_invoice_refs_for_update() if invoice_refs: bcol_refund_records = cls._get_colin_bcol_records_for_invoices(invoice_refs) - current_app.logger.debug('BCOL refunded invoice numbers: %s', bcol_refund_records) + current_app.logger.debug("BCOL refunded invoice numbers: %s", bcol_refund_records) if bcol_refund_records: cls._compare_and_update_records(invoice_refs, bcol_refund_records) else: - current_app.logger.debug('No BCOL refunds to confirm.') + current_app.logger.debug("No BCOL refunds to confirm.") @classmethod def _get_paydb_invoice_refs_for_update(cls) -> List[InvoiceReference]: """Get outstanding refund requested BCOL invoice references.""" - current_app.logger.debug('Collecting refund requested BCOL invoices...') - return db.session.query(InvoiceReference) \ - .join(Invoice, Invoice.id == InvoiceReference.invoice_id) \ - .join(Payment, Payment.invoice_number == InvoiceReference.invoice_number) \ - .filter(Payment.payment_system_code == PaymentSystem.BCOL.value) \ - .filter(Invoice.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value).all() + current_app.logger.debug("Collecting refund requested BCOL invoices...") + return ( + db.session.query(InvoiceReference) + .join(Invoice, Invoice.id == InvoiceReference.invoice_id) + .join(Payment, Payment.invoice_number == InvoiceReference.invoice_number) + .filter(Payment.payment_system_code == PaymentSystem.BCOL.value) + .filter(Invoice.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) + .all() + ) @classmethod def _get_colin_bcol_records_for_invoices(cls, invoice_refs: List[InvoiceReference]) -> Dict[str, Decimal]: """Get BCOL refund records for the given invoice references.""" - current_app.logger.debug('Refund requested BCOL invoice references: %s', invoice_refs) + current_app.logger.debug("Refund requested BCOL invoice references: %s", invoice_refs) # split invoice refs into groups of 1000 invoice_ref_chunks = [] for i in range(0, len(invoice_refs), 1000): - invoice_ref_chunks.append(invoice_refs[i:i + 1000]) + invoice_ref_chunks.append(invoice_refs[i : i + 1000]) - current_app.logger.debug('Connecting to Oracle instance...') + current_app.logger.debug("Connecting to Oracle instance...") cursor = oracle_db.connection.cursor() bcol_refunds_all = {} # do for each group of 1000 (oracle wont let you do more) for invoice_ref_grp in invoice_ref_chunks: - invoice_numbers_str = ', '.join("'" + str(x.invoice_number) + "'" for x in invoice_ref_grp) + invoice_numbers_str = ", ".join("'" + str(x.invoice_number) + "'" for x in invoice_ref_grp) - current_app.logger.debug('Collecting COLIN BCOL refund records...') + current_app.logger.debug("Collecting COLIN BCOL refund records...") # key == invoice_number bcol_refunds = cursor.execute( f""" @@ -107,10 +110,12 @@ def _compare_and_update_records(cls, invoice_refs: List[InvoiceReference], bcol_ invoice = Invoice.find_by_id(invoice_ref.invoice_id) if invoice.total + bcol_records[invoice_ref.invoice_number] != 0: # send sentry error and skip - capture_message(f'Invoice refund total mismatch for {invoice_ref.invoice_number}.' - f'PAY-DB: {invoice.total} COLIN-DB: {bcol_records[invoice_ref.invoice_number]}', - level='error') - current_app.logger.error('Invoice refund total mismatch for %s', invoice_ref.invoice_number) + capture_message( + f"Invoice refund total mismatch for {invoice_ref.invoice_number}." + f"PAY-DB: {invoice.total} COLIN-DB: {bcol_records[invoice_ref.invoice_number]}", + level="error", + ) + current_app.logger.error("Invoice refund total mismatch for %s", invoice_ref.invoice_number) continue # refund was processed and value is correct. Update invoice state and refund date diff --git a/jobs/payment-jobs/tasks/cfs_bank_name_updater.py b/jobs/payment-jobs/tasks/cfs_bank_name_updater.py index f8a49a8f4..258c8926b 100644 --- a/jobs/payment-jobs/tasks/cfs_bank_name_updater.py +++ b/jobs/payment-jobs/tasks/cfs_bank_name_updater.py @@ -34,21 +34,18 @@ import config from utils.logger import setup_logging -setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first +setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): +def create_app(run_mode=os.getenv("FLASK_ENV", "production")): """Return a configured Flask App using the Factory method.""" app = Flask(__name__) app.config.from_object(config.CONFIGURATION[run_mode]) # Configure Sentry - if str(app.config.get('SENTRY_ENABLE')).lower() == 'true': - if app.config.get('SENTRY_DSN', None): - sentry_sdk.init( - dsn=app.config.get('SENTRY_DSN'), - integrations=[FlaskIntegration()] - ) + if str(app.config.get("SENTRY_ENABLE")).lower() == "true": + if app.config.get("SENTRY_DSN", None): + sentry_sdk.init(dsn=app.config.get("SENTRY_DSN"), integrations=[FlaskIntegration()]) db.init_app(app) ma.init_app(app) @@ -62,103 +59,123 @@ def register_shellcontext(app): def shell_context(): """Shell context objects.""" - return { - 'app': app - } # pragma: no cover + return {"app": app} # pragma: no cover app.shell_context_processor(shell_context) def run_update(pay_account_id, num_records): """Update bank info.""" - current_app.logger.info(f'<<<< Running Update for account id from :{pay_account_id} and total:{num_records} >>>>') - pad_accounts: List[PaymentAccountModel] = db.session.query(PaymentAccountModel).filter( - PaymentAccountModel.payment_method == PaymentMethod.PAD.value) \ - .filter(PaymentAccountModel.id >= pay_account_id) \ - .order_by(PaymentAccountModel.id.asc()) \ - .limit(num_records) \ + current_app.logger.info(f"<<<< Running Update for account id from :{pay_account_id} and total:{num_records} >>>>") + pad_accounts: List[PaymentAccountModel] = ( + db.session.query(PaymentAccountModel) + .filter(PaymentAccountModel.payment_method == PaymentMethod.PAD.value) + .filter(PaymentAccountModel.id >= pay_account_id) + .order_by(PaymentAccountModel.id.asc()) + .limit(num_records) .all() - access_token: str = CFSService.get_token().json().get('access_token') - current_app.logger.info(f'<<<< Total number of records founds: {len(pad_accounts)}') - current_app.logger.info(f'<<<< records founds: {[accnt.id for accnt in pad_accounts]}') + ) + access_token: str = CFSService.get_token().json().get("access_token") + current_app.logger.info(f"<<<< Total number of records founds: {len(pad_accounts)}") + current_app.logger.info(f"<<<< records founds: {[accnt.id for accnt in pad_accounts]}") if len(pad_accounts) == 0: return for payment_account in pad_accounts: cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, PaymentMethod.PAD.value) current_app.logger.info( - f'<<<< Running Update for account id :{payment_account.id} and cfs_account:{cfs_account.id} >>>>') + f"<<<< Running Update for account id :{payment_account.id} and cfs_account:{cfs_account.id} >>>>" + ) # payment_details = get_bank_info(cfs_account.cfs_party, cfs_account.cfs_account, cfs_account.cfs_site) # current_app.logger.info(payment_details) - name = re.sub(r'[^a-zA-Z0-9]+', ' ', payment_account.name) + name = re.sub(r"[^a-zA-Z0-9]+", " ", payment_account.name) payment_info: Dict[str, any] = { - 'bankInstitutionNumber': cfs_account.bank_number, - 'bankTransitNumber': cfs_account.bank_branch_number, - 'bankAccountNumber': cfs_account.bank_account_number, - 'bankAccountName': name + "bankInstitutionNumber": cfs_account.bank_number, + "bankTransitNumber": cfs_account.bank_branch_number, + "bankAccountNumber": cfs_account.bank_account_number, + "bankAccountName": name, } - save_bank_details(access_token, cfs_account.cfs_party, - cfs_account.cfs_account, - cfs_account.cfs_site, payment_info) + save_bank_details( + access_token, + cfs_account.cfs_party, + cfs_account.cfs_account, + cfs_account.cfs_site, + payment_info, + ) current_app.logger.info( - f'<<<< Successfully Updated for account id :{payment_account.id} and cfs_account:{cfs_account.id} >>>>') + f"<<<< Successfully Updated for account id :{payment_account.id} and cfs_account:{cfs_account.id} >>>>" + ) -def get_bank_info(party_number: str, # pylint: disable=too-many-arguments - account_number: str, - site_number: str): +def get_bank_info( + party_number: str, # pylint: disable=too-many-arguments + account_number: str, + site_number: str, +): """Get bank details to the site.""" - current_app.logger.debug('4}', - 'branch_number': f'{branch_number:0>5}', - 'bank_account_number': str(payment_info.get('bankAccountNumber')), - 'country_code': DEFAULT_COUNTRY, - 'currency_code': DEFAULT_CURRENCY + "bank_account_name": name, + "bank_number": f"{bank_number:0>4}", + "branch_number": f"{branch_number:0>5}", + "bank_account_number": str(payment_info.get("bankAccountNumber")), + "country_code": DEFAULT_COUNTRY, + "currency_code": DEFAULT_CURRENCY, } - site_payment_response = OAuthService.post(site_payment_url, access_token, AuthHeaderType.BEARER, - ContentType.JSON, - payment_details).json() - current_app.logger.debug('<<<<<<') + site_payment_response = OAuthService.post( + site_payment_url, + access_token, + AuthHeaderType.BEARER, + ContentType.JSON, + payment_details, + ).json() + current_app.logger.debug("<<<<<<") current_app.logger.debug(site_payment_response) - current_app.logger.debug('>>>>>>') + current_app.logger.debug(">>>>>>") - current_app.logger.debug('>Updated CFS payment details') + current_app.logger.debug(">Updated CFS payment details") return payment_details -if __name__ == '__main__': +if __name__ == "__main__": # first arg is account id to start with. Pay Account ID # second argument is how many records should it update.Just a stepper for reducing CFS load - print('len:', len(sys.argv)) + print("len:", len(sys.argv)) if len(sys.argv) <= 2: - print('No valid args passed.Exiting job without running any actions***************') + print("No valid args passed.Exiting job without running any actions***************") COUNT = sys.argv[2] if len(sys.argv) == 3 else 10 application = create_app() application.app_context().push() diff --git a/jobs/payment-jobs/tasks/cfs_create_account_task.py b/jobs/payment-jobs/tasks/cfs_create_account_task.py index 775a47705..0c779c4e3 100644 --- a/jobs/payment-jobs/tasks/cfs_create_account_task.py +++ b/jobs/payment-jobs/tasks/cfs_create_account_task.py @@ -25,6 +25,7 @@ from pay_api.utils.enums import AuthHeaderType, CfsAccountStatus, ContentType, PaymentMethod from sbc_common_components.utils.enums import QueueMessageTypes from sentry_sdk import capture_message + from services import routing_slip from utils import mailer from utils.auth import get_token @@ -44,7 +45,7 @@ def create_accounts(cls): # pylint: disable=too-many-locals """ # Pass payment method if offline account creation has be restricted based on payment method. pending_accounts = CfsAccountModel.find_all_pending_accounts() - current_app.logger.info(f'Found {len(pending_accounts)} CFS Accounts to be created.') + current_app.logger.info(f"Found {len(pending_accounts)} CFS Accounts to be created.") if len(pending_accounts) == 0: return @@ -54,85 +55,104 @@ def create_accounts(cls): # pylint: disable=too-many-locals # Find the payment account and create the pay system instance. try: pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(pending_account.account_id) - if pay_account.payment_method in (PaymentMethod.CASH.value, PaymentMethod.CHEQUE.value): + if pay_account.payment_method in ( + PaymentMethod.CASH.value, + PaymentMethod.CHEQUE.value, + ): routing_slip.create_cfs_account(pending_account, pay_account) else: cls._create_cfs_account(pending_account, pay_account, auth_token) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on creating cfs_account={pending_account.account_id}, ' - f'ERROR : {str(e)}', level='error') + f"Error on creating cfs_account={pending_account.account_id}, " f"ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @classmethod def _get_account_contact(cls, auth_token: str, auth_account_id: str): """Return account contact by calling auth API.""" - get_contact_endpoint = current_app.config.get('AUTH_API_ENDPOINT') + f'orgs/{auth_account_id}/contacts' + get_contact_endpoint = current_app.config.get("AUTH_API_ENDPOINT") + f"orgs/{auth_account_id}/contacts" contact_response = OAuthService.get(get_contact_endpoint, auth_token, AuthHeaderType.BEARER, ContentType.JSON) - return contact_response.json().get('contacts')[0] + return contact_response.json().get("contacts")[0] @classmethod - def _create_cfs_account(cls, pending_account: CfsAccountModel, pay_account: PaymentAccountModel, auth_token: str): + def _create_cfs_account( + cls, + pending_account: CfsAccountModel, + pay_account: PaymentAccountModel, + auth_token: str, + ): current_app.logger.info( - f'Creating pay system instance for {pay_account.payment_method} for account {pay_account.id}.') + f"Creating pay system instance for {pay_account.payment_method} for account {pay_account.id}." + ) # For an existing CFS Account, call update.. This is to handle PAD update when CFS is offline try: account_contact = cls._get_account_contact(auth_token, pay_account.auth_account_id) contact_info: Dict[str, str] = { - 'city': account_contact.get('city'), - 'postalCode': account_contact.get('postalCode'), - 'province': account_contact.get('region'), - 'addressLine1': account_contact.get('street'), - 'country': account_contact.get('country') + "city": account_contact.get("city"), + "postalCode": account_contact.get("postalCode"), + "province": account_contact.get("region"), + "addressLine1": account_contact.get("street"), + "country": account_contact.get("country"), } payment_info: Dict[str, any] = { - 'bankInstitutionNumber': pending_account.bank_number, - 'bankTransitNumber': pending_account.bank_branch_number, - 'bankAccountNumber': pending_account.bank_account_number, - 'bankAccountName': pay_account.name + "bankInstitutionNumber": pending_account.bank_number, + "bankTransitNumber": pending_account.bank_branch_number, + "bankAccountNumber": pending_account.bank_account_number, + "bankAccountName": pay_account.name, } if pay_account.payment_method == PaymentMethod.EFT.value: - cfs_account_details = CFSService.create_cfs_account(identifier=pay_account.auth_account_id, - contact_info=contact_info, - receipt_method=CFS_RCPT_EFT_WIRE) - pending_account.payment_instrument_number = cfs_account_details.get('payment_instrument_number', - None) - pending_account.cfs_account = cfs_account_details.get('account_number') - pending_account.cfs_site = cfs_account_details.get('site_number') - pending_account.cfs_party = cfs_account_details.get('party_number') + cfs_account_details = CFSService.create_cfs_account( + identifier=pay_account.auth_account_id, + contact_info=contact_info, + receipt_method=CFS_RCPT_EFT_WIRE, + ) + pending_account.payment_instrument_number = cfs_account_details.get("payment_instrument_number", None) + pending_account.cfs_account = cfs_account_details.get("account_number") + pending_account.cfs_site = cfs_account_details.get("site_number") + pending_account.cfs_party = cfs_account_details.get("party_number") elif pending_account.cfs_account and pending_account.cfs_party and pending_account.cfs_site: # This means, PAD account details have changed. So update banking details for this CFS account - bank_details = CFSService.update_bank_details(name=pay_account.auth_account_id, - party_number=pending_account.cfs_party, - account_number=pending_account.cfs_account, - site_number=pending_account.cfs_site, - payment_info=payment_info) - pending_account.payment_instrument_number = bank_details.get('payment_instrument_number', None) + bank_details = CFSService.update_bank_details( + name=pay_account.auth_account_id, + party_number=pending_account.cfs_party, + account_number=pending_account.cfs_account, + site_number=pending_account.cfs_site, + payment_info=payment_info, + ) + pending_account.payment_instrument_number = bank_details.get("payment_instrument_number", None) pending_account.payment_method = PaymentMethod.PAD.value else: # It's a new account, now create # If the account have banking information, then create a PAD account else a regular account. - if pending_account.bank_number and pending_account.bank_branch_number \ - and pending_account.bank_account_number: - cfs_account_details = CFSService.create_cfs_account(identifier=pay_account.auth_account_id, - contact_info=contact_info, - payment_info=payment_info, - receipt_method=RECEIPT_METHOD_PAD_DAILY) + if ( + pending_account.bank_number + and pending_account.bank_branch_number + and pending_account.bank_account_number + ): + cfs_account_details = CFSService.create_cfs_account( + identifier=pay_account.auth_account_id, + contact_info=contact_info, + payment_info=payment_info, + receipt_method=RECEIPT_METHOD_PAD_DAILY, + ) pending_account.payment_method = PaymentMethod.PAD.value else: - cfs_account_details = CFSService.create_cfs_account(identifier=pay_account.auth_account_id, - contact_info=contact_info, - receipt_method=None) - - pending_account.payment_instrument_number = cfs_account_details.get('payment_instrument_number', - None) - pending_account.cfs_account = cfs_account_details.get('account_number') - pending_account.cfs_site = cfs_account_details.get('site_number') - pending_account.cfs_party = cfs_account_details.get('party_number') + cfs_account_details = CFSService.create_cfs_account( + identifier=pay_account.auth_account_id, + contact_info=contact_info, + receipt_method=None, + ) + + pending_account.payment_instrument_number = cfs_account_details.get("payment_instrument_number", None) + pending_account.cfs_account = cfs_account_details.get("account_number") + pending_account.cfs_site = cfs_account_details.get("site_number") + pending_account.cfs_party = cfs_account_details.get("party_number") if not pending_account.payment_method: pending_account.payment_method = pay_account.payment_method @@ -140,25 +160,35 @@ def _create_cfs_account(cls, pending_account: CfsAccountModel, pay_account: Paym is_user_error = False if pay_account.payment_method == PaymentMethod.PAD.value: is_user_error = CreateAccountTask._check_user_error(e.response) # pylint: disable=no-member - capture_message(f'Error on creating CFS Account: account id={pay_account.id}, ' - f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') + capture_message( + f"Error on creating CFS Account: account id={pay_account.id}, " + f"auth account : {pay_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) pending_account.rollback() if is_user_error: - capture_message(f'User Input needed for creating CFS Account: account id={pay_account.id}, ' - f'auth account : {pay_account.auth_account_id}, ERROR : Invalid Bank Details', - level='error') + capture_message( + f"User Input needed for creating CFS Account: account id={pay_account.id}, " + f"auth account : {pay_account.auth_account_id}, ERROR : Invalid Bank Details", + level="error", + ) mailer.publish_mailer_events(QueueMessageTypes.PAD_SETUP_FAILED.value, pay_account) pending_account.status = CfsAccountStatus.INACTIVE.value pending_account.save() return # If the account has an activation time set it should have PENDING_PAD_ACTIVATION status. - is_account_in_pad_confirmation_period = pay_account.pad_activation_date is not None and \ - pay_account.pad_activation_date > datetime.now(tz=timezone.utc).replace(tzinfo=None) - pending_account.status = CfsAccountStatus.PENDING_PAD_ACTIVATION.value if \ - is_account_in_pad_confirmation_period else CfsAccountStatus.ACTIVE.value + is_account_in_pad_confirmation_period = ( + pay_account.pad_activation_date is not None + and pay_account.pad_activation_date > datetime.now(tz=timezone.utc).replace(tzinfo=None) + ) + pending_account.status = ( + CfsAccountStatus.PENDING_PAD_ACTIVATION.value + if is_account_in_pad_confirmation_period + else CfsAccountStatus.ACTIVE.value + ) pending_account.save() @staticmethod @@ -169,9 +199,9 @@ def _check_user_error(response) -> bool: # [Errors = [34] Bank Account Number is Invalid] # [Errors = [32] Branch Number is Invalid] # [Errors = [31] Bank Number is Invalid] - error_strings = ['Bank Account Number', 'Branch Number', 'Bank Number'] - if cas_error := headers.get('CAS-Returned-Messages', None): + error_strings = ["Bank Account Number", "Branch Number", "Bank Number"] + if cas_error := headers.get("CAS-Returned-Messages", None): # searches for error message and invalid word - if any(re.match(f'.+{word}.+invalid.+', cas_error, re.IGNORECASE) for word in error_strings): + if any(re.match(f".+{word}.+invalid.+", cas_error, re.IGNORECASE) for word in error_strings): return True return False diff --git a/jobs/payment-jobs/tasks/cfs_create_invoice_task.py b/jobs/payment-jobs/tasks/cfs_create_invoice_task.py index a15aa354a..15f6e2571 100644 --- a/jobs/payment-jobs/tasks/cfs_create_invoice_task.py +++ b/jobs/payment-jobs/tasks/cfs_create_invoice_task.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. """Task to create CFS invoices offline.""" +import time from datetime import datetime, timezone from decimal import Decimal -import time from typing import List from flask import current_app @@ -32,7 +32,13 @@ from pay_api.services.payment import Payment from pay_api.services.payment_account import PaymentAccount as PaymentAccountService from pay_api.utils.enums import ( - CfsAccountStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus, PaymentSystem) + CfsAccountStatus, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, + PaymentStatus, + PaymentSystem, +) from pay_api.utils.util import generate_transaction_number from sbc_common_components.utils.enums import QueueMessageTypes from sentry_sdk import capture_message @@ -57,61 +63,71 @@ def create_invoices(cls): 2.1 Roll up all transactions and create one invoice in CFS. 3. Update the invoice status as IN TRANSIT """ - current_app.logger.info('<< Starting PAD Invoice Creation') + current_app.logger.info("<< Starting PAD Invoice Creation") cls._create_pad_invoices() - current_app.logger.info('>> Done PAD Invoice Creation') + current_app.logger.info(">> Done PAD Invoice Creation") - current_app.logger.info('<< Starting EFT Invoice Creation') + current_app.logger.info("<< Starting EFT Invoice Creation") cls._create_eft_invoices() - current_app.logger.info('>> Done EFT Invoice Creation') + current_app.logger.info(">> Done EFT Invoice Creation") - current_app.logger.info('<< Starting Online Banking Invoice Creation') + current_app.logger.info("<< Starting Online Banking Invoice Creation") cls._create_online_banking_invoices() - current_app.logger.info('>> Done Online Banking Invoice Creation') + current_app.logger.info(">> Done Online Banking Invoice Creation") - current_app.logger.info('<< Starting CANCEL Routing Slip Invoices') + current_app.logger.info("<< Starting CANCEL Routing Slip Invoices") cls._cancel_rs_invoices() - current_app.logger.info('>> Done CANCEL Routing Slip Invoices') + current_app.logger.info(">> Done CANCEL Routing Slip Invoices") # Cancel first then create, else receipt apply would fail. - current_app.logger.info('<< Starting Routing Slip Invoice Creation') + current_app.logger.info("<< Starting Routing Slip Invoice Creation") cls._create_rs_invoices() - current_app.logger.info('>> Done Routing Slip Invoice Creation') + current_app.logger.info(">> Done Routing Slip Invoice Creation") @classmethod def _cancel_rs_invoices(cls): """Cancel routing slip invoices in CFS.""" - invoices: List[InvoiceModel] = InvoiceModel.query \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) \ - .filter(InvoiceModel.routing_slip is not None) \ - .order_by(InvoiceModel.created_on.asc()).all() - - current_app.logger.info(f'Found {len(invoices)} to be cancelled in CFS.') + invoices: List[InvoiceModel] = ( + InvoiceModel.query.filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) + .filter(InvoiceModel.routing_slip is not None) + .order_by(InvoiceModel.created_on.asc()) + .all() + ) + + current_app.logger.info(f"Found {len(invoices)} to be cancelled in CFS.") for invoice in invoices: - current_app.logger.debug(f'Calling the invoice {invoice.id}') + current_app.logger.debug(f"Calling the invoice {invoice.id}") routing_slip = RoutingSlipModel.find_by_number(invoice.routing_slip) routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( - routing_slip.payment_account_id) + routing_slip.payment_account_id + ) cfs_account = CfsAccountModel.find_effective_by_payment_method( - routing_slip_payment_account.id, PaymentMethod.INTERNAL.value) + routing_slip_payment_account.id, PaymentMethod.INTERNAL.value + ) # Find COMPLETED invoice reference; as unapply has to be done only if invoice is created and applied in CFS. - invoice_reference = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value) + invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value + ) if invoice_reference: - current_app.logger.debug(f'Found invoice reference - {invoice_reference.invoice_number}') + current_app.logger.debug(f"Found invoice reference - {invoice_reference.invoice_number}") try: receipts: List[ReceiptModel] = ReceiptModel.find_all_receipts_for_invoice(invoice_id=invoice.id) for receipt in receipts: - CFSService.unapply_receipt(cfs_account, receipt.receipt_number, - invoice_reference.invoice_number) + CFSService.unapply_receipt( + cfs_account, + receipt.receipt_number, + invoice_reference.invoice_number, + ) # This used to be adjust invoice, but the suggested way from Tara is to use reverse invoice. CFSService.reverse_invoice(invoice_reference.invoice_number) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on cancelling Routing Slip invoice: invoice id={invoice.id}, ' - f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') + f"Error on cancelling Routing Slip invoice: invoice id={invoice.id}, " + f"routing slip : {routing_slip.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @@ -129,37 +145,47 @@ def _create_rs_invoices(cls): # pylint: disable=too-many-locals # find all routing slip invoices [cash or cheque] # create invoices in csf # do the receipt apply - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .join(RoutingSlipModel, RoutingSlipModel.number == InvoiceModel.routing_slip) \ - .join(CfsAccountModel, CfsAccountModel.account_id == RoutingSlipModel.payment_account_id) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) \ - .filter(CfsAccountModel.status.in_([CfsAccountStatus.ACTIVE.value, CfsAccountStatus.FREEZE.value])) \ - .filter(CfsAccountModel.payment_method == PaymentMethod.INTERNAL.value) \ - .filter(InvoiceModel.routing_slip is not None) \ - .order_by(InvoiceModel.created_on.asc()).all() - - current_app.logger.info(f'Found {len(invoices)} to be created in CFS.') + invoices: List[InvoiceModel] = ( + db.session.query(InvoiceModel) + .join(RoutingSlipModel, RoutingSlipModel.number == InvoiceModel.routing_slip) + .join( + CfsAccountModel, + CfsAccountModel.account_id == RoutingSlipModel.payment_account_id, + ) + .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) + .filter(CfsAccountModel.status.in_([CfsAccountStatus.ACTIVE.value, CfsAccountStatus.FREEZE.value])) + .filter(CfsAccountModel.payment_method == PaymentMethod.INTERNAL.value) + .filter(InvoiceModel.routing_slip is not None) + .order_by(InvoiceModel.created_on.asc()) + .all() + ) + + current_app.logger.info(f"Found {len(invoices)} to be created in CFS.") for invoice in invoices: # Create a CFS invoice - current_app.logger.debug(f'Creating cfs invoice for invoice {invoice.id}') + current_app.logger.debug(f"Creating cfs invoice for invoice {invoice.id}") routing_slip = RoutingSlipModel.find_by_number(invoice.routing_slip) # If routing slip is not found in Pay-DB, assume legacy RS and move on to next one. if not routing_slip: continue routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( - routing_slip.payment_account_id) + routing_slip.payment_account_id + ) # apply invoice to the active CFS_ACCOUNT which will be the parent routing slip - active_cfs_account = CfsAccountModel.find_effective_by_payment_method(routing_slip_payment_account.id, - PaymentMethod.INTERNAL.value) + active_cfs_account = CfsAccountModel.find_effective_by_payment_method( + routing_slip_payment_account.id, PaymentMethod.INTERNAL.value + ) try: - invoice_response = CFSService.create_account_invoice(transaction_number=invoice.id, - line_items=invoice.payment_line_items, - cfs_account=active_cfs_account) + invoice_response = CFSService.create_account_invoice( + transaction_number=invoice.id, + line_items=invoice.payment_line_items, + cfs_account=active_cfs_account, + ) except Exception as e: # NOQA # pylint: disable=broad-except # There is a chance that the error is a timeout from CAS side, # so to make sure we are not missing any data, make a GET call for the invoice we tried to create @@ -171,28 +197,32 @@ def _create_rs_invoices(cls): # pylint: disable=too-many-locals # since this is a job, delay doesn't cause any performance issue time.sleep(10) invoice_number = generate_transaction_number(str(invoice.id)) - invoice_response = CFSService.get_invoice( - cfs_account=active_cfs_account, inv_number=invoice_number - ) - has_invoice_created = invoice_response.get('invoice_number', None) == invoice_number + invoice_response = CFSService.get_invoice(cfs_account=active_cfs_account, inv_number=invoice_number) + has_invoice_created = invoice_response.get("invoice_number", None) == invoice_number except Exception as exc: # NOQA # pylint: disable=broad-except,unused-variable # Ignore this error, as it is irrelevant and error on outer level is relevant. pass # If no invoice is created raise an error for sentry if not has_invoice_created: - capture_message(f'Error on creating routing slip invoice: account id={invoice.payment_account.id}, ' - f'auth account : {invoice.payment_account.auth_account_id}, ERROR : {str(e)}', - level='error') + capture_message( + f"Error on creating routing slip invoice: account id={invoice.payment_account.id}, " + f"auth account : {invoice.payment_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue - invoice_number = invoice_response.get('invoice_number', None) + invoice_number = invoice_response.get("invoice_number", None) - current_app.logger.info(f'invoice_number {invoice_number} created in CFS.') + current_app.logger.info(f"invoice_number {invoice_number} created in CFS.") has_error_in_apply_receipt = RoutingSlipTask.apply_routing_slips_to_invoice( - routing_slip_payment_account, active_cfs_account, routing_slip, invoice, invoice_number + routing_slip_payment_account, + active_cfs_account, + routing_slip, + invoice, + invoice_number, ) if has_error_in_apply_receipt: @@ -200,19 +230,21 @@ def _create_rs_invoices(cls): # pylint: disable=too-many-locals continue invoice_reference: InvoiceReference = InvoiceReference.create( - invoice.id, invoice_number, - invoice_response.get('pbc_ref_number', None)) + invoice.id, invoice_number, invoice_response.get("pbc_ref_number", None) + ) - current_app.logger.debug('>create_invoice') + current_app.logger.debug(">create_invoice") invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value - Payment.create(payment_method=PaymentMethod.INTERNAL.value, - payment_system=PaymentSystem.INTERNAL.value, - payment_status=PaymentStatus.COMPLETED.value, - invoice_number=invoice_reference.invoice_number, - invoice_amount=invoice.total, - payment_account_id=invoice.payment_account_id) + Payment.create( + payment_method=PaymentMethod.INTERNAL.value, + payment_system=PaymentSystem.INTERNAL.value, + payment_status=PaymentStatus.COMPLETED.value, + invoice_number=invoice_reference.invoice_number, + invoice_amount=invoice.total, + payment_account_id=invoice.payment_account_id, + ) # leave the status as PAID invoice.invoice_status_code = InvoiceStatus.PAID.value @@ -223,51 +255,69 @@ def _create_rs_invoices(cls): # pylint: disable=too-many-locals @classmethod def _create_pad_invoices(cls): # pylint: disable=too-many-locals """Create PAD invoices in to CFS system.""" - inv_subquery = db.session.query(InvoiceModel.payment_account_id) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value).subquery() - - pad_accounts: List[PaymentAccountModel] = db.session.query(PaymentAccountModel) \ - .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ - .filter(CfsAccountModel.status != CfsAccountStatus.FREEZE.value) \ - .filter(CfsAccountModel.payment_method == PaymentMethod.PAD.value) \ - .filter(PaymentAccountModel.id.in_(select(inv_subquery))).all() - - current_app.logger.info(f'Found {len(pad_accounts)} with PAD transactions.') + inv_subquery = ( + db.session.query(InvoiceModel.payment_account_id) + .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) + .subquery() + ) + + pad_accounts: List[PaymentAccountModel] = ( + db.session.query(PaymentAccountModel) + .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) + .filter(CfsAccountModel.status != CfsAccountStatus.FREEZE.value) + .filter(CfsAccountModel.payment_method == PaymentMethod.PAD.value) + .filter(PaymentAccountModel.id.in_(select(inv_subquery))) + .all() + ) + + current_app.logger.info(f"Found {len(pad_accounts)} with PAD transactions.") for account in pad_accounts: - account_invoices = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.payment_account_id == account.id) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) \ - .filter(InvoiceModel.id.notin_(cls._active_invoice_reference_subquery())) \ - .order_by(InvoiceModel.created_on.desc()).all() + account_invoices = ( + db.session.query(InvoiceModel) + .filter(InvoiceModel.payment_account_id == account.id) + .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) + .filter(InvoiceModel.id.notin_(cls._active_invoice_reference_subquery())) + .order_by(InvoiceModel.created_on.desc()) + .all() + ) payment_account: PaymentAccountService = PaymentAccountService.find_by_id(account.id) if len(account_invoices) == 0: continue current_app.logger.debug( - f'Found {len(account_invoices)} invoices for account {payment_account.auth_account_id}') + f"Found {len(account_invoices)} invoices for account {payment_account.auth_account_id}" + ) - cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method(payment_account.id, - PaymentMethod.PAD.value) - if cfs_account.status not in (CfsAccountStatus.ACTIVE.value, CfsAccountStatus.INACTIVE.value): - current_app.logger.info(f'CFS status for account {payment_account.auth_account_id} ' - f'is {payment_account.cfs_account_status} skipping.') + cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method( + payment_account.id, PaymentMethod.PAD.value + ) + if cfs_account.status not in ( + CfsAccountStatus.ACTIVE.value, + CfsAccountStatus.INACTIVE.value, + ): + current_app.logger.info( + f"CFS status for account {payment_account.auth_account_id} " + f"is {payment_account.cfs_account_status} skipping." + ) continue lines = [] - invoice_total = Decimal('0') + invoice_total = Decimal("0") for invoice in account_invoices: lines.extend(invoice.payment_line_items) invoice_total += invoice.total invoice_number = account_invoices[-1].id try: # Get the first invoice id as the trx number for CFS - invoice_response = CFSService.create_account_invoice(transaction_number=invoice_number, - line_items=lines, - cfs_account=cfs_account) + invoice_response = CFSService.create_account_invoice( + transaction_number=invoice_number, + line_items=lines, + cfs_account=cfs_account, + ) except Exception as e: # NOQA # pylint: disable=broad-except # There is a chance that the error is a timeout from CAS side, # so to make sure we are not missing any data, make a GET call for the invoice we tried to create @@ -278,42 +328,47 @@ def _create_pad_invoices(cls): # pylint: disable=too-many-locals # add a 10 seconds delay here as safe bet, as CFS takes time to create the invoice time.sleep(10) invoice_number = generate_transaction_number(str(invoice_number)) - invoice_response = CFSService.get_invoice( - cfs_account=cfs_account, inv_number=invoice_number - ) - has_invoice_created = invoice_response.get('invoice_number', None) == invoice_number - invoice_total_matches = Decimal(invoice_response.get('total', '0')) == invoice_total + invoice_response = CFSService.get_invoice(cfs_account=cfs_account, inv_number=invoice_number) + has_invoice_created = invoice_response.get("invoice_number", None) == invoice_number + invoice_total_matches = Decimal(invoice_response.get("total", "0")) == invoice_total except Exception as exc: # NOQA # pylint: disable=broad-except,unused-variable # Ignore this error, as it is irrelevant and error on outer level is relevant. pass # If no invoice is created raise an error for sentry if not has_invoice_created: - capture_message(f'Error on creating PAD invoice: account id={payment_account.id}, ' - f'auth account : {payment_account.auth_account_id}, ERROR : {str(e)}', - level='error') + capture_message( + f"Error on creating PAD invoice: account id={payment_account.id}, " + f"auth account : {payment_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue if not invoice_total_matches: - capture_message(f'Error on creating PAD invoice: account id={payment_account.id}, ' - f'auth account : {payment_account.auth_account_id}, Invoice exists: ' - f' CAS total: {invoice_response.get("total", 0)}, PAY-BC total: {invoice_total}', - level='error') + capture_message( + f"Error on creating PAD invoice: account id={payment_account.id}, " + f"auth account : {payment_account.auth_account_id}, Invoice exists: " + f' CAS total: {invoice_response.get("total", 0)}, PAY-BC total: {invoice_total}', + level="error", + ) current_app.logger.error(e) continue additional_params = { - 'invoice_total': float(invoice_total), - 'invoice_process_date': f'{datetime.now(tz=timezone.utc)}' + "invoice_total": float(invoice_total), + "invoice_process_date": f"{datetime.now(tz=timezone.utc)}", } - mailer.publish_mailer_events(QueueMessageTypes.PAD_INVOICE_CREATED.value, payment_account, - additional_params) + mailer.publish_mailer_events( + QueueMessageTypes.PAD_INVOICE_CREATED.value, + payment_account, + additional_params, + ) # Iterate invoice and create invoice reference records for invoice in account_invoices: invoice_reference = InvoiceReferenceModel( invoice_id=invoice.id, - invoice_number=invoice_response.get('invoice_number'), - reference_number=invoice_response.get('pbc_ref_number', None), - status_code=InvoiceReferenceStatus.ACTIVE.value + invoice_number=invoice_response.get("invoice_number"), + reference_number=invoice_response.get("pbc_ref_number", None), + status_code=InvoiceReferenceStatus.ACTIVE.value, ) db.session.add(invoice_reference) invoice.cfs_account_id = cfs_account.id @@ -322,24 +377,31 @@ def _create_pad_invoices(cls): # pylint: disable=too-many-locals @classmethod def _return_eft_accounts(cls): """Return EFT accounts.""" - invoice_subquery = db.session.query(InvoiceModel.payment_account_id) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value).subquery() - - eft_accounts: List[PaymentAccountModel] = db.session.query(PaymentAccountModel) \ - .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ - .filter(CfsAccountModel.status != CfsAccountStatus.FREEZE.value) \ - .filter(CfsAccountModel.payment_method == PaymentMethod.EFT.value) \ - .filter(PaymentAccountModel.id.in_(select(invoice_subquery))).all() - - current_app.logger.info(f'Found {len(eft_accounts)} with EFT transactions.') + invoice_subquery = ( + db.session.query(InvoiceModel.payment_account_id) + .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) + .subquery() + ) + + eft_accounts: List[PaymentAccountModel] = ( + db.session.query(PaymentAccountModel) + .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) + .filter(CfsAccountModel.status != CfsAccountStatus.FREEZE.value) + .filter(CfsAccountModel.payment_method == PaymentMethod.EFT.value) + .filter(PaymentAccountModel.id.in_(select(invoice_subquery))) + .all() + ) + + current_app.logger.info(f"Found {len(eft_accounts)} with EFT transactions.") return eft_accounts @classmethod def _active_invoice_reference_subquery(cls): - return db.session.query(InvoiceReferenceModel.invoice_id). \ - filter(InvoiceReferenceModel.status_code.in_((InvoiceReferenceStatus.ACTIVE.value,))) + return db.session.query(InvoiceReferenceModel.invoice_id).filter( + InvoiceReferenceModel.status_code.in_((InvoiceReferenceStatus.ACTIVE.value,)) + ) @classmethod def _create_eft_invoices(cls): @@ -348,30 +410,40 @@ def _create_eft_invoices(cls): # information back from the API. You need that information when creating an adjustment otherwise revenue # will flow to the wrong lines. for eft_account in cls._return_eft_accounts(): - invoices = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.payment_account_id == eft_account.id) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) \ - .filter(InvoiceModel.id.notin_(cls._active_invoice_reference_subquery())) \ - .order_by(InvoiceModel.created_on.desc()).all() + invoices = ( + db.session.query(InvoiceModel) + .filter(InvoiceModel.payment_account_id == eft_account.id) + .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) + .filter(InvoiceModel.id.notin_(cls._active_invoice_reference_subquery())) + .order_by(InvoiceModel.created_on.desc()) + .all() + ) if not invoices or not (payment_account := PaymentAccountService.find_by_id(eft_account.id)): continue - cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method(payment_account.id, - PaymentMethod.EFT.value) - if cfs_account.status not in (CfsAccountStatus.ACTIVE.value, CfsAccountStatus.INACTIVE.value): - current_app.logger.info(f'CFS status for account {payment_account.auth_account_id} ' - f'is {payment_account.cfs_account_status} skipping.') + cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method( + payment_account.id, PaymentMethod.EFT.value + ) + if cfs_account.status not in ( + CfsAccountStatus.ACTIVE.value, + CfsAccountStatus.INACTIVE.value, + ): + current_app.logger.info( + f"CFS status for account {payment_account.auth_account_id} " + f"is {payment_account.cfs_account_status} skipping." + ) continue - current_app.logger.info( - f'Found {len(invoices)} EFT invoices for account {payment_account.auth_account_id}') + current_app.logger.info(f"Found {len(invoices)} EFT invoices for account {payment_account.auth_account_id}") for invoice in invoices: - current_app.logger.debug(f'Creating cfs invoice for invoice {invoice.id}') + current_app.logger.debug(f"Creating cfs invoice for invoice {invoice.id}") try: - invoice_response = CFSService.create_account_invoice(transaction_number=invoice.id, - line_items=invoice.payment_line_items, - cfs_account=cfs_account) + invoice_response = CFSService.create_account_invoice( + transaction_number=invoice.id, + line_items=invoice.payment_line_items, + cfs_account=cfs_account, + ) except Exception as e: # NOQA # pylint: disable=broad-except # There is a chance that the error is a timeout from CAS side, # so to make sure we are not missing any data, make a GET call for the invoice we tried to create @@ -383,27 +455,29 @@ def _create_eft_invoices(cls): # since this is a job, delay doesn't cause any performance issue time.sleep(10) invoice_number = generate_transaction_number(str(invoice.id)) - invoice_response = CFSService.get_invoice( - cfs_account=cfs_account, inv_number=invoice_number - ) - has_invoice_created = invoice_response.get('invoice_number', None) == invoice_number - invoice_total_matches = Decimal(invoice_response.get('total', '0')) == invoice.total + invoice_response = CFSService.get_invoice(cfs_account=cfs_account, inv_number=invoice_number) + has_invoice_created = invoice_response.get("invoice_number", None) == invoice_number + invoice_total_matches = Decimal(invoice_response.get("total", "0")) == invoice.total except Exception as exc: # NOQA # pylint: disable=broad-except,unused-variable # Ignore this error, as it is irrelevant and error on outer level is relevant. pass if not has_invoice_created: - capture_message(f'Error on creating EFT invoice: account id={invoice.payment_account.id}, ' - f'auth account : {invoice.payment_account.auth_account_id}, ERROR : {str(e)}', - level='error') + capture_message( + f"Error on creating EFT invoice: account id={invoice.payment_account.id}, " + f"auth account : {invoice.payment_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue if not invoice_total_matches: - capture_message(f'Error on creating EFT invoice: account id={payment_account.id}, ' - f'auth account : {payment_account.auth_account_id}, Invoice exists: ' - f' CAS total: {invoice_response.get("total", 0)}, ' - f'PAY-BC total: {invoice.total}', - level='error') + capture_message( + f"Error on creating EFT invoice: account id={payment_account.id}, " + f"auth account : {payment_account.auth_account_id}, Invoice exists: " + f' CAS total: {invoice_response.get("total", 0)}, ' + f"PAY-BC total: {invoice.total}", + level="error", + ) current_app.logger.error(e) continue @@ -411,8 +485,8 @@ def _create_eft_invoices(cls): # Create ACTIVE invoice reference invoice_reference = EftService.create_invoice_reference( invoice=invoice, - invoice_number=invoice_response.get('invoice_number'), - reference_number=invoice_response.get('pbc_ref_number', None) + invoice_number=invoice_response.get("invoice_number"), + reference_number=invoice_response.get("pbc_ref_number", None), ) db.session.add(invoice_reference) db.session.commit() @@ -425,39 +499,48 @@ def _create_online_banking_invoices(cls): @classmethod def _create_single_invoice_per_purchase(cls, payment_method: PaymentMethod): """Create one CFS invoice per purchase.""" - invoices: List[InvoiceModel] = InvoiceModel.query \ - .filter_by(payment_method_code=payment_method.value) \ - .filter_by(invoice_status_code=InvoiceStatus.CREATED.value) \ - .order_by(InvoiceModel.created_on.asc()).all() - - current_app.logger.info(f'Found {len(invoices)} to be created in CFS.') + invoices: List[InvoiceModel] = ( + InvoiceModel.query.filter_by(payment_method_code=payment_method.value) + .filter_by(invoice_status_code=InvoiceStatus.CREATED.value) + .order_by(InvoiceModel.created_on.asc()) + .all() + ) + + current_app.logger.info(f"Found {len(invoices)} to be created in CFS.") for invoice in invoices: payment_account: PaymentAccountService = PaymentAccountService.find_by_id(invoice.payment_account_id) # Adding this in for the future when we can switch between BCOL and ONLINE_BANKING. - cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method(payment_account.id, - PaymentMethod.ONLINE_BANKING.value) + cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method( + payment_account.id, PaymentMethod.ONLINE_BANKING.value + ) if invoice.payment_method_code == PaymentMethod.ONLINE_BANKING.value: corp_type: CorpTypeModel = CorpTypeModel.find_by_code(invoice.corp_type_code) if not corp_type.is_online_banking_allowed: continue - current_app.logger.debug(f'Creating cfs invoice for invoice {invoice.id}') + current_app.logger.debug(f"Creating cfs invoice for invoice {invoice.id}") try: - invoice_response = CFSService.create_account_invoice(transaction_number=invoice.id, - line_items=invoice.payment_line_items, - cfs_account=cfs_account) + invoice_response = CFSService.create_account_invoice( + transaction_number=invoice.id, + line_items=invoice.payment_line_items, + cfs_account=cfs_account, + ) except Exception as e: # NOQA # pylint: disable=broad-except - capture_message(f'Error on creating Online Banking invoice: account id={payment_account.id}, ' - f'auth account : {payment_account.auth_account_id}, ERROR : {str(e)}', level='error') + capture_message( + f"Error on creating Online Banking invoice: account id={payment_account.id}, " + f"auth account : {payment_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue # Create invoice reference, payment record and a payment transaction InvoiceReference.create( invoice_id=invoice.id, - invoice_number=invoice_response.get('invoice_number'), - reference_number=invoice_response.get('pbc_ref_number', None)) + invoice_number=invoice_response.get("invoice_number"), + reference_number=invoice_response.get("pbc_ref_number", None), + ) invoice.cfs_account_id = payment_account.cfs_account_id invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value diff --git a/jobs/payment-jobs/tasks/common/cgi_ap.py b/jobs/payment-jobs/tasks/common/cgi_ap.py index 0549c5ad3..10fbac333 100644 --- a/jobs/payment-jobs/tasks/common/cgi_ap.py +++ b/jobs/payment-jobs/tasks/common/cgi_ap.py @@ -18,6 +18,7 @@ from flask import current_app from pay_api.utils.enums import DisbursementMethod, EjvFileType from pay_api.utils.util import get_fiscal_year + from tasks.common.dataclasses import APLine from .cgi_ejv import CgiEjv @@ -29,53 +30,61 @@ class CgiAP(CgiEjv): ap_type: EjvFileType @classmethod - def get_batch_header(cls, batch_number, batch_type: str = 'AP'): + def get_batch_header(cls, batch_number, batch_type: str = "AP"): """Return batch header string.""" - return f'{cls._feeder_number()}{batch_type}BH{cls.DELIMITER}{cls._feeder_number()}' \ - f'{get_fiscal_year(datetime.now(tz=timezone.utc))}' \ - f'{batch_number}{cls._message_version()}{cls.DELIMITER}{os.linesep}' + return ( + f"{cls._feeder_number()}{batch_type}BH{cls.DELIMITER}{cls._feeder_number()}" + f"{get_fiscal_year(datetime.now(tz=timezone.utc))}" + f"{batch_number}{cls._message_version()}{cls.DELIMITER}{os.linesep}" + ) @classmethod - def get_batch_trailer(cls, batch_number, batch_total, batch_type: str = 'AP', control_total: int = 0): + def get_batch_trailer(cls, batch_number, batch_total, batch_type: str = "AP", control_total: int = 0): """Return batch trailer string.""" - return f'{cls._feeder_number()}{batch_type}BT{cls.DELIMITER}{cls._feeder_number()}' \ - f'{get_fiscal_year(datetime.now(tz=timezone.utc))}{batch_number}' \ - f'{control_total:0>15}{cls.format_amount(batch_total)}{cls.DELIMITER}{os.linesep}' + return ( + f"{cls._feeder_number()}{batch_type}BT{cls.DELIMITER}{cls._feeder_number()}" + f"{get_fiscal_year(datetime.now(tz=timezone.utc))}{batch_number}" + f"{control_total:0>15}{cls.format_amount(batch_total)}{cls.DELIMITER}{os.linesep}" + ) @classmethod def get_ap_header(cls, total, invoice_number, invoice_date, supplier_number: str = None): """Get AP Invoice Header string.""" - invoice_type = 'ST' + invoice_type = "ST" remit_code = f"{current_app.config.get('CGI_AP_REMITTANCE_CODE'):<4}" - currency = 'CAD' + currency = "CAD" effective_date = cls._get_date(datetime.now(tz=timezone.utc)) invoice_date = cls._get_date(invoice_date) oracle_invoice_batch_name = cls._get_oracle_invoice_batch_name(invoice_number) - disbursement_method = (DisbursementMethod.CHEQUE.value - if cls.ap_type == EjvFileType.REFUND else DisbursementMethod.EFT.value) - term = f'{cls.EMPTY:<50}' if cls.ap_type == EjvFileType.REFUND else f'Immediate{cls.EMPTY:<41}' - ap_header = f'{cls._feeder_number()}APIH{cls.DELIMITER}{cls._supplier_number(supplier_number)}' \ - f'{cls._supplier_location()}{invoice_number:<50}{cls._po_number()}{invoice_type}{invoice_date}' \ - f'GEN {disbursement_method} N{remit_code}{cls.format_amount(total)}{currency}{effective_date}' \ - f'{term}{cls.EMPTY:<60}{cls.EMPTY:<8}{cls.EMPTY:<8}' \ - f'{oracle_invoice_batch_name:<30}{cls.EMPTY:<9}Y{cls.EMPTY:<110}{cls.DELIMITER}{os.linesep}' + disbursement_method = ( + DisbursementMethod.CHEQUE.value if cls.ap_type == EjvFileType.REFUND else DisbursementMethod.EFT.value + ) + term = f"{cls.EMPTY:<50}" if cls.ap_type == EjvFileType.REFUND else f"Immediate{cls.EMPTY:<41}" + ap_header = ( + f"{cls._feeder_number()}APIH{cls.DELIMITER}{cls._supplier_number(supplier_number)}" + f"{cls._supplier_location()}{invoice_number:<50}{cls._po_number()}{invoice_type}{invoice_date}" + f"GEN {disbursement_method} N{remit_code}{cls.format_amount(total)}{currency}{effective_date}" + f"{term}{cls.EMPTY:<60}{cls.EMPTY:<8}{cls.EMPTY:<8}" + f"{oracle_invoice_batch_name:<30}{cls.EMPTY:<9}Y{cls.EMPTY:<110}{cls.DELIMITER}{os.linesep}" + ) return ap_header @classmethod def get_ap_invoice_line(cls, ap_line: APLine, supplier_number: str = None): """Get AP Invoice Line string.""" - commit_line_number = f'{cls.EMPTY:<4}' + commit_line_number = f"{cls.EMPTY:<4}" # Pad Zeros to four digits. EG. 0001 - line_number = f'{ap_line.line_number:04}' + line_number = f"{ap_line.line_number:04}" effective_date = cls._get_date(datetime.now(tz=timezone.utc)) line_code = cls._get_line_code(ap_line) - ap_line = \ - f'{cls._feeder_number()}APIL{cls.DELIMITER}{cls._supplier_number(supplier_number)}' \ - f'{cls._supplier_location()}{ap_line.invoice_number:<50}{line_number}{commit_line_number}' \ - f'{cls.format_amount(ap_line.total)}{line_code}{cls._distribution(ap_line.distribution)}{cls.EMPTY:<55}' \ - f'{effective_date}{cls.EMPTY:<10}{cls.EMPTY:<15}{cls.EMPTY:<15}{cls.EMPTY:<15}{cls.EMPTY:<15}' \ - f'{cls.EMPTY:<20}{cls.EMPTY:<4}{cls.EMPTY:<30}{cls.EMPTY:<25}{cls.EMPTY:<30}{cls.EMPTY:<8}{cls.EMPTY:<1}' \ - f'{cls._dist_vendor(supplier_number)}{cls.EMPTY:<110}{cls.DELIMITER}{os.linesep}' + ap_line = ( + f"{cls._feeder_number()}APIL{cls.DELIMITER}{cls._supplier_number(supplier_number)}" + f"{cls._supplier_location()}{ap_line.invoice_number:<50}{line_number}{commit_line_number}" + f"{cls.format_amount(ap_line.total)}{line_code}{cls._distribution(ap_line.distribution)}{cls.EMPTY:<55}" + f"{effective_date}{cls.EMPTY:<10}{cls.EMPTY:<15}{cls.EMPTY:<15}{cls.EMPTY:<15}{cls.EMPTY:<15}" + f"{cls.EMPTY:<20}{cls.EMPTY:<4}{cls.EMPTY:<30}{cls.EMPTY:<25}{cls.EMPTY:<30}{cls.EMPTY:<8}{cls.EMPTY:<1}" + f"{cls._dist_vendor(supplier_number)}{cls.EMPTY:<110}{cls.DELIMITER}{os.linesep}" + ) return ap_line @classmethod @@ -84,77 +93,86 @@ def get_ap_address(cls, refund_details, routing_slip_number): name_1 = f"{refund_details['name'][:40]:<40}" name_2 = f"{refund_details['name'][40:80]:<40}" - street = refund_details['mailingAddress']['street'] - street_additional = f"{refund_details['mailingAddress']['streetAdditional'][:40]:<40}" \ - if 'streetAdditional' in refund_details['mailingAddress'] else f'{cls.EMPTY:<40}' - address_1 = f'{street[:40]:<40}' + street = refund_details["mailingAddress"]["street"] + street_additional = ( + f"{refund_details['mailingAddress']['streetAdditional'][:40]:<40}" + if "streetAdditional" in refund_details["mailingAddress"] + else f"{cls.EMPTY:<40}" + ) + address_1 = f"{street[:40]:<40}" address_2, address_3 = None, None if len(street) > 80: - address_3 = f'{street[80:120]:<40}' + address_3 = f"{street[80:120]:<40}" elif len(street) > 40: - address_2 = f'{street[40:80]:<40}' + address_2 = f"{street[40:80]:<40}" address_3 = street_additional else: address_2 = street_additional - address_3 = f'{cls.EMPTY:<40}' + address_3 = f"{cls.EMPTY:<40}" city = f"{refund_details['mailingAddress']['city'][:25]:<25}" prov = f"{refund_details['mailingAddress']['region'][:2]:<2}" postal_code = f"{refund_details['mailingAddress']['postalCode'][:10].replace(' ', ''):<10}" country = f"{refund_details['mailingAddress']['country'][:2]:<2}" - ap_address = f'{cls._feeder_number()}APNA{cls.DELIMITER}{cls._supplier_number()}{cls._supplier_location()}' \ - f'{routing_slip_number:<50}{name_1}{name_2}{address_1}{address_2}{address_3}' \ - f'{city}{prov}{postal_code}{country}{cls.DELIMITER}{os.linesep}' + ap_address = ( + f"{cls._feeder_number()}APNA{cls.DELIMITER}{cls._supplier_number()}{cls._supplier_location()}" + f"{routing_slip_number:<50}{name_1}{name_2}{address_1}{address_2}{address_3}" + f"{city}{prov}{postal_code}{country}{cls.DELIMITER}{os.linesep}" + ) return ap_address @classmethod def get_eft_ap_comment(cls, comment, refund_id, short_name_id, supplier_number): """Get AP Comment Override. EFT only.""" - line_text = '0001' - combined_comment = f'{cls.EMPTY:<1}{short_name_id}{cls.EMPTY:<1}-{cls.EMPTY:<1}{comment}'[:40] - ap_comment = f'{cls._feeder_number()}APIC{cls.DELIMITER}{cls._supplier_number(supplier_number)}' \ - f'{cls._supplier_location()}{refund_id:<50}{line_text}{combined_comment}' \ - f'{cls.DELIMITER}{os.linesep}' + line_text = "0001" + combined_comment = f"{cls.EMPTY:<1}{short_name_id}{cls.EMPTY:<1}-{cls.EMPTY:<1}{comment}"[:40] + ap_comment = ( + f"{cls._feeder_number()}APIC{cls.DELIMITER}{cls._supplier_number(supplier_number)}" + f"{cls._supplier_location()}{refund_id:<50}{line_text}{combined_comment}" + f"{cls.DELIMITER}{os.linesep}" + ) return ap_comment @classmethod def get_rs_ap_comment(cls, refund_details, routing_slip_number): """Get AP Comment Override. Routing slip only.""" - if not (cheque_advice := refund_details.get('chequeAdvice', '')): + if not (cheque_advice := refund_details.get("chequeAdvice", "")): return None cheque_advice = cheque_advice[:40] - line_text = '0001' - ap_comment = f'{cls._feeder_number()}APIC{cls.DELIMITER}{cls._supplier_number()}' \ - f'{cls._supplier_location()}{routing_slip_number:<50}{line_text}{cheque_advice}' \ - f'{cls.DELIMITER}{os.linesep}' + line_text = "0001" + ap_comment = ( + f"{cls._feeder_number()}APIC{cls.DELIMITER}{cls._supplier_number()}" + f"{cls._supplier_location()}{routing_slip_number:<50}{line_text}{cheque_advice}" + f"{cls.DELIMITER}{os.linesep}" + ) return ap_comment @classmethod def _supplier_number(cls, supplier_number: str = None): """Return vendor number.""" if supplier_number: - return f'{supplier_number:<9}' + return f"{supplier_number:<9}" match cls.ap_type: case EjvFileType.NON_GOV_DISBURSEMENT: return f"{current_app.config.get('BCA_SUPPLIER_NUMBER'):<9}" case EjvFileType.REFUND: return f"{current_app.config.get('CGI_AP_SUPPLIER_NUMBER'):<9}" case _: - raise RuntimeError('ap_type not selected.') + raise RuntimeError("ap_type not selected.") @classmethod def _dist_vendor(cls, supplier_number: str = None): """Return distribution vendor number.""" if supplier_number: - return f'{supplier_number:<30}' + return f"{supplier_number:<30}" match cls.ap_type: case EjvFileType.NON_GOV_DISBURSEMENT: return f"{current_app.config.get('BCA_SUPPLIER_NUMBER'):<30}" case EjvFileType.REFUND: return f"{current_app.config.get('CGI_AP_SUPPLIER_NUMBER'):<30}" case _: - raise RuntimeError('ap_type not selected.') + raise RuntimeError("ap_type not selected.") @classmethod def _supplier_location(cls): @@ -167,51 +185,51 @@ def _supplier_location(cls): case EjvFileType.EFT_REFUND: return f"{current_app.config.get('EFT_AP_SUPPLIER_LOCATION'):<3}" case _: - raise RuntimeError('ap_type not selected.') + raise RuntimeError("ap_type not selected.") @classmethod def _po_number(cls): """Return PO Number.""" - return f'{cls.EMPTY:<20}' + return f"{cls.EMPTY:<20}" @classmethod def _get_date(cls, date): """Return date.""" - return date.strftime('%Y%m%d') + return date.strftime("%Y%m%d") @classmethod def _distribution(cls, distribution_code: str = None): """Return Distribution Code string.""" match cls.ap_type: case EjvFileType.NON_GOV_DISBURSEMENT: - return f'{distribution_code}0000000000{cls.EMPTY:<16}' + return f"{distribution_code}0000000000{cls.EMPTY:<16}" case EjvFileType.REFUND: return f"{current_app.config.get('CGI_AP_DISTRIBUTION')}{cls.EMPTY:<16}" case EjvFileType.EFT_REFUND: return f"{current_app.config.get('EFT_AP_DISTRIBUTION')}{cls.EMPTY:<16}" case _: - raise RuntimeError('ap_type not selected.') + raise RuntimeError("ap_type not selected.") @classmethod def _get_oracle_invoice_batch_name(cls, invoice_number): """Return Oracle Invoice Batch Name.""" match cls.ap_type: case EjvFileType.NON_GOV_DISBURSEMENT: - return f'{invoice_number}'[:30] + return f"{invoice_number}"[:30] case EjvFileType.REFUND: - return f'REFUND_FAS_RS_{invoice_number}'[:30] + return f"REFUND_FAS_RS_{invoice_number}"[:30] case EjvFileType.EFT_REFUND: - return f'REFUND_EFT_{invoice_number}'[:30] + return f"REFUND_EFT_{invoice_number}"[:30] case _: - raise RuntimeError('ap_type not selected.') + raise RuntimeError("ap_type not selected.") @classmethod def _get_line_code(cls, ap_line: APLine): """Get line code.""" match cls.ap_type: case EjvFileType.REFUND | EjvFileType.EFT_REFUND: - return 'D' + return "D" case EjvFileType.NON_GOV_DISBURSEMENT: - return 'C' if ap_line.is_reversal else 'D' + return "C" if ap_line.is_reversal else "D" case _: - raise RuntimeError('ap_type not selected.') + raise RuntimeError("ap_type not selected.") diff --git a/jobs/payment-jobs/tasks/common/cgi_ejv.py b/jobs/payment-jobs/tasks/common/cgi_ejv.py index 41804afee..c9956c108 100644 --- a/jobs/payment-jobs/tasks/common/cgi_ejv.py +++ b/jobs/payment-jobs/tasks/common/cgi_ejv.py @@ -29,33 +29,33 @@ class CgiEjv: """Base class for CGI EJV.""" DELIMITER = chr(29) # '<0x1d>' - EMPTY = '' + EMPTY = "" @classmethod def get_file_name(cls): """Return file name.""" - date_time = get_nearest_business_day(datetime.now(tz=timezone.utc)).strftime('%Y%m%d%H%M%S') - return f'INBOX.F{cls._feeder_number()}.{date_time}' + date_time = get_nearest_business_day(datetime.now(tz=timezone.utc)).strftime("%Y%m%d%H%M%S") + return f"INBOX.F{cls._feeder_number()}.{date_time}" @classmethod def get_journal_batch_name(cls, batch_number: str): """Return journal batch name.""" - return f'{cls.ministry()}{batch_number}{cls.EMPTY:<14}' + return f"{cls.ministry()}{batch_number}{cls.EMPTY:<14}" @classmethod def _feeder_number(cls): """Return feeder number.""" - return current_app.config.get('CGI_FEEDER_NUMBER') + return current_app.config.get("CGI_FEEDER_NUMBER") @classmethod def ministry(cls): """Return ministry prefix.""" - return current_app.config.get('CGI_MINISTRY_PREFIX') + return current_app.config.get("CGI_MINISTRY_PREFIX") @classmethod def _message_version(cls): """Return message version.""" - return current_app.config.get('CGI_MESSAGE_VERSION') + return current_app.config.get("CGI_MESSAGE_VERSION") @classmethod def _supplier_number(cls): @@ -63,31 +63,44 @@ def _supplier_number(cls): return f"{current_app.config.get('CGI_EJV_SUPPLIER_NUMBER'):<9}" @classmethod - def get_jv_line(cls, # pylint:disable=too-many-arguments - batch_type, distribution, description, effective_date, flow_through, journal_name, amount, - line_number, credit_debit): + def get_jv_line( # pylint:disable=too-many-arguments + cls, + batch_type, + distribution, + description, + effective_date, + flow_through, + journal_name, + amount, + line_number, + credit_debit, + ): """Return jv line string.""" - return f'{cls._feeder_number()}{batch_type}JD{cls.DELIMITER}{journal_name}' \ - f'{line_number:0>5}{effective_date}{distribution}{cls._supplier_number()}' \ - f'{cls.format_amount(amount)}{credit_debit}{description}{flow_through}' \ - f'{cls.DELIMITER}{os.linesep}' + return ( + f"{cls._feeder_number()}{batch_type}JD{cls.DELIMITER}{journal_name}" + f"{line_number:0>5}{effective_date}{distribution}{cls._supplier_number()}" + f"{cls.format_amount(amount)}{credit_debit}{description}{flow_through}" + f"{cls.DELIMITER}{os.linesep}" + ) @classmethod def get_batch_header(cls, batch_number, batch_type): """Return batch header string.""" - return f'{cls._feeder_number()}{batch_type}BH{cls.DELIMITER}{cls._feeder_number()}' \ - f'{get_fiscal_year(datetime.now(tz=timezone.utc))}' \ - f'{batch_number}{cls._message_version()}{cls.DELIMITER}{os.linesep}' + return ( + f"{cls._feeder_number()}{batch_type}BH{cls.DELIMITER}{cls._feeder_number()}" + f"{get_fiscal_year(datetime.now(tz=timezone.utc))}" + f"{batch_number}{cls._message_version()}{cls.DELIMITER}{os.linesep}" + ) @classmethod def get_effective_date(cls): """Return effective date..""" - return get_nearest_business_day(datetime.now(tz=timezone.utc)).strftime('%Y%m%d') + return get_nearest_business_day(datetime.now(tz=timezone.utc)).strftime("%Y%m%d") @classmethod def format_amount(cls, amount: float): """Format and return amount to fix 2 decimal places and total of length 15 prefixed with zeroes.""" - formatted_amount: str = f'{amount:.2f}' + formatted_amount: str = f"{amount:.2f}" return formatted_amount.zfill(15) @classmethod @@ -97,65 +110,73 @@ def upload_to_minio(cls, content, file_name, file_size): put_object(content, file_name, file_size) except Exception as e: # NOQA # pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.error(f'upload to minio failed for the file: {file_name}') + current_app.logger.error(f"upload to minio failed for the file: {file_name}") @classmethod def get_distribution_string(cls, dist_code: DistributionCodeModel): """Return GL code combination for the distribution.""" - return f'{dist_code.client}{dist_code.responsibility_centre}{dist_code.service_line}' \ - f'{dist_code.stob}{dist_code.project_code}0000000000{cls.EMPTY:<16}' + return ( + f"{dist_code.client}{dist_code.responsibility_centre}{dist_code.service_line}" + f"{dist_code.stob}{dist_code.project_code}0000000000{cls.EMPTY:<16}" + ) @classmethod def upload(cls, ejv_content, file_name, file_path_with_name, trg_file_path): """Upload to ftp and to minio.""" upload_to_ftp(file_path_with_name, trg_file_path) # Upload to MINIO - cls.upload_to_minio(content=ejv_content.encode(), - file_name=file_name, - file_size=os.stat(file_path_with_name).st_size) + cls.upload_to_minio( + content=ejv_content.encode(), + file_name=file_name, + file_size=os.stat(file_path_with_name).st_size, + ) @classmethod def get_jv_header(cls, batch_type, journal_batch_name, journal_name, total): """Get JV Header string.""" - ejv_content = f'{cls._feeder_number()}{batch_type}JH{cls.DELIMITER}{journal_name}' \ - f'{journal_batch_name}{cls.format_amount(total)}ACAD{cls.EMPTY:<100}{cls.EMPTY:<110}' \ - f'{cls.DELIMITER}{os.linesep}' + ejv_content = ( + f"{cls._feeder_number()}{batch_type}JH{cls.DELIMITER}{journal_name}" + f"{journal_batch_name}{cls.format_amount(total)}ACAD{cls.EMPTY:<100}{cls.EMPTY:<110}" + f"{cls.DELIMITER}{os.linesep}" + ) return ejv_content @classmethod def get_batch_trailer(cls, batch_number, batch_total, batch_type, control_total): """Return batch trailer string.""" - return f'{cls._feeder_number()}{batch_type}BT{cls.DELIMITER}{cls._feeder_number()}' \ - f'{get_fiscal_year(datetime.now(tz=timezone.utc))}{batch_number}' \ - f'{control_total:0>15}{cls.format_amount(batch_total)}{cls.DELIMITER}{os.linesep}' + return ( + f"{cls._feeder_number()}{batch_type}BT{cls.DELIMITER}{cls._feeder_number()}" + f"{get_fiscal_year(datetime.now(tz=timezone.utc))}{batch_number}" + f"{control_total:0>15}{cls.format_amount(batch_total)}{cls.DELIMITER}{os.linesep}" + ) @classmethod def get_journal_name(cls, ejv_header_id: int): """Return journal name.""" - return f'{cls.ministry()}{ejv_header_id:0>8}' + return f"{cls.ministry()}{ejv_header_id:0>8}" @classmethod def get_batch_number(cls, ejv_file_id: int) -> str: """Return batch number.""" - return f'{ejv_file_id:0>9}' + return f"{ejv_file_id:0>9}" @classmethod def get_trg_suffix(cls): """Return trigger file suffix.""" - return current_app.config.get('CGI_TRIGGER_FILE_SUFFIX') + return current_app.config.get("CGI_TRIGGER_FILE_SUFFIX") @classmethod def create_inbox_and_trg_files(cls, ejv_content): """Create inbox and trigger files.""" file_path: str = tempfile.gettempdir() file_name = cls.get_file_name() - file_path_with_name = f'{file_path}/{file_name}' - trg_file_path = f'{file_path_with_name}.{cls.get_trg_suffix()}' - with open(file_path_with_name, 'a+', encoding='utf-8') as jv_file: + file_path_with_name = f"{file_path}/{file_name}" + trg_file_path = f"{file_path_with_name}.{cls.get_trg_suffix()}" + with open(file_path_with_name, "a+", encoding="utf-8") as jv_file: jv_file.write(ejv_content) jv_file.close() # TRG File - with open(trg_file_path, 'a+', encoding='utf-8') as trg_file: - trg_file.write('') + with open(trg_file_path, "a+", encoding="utf-8") as trg_file: + trg_file.write("") trg_file.close() return file_path_with_name, trg_file_path, file_name diff --git a/jobs/payment-jobs/tasks/common/dataclasses.py b/jobs/payment-jobs/tasks/common/dataclasses.py index 2e260971a..674503df7 100644 --- a/jobs/payment-jobs/tasks/common/dataclasses.py +++ b/jobs/payment-jobs/tasks/common/dataclasses.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. """Common dataclasses for tasks, dataclasses allow for cleaner code with autocompletion in vscode.""" -from decimal import Decimal - from dataclasses import dataclass +from decimal import Decimal from typing import List, Optional + from dataclass_wizard import JSONWizard from pay_api.models import DistributionCode as DistributionCodeModel from pay_api.models import Invoice as InvoiceModel from pay_api.models import PartnerDisbursements as PartnerDisbursementModel from pay_api.models import PaymentLineItem as LineItemModel from pay_api.utils.enums import InvoiceStatus + from tasks.common.enums import PaymentDetailsGlStatus @@ -80,12 +81,24 @@ class APLine: distribution: Optional[str] = None @classmethod - def from_invoice_and_line_item(cls, invoice: InvoiceModel, line_item: LineItemModel, line_number: int, - distribution: str): + def from_invoice_and_line_item( + cls, + invoice: InvoiceModel, + line_item: LineItemModel, + line_number: int, + distribution: str, + ): """Build dataclass object from invoice.""" # Note the invoice_date should be the payment_date in the future. - return cls(total=line_item.total, invoice_number=invoice.id, - line_number=line_number, - is_reversal=invoice.invoice_status_code in - [InvoiceStatus.REFUNDED.value, InvoiceStatus.REFUND_REQUESTED.value, InvoiceStatus.CREDITED.value], - distribution=distribution) + return cls( + total=line_item.total, + invoice_number=invoice.id, + line_number=line_number, + is_reversal=invoice.invoice_status_code + in [ + InvoiceStatus.REFUNDED.value, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.CREDITED.value, + ], + distribution=distribution, + ) diff --git a/jobs/payment-jobs/tasks/common/enums.py b/jobs/payment-jobs/tasks/common/enums.py index ba6157840..f549e8f07 100644 --- a/jobs/payment-jobs/tasks/common/enums.py +++ b/jobs/payment-jobs/tasks/common/enums.py @@ -18,8 +18,8 @@ class PaymentDetailsGlStatus(Enum): """Payment details GL status.""" - PAID = 'PAID' - INPRG = 'INPRG' - RJCT = 'RJCT' # Should have refundglerrormessage - CMPLT = 'CMPLT' - DECLINED = 'DECLINED' + PAID = "PAID" + INPRG = "INPRG" + RJCT = "RJCT" # Should have refundglerrormessage + CMPLT = "CMPLT" + DECLINED = "DECLINED" diff --git a/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py b/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py index fc102d243..0d256c7eb 100644 --- a/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py +++ b/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py @@ -23,7 +23,13 @@ from pay_api.services.direct_pay_service import DirectPayService from pay_api.services.oauth_service import OAuthService from pay_api.utils.enums import ( - AuthHeaderType, ContentType, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus) + AuthHeaderType, + ContentType, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, + PaymentStatus, +) from sentry_sdk import capture_message from tasks.common.dataclasses import OrderStatus @@ -63,19 +69,24 @@ def handle_non_complete_credit_card_refunds(cls): Log the error down, contact PAYBC if this happens. Set refund.gl_error = """ - include_invoice_statuses = [InvoiceStatus.REFUND_REQUESTED.value, InvoiceStatus.REFUNDED.value] - invoices: List[InvoiceModel] = InvoiceModel.query \ - .outerjoin(RefundModel, RefundModel.invoice_id == Invoice.id)\ - .filter(InvoiceModel.payment_method_code == PaymentMethod.DIRECT_PAY.value) \ - .filter(InvoiceModel.invoice_status_code.in_(include_invoice_statuses)) \ - .filter(RefundModel.gl_posted.is_(None) & RefundModel.gl_error.is_(None)) \ - .order_by(InvoiceModel.created_on.asc()).all() - - current_app.logger.info(f'Found {len(invoices)} invoices to process for refunds.') + include_invoice_statuses = [ + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.REFUNDED.value, + ] + invoices: List[InvoiceModel] = ( + InvoiceModel.query.outerjoin(RefundModel, RefundModel.invoice_id == Invoice.id) + .filter(InvoiceModel.payment_method_code == PaymentMethod.DIRECT_PAY.value) + .filter(InvoiceModel.invoice_status_code.in_(include_invoice_statuses)) + .filter(RefundModel.gl_posted.is_(None) & RefundModel.gl_error.is_(None)) + .order_by(InvoiceModel.created_on.asc()) + .all() + ) + + current_app.logger.info(f"Found {len(invoices)} invoices to process for refunds.") for invoice in invoices: try: # Cron is setup to run between 6 to 8 UTC. Feedback is updated after 11pm. - current_app.logger.debug(f'Processing invoice: {invoice.id} - created on: {invoice.created_on}') + current_app.logger.debug(f"Processing invoice: {invoice.id} - created on: {invoice.created_on}") status = OrderStatus.from_dict(cls._query_order_status(invoice)) if cls._is_glstatus_rejected_or_declined(status): cls._refund_error(status, invoice) @@ -84,34 +95,45 @@ def handle_non_complete_credit_card_refunds(cls): elif cls._is_status_complete(status): cls._refund_complete(invoice) else: - current_app.logger.info('No action taken for invoice.') + current_app.logger.info("No action taken for invoice.") except Exception as e: # NOQA # pylint: disable=broad-except disable=invalid-name - capture_message(f'Error on processing credit card refund - invoice: {invoice.id}' - f'status={invoice.invoice_status_code} ERROR : {str(e)}', level='error') + capture_message( + f"Error on processing credit card refund - invoice: {invoice.id}" + f"status={invoice.invoice_status_code} ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e, exc_info=True) @classmethod def _query_order_status(cls, invoice: Invoice): """Request order status for CFS.""" - access_token: str = DirectPayService().get_token().json().get('access_token') - paybc_ref_number: str = current_app.config.get('PAYBC_DIRECT_PAY_REF_NUMBER') - paybc_svc_base_url = current_app.config.get('PAYBC_DIRECT_PAY_BASE_URL') + access_token: str = DirectPayService().get_token().json().get("access_token") + paybc_ref_number: str = current_app.config.get("PAYBC_DIRECT_PAY_REF_NUMBER") + paybc_svc_base_url = current_app.config.get("PAYBC_DIRECT_PAY_BASE_URL") completed_reference = list( - filter(lambda reference: (reference.status_code == InvoiceReferenceStatus.COMPLETED.value), - invoice.references))[0] - payment_url: str = \ - f'{paybc_svc_base_url}/paybc/payment/{paybc_ref_number}/{completed_reference.invoice_number}' + filter( + lambda reference: (reference.status_code == InvoiceReferenceStatus.COMPLETED.value), + invoice.references, + ) + )[0] + payment_url: str = f"{paybc_svc_base_url}/paybc/payment/{paybc_ref_number}/{completed_reference.invoice_number}" payment_response = OAuthService.get(payment_url, access_token, AuthHeaderType.BEARER, ContentType.JSON).json() return payment_response @classmethod def _refund_error(cls, status: OrderStatus, invoice: Invoice): """Log error for rejected GL status.""" - current_app.logger.error(f'Refund error - Invoice: {invoice.id} - detected RJCT/DECLINED on refund,' - "contact PAYBC if it's RJCT.") - errors = ' '.join([refund_data.refundglerrormessage.strip() for revenue_line in status.revenue - for refund_data in revenue_line.refund_data])[:250] - current_app.logger.error(f'Refund error - Invoice: {invoice.id} - glerrormessage: {errors}') + current_app.logger.error( + f"Refund error - Invoice: {invoice.id} - detected RJCT/DECLINED on refund," "contact PAYBC if it's RJCT." + ) + errors = " ".join( + [ + refund_data.refundglerrormessage.strip() + for revenue_line in status.revenue + for refund_data in revenue_line.refund_data + ] + )[:250] + current_app.logger.error(f"Refund error - Invoice: {invoice.id} - glerrormessage: {errors}") refund = RefundModel.find_by_invoice_id(invoice.id) refund.gl_error = errors refund.save() @@ -128,8 +150,7 @@ def _refund_complete(cls, invoice: Invoice): """Refund was successfully posted to a GL. Set gl_posted to now (filtered out).""" # Set these to refunded, incase we skipped the PAID state and went to CMPLT cls._set_invoice_and_payment_to_refunded(invoice) - current_app.logger.info( - 'Refund complete - GL was posted - setting refund.gl_posted to now.') + current_app.logger.info("Refund complete - GL was posted - setting refund.gl_posted to now.") refund = RefundModel.find_by_invoice_id(invoice.id) refund.gl_posted = datetime.now(tz=timezone.utc) refund.save() @@ -137,8 +158,11 @@ def _refund_complete(cls, invoice: Invoice): @staticmethod def _is_glstatus_rejected_or_declined(status: OrderStatus) -> bool: """Check for bad refundglstatus.""" - return any(refund_data.refundglstatus in [PaymentDetailsGlStatus.RJCT, PaymentDetailsGlStatus.DECLINED] - for line in status.revenue for refund_data in line.refund_data) + return any( + refund_data.refundglstatus in [PaymentDetailsGlStatus.RJCT, PaymentDetailsGlStatus.DECLINED] + for line in status.revenue + for refund_data in line.refund_data + ) @staticmethod def _is_status_paid_and_invoice_refund_requested(status: OrderStatus, invoice: Invoice) -> bool: @@ -165,7 +189,7 @@ def _is_status_complete(status: OrderStatus) -> bool: @staticmethod def _set_invoice_and_payment_to_refunded(invoice: Invoice): """Set invoice and payment to REFUNDED.""" - current_app.logger.info('Invoice & Payment set to REFUNDED, add refund_date.') + current_app.logger.info("Invoice & Payment set to REFUNDED, add refund_date.") invoice.invoice_status_code = InvoiceStatus.REFUNDED.value invoice.refund_date = datetime.now(tz=timezone.utc) invoice.save() diff --git a/jobs/payment-jobs/tasks/distribution_task.py b/jobs/payment-jobs/tasks/distribution_task.py index 10ae79b7a..22cd224ce 100644 --- a/jobs/payment-jobs/tasks/distribution_task.py +++ b/jobs/payment-jobs/tasks/distribution_task.py @@ -24,10 +24,9 @@ from pay_api.services.oauth_service import OAuthService from pay_api.utils.enums import AuthHeaderType, ContentType, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod - -STATUS_PAID = 'PAID' -STATUS_NOT_PROCESSED = ('PAID', 'RJCT') -DECIMAL_PRECISION = '.2f' +STATUS_PAID = "PAID" +STATUS_NOT_PROCESSED = ("PAID", "RJCT") +DECIMAL_PRECISION = ".2f" class DistributionTask: @@ -44,18 +43,19 @@ def update_failed_distributions(cls): # pylint:disable=too-many-locals 4. If yes, update the revenue details. 5. Update the invoice status as PAID or REFUNDED and save. """ - invoice_statuses = [InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value, - InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value] - gl_update_invoices = InvoiceModel.query.filter( - InvoiceModel.invoice_status_code.in_(invoice_statuses)).all() - current_app.logger.debug(f'Found {len(gl_update_invoices)} invoices to update revenue details.') + invoice_statuses = [ + InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value, + InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value, + ] + gl_update_invoices = InvoiceModel.query.filter(InvoiceModel.invoice_status_code.in_(invoice_statuses)).all() + current_app.logger.debug(f"Found {len(gl_update_invoices)} invoices to update revenue details.") if len(gl_update_invoices) == 0: return - access_token: str = DirectPayService().get_token().json().get('access_token') - paybc_ref_number: str = current_app.config.get('PAYBC_DIRECT_PAY_REF_NUMBER') - paybc_svc_base_url = current_app.config.get('PAYBC_DIRECT_PAY_BASE_URL') + access_token: str = DirectPayService().get_token().json().get("access_token") + paybc_ref_number: str = current_app.config.get("PAYBC_DIRECT_PAY_REF_NUMBER") + paybc_svc_base_url = current_app.config.get("PAYBC_DIRECT_PAY_BASE_URL") for gl_update_invoice in gl_update_invoices: payment: PaymentModel = PaymentModel.find_payment_for_invoice(gl_update_invoice.id) # For now handle only GL updates for Direct Pay, more to come in future @@ -64,20 +64,24 @@ def update_failed_distributions(cls): # pylint:disable=too-many-locals continue active_reference = list( - filter(lambda reference: (reference.status_code == InvoiceReferenceStatus.COMPLETED.value), - gl_update_invoice.references))[0] - payment_url: str = \ - f'{paybc_svc_base_url}/paybc/payment/{paybc_ref_number}/{active_reference.invoice_number}' + filter( + lambda reference: (reference.status_code == InvoiceReferenceStatus.COMPLETED.value), + gl_update_invoice.references, + ) + )[0] + payment_url: str = ( + f"{paybc_svc_base_url}/paybc/payment/{paybc_ref_number}/{active_reference.invoice_number}" + ) payment_details: dict = cls.get_payment_details(payment_url, access_token) if not payment_details: - current_app.logger.error('No payment details found for invoice.') + current_app.logger.error("No payment details found for invoice.") continue target_status, target_gl_status = cls.get_status_fields(gl_update_invoice.invoice_status_code) if target_status is None or payment_details.get(target_status) == STATUS_PAID: has_gl_completed: bool = True - for revenue in payment_details.get('revenue'): + for revenue in payment_details.get("revenue"): if revenue.get(target_gl_status) in STATUS_NOT_PROCESSED: has_gl_completed = False if not has_gl_completed: @@ -89,42 +93,52 @@ def get_status_fields(cls, invoice_status_code: str) -> tuple: """Get status fields for invoice status code.""" if invoice_status_code == InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value: # Refund doesn't use a top level status, as partial refunds may occur. - return None, 'refundglstatus' - return 'paymentstatus', 'glstatus' + return None, "refundglstatus" + return "paymentstatus", "glstatus" @classmethod def update_revenue_lines(cls, invoice: InvoiceModel, payment_url: str, access_token: str): """Update revenue lines for the invoice.""" post_revenue_payload = cls.generate_post_revenue_payload(invoice) - OAuthService.post(payment_url, access_token, AuthHeaderType.BEARER, ContentType.JSON, - post_revenue_payload) + OAuthService.post( + payment_url, + access_token, + AuthHeaderType.BEARER, + ContentType.JSON, + post_revenue_payload, + ) @classmethod def generate_post_revenue_payload(cls, invoice: InvoiceModel): """Generate the payload for POSTing revenue to paybc.""" - post_revenue_payload = { - 'revenue': [] - } + post_revenue_payload = {"revenue": []} payment_line_items = PaymentLineItemModel.find_by_invoice_ids([invoice.id]) index: int = 0 for payment_line_item in payment_line_items: fee_distribution_code: DistributionCodeModel = DistributionCodeModel.find_by_id( - payment_line_item.fee_distribution_id) + payment_line_item.fee_distribution_id + ) if payment_line_item.total is not None and payment_line_item.total > 0: index = index + 1 - post_revenue_payload['revenue'].append( - cls.get_revenue_details(index, fee_distribution_code, payment_line_item.total)) + post_revenue_payload["revenue"].append( + cls.get_revenue_details(index, fee_distribution_code, payment_line_item.total) + ) if payment_line_item.service_fees is not None and payment_line_item.service_fees > 0: service_fee_distribution_code = DistributionCodeModel.find_by_id( - fee_distribution_code.distribution_code_id) + fee_distribution_code.distribution_code_id + ) index = index + 1 - post_revenue_payload['revenue'].append( - cls.get_revenue_details(index, service_fee_distribution_code, - payment_line_item.service_fees)) + post_revenue_payload["revenue"].append( + cls.get_revenue_details( + index, + service_fee_distribution_code, + payment_line_item.service_fees, + ) + ) return post_revenue_payload @classmethod @@ -136,14 +150,16 @@ def get_payment_details(cls, payment_url: str, access_token: str): @classmethod def get_revenue_details(cls, index: int, dist_code: DistributionCodeModel, amount: float): """Get the receipt details by calling PayBC web service.""" - revenue_account = f'{dist_code.client}.{dist_code.responsibility_centre}.' \ - f'{dist_code.service_line}.{dist_code.stob}.{dist_code.project_code}' \ - f'.000000.0000' + revenue_account = ( + f"{dist_code.client}.{dist_code.responsibility_centre}." + f"{dist_code.service_line}.{dist_code.stob}.{dist_code.project_code}" + f".000000.0000" + ) return { - 'lineNumber': str(index), - 'revenueAccount': revenue_account, - 'revenueAmount': format(amount, DECIMAL_PRECISION) + "lineNumber": str(index), + "revenueAccount": revenue_account, + "revenueAmount": format(amount, DECIMAL_PRECISION), } @classmethod @@ -158,4 +174,4 @@ def update_invoice_to_refunded_or_paid(cls, invoice: InvoiceModel): else: invoice.invoice_status_code = InvoiceStatus.PAID.value invoice.save() - current_app.logger.info(f'Updated invoice : {invoice.id}') + current_app.logger.info(f"Updated invoice : {invoice.id}") diff --git a/jobs/payment-jobs/tasks/eft_overpayment_notification_task.py b/jobs/payment-jobs/tasks/eft_overpayment_notification_task.py index 97efa7c4f..552639001 100644 --- a/jobs/payment-jobs/tasks/eft_overpayment_notification_task.py +++ b/jobs/payment-jobs/tasks/eft_overpayment_notification_task.py @@ -41,25 +41,35 @@ class EFTOverpaymentNotificationTask: # pylint: disable=too-few-public-methods @classmethod def _get_short_names_with_credits_remaining(cls): """Create base query for returning short names with any remaining credits by filter date.""" - query = (db.session.query(EFTShortnameModel) - .distinct(EFTShortnameModel.id) - .join(EFTCreditModel, EFTCreditModel.short_name_id == EFTShortnameModel.id) - .filter(EFTCreditModel.remaining_amount > 0) - .order_by(EFTShortnameModel.id, EFTCreditModel.created_on.asc()) - ) + query = ( + db.session.query(EFTShortnameModel) + .distinct(EFTShortnameModel.id) + .join(EFTCreditModel, EFTCreditModel.short_name_id == EFTShortnameModel.id) + .filter(EFTCreditModel.remaining_amount > 0) + .order_by(EFTShortnameModel.id, EFTCreditModel.created_on.asc()) + ) return query @classmethod def _get_today_overpaid_linked_short_names(cls): """Get linked short names that have received a payment today and overpaid.""" filter_date = cls.date_override if cls.date_override is not None else datetime.now(tz=timezone.utc).date() - query = (cls._get_short_names_with_credits_remaining() - .join(EFTShortnameLinkModel, - and_(EFTShortnameLinkModel.eft_short_name_id == EFTShortnameModel.id, - EFTShortnameLinkModel.status_code.in_([EFTShortnameStatus.LINKED.value, - EFTShortnameStatus.PENDING.value])) - ) - .filter(func.date(EFTCreditModel.created_on) == filter_date)) + query = ( + cls._get_short_names_with_credits_remaining() + .join( + EFTShortnameLinkModel, + and_( + EFTShortnameLinkModel.eft_short_name_id == EFTShortnameModel.id, + EFTShortnameLinkModel.status_code.in_( + [ + EFTShortnameStatus.LINKED.value, + EFTShortnameStatus.PENDING.value, + ] + ), + ), + ) + .filter(func.date(EFTCreditModel.created_on) == filter_date) + ) return query.all() @classmethod @@ -67,15 +77,23 @@ def _get_unlinked_short_names_for_duration(cls, days_duration: int = 30): """Get short names that have been unlinked for a duration in days.""" execution_date = cls.date_override if cls.date_override is not None else datetime.now(tz=timezone.utc).date() duration_date = execution_date - timedelta(days=days_duration) - query = (cls._get_short_names_with_credits_remaining() - .outerjoin(EFTShortnameLinkModel, - and_(EFTShortnameLinkModel.eft_short_name_id == EFTShortnameModel.id, - EFTShortnameLinkModel.status_code.in_([EFTShortnameStatus.LINKED.value, - EFTShortnameStatus.PENDING.value])) - ) - .filter(EFTShortnameLinkModel.id.is_(None)) - .filter(func.date(EFTShortnameModel.created_on) == duration_date) - ) + query = ( + cls._get_short_names_with_credits_remaining() + .outerjoin( + EFTShortnameLinkModel, + and_( + EFTShortnameLinkModel.eft_short_name_id == EFTShortnameModel.id, + EFTShortnameLinkModel.status_code.in_( + [ + EFTShortnameStatus.LINKED.value, + EFTShortnameStatus.PENDING.value, + ] + ), + ), + ) + .filter(EFTShortnameLinkModel.id.is_(None)) + .filter(func.date(EFTShortnameModel.created_on) == duration_date) + ) return query.all() @@ -90,8 +108,8 @@ def process_overpayment_notification(cls, date_override=None): try: cls.short_names = {} if date_override: - cls.date_override = datetime.strptime(date_override, '%Y-%m-%d') if date_override else None - current_app.logger.info(f'Using date override : {date_override}') + cls.date_override = datetime.strptime(date_override, "%Y-%m-%d") if date_override else None + current_app.logger.info(f"Using date override : {date_override}") # Get short names that have EFT credit rows created based on current / override date indicating payment # was received and credits remaining indicate overpayment @@ -102,14 +120,14 @@ def process_overpayment_notification(cls, date_override=None): cls._update_short_name_dict(linked_short_names) cls._update_short_name_dict(unlinked_short_names) - current_app.logger.info(f'Sending over payment notifications for {len(cls.short_names)} short names.') + current_app.logger.info(f"Sending over payment notifications for {len(cls.short_names)} short names.") cls._send_notifications() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - 'Error on processing over payment notifications' - f'ERROR : {str(e)}', level='error') - current_app.logger.error( - 'Error on processing over payment notifications', exc_info=True) + "Error on processing over payment notifications" f"ERROR : {str(e)}", + level="error", + ) + current_app.logger.error("Error on processing over payment notifications", exc_info=True) @classmethod def _send_notifications(cls): @@ -121,10 +139,12 @@ def _send_notifications(cls): for short_name_id, name in cls.short_names.items(): credit_balance = EFTCreditModel.get_eft_credit_balance(short_name_id) template_params = { - 'shortNameId': short_name_id, - 'shortName': name, - 'unsettledAmount': f'{credit_balance:,.2f}' + "shortNameId": short_name_id, + "shortName": name, + "unsettledAmount": f"{credit_balance:,.2f}", } - send_email(recipients=qualified_receiver_recipients, - subject=f'Pending Unsettled Amount for Short Name {name}', - body=_render_eft_overpayment_template(template_params)) + send_email( + recipients=qualified_receiver_recipients, + subject=f"Pending Unsettled Amount for Short Name {name}", + body=_render_eft_overpayment_template(template_params), + ) diff --git a/jobs/payment-jobs/tasks/eft_statement_due_task.py b/jobs/payment-jobs/tasks/eft_statement_due_task.py index 6e470ebb1..492adb6a6 100644 --- a/jobs/payment-jobs/tasks/eft_statement_due_task.py +++ b/jobs/payment-jobs/tasks/eft_statement_due_task.py @@ -27,8 +27,8 @@ from pay_api.models.statement import Statement as StatementModel from pay_api.models.statement_invoices import StatementInvoices as StatementInvoicesModel from pay_api.models.statement_recipients import StatementRecipients as StatementRecipientsModel -from pay_api.services.flags import flags from pay_api.services import NonSufficientFundsService +from pay_api.services.flags import flags from pay_api.services.statement import Statement from pay_api.services.statement_settings import StatementSettings as StatementSettingsService from pay_api.utils.enums import InvoiceStatus, PaymentMethod, StatementFrequency @@ -43,15 +43,18 @@ # IMPORTANT: Due to the nature of dates, run this job at least 08:00 UTC or greater. # Otherwise it could be triggered the day before due to timeshift for PDT/PST. # It also needs to run after the statements job. -class EFTStatementDueTask: # pylint: disable=too-few-public-methods +class EFTStatementDueTask: # pylint: disable=too-few-public-methods """Task to notify admin for unpaid statements. This is currently for EFT payment method invoices only. This may be expanded to PAD and ONLINE BANKING in the future. """ - unpaid_status = [InvoiceStatus.SETTLEMENT_SCHEDULED.value, InvoiceStatus.PARTIAL.value, - InvoiceStatus.APPROVED.value] + unpaid_status = [ + InvoiceStatus.SETTLEMENT_SCHEDULED.value, + InvoiceStatus.PARTIAL.value, + InvoiceStatus.APPROVED.value, + ] action_date_override = None auth_account_override = None statement_date_override = None @@ -60,27 +63,30 @@ class EFTStatementDueTask: # pylint: disable=too-few-public-methods def process_override_command(cls, action, date_override): """Process override action.""" if date_override is None: - current_app.logger.error(f'Expecting date override for action: {action}.') + current_app.logger.error(f"Expecting date override for action: {action}.") - date_override = datetime.strptime(date_override, '%Y-%m-%d') + date_override = datetime.strptime(date_override, "%Y-%m-%d") match action: - case 'NOTIFICATION': + case "NOTIFICATION": cls.action_date_override = date_override.date() cls.statement_date_override = date_override cls._notify_for_monthly() - case 'OVERDUE': + case "OVERDUE": cls.action_date_override = date_override cls._update_invoice_overdue_status() case _: - current_app.logger.error(f'Unsupported action override: {action}.') + current_app.logger.error(f"Unsupported action override: {action}.") @classmethod - def process_unpaid_statements(cls, - action_override=None, - date_override=None, - auth_account_override=None, statement_date_override=None): + def process_unpaid_statements( + cls, + action_override=None, + date_override=None, + auth_account_override=None, + statement_date_override=None, + ): """Notify for unpaid statements with an amount owing.""" - eft_enabled = flags.is_on('enable-eft-payment-method', default=False) + eft_enabled = flags.is_on("enable-eft-payment-method", default=False) if eft_enabled: cls.auth_account_override = auth_account_override cls.statement_date_override = statement_date_override @@ -97,68 +103,97 @@ def _update_invoice_overdue_status(cls): # Needs to be non timezone aware. if cls.action_date_override: now = cls.action_date_override.replace(hour=8) - offset_hours = -now.astimezone(pytz.timezone('America/Vancouver')).utcoffset().total_seconds() / 60 / 60 + offset_hours = -now.astimezone(pytz.timezone("America/Vancouver")).utcoffset().total_seconds() / 60 / 60 now = now.replace(hour=int(offset_hours), minute=0, second=0) else: now = datetime.now(tz=timezone.utc).replace(tzinfo=None) - query = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value, - InvoiceModel.overdue_date.isnot(None), - InvoiceModel.overdue_date <= now, - InvoiceModel.invoice_status_code.in_(cls.unpaid_status)) + query = db.session.query(InvoiceModel).filter( + InvoiceModel.payment_method_code == PaymentMethod.EFT.value, + InvoiceModel.overdue_date.isnot(None), + InvoiceModel.overdue_date <= now, + InvoiceModel.invoice_status_code.in_(cls.unpaid_status), + ) if cls.auth_account_override: - current_app.logger.info(f'Using auth account override for auth_account_id: {cls.auth_account_override}') - payment_account_id = db.session.query(PaymentAccountModel.id) \ - .filter(PaymentAccountModel.auth_account_id == cls.auth_account_override) \ + current_app.logger.info(f"Using auth account override for auth_account_id: {cls.auth_account_override}") + payment_account_id = ( + db.session.query(PaymentAccountModel.id) + .filter(PaymentAccountModel.auth_account_id == cls.auth_account_override) .one() + ) query = query.filter(InvoiceModel.payment_account_id == payment_account_id[0]) - query.update({InvoiceModel.invoice_status_code: InvoiceStatus.OVERDUE.value}, synchronize_session='fetch') + query.update( + {InvoiceModel.invoice_status_code: InvoiceStatus.OVERDUE.value}, + synchronize_session="fetch", + ) db.session.commit() @classmethod def add_to_non_sufficient_funds(cls, payment_account): """Add the invoice to the non sufficient funds table.""" - invoices = db.session.query(InvoiceModel.id, InvoiceReferenceModel.invoice_number) \ - .join(InvoiceReferenceModel, InvoiceReferenceModel.invoice_id == InvoiceModel.id) \ - .filter(InvoiceModel.payment_account_id == payment_account.id, - InvoiceModel.invoice_status_code == InvoiceStatus.OVERDUE.value, - InvoiceModel.id.notin_( - db.session.query(NonSufficientFundsModel.invoice_id) - )).distinct().all() + invoices = ( + db.session.query(InvoiceModel.id, InvoiceReferenceModel.invoice_number) + .join( + InvoiceReferenceModel, + InvoiceReferenceModel.invoice_id == InvoiceModel.id, + ) + .filter( + InvoiceModel.payment_account_id == payment_account.id, + InvoiceModel.invoice_status_code == InvoiceStatus.OVERDUE.value, + InvoiceModel.id.notin_(db.session.query(NonSufficientFundsModel.invoice_id)), + ) + .distinct() + .all() + ) cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, PaymentMethod.EFT.value) for invoice_tuple in invoices: - NonSufficientFundsService.save_non_sufficient_funds(invoice_id=invoice_tuple[0], - invoice_number=invoice_tuple[1], - cfs_account=cfs_account.cfs_account, - description='EFT invoice overdue') + NonSufficientFundsService.save_non_sufficient_funds( + invoice_id=invoice_tuple[0], + invoice_number=invoice_tuple[1], + cfs_account=cfs_account.cfs_account, + description="EFT invoice overdue", + ) @classmethod def _notify_for_monthly(cls): """Notify for unpaid monthly statements with an amount owing.""" previous_month = cls.statement_date_override or current_local_time().replace(day=1) - timedelta(days=1) - statement_settings = StatementSettingsService.find_accounts_settings_by_frequency(previous_month, - StatementFrequency.MONTHLY) - eft_payment_accounts = [pay_account for _, pay_account in statement_settings - if pay_account.payment_method == PaymentMethod.EFT.value] + statement_settings = StatementSettingsService.find_accounts_settings_by_frequency( + previous_month, StatementFrequency.MONTHLY + ) + eft_payment_accounts = [ + pay_account + for _, pay_account in statement_settings + if pay_account.payment_method == PaymentMethod.EFT.value + ] if cls.auth_account_override: - current_app.logger.info(f'Using auth account override for auth_account_id: {cls.auth_account_override}') - eft_payment_accounts = [pay_account for pay_account in eft_payment_accounts - if pay_account.auth_account_id == cls.auth_account_override] + current_app.logger.info(f"Using auth account override for auth_account_id: {cls.auth_account_override}") + eft_payment_accounts = [ + pay_account + for pay_account in eft_payment_accounts + if pay_account.auth_account_id == cls.auth_account_override + ] - current_app.logger.info(f'Processing {len(eft_payment_accounts)} EFT accounts for monthly reminders.') + current_app.logger.info(f"Processing {len(eft_payment_accounts)} EFT accounts for monthly reminders.") for payment_account in eft_payment_accounts: try: - if not (statement := cls._find_most_recent_statement( - payment_account.auth_account_id, StatementFrequency.MONTHLY.value)): + if not ( + statement := cls._find_most_recent_statement( + payment_account.auth_account_id, + StatementFrequency.MONTHLY.value, + ) + ): continue action, due_date = cls._determine_action_and_due_date_by_invoice(statement) - total_due = Statement.get_summary(payment_account.auth_account_id, statement.id)['total_due'] + total_due = Statement.get_summary(payment_account.auth_account_id, statement.id)["total_due"] if action and total_due > 0: if action == StatementNotificationAction.OVERDUE: - current_app.logger.info('Freezing payment account id: %s locking auth account id: %s', - payment_account.id, payment_account.auth_account_id) + current_app.logger.info( + "Freezing payment account id: %s locking auth account id: %s", + payment_account.id, + payment_account.auth_account_id, + ) # The locking email is sufficient for overdue, no seperate email required. - additional_emails = current_app.config.get('EFT_OVERDUE_NOTIFY_EMAILS') + additional_emails = current_app.config.get("EFT_OVERDUE_NOTIFY_EMAILS") AuthEvent.publish_lock_account_event(payment_account, additional_emails) statement.overdue_notification_date = datetime.now(tz=timezone.utc) # Saving here because the code below can exception, we don't want to send the lock email twice. @@ -168,36 +203,46 @@ def _notify_for_monthly(cls): cls.add_to_non_sufficient_funds(payment_account) continue if emails := cls._determine_recipient_emails(statement): - current_app.logger.info(f'Sending statement {statement.id} {action}' - f' notification for auth_account_id=' - f'{payment_account.auth_account_id}, payment_account_id=' - f'{payment_account.id}') + current_app.logger.info( + f"Sending statement {statement.id} {action}" + f" notification for auth_account_id=" + f"{payment_account.auth_account_id}, payment_account_id=" + f"{payment_account.id}" + ) links_count = EFTShortnameLinksModel.get_short_name_links_count(payment_account.auth_account_id) publish_payment_notification( - StatementNotificationInfo(auth_account_id=payment_account.auth_account_id, - statement=statement, - action=action, - due_date=due_date, - emails=emails, - total_amount_owing=total_due, - short_name_links_count=links_count)) + StatementNotificationInfo( + auth_account_id=payment_account.auth_account_id, + statement=statement, + action=action, + due_date=due_date, + emails=emails, + total_amount_owing=total_due, + short_name_links_count=links_count, + ) + ) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on unpaid statement notification auth_account_id={payment_account.auth_account_id}, ' - f'ERROR : {str(e)}', level='error') + f"Error on unpaid statement notification auth_account_id={payment_account.auth_account_id}, " + f"ERROR : {str(e)}", + level="error", + ) current_app.logger.error( - f'Error on unpaid statement notification auth_account_id={payment_account.auth_account_id}', - exc_info=True) + f"Error on unpaid statement notification auth_account_id={payment_account.auth_account_id}", + exc_info=True, + ) continue @classmethod def _find_most_recent_statement(cls, auth_account_id: str, statement_frequency: str) -> StatementModel: """Find all payment and invoices specific to a statement.""" - query = db.session.query(StatementModel) \ - .join(PaymentAccountModel) \ - .filter(PaymentAccountModel.auth_account_id == auth_account_id) \ - .filter(StatementModel.frequency == statement_frequency) \ + query = ( + db.session.query(StatementModel) + .join(PaymentAccountModel) + .filter(PaymentAccountModel.auth_account_id == auth_account_id) + .filter(StatementModel.frequency == statement_frequency) .order_by(StatementModel.to_date.desc()) + ) statement = query.first() return statement if statement and statement.overdue_notification_date is None else None @@ -205,12 +250,17 @@ def _find_most_recent_statement(cls, auth_account_id: str, statement_frequency: @classmethod def _determine_action_and_due_date_by_invoice(cls, statement: StatementModel): """Find the most overdue invoice for a statement and provide an action.""" - invoice = db.session.query(InvoiceModel) \ - .join(StatementInvoicesModel, StatementInvoicesModel.invoice_id == InvoiceModel.id) \ - .filter(StatementInvoicesModel.statement_id == statement.id) \ - .filter(InvoiceModel.overdue_date.isnot(None)) \ - .order_by(InvoiceModel.overdue_date.asc()) \ + invoice = ( + db.session.query(InvoiceModel) + .join( + StatementInvoicesModel, + StatementInvoicesModel.invoice_id == InvoiceModel.id, + ) + .filter(StatementInvoicesModel.statement_id == statement.id) + .filter(InvoiceModel.overdue_date.isnot(None)) + .order_by(InvoiceModel.overdue_date.asc()) .first() + ) if invoice is None: return None, None @@ -239,11 +289,11 @@ def _determine_action_and_due_date_by_invoice(cls, statement: StatementModel): @classmethod def _determine_recipient_emails(cls, statement: StatementRecipientsModel) -> str: - if (recipients := StatementRecipientsModel.find_all_recipients_for_payment_id(statement.payment_account_id)): - recipients = ','.join([str(recipient.email) for recipient in recipients]) + if recipients := StatementRecipientsModel.find_all_recipients_for_payment_id(statement.payment_account_id): + recipients = ",".join([str(recipient.email) for recipient in recipients]) return recipients - current_app.logger.error(f'No recipients found for payment_account_id: {statement.payment_account_id}. Skip.') + current_app.logger.error(f"No recipients found for payment_account_id: {statement.payment_account_id}. Skip.") return None @classmethod diff --git a/jobs/payment-jobs/tasks/eft_task.py b/jobs/payment-jobs/tasks/eft_task.py index d57cde285..6004f1378 100644 --- a/jobs/payment-jobs/tasks/eft_task.py +++ b/jobs/payment-jobs/tasks/eft_task.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. """Task for linking electronic funds transfers.""" -from datetime import datetime, timezone -from typing import List from dataclasses import dataclass +from datetime import datetime, timezone from decimal import Decimal -from flask import current_app +from typing import List +from flask import current_app from pay_api.models import CfsAccount as CfsAccountModel from pay_api.models import EFTCreditInvoiceLink as EFTCreditInvoiceLinkModel from pay_api.models import EFTShortnamesHistorical as EFTShortnameHistoryModel @@ -30,8 +30,16 @@ from pay_api.services.eft_service import EftService from pay_api.services.invoice import Invoice as InvoiceService from pay_api.utils.enums import ( - CfsAccountStatus, DisbursementStatus, EFTCreditInvoiceStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, - PaymentStatus, PaymentSystem, ReverseOperation) + CfsAccountStatus, + DisbursementStatus, + EFTCreditInvoiceStatus, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, + PaymentStatus, + PaymentSystem, + ReverseOperation, +) from sentry_sdk import capture_message from sqlalchemy import func, or_ from sqlalchemy.orm import lazyload, registry @@ -46,29 +54,47 @@ class EFTTask: # pylint:disable=too-few-public-methods overdue_account_ids = {} @classmethod - def get_eft_credit_invoice_links_by_status(cls, status: str) \ - -> List[tuple[InvoiceModel, EFTCreditInvoiceLinkModel, CfsAccountModel]]: + def get_eft_credit_invoice_links_by_status( + cls, status: str + ) -> List[tuple[InvoiceModel, EFTCreditInvoiceLinkModel, CfsAccountModel]]: """Get electronic funds transfer by state.""" - latest_cfs_account = db.session.query(func.max(CfsAccountModel.id).label('max_id_per_payment_account')) \ - .filter(CfsAccountModel.payment_method == PaymentMethod.EFT.value) \ - .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value) \ - .group_by(CfsAccountModel.account_id).subquery('latest_cfs_account') + latest_cfs_account = ( + db.session.query(func.max(CfsAccountModel.id).label("max_id_per_payment_account")) + .filter(CfsAccountModel.payment_method == PaymentMethod.EFT.value) + .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value) + .group_by(CfsAccountModel.account_id) + .subquery("latest_cfs_account") + ) - cil_rollup = db.session.query(func.min(EFTCreditInvoiceLinkModel.id).label('id'), - EFTCreditInvoiceLinkModel.invoice_id, - EFTCreditInvoiceLinkModel.status_code, - EFTCreditInvoiceLinkModel.receipt_number, - func.array_agg(EFTCreditInvoiceLinkModel.id) # pylint: disable=not-callable - .label('link_ids'), - func.sum(EFTCreditInvoiceLinkModel.amount).label('rollup_amount')) \ - .join(InvoiceReferenceModel, InvoiceReferenceModel.invoice_id == EFTCreditInvoiceLinkModel.invoice_id) \ - .filter(EFTCreditInvoiceLinkModel.status_code == status) \ - .filter(InvoiceReferenceModel.status_code.in_([InvoiceReferenceStatus.ACTIVE.value, - InvoiceReferenceStatus.COMPLETED.value])) \ - .group_by(EFTCreditInvoiceLinkModel.invoice_id, - EFTCreditInvoiceLinkModel.status_code, - EFTCreditInvoiceLinkModel.receipt_number) \ + cil_rollup = ( + db.session.query( + func.min(EFTCreditInvoiceLinkModel.id).label("id"), + EFTCreditInvoiceLinkModel.invoice_id, + EFTCreditInvoiceLinkModel.status_code, + EFTCreditInvoiceLinkModel.receipt_number, + func.array_agg(EFTCreditInvoiceLinkModel.id).label("link_ids"), # pylint: disable=not-callable + func.sum(EFTCreditInvoiceLinkModel.amount).label("rollup_amount"), + ) + .join( + InvoiceReferenceModel, + InvoiceReferenceModel.invoice_id == EFTCreditInvoiceLinkModel.invoice_id, + ) + .filter(EFTCreditInvoiceLinkModel.status_code == status) + .filter( + InvoiceReferenceModel.status_code.in_( + [ + InvoiceReferenceStatus.ACTIVE.value, + InvoiceReferenceStatus.COMPLETED.value, + ] + ) + ) + .group_by( + EFTCreditInvoiceLinkModel.invoice_id, + EFTCreditInvoiceLinkModel.status_code, + EFTCreditInvoiceLinkModel.receipt_number, + ) .subquery() + ) # This needs to be local unfortunately so it doesn't get remapped. @dataclass @@ -81,33 +107,54 @@ class EFTCILRollup: link_ids: List[int] rollup_amount: Decimal - registry().map_imperatively(EFTCILRollup, cil_rollup, primary_key=[cil_rollup.c.invoice_id, - cil_rollup.c.status_code, - cil_rollup.c.receipt_number]) + registry().map_imperatively( + EFTCILRollup, + cil_rollup, + primary_key=[ + cil_rollup.c.invoice_id, + cil_rollup.c.status_code, + cil_rollup.c.receipt_number, + ], + ) - query = db.session.query(InvoiceModel, CfsAccountModel, EFTCILRollup) \ - .join(cil_rollup, InvoiceModel.id == cil_rollup.c.invoice_id) \ - .join(CfsAccountModel, CfsAccountModel.account_id == InvoiceModel.payment_account_id) \ - .join(latest_cfs_account, CfsAccountModel.id == latest_cfs_account.c.max_id_per_payment_account) \ - .options(lazyload('*')) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ + query = ( + db.session.query(InvoiceModel, CfsAccountModel, EFTCILRollup) + .join(cil_rollup, InvoiceModel.id == cil_rollup.c.invoice_id) + .join( + CfsAccountModel, + CfsAccountModel.account_id == InvoiceModel.payment_account_id, + ) + .join( + latest_cfs_account, + CfsAccountModel.id == latest_cfs_account.c.max_id_per_payment_account, + ) + .options(lazyload("*")) + .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) .filter(InvoiceModel.total == cil_rollup.c.rollup_amount) + ) match status: case EFTCreditInvoiceStatus.CANCELLED.value: # Handles 3. EFT Credit Link - PENDING, CANCEL that link reverse invoice. See eft_service refund. - query = query.filter( - InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) + query = query.filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) case EFTCreditInvoiceStatus.PENDING.value: query = query.filter(InvoiceModel.disbursement_status_code.is_(None)) - query = query.filter(InvoiceModel.invoice_status_code.in_([InvoiceStatus.APPROVED.value, - InvoiceStatus.OVERDUE.value])) + query = query.filter( + InvoiceModel.invoice_status_code.in_([InvoiceStatus.APPROVED.value, InvoiceStatus.OVERDUE.value]) + ) case EFTCreditInvoiceStatus.PENDING_REFUND.value: # Handles 4. EFT Credit Link - COMPLETED from refund flow. See eft_service refund. - query = query.filter(or_(InvoiceModel.disbursement_status_code.is_( - None), InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value)) - query = query.filter(InvoiceModel.invoice_status_code.in_([InvoiceStatus.PAID.value, - InvoiceStatus.REFUND_REQUESTED.value])) + query = query.filter( + or_( + InvoiceModel.disbursement_status_code.is_(None), + InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value, + ) + ) + query = query.filter( + InvoiceModel.invoice_status_code.in_( + [InvoiceStatus.PAID.value, InvoiceStatus.REFUND_REQUESTED.value] + ) + ) case _: pass return query.order_by(InvoiceModel.payment_account_id, cil_rollup.c.invoice_id).all() @@ -119,23 +166,29 @@ def link_electronic_funds_transfers_cfs(cls) -> dict: cls.history_group_ids = set() for invoice, cfs_account, cil_rollup in credit_invoice_links: try: - current_app.logger.info(f'PayAccount: {invoice.payment_account_id} Id: {cil_rollup.id} -' - f' Invoice Id: {invoice.id} - Amount: {cil_rollup.rollup_amount}') + current_app.logger.info( + f"PayAccount: {invoice.payment_account_id} Id: {cil_rollup.id} -" + f" Invoice Id: {invoice.id} - Amount: {cil_rollup.rollup_amount}" + ) if invoice.invoice_status_code == InvoiceStatus.OVERDUE.value: cls.overdue_account_ids[invoice.payment_account_id] = cfs_account.payment_account - receipt_number = f'EFTCIL{cil_rollup.id}' + receipt_number = f"EFTCIL{cil_rollup.id}" cls._create_receipt_and_invoice(cfs_account, cil_rollup, invoice, receipt_number) cls._update_cil_and_shortname_history(cil_rollup, receipt_number=receipt_number) db.session.commit() EftService().complete_post_invoice(invoice, None) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on linking EFT invoice links in CFS ' - f'Account id={invoice.payment_account_id} ' - f'EFT Credit invoice Link : {cil_rollup.id}' - f'ERROR : {str(e)}', level='error') - current_app.logger.error(f'Error Account id={invoice.payment_account_id} - ' - f'EFT Credit invoice Link : {cil_rollup.id}', exc_info=True) + f"Error on linking EFT invoice links in CFS " + f"Account id={invoice.payment_account_id} " + f"EFT Credit invoice Link : {cil_rollup.id}" + f"ERROR : {str(e)}", + level="error", + ) + current_app.logger.error( + f"Error Account id={invoice.payment_account_id} - " f"EFT Credit invoice Link : {cil_rollup.id}", + exc_info=True, + ) db.session.rollback() continue cls.unlock_overdue_accounts() @@ -143,25 +196,32 @@ def link_electronic_funds_transfers_cfs(cls) -> dict: @classmethod def reverse_electronic_funds_transfers_cfs(cls): """Reverse electronic funds transfers receipts in CFS and reset invoices.""" - cils = cls.get_eft_credit_invoice_links_by_status(EFTCreditInvoiceStatus.PENDING_REFUND.value) + \ - cls.get_eft_credit_invoice_links_by_status(EFTCreditInvoiceStatus.CANCELLED.value) + cils = cls.get_eft_credit_invoice_links_by_status( + EFTCreditInvoiceStatus.PENDING_REFUND.value + ) + cls.get_eft_credit_invoice_links_by_status(EFTCreditInvoiceStatus.CANCELLED.value) cls.history_group_ids = set() for invoice, cfs_account, cil_rollup in cils: try: - current_app.logger.info(f'PayAccount: {invoice.payment_account_id} Id: {cil_rollup.id} -' - f' Invoice Id: {invoice.id} - Amount: {cil_rollup.rollup_amount}') + current_app.logger.info( + f"PayAccount: {invoice.payment_account_id} Id: {cil_rollup.id} -" + f" Invoice Id: {invoice.id} - Amount: {cil_rollup.rollup_amount}" + ) receipt_number = cil_rollup.receipt_number cls._rollback_receipt_and_invoice(cfs_account, invoice, receipt_number, cil_rollup.status_code) cls._update_cil_and_shortname_history(cil_rollup) db.session.commit() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on reversing EFT invoice links in CFS ' - f'Account id={invoice.payment_account_id} ' - f'EFT Credit invoice Link : {cil_rollup.id}' - f'ERROR : {str(e)}', level='error') - current_app.logger.error(f'Error Account id={invoice.payment_account_id} - ' - f'EFT Credit invoice Link : {cil_rollup.id}', exc_info=True) + f"Error on reversing EFT invoice links in CFS " + f"Account id={invoice.payment_account_id} " + f"EFT Credit invoice Link : {cil_rollup.id}" + f"ERROR : {str(e)}", + level="error", + ) + current_app.logger.error( + f"Error Account id={invoice.payment_account_id} - " f"EFT Credit invoice Link : {cil_rollup.id}", + exc_info=True, + ) db.session.rollback() continue cls.handle_unlinked_refund_requested_invoices() @@ -170,38 +230,47 @@ def reverse_electronic_funds_transfers_cfs(cls): def handle_unlinked_refund_requested_invoices(cls): """Handle unlinked refund requested invoices.""" # Handles 2. No EFT Credit Link - Job needs to reverse invoice in CFS from refund flow. See eft_service refund. - invoices = db.session.query(InvoiceModel).outerjoin(EFTCreditInvoiceLinkModel) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ - .filter(EFTCreditInvoiceLinkModel.id.is_(None)) \ + invoices = ( + db.session.query(InvoiceModel) + .outerjoin(EFTCreditInvoiceLinkModel) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) + .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) + .filter(EFTCreditInvoiceLinkModel.id.is_(None)) .all() + ) for invoice in invoices: - cfs_account = CfsAccountModel.find_effective_by_payment_method(invoice.payment_account_id, - PaymentMethod.EFT.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + invoice.payment_account_id, PaymentMethod.EFT.value + ) if not cfs_account: - current_app.logger.error(f'No EFT CFS Account found for pay account id={invoice.payment_account_id}') + current_app.logger.error(f"No EFT CFS Account found for pay account id={invoice.payment_account_id}") continue invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status( - invoice.id, InvoiceReferenceStatus.ACTIVE.value) + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) try: cls._handle_invoice_refund(invoice, invoice_reference) db.session.commit() - except Exception as e: # NOQA # pylint: disable=broad-except + except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on reversing unlinked REFUND_REQUESTED EFT invoice in CFS ' - f'Account id={invoice.payment_account_id} ' - f'Invoice id : {invoice.id}' - f'ERROR : {str(e)}', level='error') - current_app.logger.error(f'Error Account id={invoice.payment_account_id} - ' - f'Invoice id : {invoice.id}', exc_info=True) + f"Error on reversing unlinked REFUND_REQUESTED EFT invoice in CFS " + f"Account id={invoice.payment_account_id} " + f"Invoice id : {invoice.id}" + f"ERROR : {str(e)}", + level="error", + ) + current_app.logger.error( + f"Error Account id={invoice.payment_account_id} - " f"Invoice id : {invoice.id}", + exc_info=True, + ) db.session.rollback() continue @classmethod def unlock_overdue_accounts(cls): """Check and unlock overdue EFT accounts.""" - for (payment_account_id, payment_account) in cls.overdue_account_ids.items(): + for payment_account_id, payment_account in cls.overdue_account_ids.items(): if InvoiceService.has_overdue_invoices(payment_account_id): continue payment_account.has_overdue_invoices = None @@ -211,8 +280,11 @@ def unlock_overdue_accounts(cls): @classmethod def _get_eft_history_by_group_id(cls, related_group_id: int) -> EFTShortnameHistoryModel: """Get EFT short name historical record by related group id.""" - return (db.session.query(EFTShortnameHistoryModel) - .filter(EFTShortnameHistoryModel.related_group_link_id == related_group_id)).one_or_none() + return ( + db.session.query(EFTShortnameHistoryModel).filter( + EFTShortnameHistoryModel.related_group_link_id == related_group_id + ) + ).one_or_none() @classmethod def _finalize_shortname_history(cls, group_set: set, invoice_link: EFTCreditInvoiceLinkModel): @@ -229,49 +301,68 @@ def _finalize_shortname_history(cls, group_set: set, invoice_link: EFTCreditInvo @classmethod def _update_cil_and_shortname_history(cls, cil_rollup, receipt_number=None): """Update electronic invoice links.""" - cils = db.session.query(EFTCreditInvoiceLinkModel).filter( - EFTCreditInvoiceLinkModel.id.in_(cil_rollup.link_ids)).all() + cils = ( + db.session.query(EFTCreditInvoiceLinkModel) + .filter(EFTCreditInvoiceLinkModel.id.in_(cil_rollup.link_ids)) + .all() + ) for cil in cils: if cil.status_code != EFTCreditInvoiceStatus.CANCELLED.value: - cil.status_code = EFTCreditInvoiceStatus.COMPLETED.value if receipt_number \ - else EFTCreditInvoiceStatus.REFUNDED.value + cil.status_code = ( + EFTCreditInvoiceStatus.COMPLETED.value if receipt_number else EFTCreditInvoiceStatus.REFUNDED.value + ) cil.receipt_number = receipt_number or cil.receipt_number cil.flush() cls._finalize_shortname_history(cls.history_group_ids, cil) @classmethod - def _create_receipt_and_invoice(cls, - cfs_account: CfsAccountModel, - cil_rollup, - invoice: InvoiceModel, - receipt_number: str): + def _create_receipt_and_invoice( + cls, + cfs_account: CfsAccountModel, + cil_rollup, + invoice: InvoiceModel, + receipt_number: str, + ): """Create receipt in CFS and marks invoice as paid, with payment and receipt rows.""" - if not (invoice_reference := InvoiceReferenceModel.find_by_invoice_id_and_status( - cil_rollup.invoice_id, InvoiceReferenceStatus.ACTIVE.value - )): - raise LookupError(f'Active Invoice reference not ' - f'found for invoice id: {invoice.id}') + if not ( + invoice_reference := InvoiceReferenceModel.find_by_invoice_id_and_status( + cil_rollup.invoice_id, InvoiceReferenceStatus.ACTIVE.value + ) + ): + raise LookupError(f"Active Invoice reference not " f"found for invoice id: {invoice.id}") if invoice_reference.is_consolidated: original_invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status( - cil_rollup.invoice_id, InvoiceReferenceStatus.CANCELLED.value, exclude_consolidated=True + cil_rollup.invoice_id, + InvoiceReferenceStatus.CANCELLED.value, + exclude_consolidated=True, ) if not original_invoice_reference: - raise LookupError(f'Non consolidated cancelled invoice reference not ' - f'found for invoice id: {invoice.id}') - invoice_response = CFSService.get_invoice(cfs_account=cfs_account, - inv_number=original_invoice_reference.invoice_number) - cfs_total = Decimal(invoice_response.get('total', '0')) + raise LookupError( + f"Non consolidated cancelled invoice reference not " f"found for invoice id: {invoice.id}" + ) + invoice_response = CFSService.get_invoice( + cfs_account=cfs_account, + inv_number=original_invoice_reference.invoice_number, + ) + cfs_total = Decimal(invoice_response.get("total", "0")) invoice_total_matches = cfs_total == invoice.total if not invoice_total_matches: - raise ValueError(f'SBC-PAY Invoice total {invoice.total} does not match CFS total {cfs_total}') + raise ValueError(f"SBC-PAY Invoice total {invoice.total} does not match CFS total {cfs_total}") # Guard against double reversing an invoice because an invoice reference can have many invoice ids - if InvoiceReferenceModel.query.filter( - InvoiceReferenceModel.invoice_number == invoice_reference.invoice_number) \ - .filter(InvoiceReferenceModel.is_consolidated.is_(True)) \ - .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.CANCELLED.value).count() == 0: + if ( + InvoiceReferenceModel.query.filter( + InvoiceReferenceModel.invoice_number == invoice_reference.invoice_number + ) + .filter(InvoiceReferenceModel.is_consolidated.is_(True)) + .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.CANCELLED.value) + .count() + == 0 + ): # Note we do the opposite of this in payment_account. - current_app.logger.info(f'Consolidated invoice found, reversing consolidated ' - f'invoice {invoice_reference.invoice_number}.') + current_app.logger.info( + f"Consolidated invoice found, reversing consolidated " + f"invoice {invoice_reference.invoice_number}." + ) CFSService.reverse_invoice(invoice_reference.invoice_number) invoice_reference.status_code = InvoiceReferenceStatus.CANCELLED.value invoice_reference.flush() @@ -284,48 +375,56 @@ def _create_receipt_and_invoice(cls, CFSService.create_cfs_receipt( cfs_account=cfs_account, rcpt_number=receipt_number, - rcpt_date=datetime.now(tz=timezone.utc).strftime('%Y-%m-%d'), + rcpt_date=datetime.now(tz=timezone.utc).strftime("%Y-%m-%d"), amount=cil_rollup.rollup_amount, payment_method=PaymentMethod.EFT.value, - access_token=CFSService.get_token(PaymentSystem.FAS).json().get('access_token')) + access_token=CFSService.get_token(PaymentSystem.FAS).json().get("access_token"), + ) CFSService.apply_receipt(cfs_account, receipt_number, invoice_reference.invoice_number) - ReceiptModel(receipt_number=receipt_number, - receipt_amount=cil_rollup.rollup_amount, - invoice_id=invoice_reference.invoice_id, - receipt_date=datetime.now(tz=timezone.utc)).flush() - PaymentModel(payment_method_code=PaymentMethod.EFT.value, - payment_status_code=PaymentStatus.COMPLETED.value, - payment_system_code=PaymentSystem.PAYBC.value, - invoice_number=invoice.id, - invoice_amount=invoice.total, - payment_account_id=cfs_account.account_id, - payment_date=datetime.now(tz=timezone.utc), - paid_amount=cil_rollup.rollup_amount, - receipt_number=receipt_number).flush() + ReceiptModel( + receipt_number=receipt_number, + receipt_amount=cil_rollup.rollup_amount, + invoice_id=invoice_reference.invoice_id, + receipt_date=datetime.now(tz=timezone.utc), + ).flush() + PaymentModel( + payment_method_code=PaymentMethod.EFT.value, + payment_status_code=PaymentStatus.COMPLETED.value, + payment_system_code=PaymentSystem.PAYBC.value, + invoice_number=invoice.id, + invoice_amount=invoice.total, + payment_account_id=cfs_account.account_id, + payment_date=datetime.now(tz=timezone.utc), + paid_amount=cil_rollup.rollup_amount, + receipt_number=receipt_number, + ).flush() invoice.invoice_status_code = InvoiceStatus.PAID.value invoice.paid = cil_rollup.rollup_amount invoice.payment_date = datetime.now(tz=timezone.utc) invoice.flush() @classmethod - def _rollback_receipt_and_invoice(cls, cfs_account: CfsAccountModel, - invoice: InvoiceModel, - receipt_number: str, - cil_status_code: str): + def _rollback_receipt_and_invoice( + cls, + cfs_account: CfsAccountModel, + invoice: InvoiceModel, + receipt_number: str, + cil_status_code: str, + ): """Rollback receipt in CFS and reset invoice status.""" invoice_reference_requirement = { EFTCreditInvoiceStatus.PENDING_REFUND.value: InvoiceReferenceStatus.COMPLETED.value, - EFTCreditInvoiceStatus.CANCELLED.value: InvoiceReferenceStatus.ACTIVE.value + EFTCreditInvoiceStatus.CANCELLED.value: InvoiceReferenceStatus.ACTIVE.value, } invoice_reference_status = invoice_reference_requirement.get(cil_status_code) - invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status( - invoice.id, invoice_reference_status - ) + invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status(invoice.id, invoice_reference_status) if invoice_reference and invoice_reference.is_consolidated: - raise ValueError(f'Cannot reverse a consolidated invoice {invoice_reference.invoice_number}') + raise ValueError(f"Cannot reverse a consolidated invoice {invoice_reference.invoice_number}") if cil_status_code != EFTCreditInvoiceStatus.CANCELLED.value and not invoice_reference: - raise LookupError(f'{invoice_reference_status} invoice reference ' - f'not found for invoice id: {invoice.id} - {invoice.invoice_status_code}') + raise LookupError( + f"{invoice_reference_status} invoice reference " + f"not found for invoice id: {invoice.id} - {invoice.invoice_status_code}" + ) is_invoice_refund = invoice.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value is_reversal = not is_invoice_refund if receipt_number: @@ -346,13 +445,11 @@ def _rollback_receipt_and_invoice(cls, cfs_account: CfsAccountModel, db.session.delete(receipt) @classmethod - def _handle_invoice_refund(cls, - invoice: InvoiceModel, - invoice_reference: InvoiceReferenceModel): + def _handle_invoice_refund(cls, invoice: InvoiceModel, invoice_reference: InvoiceReferenceModel): """Handle invoice refunds adjustment on a non-rolled up invoice.""" if invoice_reference: if invoice_reference.is_consolidated: - raise ValueError(f'Cannot reverse a consolidated invoice: {invoice_reference.invoice_number}') + raise ValueError(f"Cannot reverse a consolidated invoice: {invoice_reference.invoice_number}") CFSService.reverse_invoice(invoice_reference.invoice_number) invoice_reference.status_code = InvoiceReferenceStatus.CANCELLED.value invoice_reference.flush() diff --git a/jobs/payment-jobs/tasks/ejv_partner_distribution_task.py b/jobs/payment-jobs/tasks/ejv_partner_distribution_task.py index 2ecad9a15..b1abcc7d3 100644 --- a/jobs/payment-jobs/tasks/ejv_partner_distribution_task.py +++ b/jobs/payment-jobs/tasks/ejv_partner_distribution_task.py @@ -15,9 +15,9 @@ import time from datetime import datetime, timedelta, timezone +from decimal import Decimal from typing import List -from decimal import Decimal from flask import current_app from pay_api.models import CorpType as CorpTypeModel from pay_api.models import DistributionCode as DistributionCodeModel @@ -55,8 +55,8 @@ def create_ejv_file(cls): 4. Upload to sftp for processing. First upload JV file and then a TRG file. 5. Update the statuses and create records to for the batch. """ - cls._create_ejv_file_for_partner(batch_type='GI') # Internal ministry - cls._create_ejv_file_for_partner(batch_type='GA') # External ministry + cls._create_ejv_file_for_partner(batch_type="GI") # Internal ministry + cls._create_ejv_file_for_partner(batch_type="GA") # External ministry @staticmethod def get_disbursement_by_distribution_for_partner(partner): @@ -66,106 +66,145 @@ def get_disbursement_by_distribution_for_partner(partner): # ##################################################### Original (Legacy way) - invoice.disbursement_status_code # Eventually we'll abandon this and use the PartnerDisbursements table for all disbursements. # We'd need a migration and more changes to move it to the table. - skip_payment_methods = [PaymentMethod.INTERNAL.value, PaymentMethod.DRAWDOWN.value, PaymentMethod.EFT.value] - disbursement_date = datetime.now(tz=timezone.utc).replace(tzinfo=None) - \ - timedelta(days=current_app.config.get( - 'DISBURSEMENT_DELAY_IN_DAYS')) - base_query = db.session.query(InvoiceModel, PaymentLineItemModel, DistributionCodeModel) \ - .join(PaymentLineItemModel, PaymentLineItemModel.invoice_id == InvoiceModel.id) \ - .join(DistributionCodeModel, - DistributionCodeModel.distribution_code_id == PaymentLineItemModel.fee_distribution_id) \ - .filter(InvoiceModel.payment_method_code.notin_(skip_payment_methods)) \ - .filter(InvoiceModel.corp_type_code == partner.code) \ - .filter(PaymentLineItemModel.total > 0) \ - .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) \ + skip_payment_methods = [ + PaymentMethod.INTERNAL.value, + PaymentMethod.DRAWDOWN.value, + PaymentMethod.EFT.value, + ] + disbursement_date = datetime.now(tz=timezone.utc).replace(tzinfo=None) - timedelta( + days=current_app.config.get("DISBURSEMENT_DELAY_IN_DAYS") + ) + base_query = ( + db.session.query(InvoiceModel, PaymentLineItemModel, DistributionCodeModel) + .join(PaymentLineItemModel, PaymentLineItemModel.invoice_id == InvoiceModel.id) + .join( + DistributionCodeModel, + DistributionCodeModel.distribution_code_id == PaymentLineItemModel.fee_distribution_id, + ) + .filter(InvoiceModel.payment_method_code.notin_(skip_payment_methods)) + .filter(InvoiceModel.corp_type_code == partner.code) + .filter(PaymentLineItemModel.total > 0) + .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) .order_by(DistributionCodeModel.distribution_code_id, PaymentLineItemModel.id) + ) - transactions = base_query.filter((InvoiceModel.disbursement_status_code.is_(None)) | - (InvoiceModel.disbursement_status_code == DisbursementStatus.ERRORED.value)) \ - .filter(~InvoiceModel.receipts.any(cast(ReceiptModel.receipt_date, Date) >= disbursement_date.date())) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ + transactions = ( + base_query.filter( + (InvoiceModel.disbursement_status_code.is_(None)) + | (InvoiceModel.disbursement_status_code == DisbursementStatus.ERRORED.value) + ) + .filter(~InvoiceModel.receipts.any(cast(ReceiptModel.receipt_date, Date) >= disbursement_date.date())) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) .all() + ) # REFUND_REQUESTED for credit card payments, CREDITED for AR and REFUNDED for other payments. - reversals = base_query.filter(InvoiceModel.invoice_status_code.in_([InvoiceStatus.REFUNDED.value, - InvoiceStatus.REFUND_REQUESTED.value, - InvoiceStatus.CREDITED.value])) \ - .filter(InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value) \ + reversals = ( + base_query.filter( + InvoiceModel.invoice_status_code.in_( + [ + InvoiceStatus.REFUNDED.value, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.CREDITED.value, + ] + ) + ) + .filter(InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value) .all() + ) disbursement_rows = [] distribution_code_totals = {} for invoice, payment_line_item, distribution_code in transactions + reversals: distribution_code_totals.setdefault(distribution_code.distribution_code_id, 0) distribution_code_totals[distribution_code.distribution_code_id] += payment_line_item.total - disbursement_rows.append(Disbursement( - bcreg_distribution_code=distribution_code, - partner_distribution_code=distribution_code.disbursement_distribution_code, - target=invoice, - line_item=DisbursementLineItem( - amount=payment_line_item.total, - flow_through=f'{invoice.id:<110}', - description_identifier=f'#{invoice.id}', - is_reversal=invoice.invoice_status_code in [InvoiceStatus.REFUNDED.value, - InvoiceStatus.REFUND_REQUESTED.value, - InvoiceStatus.CREDITED.value], - target_type=EJVLinkType.INVOICE.value, - identifier=invoice.id + disbursement_rows.append( + Disbursement( + bcreg_distribution_code=distribution_code, + partner_distribution_code=distribution_code.disbursement_distribution_code, + target=invoice, + line_item=DisbursementLineItem( + amount=payment_line_item.total, + flow_through=f"{invoice.id:<110}", + description_identifier=f"#{invoice.id}", + is_reversal=invoice.invoice_status_code + in [ + InvoiceStatus.REFUNDED.value, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.CREDITED.value, + ], + target_type=EJVLinkType.INVOICE.value, + identifier=invoice.id, + ), ) - )) + ) # ################################################################# END OF Legacy way of handling disbursements. # Partner disbursements - New # Partial refunds need to be added to here later, although they should be fairly rare as most of them are from # NRO (NRO is internal, meaning no disbursement needed.) - partner_disbursements = db.session.query(PartnerDisbursementsModel, - PaymentLineItemModel, - DistributionCodeModel) \ - .join(PaymentLineItemModel, and_(PaymentLineItemModel.invoice_id == PartnerDisbursementsModel.target_id, - PartnerDisbursementsModel.target_type == EJVLinkType.INVOICE.value)) \ - .join(DistributionCodeModel, - DistributionCodeModel.distribution_code_id == PaymentLineItemModel.fee_distribution_id) \ - .filter(PartnerDisbursementsModel.status_code == DisbursementStatus.WAITING_FOR_RECEIPT.value) \ - .filter(PartnerDisbursementsModel.partner_code == partner.code) \ - .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) \ - .filter(~InvoiceModel.receipts.any(cast(ReceiptModel.receipt_date, Date) >= disbursement_date.date())) \ - .order_by(DistributionCodeModel.distribution_code_id, PaymentLineItemModel.id) \ + partner_disbursements = ( + db.session.query(PartnerDisbursementsModel, PaymentLineItemModel, DistributionCodeModel) + .join( + PaymentLineItemModel, + and_( + PaymentLineItemModel.invoice_id == PartnerDisbursementsModel.target_id, + PartnerDisbursementsModel.target_type == EJVLinkType.INVOICE.value, + ), + ) + .join( + DistributionCodeModel, + DistributionCodeModel.distribution_code_id == PaymentLineItemModel.fee_distribution_id, + ) + .filter(PartnerDisbursementsModel.status_code == DisbursementStatus.WAITING_FOR_RECEIPT.value) + .filter(PartnerDisbursementsModel.partner_code == partner.code) + .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) + .filter(~InvoiceModel.receipts.any(cast(ReceiptModel.receipt_date, Date) >= disbursement_date.date())) + .order_by(DistributionCodeModel.distribution_code_id, PaymentLineItemModel.id) .all() + ) - for partner_disbursement, payment_line_item, distribution_code in partner_disbursements: - suffix = 'PR' if partner_disbursement.target_type == EJVLinkType.PARTIAL_REFUND else '' - flow_through = f'{payment_line_item.invoice_id}-{partner_disbursement.id}' - if suffix != '': - flow_through += f'-{suffix}' + for ( + partner_disbursement, + payment_line_item, + distribution_code, + ) in partner_disbursements: + suffix = "PR" if partner_disbursement.target_type == EJVLinkType.PARTIAL_REFUND else "" + flow_through = f"{payment_line_item.invoice_id}-{partner_disbursement.id}" + if suffix != "": + flow_through += f"-{suffix}" distribution_code_totals.setdefault(distribution_code.distribution_code_id, 0) distribution_code_totals[distribution_code.distribution_code_id] += partner_disbursement.amount - disbursement_rows.append(Disbursement( - bcreg_distribution_code=distribution_code, - partner_distribution_code=distribution_code.disbursement_distribution_code, - target=partner_disbursement, - line_item=DisbursementLineItem( - amount=partner_disbursement.amount, - flow_through=flow_through, - description_identifier='#' + flow_through, - is_reversal=partner_disbursement.is_reversal, - target_type=partner_disbursement.target_type, - identifier=partner_disbursement.target_id + disbursement_rows.append( + Disbursement( + bcreg_distribution_code=distribution_code, + partner_distribution_code=distribution_code.disbursement_distribution_code, + target=partner_disbursement, + line_item=DisbursementLineItem( + amount=partner_disbursement.amount, + flow_through=flow_through, + description_identifier="#" + flow_through, + is_reversal=partner_disbursement.is_reversal, + target_type=partner_disbursement.target_type, + identifier=partner_disbursement.target_id, + ), ) - )) + ) disbursement_rows.sort(key=lambda x: x.bcreg_distribution_code.distribution_code_id) return disbursement_rows, distribution_code_totals @classmethod def _create_ejv_file_for_partner(cls, batch_type: str): # pylint:disable=too-many-locals, too-many-statements """Create EJV file for the partner and upload.""" - ejv_content, batch_total, control_total = '', Decimal('0'), Decimal('0') + ejv_content, batch_total, control_total = "", Decimal("0"), Decimal("0") today = datetime.now(tz=timezone.utc) - disbursement_desc = current_app.config.get('CGI_DISBURSEMENT_DESC'). \ - format(today.strftime('%B').upper(), f'{today.day:0>2}')[:100] - disbursement_desc = f'{disbursement_desc:<100}' + disbursement_desc = current_app.config.get("CGI_DISBURSEMENT_DESC").format( + today.strftime("%B").upper(), f"{today.day:0>2}" + )[:100] + disbursement_desc = f"{disbursement_desc:<100}" ejv_file_model = EjvFileModel( file_type=EjvFileType.DISBURSEMENT.value, file_ref=cls.get_file_name(), - disbursement_status_code=DisbursementStatus.UPLOADED.value + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).flush() batch_number = cls.get_batch_number(ejv_file_model.id) batch_header = cls.get_batch_header(batch_number, batch_type) @@ -180,7 +219,7 @@ def _create_ejv_file_for_partner(cls, batch_type: str): # pylint:disable=too-ma ejv_header_model = EjvHeaderModel( partner_code=partner.code, disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file_model.id + ejv_file_id=ejv_file_model.id, ).flush() journal_name = cls.get_journal_name(ejv_header_model.id) sequence = 1 @@ -191,37 +230,46 @@ def _create_ejv_file_for_partner(cls, batch_type: str): # pylint:disable=too-ma # debit_distribution and credit_distribution stays as is for invoices which are not PAID if last_distribution_code != disbursement.bcreg_distribution_code.distribution_code_id: header_total = distribution_code_totals[disbursement.bcreg_distribution_code.distribution_code_id] - ejv_content = '{}{}'.format(ejv_content, # pylint:disable=consider-using-f-string - cls.get_jv_header(batch_type, cls.get_journal_batch_name(batch_number), - journal_name, header_total)) + ejv_content = "{}{}".format( + ejv_content, # pylint:disable=consider-using-f-string + cls.get_jv_header( + batch_type, + cls.get_journal_batch_name(batch_number), + journal_name, + header_total, + ), + ) control_total += 1 last_distribution_code = disbursement.bcreg_distribution_code.distribution_code_id line_number = 1 batch_total += disbursement.line_item.amount dl = disbursement.line_item - description = disbursement_desc[:-len(dl.description_identifier)] + dl.description_identifier - description = f'{description[:100]:<100}' + description = disbursement_desc[: -len(dl.description_identifier)] + dl.description_identifier + description = f"{description[:100]:<100}" for credit_debit_row in range(1, 3): target_distribution = cls.get_distribution_string( - disbursement.partner_distribution_code if credit_debit_row == 1 else - disbursement.bcreg_distribution_code + disbursement.partner_distribution_code + if credit_debit_row == 1 + else disbursement.bcreg_distribution_code ) # For payment flow, credit the GL partner code, debit the BCREG GL code. # Reversal is the opposite debit the GL partner code, credit the BCREG GL Code. - credit_debit = 'C' if credit_debit_row == 1 else 'D' + credit_debit = "C" if credit_debit_row == 1 else "D" if dl.is_reversal is True: - credit_debit = 'D' if credit_debit == 'C' else 'C' - jv_line = cls.get_jv_line(batch_type, - target_distribution, - description, - effective_date, - f'{dl.flow_through:<110}', - journal_name, - dl.amount, - line_number, - credit_debit) - ejv_content = '{}{}'.format(ejv_content, jv_line) # pylint:disable=consider-using-f-string + credit_debit = "D" if credit_debit == "C" else "C" + jv_line = cls.get_jv_line( + batch_type, + target_distribution, + description, + effective_date, + f"{dl.flow_through:<110}", + journal_name, + dl.amount, + line_number, + credit_debit, + ) + ejv_content = "{}{}".format(ejv_content, jv_line) # pylint:disable=consider-using-f-string line_number += 1 control_total += 1 @@ -235,7 +283,7 @@ def _create_ejv_file_for_partner(cls, batch_type: str): # pylint:disable=too-ma return jv_batch_trailer = cls.get_batch_trailer(batch_number, batch_total, batch_type, control_total) - ejv_content = f'{batch_header}{ejv_content}{jv_batch_trailer}' + ejv_content = f"{batch_header}{ejv_content}{jv_batch_trailer}" file_path_with_name, trg_file_path, file_name = cls.create_inbox_and_trg_files(ejv_content) cls.upload(ejv_content, file_name, file_path_with_name, trg_file_path) @@ -245,10 +293,9 @@ def _create_ejv_file_for_partner(cls, batch_type: str): # pylint:disable=too-ma time.sleep(1) @classmethod - def _update_disbursement_status_and_ejv_link(cls, - disbursement: Disbursement, - ejv_header_model: EjvHeaderModel, - sequence: int): + def _update_disbursement_status_and_ejv_link( + cls, disbursement: Disbursement, ejv_header_model: EjvHeaderModel, sequence: int + ): """Update disbursement status and create EJV Link.""" if isinstance(disbursement.target, InvoiceModel): disbursement.target.disbursement_status_code = DisbursementStatus.UPLOADED.value @@ -260,39 +307,53 @@ def _update_disbursement_status_and_ejv_link(cls, disbursement.target.status_code = DisbursementStatus.UPLOADED.value disbursement.target.processed_on = datetime.now(tz=timezone.utc) else: - raise NotImplementedError('Unknown disbursement type') + raise NotImplementedError("Unknown disbursement type") - db.session.add(EjvLinkModel(link_id=disbursement.line_item.identifier, - link_type=disbursement.line_item.target_type, - ejv_header_id=ejv_header_model.id, - disbursement_status_code=DisbursementStatus.UPLOADED.value, - sequence=sequence)) + db.session.add( + EjvLinkModel( + link_id=disbursement.line_item.identifier, + link_type=disbursement.line_item.target_type, + ejv_header_id=ejv_header_model.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + sequence=sequence, + ) + ) @classmethod def _get_partners_by_batch_type(cls, batch_type) -> List[CorpTypeModel]: """Return partners by batch type.""" # CREDIT : Ministry GL code -> disbursement_distribution_code_id on distribution_codes table # DEBIT : BC Registry GL Code -> distribution_code on fee_schedule, starts with 112 - bc_reg_client_code = current_app.config.get('CGI_BCREG_CLIENT_CODE') # 112 + bc_reg_client_code = current_app.config.get("CGI_BCREG_CLIENT_CODE") # 112 # Rule for GA. Credit is 112 and debit is 112. # Rule for GI. Debit is 112 and credit is not 112. - query = db.session.query(DistributionCodeModel.distribution_code_id) \ - .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) \ - .filter(DistributionCodeModel.account_id.is_(None)) \ - .filter(DistributionCodeModel.disbursement_distribution_code_id.is_(None)) \ - .filter_boolean(batch_type == 'GA', DistributionCodeModel.client == bc_reg_client_code) \ - .filter_boolean(batch_type == 'GI', DistributionCodeModel.client != bc_reg_client_code) + query = ( + db.session.query(DistributionCodeModel.distribution_code_id) + .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) + .filter(DistributionCodeModel.account_id.is_(None)) + .filter(DistributionCodeModel.disbursement_distribution_code_id.is_(None)) + .filter_boolean(batch_type == "GA", DistributionCodeModel.client == bc_reg_client_code) + .filter_boolean(batch_type == "GI", DistributionCodeModel.client != bc_reg_client_code) + ) # Find all distribution codes who have these partner distribution codes as disbursement. partner_distribution_codes = db.session.query(DistributionCodeModel.distribution_code_id).filter( - DistributionCodeModel.disbursement_distribution_code_id.in_(query)) + DistributionCodeModel.disbursement_distribution_code_id.in_(query) + ) - corp_type_query = db.session.query(FeeScheduleModel.corp_type_code) \ - .join(DistributionCodeLinkModel, - DistributionCodeLinkModel.fee_schedule_id == FeeScheduleModel.fee_schedule_id) \ + corp_type_query = ( + db.session.query(FeeScheduleModel.corp_type_code) + .join( + DistributionCodeLinkModel, + DistributionCodeLinkModel.fee_schedule_id == FeeScheduleModel.fee_schedule_id, + ) .filter(DistributionCodeLinkModel.distribution_code_id.in_(partner_distribution_codes)) + ) - result = db.session.query(CorpTypeModel) \ - .filter(CorpTypeModel.has_partner_disbursements.is_(True)) \ - .filter(CorpTypeModel.code.in_(corp_type_query)).all() + result = ( + db.session.query(CorpTypeModel) + .filter(CorpTypeModel.has_partner_disbursements.is_(True)) + .filter(CorpTypeModel.code.in_(corp_type_query)) + .all() + ) return result diff --git a/jobs/payment-jobs/tasks/ejv_payment_task.py b/jobs/payment-jobs/tasks/ejv_payment_task.py index ca90bad01..cb25b1972 100644 --- a/jobs/payment-jobs/tasks/ejv_payment_task.py +++ b/jobs/payment-jobs/tasks/ejv_payment_task.py @@ -26,7 +26,13 @@ from pay_api.models import PaymentAccount as PaymentAccountModel from pay_api.models import db from pay_api.utils.enums import ( - DisbursementStatus, EjvFileType, EJVLinkType, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod) + DisbursementStatus, + EjvFileType, + EJVLinkType, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, +) from pay_api.utils.util import generate_transaction_number from tasks.common.cgi_ejv import CgiEjv @@ -47,20 +53,20 @@ def create_ejv_file(cls): 5. Upload to sftp for processing. First upload JV file and then a TRG file. 6. Update the statuses and create records to for the batch. """ - cls._create_ejv_file_for_gov_account(batch_type='GI') - cls._create_ejv_file_for_gov_account(batch_type='GA') + cls._create_ejv_file_for_gov_account(batch_type="GI") + cls._create_ejv_file_for_gov_account(batch_type="GA") @classmethod def _create_ejv_file_for_gov_account(cls, batch_type: str): # pylint:disable=too-many-locals, too-many-statements """Create EJV file for the partner and upload.""" - ejv_content: str = '' + ejv_content: str = "" batch_total: float = 0 control_total: int = 0 ejv_file_model: EjvFileModel = EjvFileModel( file_type=EjvFileType.PAYMENT.value, file_ref=cls.get_file_name(), - disbursement_status_code=DisbursementStatus.UPLOADED.value + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).flush() batch_number = cls.get_batch_number(ejv_file_model.id) @@ -68,21 +74,21 @@ def _create_ejv_file_for_gov_account(cls, batch_type: str): # pylint:disable=to batch_header: str = cls.get_batch_header(batch_number, batch_type) - current_app.logger.info('Processing accounts.') + current_app.logger.info("Processing accounts.") for account_id in account_ids: - account_jv: str = '' + account_jv: str = "" # Find all invoices for the gov account to pay. invoices = cls._get_invoices_for_payment(account_id) pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(account_id) if not invoices or not pay_account.billable: continue - disbursement_desc = f'{pay_account.name[:100]:<100}' + disbursement_desc = f"{pay_account.name[:100]:<100}" effective_date: str = cls.get_effective_date() ejv_header_model: EjvFileModel = EjvHeaderModel( payment_account_id=account_id, disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file_model.id + ejv_file_id=ejv_file_model.id, ).flush() journal_name: str = cls.get_journal_name(ejv_header_model.id) debit_distribution_code: DistributionCodeModel = DistributionCodeModel.find_by_active_for_account( @@ -92,7 +98,7 @@ def _create_ejv_file_for_gov_account(cls, batch_type: str): # pylint:disable=to line_number: int = 0 total: float = 0 - current_app.logger.info(f'Processing invoices for account_id: {account_id}.') + current_app.logger.info(f"Processing invoices for account_id: {account_id}.") for inv in invoices: # If it's a JV reversal credit and debit is reversed. is_jv_reversal = inv.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value @@ -100,91 +106,132 @@ def _create_ejv_file_for_gov_account(cls, batch_type: str): # pylint:disable=to # If it's reversal, If there is no COMPLETED invoice reference, then no need to reverse it. # Else mark it as CANCELLED, as new invoice reference will be created if is_jv_reversal: - if (inv_ref := InvoiceReferenceModel.find_by_invoice_id_and_status( - inv.id, InvoiceReferenceStatus.COMPLETED.value - )) is None: + if ( + inv_ref := InvoiceReferenceModel.find_by_invoice_id_and_status( + inv.id, InvoiceReferenceStatus.COMPLETED.value + ) + ) is None: continue inv_ref.status_code = InvoiceReferenceStatus.CANCELLED.value line_items = inv.payment_line_items - invoice_number = f'#{inv.id}' - description = disbursement_desc[:-len(invoice_number)] + invoice_number - description = f'{description[:100]:<100}' + invoice_number = f"#{inv.id}" + description = disbursement_desc[: -len(invoice_number)] + invoice_number + description = f"{description[:100]:<100}" for line in line_items: # Line can have 2 distribution, 1 for the total and another one for service fees. line_distribution_code: DistributionCodeModel = DistributionCodeModel.find_by_id( - line.fee_distribution_id) + line.fee_distribution_id + ) if line.total > 0: total += line.total line_distribution = cls.get_distribution_string(line_distribution_code) - flow_through = f'{line.invoice_id:<110}' + flow_through = f"{line.invoice_id:<110}" # Credit to BCREG GL for a transaction (non-reversal) line_number += 1 control_total += 1 # If it's normal payment then the Line distribution goes as Credit, # else it goes as Debit as we need to debit the fund from BC registry GL. - account_jv = account_jv + cls.get_jv_line(batch_type, line_distribution, description, - effective_date, flow_through, journal_name, - line.total, - line_number, 'C' if not is_jv_reversal else 'D') + account_jv = account_jv + cls.get_jv_line( + batch_type, + line_distribution, + description, + effective_date, + flow_through, + journal_name, + line.total, + line_number, + "C" if not is_jv_reversal else "D", + ) # Debit from GOV ACCOUNT GL for a transaction (non-reversal) line_number += 1 control_total += 1 # If it's normal payment then the Gov account GL goes as Debit, # else it goes as Credit as we need to credit the fund back to ministry. - account_jv = account_jv + cls.get_jv_line(batch_type, debit_distribution, description, - effective_date, flow_through, journal_name, - line.total, - line_number, 'D' if not is_jv_reversal else 'C') + account_jv = account_jv + cls.get_jv_line( + batch_type, + debit_distribution, + description, + effective_date, + flow_through, + journal_name, + line.total, + line_number, + "D" if not is_jv_reversal else "C", + ) if line.service_fees > 0: service_fee_distribution_code: DistributionCodeModel = DistributionCodeModel.find_by_id( - line_distribution_code.service_fee_distribution_code_id) + line_distribution_code.service_fee_distribution_code_id + ) total += line.service_fees service_fee_distribution = cls.get_distribution_string(service_fee_distribution_code) - flow_through = f'{line.invoice_id:<110}' + flow_through = f"{line.invoice_id:<110}" # Credit to BCREG GL for a transaction (non-reversal) line_number += 1 control_total += 1 - account_jv = account_jv + cls.get_jv_line(batch_type, service_fee_distribution, - description, - effective_date, flow_through, journal_name, - line.service_fees, - line_number, 'C' if not is_jv_reversal else 'D') + account_jv = account_jv + cls.get_jv_line( + batch_type, + service_fee_distribution, + description, + effective_date, + flow_through, + journal_name, + line.service_fees, + line_number, + "C" if not is_jv_reversal else "D", + ) # Debit from GOV ACCOUNT GL for a transaction (non-reversal) line_number += 1 control_total += 1 - account_jv = account_jv + cls.get_jv_line(batch_type, debit_distribution, description, - effective_date, flow_through, journal_name, - line.service_fees, - line_number, 'D' if not is_jv_reversal else 'C') + account_jv = account_jv + cls.get_jv_line( + batch_type, + debit_distribution, + description, + effective_date, + flow_through, + journal_name, + line.service_fees, + line_number, + "D" if not is_jv_reversal else "C", + ) batch_total += total if total > 0: # A JV header for each account. control_total += 1 - account_jv = cls.get_jv_header(batch_type, cls.get_journal_batch_name(batch_number), - journal_name, total) + account_jv + account_jv = ( + cls.get_jv_header( + batch_type, + cls.get_journal_batch_name(batch_number), + journal_name, + total, + ) + + account_jv + ) ejv_content = ejv_content + account_jv - current_app.logger.info('Creating ejv invoice link records and setting invoice status.') + current_app.logger.info("Creating ejv invoice link records and setting invoice status.") sequence = 1 for inv in invoices: - current_app.logger.debug(f'Creating EJV Invoice Link for invoice id: {inv.id}') - ejv_invoice_link = EjvLinkModel(link_id=inv.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header_model.id, - disbursement_status_code=DisbursementStatus.UPLOADED.value, - sequence=sequence) + current_app.logger.debug(f"Creating EJV Invoice Link for invoice id: {inv.id}") + ejv_invoice_link = EjvLinkModel( + link_id=inv.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header_model.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + sequence=sequence, + ) db.session.add(ejv_invoice_link) sequence += 1 - current_app.logger.debug(f'Creating Invoice Reference for invoice id: {inv.id}') + current_app.logger.debug(f"Creating Invoice Reference for invoice id: {inv.id}") inv_ref = InvoiceReferenceModel( invoice_id=inv.id, invoice_number=generate_transaction_number(inv.id), reference_number=None, - status_code=InvoiceReferenceStatus.ACTIVE.value + status_code=InvoiceReferenceStatus.ACTIVE.value, ) db.session.add(inv_ref) db.session.flush() # Instead of flushing every entity, flush all at once. @@ -194,9 +241,9 @@ def _create_ejv_file_for_gov_account(cls, batch_type: str): # pylint:disable=to return batch_trailer: str = cls.get_batch_trailer(batch_number, batch_total, batch_type, control_total) - ejv_content = f'{batch_header}{ejv_content}{batch_trailer}' + ejv_content = f"{batch_header}{ejv_content}{batch_trailer}" file_path_with_name, trg_file_path, file_name = cls.create_inbox_and_trg_files(ejv_content) - current_app.logger.info('Uploading to sftp.') + current_app.logger.info("Uploading to sftp.") cls.upload(ejv_content, file_name, file_path_with_name, trg_file_path) db.session.commit() @@ -210,26 +257,34 @@ def _get_account_ids_for_payment(cls, batch_type) -> List[int]: # DEBIT : Distribution code against account. # Rule for GA. Credit is 112 and debit is 112. For BCREG client code is 112 # Rule for GI. Credit is 112 and debit is not 112. For BCREG client code is 112 - bc_reg_client_code = current_app.config.get('CGI_BCREG_CLIENT_CODE') - account_ids = db.session.query(DistributionCodeModel.account_id) \ - .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) \ - .filter(DistributionCodeModel.account_id.isnot(None)) \ - .filter_boolean(batch_type == 'GA', DistributionCodeModel.client == bc_reg_client_code) \ - .filter_boolean(batch_type != 'GA', DistributionCodeModel.client != bc_reg_client_code) \ + bc_reg_client_code = current_app.config.get("CGI_BCREG_CLIENT_CODE") + account_ids = ( + db.session.query(DistributionCodeModel.account_id) + .filter(DistributionCodeModel.stop_ejv.is_(False) | DistributionCodeModel.stop_ejv.is_(None)) + .filter(DistributionCodeModel.account_id.isnot(None)) + .filter_boolean(batch_type == "GA", DistributionCodeModel.client == bc_reg_client_code) + .filter_boolean(batch_type != "GA", DistributionCodeModel.client != bc_reg_client_code) .all() + ) return [account_id_tuple[0] for account_id_tuple in account_ids] @classmethod def _get_invoices_for_payment(cls, account_id: int) -> List[InvoiceModel]: """Return invoices for payments.""" - valid_statuses = (InvoiceStatus.APPROVED.value, InvoiceStatus.REFUND_REQUESTED.value) - invoice_ref_subquery = db.session.query(InvoiceReferenceModel.invoice_id). \ - filter(InvoiceReferenceModel.status_code.in_((InvoiceReferenceStatus.ACTIVE.value,))) - - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.invoice_status_code.in_(valid_statuses)) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EJV.value) \ - .filter(InvoiceModel.payment_account_id == account_id) \ - .filter(InvoiceModel.id.notin_(invoice_ref_subquery)) \ + valid_statuses = ( + InvoiceStatus.APPROVED.value, + InvoiceStatus.REFUND_REQUESTED.value, + ) + invoice_ref_subquery = db.session.query(InvoiceReferenceModel.invoice_id).filter( + InvoiceReferenceModel.status_code.in_((InvoiceReferenceStatus.ACTIVE.value,)) + ) + + invoices: List[InvoiceModel] = ( + db.session.query(InvoiceModel) + .filter(InvoiceModel.invoice_status_code.in_(valid_statuses)) + .filter(InvoiceModel.payment_method_code == PaymentMethod.EJV.value) + .filter(InvoiceModel.payment_account_id == account_id) + .filter(InvoiceModel.id.notin_(invoice_ref_subquery)) .all() + ) return invoices diff --git a/jobs/payment-jobs/tasks/routing_slip_task.py b/jobs/payment-jobs/tasks/routing_slip_task.py index 46b23fd59..84d98feaa 100644 --- a/jobs/payment-jobs/tasks/routing_slip_task.py +++ b/jobs/payment-jobs/tasks/routing_slip_task.py @@ -31,8 +31,17 @@ from pay_api.services.cfs_service import CFSService from pay_api.services.receipt import Receipt from pay_api.utils.enums import ( - CfsAccountStatus, CfsReceiptStatus, InvoiceReferenceStatus, InvoiceStatus, LineItemStatus, PaymentMethod, - PaymentStatus, PaymentSystem, ReverseOperation, RoutingSlipStatus) + CfsAccountStatus, + CfsReceiptStatus, + InvoiceReferenceStatus, + InvoiceStatus, + LineItemStatus, + PaymentMethod, + PaymentStatus, + PaymentSystem, + ReverseOperation, + RoutingSlipStatus, +) from sentry_sdk import capture_message @@ -54,40 +63,40 @@ def link_routing_slips(cls): # 3. Change the payment account of child to parent. # 4. Change the status. try: - current_app.logger.debug(f'Linking Routing Slip: {routing_slip.number}') - payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( - routing_slip.payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, - PaymentMethod.INTERNAL.value) + current_app.logger.debug(f"Linking Routing Slip: {routing_slip.number}") + payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + payment_account.id, PaymentMethod.INTERNAL.value + ) # reverse routing slip receipt - if CFSService.get_receipt(cfs_account, routing_slip.number).get('status') != CfsReceiptStatus.REV.value: + if CFSService.get_receipt(cfs_account, routing_slip.number).get("status") != CfsReceiptStatus.REV.value: CFSService.reverse_rs_receipt_in_cfs(cfs_account, routing_slip.number, ReverseOperation.LINK.value) cfs_account.status = CfsAccountStatus.INACTIVE.value # apply receipt to parent cfs account parent_rs: RoutingSlipModel = RoutingSlipModel.find_by_number(routing_slip.parent_number) parent_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( - parent_rs.payment_account_id) + parent_rs.payment_account_id + ) parent_cfs_account = CfsAccountModel.find_effective_by_payment_method( - parent_payment_account.id, PaymentMethod.INTERNAL.value) + parent_payment_account.id, PaymentMethod.INTERNAL.value + ) # For linked routing slip receipts, append 'L' to the number to avoid duplicate error receipt_number = routing_slip.generate_cas_receipt_number() - CFSService.create_cfs_receipt(cfs_account=parent_cfs_account, - rcpt_number=receipt_number, - rcpt_date=routing_slip.routing_slip_date.strftime( - '%Y-%m-%d'), - amount=routing_slip.total, - payment_method=parent_payment_account.payment_method, - access_token=CFSService.get_token( - PaymentSystem.FAS).json() - .get('access_token') - ) + CFSService.create_cfs_receipt( + cfs_account=parent_cfs_account, + rcpt_number=receipt_number, + rcpt_date=routing_slip.routing_slip_date.strftime("%Y-%m-%d"), + amount=routing_slip.total, + payment_method=parent_payment_account.payment_method, + access_token=CFSService.get_token(PaymentSystem.FAS).json().get("access_token"), + ) # Add to the list if parent is NSF, to apply the receipts. if parent_rs.status == RoutingSlipStatus.NSF.value: total_invoice_amount = cls._apply_routing_slips_to_pending_invoices(parent_rs) - current_app.logger.debug(f'Total Invoice Amount : {total_invoice_amount}') + current_app.logger.debug(f"Total Invoice Amount : {total_invoice_amount}") # Update the parent routing slip status to ACTIVE parent_rs.status = RoutingSlipStatus.ACTIVE.value # linking routing slip balance is transferred ,so use the total @@ -97,8 +106,10 @@ def link_routing_slips(cls): except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on Linking Routing Slip number:={routing_slip.number}, ' - f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') + f"Error on Linking Routing Slip number:={routing_slip.number}, " + f"routing slip : {routing_slip.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @@ -114,46 +125,52 @@ def process_correction(cls): """ routing_slips = cls._get_routing_slip_by_status(RoutingSlipStatus.CORRECTION.value) - current_app.logger.info(f'Found {len(routing_slips)} to process CORRECTIONS.') + current_app.logger.info(f"Found {len(routing_slips)} to process CORRECTIONS.") for rs in routing_slips: try: - wait_for_create_invoice_job = any(x.invoice_status_code in [ - InvoiceStatus.APPROVED.value, InvoiceStatus.CREATED.value] - for x in rs.invoices) + wait_for_create_invoice_job = any( + x.invoice_status_code in [InvoiceStatus.APPROVED.value, InvoiceStatus.CREATED.value] + for x in rs.invoices + ) if wait_for_create_invoice_job: continue - current_app.logger.debug(f'Correcting Routing Slip: {rs.number}') + current_app.logger.debug(f"Correcting Routing Slip: {rs.number}") payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(rs.payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, - PaymentMethod.INTERNAL.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + payment_account.id, PaymentMethod.INTERNAL.value + ) - CFSService.reverse_rs_receipt_in_cfs(cfs_account, rs.generate_cas_receipt_number(), - ReverseOperation.CORRECTION.value) + CFSService.reverse_rs_receipt_in_cfs( + cfs_account, + rs.generate_cas_receipt_number(), + ReverseOperation.CORRECTION.value, + ) # Update the version, which generates a new receipt number. This is to avoid duplicate receipt number. rs.cas_version_suffix += 1 # Recreate the receipt with the modified total. - CFSService.create_cfs_receipt(cfs_account=cfs_account, - rcpt_number=rs.generate_cas_receipt_number(), - rcpt_date=rs.routing_slip_date.strftime( - '%Y-%m-%d'), - amount=rs.total, - payment_method=payment_account.payment_method, - access_token=CFSService.get_token( - PaymentSystem.FAS).json().get('access_token') - ) + CFSService.create_cfs_receipt( + cfs_account=cfs_account, + rcpt_number=rs.generate_cas_receipt_number(), + rcpt_date=rs.routing_slip_date.strftime("%Y-%m-%d"), + amount=rs.total, + payment_method=payment_account.payment_method, + access_token=CFSService.get_token(PaymentSystem.FAS).json().get("access_token"), + ) cls._reset_invoices_and_references_to_created(rs) cls._apply_routing_slips_to_pending_invoices(rs) - rs.status = RoutingSlipStatus.COMPLETE.value if rs.remaining_amount == 0 \ - else RoutingSlipStatus.ACTIVE.value + rs.status = ( + RoutingSlipStatus.COMPLETE.value if rs.remaining_amount == 0 else RoutingSlipStatus.ACTIVE.value + ) rs.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on Processing CORRECTION for :={rs.number}, ' - f'routing slip : {rs.id}, ERROR : {str(e)}', level='error') + f"Error on Processing CORRECTION for :={rs.number}, " f"routing slip : {rs.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @@ -168,18 +185,19 @@ def process_void(cls): 4. Adjust the remaining amount and cas_version_suffix for VOID. """ routing_slips = cls._get_routing_slip_by_status(RoutingSlipStatus.VOID.value) - current_app.logger.info(f'Found {len(routing_slips)} to process VOID.') + current_app.logger.info(f"Found {len(routing_slips)} to process VOID.") for routing_slip in routing_slips: try: - current_app.logger.debug(f'Reverse receipt {routing_slip.number}') + current_app.logger.debug(f"Reverse receipt {routing_slip.number}") if routing_slip.invoices: # FUTURE: If this is hit, and needs to change, we can do something similar to NSF. # EX. Reset the invoices to created, invoice reference to active. - raise Exception('VOID - has transactions/invoices.') # pylint: disable=broad-exception-raised + raise Exception("VOID - has transactions/invoices.") # pylint: disable=broad-exception-raised payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, - PaymentMethod.INTERNAL.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + payment_account.id, PaymentMethod.INTERNAL.value + ) # Reverse all child routing slips, as all linked routing slips are also considered as VOID. child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number) @@ -194,8 +212,10 @@ def process_void(cls): routing_slip.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on Processing VOID for :={routing_slip.number}, ' - f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') + f"Error on Processing VOID for :={routing_slip.number}, " + f"routing slip : {routing_slip.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @@ -209,16 +229,17 @@ def process_nsf(cls): 3. Add an invoice for NSF fees. """ routing_slips = cls._get_routing_slip_by_status(RoutingSlipStatus.NSF.value) - current_app.logger.info(f'Found {len(routing_slips)} to process NSF.') + current_app.logger.info(f"Found {len(routing_slips)} to process NSF.") for routing_slip in routing_slips: # 1. Reverse the routing slip receipt. # 2. Reverse all the child receipts. # 3. Change the CFS Account status to FREEZE. try: - current_app.logger.debug(f'Reverse receipt {routing_slip.number}') + current_app.logger.debug(f"Reverse receipt {routing_slip.number}") payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, - PaymentMethod.INTERNAL.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + payment_account.id, PaymentMethod.INTERNAL.value + ) # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF. child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number) @@ -226,8 +247,9 @@ def process_nsf(cls): receipt_number = rs.generate_cas_receipt_number() CFSService.reverse_rs_receipt_in_cfs(cfs_account, receipt_number, ReverseOperation.NSF.value) - for payment in db.session.query(PaymentModel) \ - .filter(PaymentModel.receipt_number == receipt_number).all(): + for payment in ( + db.session.query(PaymentModel).filter(PaymentModel.receipt_number == receipt_number).all() + ): payment.payment_status_code = PaymentStatus.FAILED.value cfs_account.status = CfsAccountStatus.FREEZE.value @@ -241,8 +263,10 @@ def process_nsf(cls): except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on Processing NSF for :={routing_slip.number}, ' - f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') + f"Error on Processing NSF for :={routing_slip.number}, " + f"routing slip : {routing_slip.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @@ -254,19 +278,29 @@ def adjust_routing_slips(cls): 1. Adjust routing slip receipts for any Write off routing slips. 2. Adjust routing slip receipts for any Refund approved routing slips. """ - current_app.logger.info('< 0).all() - current_app.logger.info(f'Found {len(routing_slips)} to write off or refund authorized.') + routing_slips = ( + db.session.query(RoutingSlipModel) + .filter( + RoutingSlipModel.status.in_(adjust_statuses), + RoutingSlipModel.remaining_amount > 0, + ) + .all() + ) + current_app.logger.info(f"Found {len(routing_slips)} to write off or refund authorized.") for routing_slip in routing_slips: try: # 1.Adjust the routing slip and it's child routing slips for the remaining balance. - current_app.logger.debug(f'Adjusting routing slip {routing_slip.number}') + current_app.logger.debug(f"Adjusting routing slip {routing_slip.number}") payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, - PaymentMethod.INTERNAL.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + payment_account.id, PaymentMethod.INTERNAL.value + ) # reverse routing slip receipt # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF. @@ -284,28 +318,38 @@ def adjust_routing_slips(cls): except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on Adjusting Routing Slip for :={routing_slip.number}, ' - f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') + f"Error on Adjusting Routing Slip for :={routing_slip.number}, " + f"routing slip : {routing_slip.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) continue @classmethod def _get_routing_slip_by_status(cls, status: RoutingSlipStatus) -> List[RoutingSlipModel]: """Get routing slip by status.""" - return db.session.query(RoutingSlipModel) \ - .join(PaymentAccountModel, PaymentAccountModel.id == RoutingSlipModel.payment_account_id) \ - .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ - .filter(RoutingSlipModel.status == status) \ - .filter(CfsAccountModel.payment_method == PaymentMethod.INTERNAL.value) \ - .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value).all() + return ( + db.session.query(RoutingSlipModel) + .join( + PaymentAccountModel, + PaymentAccountModel.id == RoutingSlipModel.payment_account_id, + ) + .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) + .filter(RoutingSlipModel.status == status) + .filter(CfsAccountModel.payment_method == PaymentMethod.INTERNAL.value) + .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value) + .all() + ) @classmethod def _reset_invoices_and_references_to_created(cls, routing_slip: RoutingSlipModel): """Reset Invoices, Invoice references and Receipts for routing slip.""" - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.routing_slip == routing_slip.number) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ + invoices: List[InvoiceModel] = ( + db.session.query(InvoiceModel) + .filter(InvoiceModel.routing_slip == routing_slip.number) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) .all() + ) for inv in invoices: # Reset the statuses inv.invoice_status_code = InvoiceStatus.CREATED.value @@ -318,11 +362,16 @@ def _reset_invoices_and_references_to_created(cls, routing_slip: RoutingSlipMode db.session.delete(receipt) @classmethod - def _create_nsf_invoice(cls, cfs_account: CfsAccountModel, rs_number: str, - payment_account: PaymentAccountModel) -> InvoiceModel: + def _create_nsf_invoice( + cls, + cfs_account: CfsAccountModel, + rs_number: str, + payment_account: PaymentAccountModel, + ) -> InvoiceModel: """Create Invoice, line item and invoice reference records.""" - fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type(corp_type_code='BCR', - filing_type_code='NSF') + fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type( + corp_type_code="BCR", filing_type_code="NSF" + ) invoice = InvoiceModel( bcol_account=payment_account.bcol_account, payment_account_id=payment_account.id, @@ -332,14 +381,15 @@ def _create_nsf_invoice(cls, cfs_account: CfsAccountModel, rs_number: str, service_fees=0, paid=0, payment_method_code=PaymentMethod.INTERNAL.value, - corp_type_code='BCR', + corp_type_code="BCR", created_on=datetime.now(tz=timezone.utc), - created_by='SYSTEM', - routing_slip=rs_number + created_by="SYSTEM", + routing_slip=rs_number, ) invoice = invoice.save() distribution: DistributionCodeModel = DistributionCodeModel.find_by_active_for_fee_schedule( - fee_schedule.fee_schedule_id) + fee_schedule.fee_schedule_id + ) line_item = PaymentLineItemModel( invoice_id=invoice.id, @@ -353,21 +403,24 @@ def _create_nsf_invoice(cls, cfs_account: CfsAccountModel, rs_number: str, future_effective_fees=0, line_item_status_code=LineItemStatus.ACTIVE.value, service_fees=0, - fee_distribution_id=distribution.distribution_code_id) + fee_distribution_id=distribution.distribution_code_id, + ) line_item.save() - invoice_response = CFSService.create_account_invoice(transaction_number=invoice.id, - line_items=invoice.payment_line_items, - cfs_account=cfs_account) + invoice_response = CFSService.create_account_invoice( + transaction_number=invoice.id, + line_items=invoice.payment_line_items, + cfs_account=cfs_account, + ) - invoice_number = invoice_response.get('invoice_number', None) - current_app.logger.info(f'invoice_number {invoice_number} created in CFS for NSF.') + invoice_number = invoice_response.get("invoice_number", None) + current_app.logger.info(f"invoice_number {invoice_number} created in CFS for NSF.") InvoiceReferenceModel( invoice_id=invoice.id, invoice_number=invoice_number, - reference_number=invoice_response.get('pbc_ref_number', None), - status_code=InvoiceReferenceStatus.ACTIVE.value + reference_number=invoice_response.get("pbc_ref_number", None), + status_code=InvoiceReferenceStatus.ACTIVE.value, ).save() return invoice @@ -375,31 +428,45 @@ def _create_nsf_invoice(cls, cfs_account: CfsAccountModel, rs_number: str, @classmethod def _apply_routing_slips_to_pending_invoices(cls, routing_slip: RoutingSlipModel) -> float: """Apply the routing slips again.""" - current_app.logger.info(f'Applying routing slips to pending invoices for routing slip: {routing_slip.number}') + current_app.logger.info(f"Applying routing slips to pending invoices for routing slip: {routing_slip.number}") routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( - routing_slip.payment_account_id) + routing_slip.payment_account_id + ) # apply invoice to the active CFS_ACCOUNT which will be the parent routing slip - active_cfs_account = CfsAccountModel.find_effective_by_payment_method(routing_slip_payment_account.id, - PaymentMethod.INTERNAL.value) + active_cfs_account = CfsAccountModel.find_effective_by_payment_method( + routing_slip_payment_account.id, PaymentMethod.INTERNAL.value + ) - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.routing_slip == routing_slip.number, - InvoiceModel.invoice_status_code.in_([InvoiceStatus.CREATED.value, InvoiceStatus.APPROVED.value])) \ + invoices: List[InvoiceModel] = ( + db.session.query(InvoiceModel) + .filter( + InvoiceModel.routing_slip == routing_slip.number, + InvoiceModel.invoice_status_code.in_([InvoiceStatus.CREATED.value, InvoiceStatus.APPROVED.value]), + ) .all() - current_app.logger.info(f'Found {len(invoices)} to apply receipt') + ) + current_app.logger.info(f"Found {len(invoices)} to apply receipt") applied_amount = 0 for inv in invoices: inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( inv.id, InvoiceReferenceStatus.ACTIVE.value ) cls.apply_routing_slips_to_invoice( - routing_slip_payment_account, active_cfs_account, routing_slip, inv, inv_ref.invoice_number + routing_slip_payment_account, + active_cfs_account, + routing_slip, + inv, + inv_ref.invoice_number, ) # IF invoice balance is zero, then update records. - if CFSService.get_invoice(cfs_account=active_cfs_account, inv_number=inv_ref.invoice_number) \ - .get('amount_due') == 0: + if ( + CFSService.get_invoice(cfs_account=active_cfs_account, inv_number=inv_ref.invoice_number).get( + "amount_due" + ) + == 0 + ): applied_amount += inv.total inv_ref.status_code = InvoiceReferenceStatus.COMPLETED.value inv.invoice_status_code = InvoiceStatus.PAID.value @@ -408,12 +475,14 @@ def _apply_routing_slips_to_pending_invoices(cls, routing_slip: RoutingSlipModel return applied_amount @classmethod - def apply_routing_slips_to_invoice(cls, # pylint: disable = too-many-arguments, too-many-locals - routing_slip_payment_account: PaymentAccountModel, - active_cfs_account: CfsAccountModel, - parent_routing_slip: RoutingSlipModel, - invoice: InvoiceModel, - invoice_number: str) -> bool: + def apply_routing_slips_to_invoice( # pylint: disable = too-many-arguments, too-many-locals + cls, + routing_slip_payment_account: PaymentAccountModel, + active_cfs_account: CfsAccountModel, + parent_routing_slip: RoutingSlipModel, + invoice: InvoiceModel, + invoice_number: str, + ) -> bool: """Apply routing slips (receipts in CFS) to invoice.""" has_errors = False child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(parent_routing_slip.number) @@ -422,37 +491,41 @@ def apply_routing_slips_to_invoice(cls, # pylint: disable = too-many-arguments, try: # apply receipt now receipt_number = routing_slip.generate_cas_receipt_number() - current_app.logger.debug(f'Apply receipt {receipt_number} on invoice {invoice_number} ' - f'for routing slip {routing_slip.number}') + current_app.logger.debug( + f"Apply receipt {receipt_number} on invoice {invoice_number} " + f"for routing slip {routing_slip.number}" + ) # If balance of receipt is zero, continue to next receipt. receipt_balance_before_apply = float( - CFSService.get_receipt(active_cfs_account, receipt_number).get('unapplied_amount') + CFSService.get_receipt(active_cfs_account, receipt_number).get("unapplied_amount") ) - current_app.logger.debug(f'Current balance on {receipt_number} = {receipt_balance_before_apply}') + current_app.logger.debug(f"Current balance on {receipt_number} = {receipt_balance_before_apply}") if receipt_balance_before_apply == 0: continue - current_app.logger.debug(f'Applying receipt {receipt_number} to {invoice_number}') + current_app.logger.debug(f"Applying receipt {receipt_number} to {invoice_number}") receipt_response = CFSService.apply_receipt(active_cfs_account, receipt_number, invoice_number) # Create receipt. receipt = Receipt() - receipt.receipt_number = receipt_response.json().get('receipt_number', None) - receipt_amount = receipt_balance_before_apply - float(receipt_response.json().get('unapplied_amount')) + receipt.receipt_number = receipt_response.json().get("receipt_number", None) + receipt_amount = receipt_balance_before_apply - float(receipt_response.json().get("unapplied_amount")) receipt.receipt_amount = receipt_amount receipt.invoice_id = invoice.id receipt.receipt_date = datetime.now(tz=timezone.utc) receipt.flush() invoice_from_cfs = CFSService.get_invoice(active_cfs_account, invoice_number) - if invoice_from_cfs.get('amount_due') == 0: + if invoice_from_cfs.get("amount_due") == 0: break except Exception as e: # NOQA # pylint: disable=broad-except capture_message( - f'Error on creating Routing Slip invoice: account id={routing_slip_payment_account.id}, ' - f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') + f"Error on creating Routing Slip invoice: account id={routing_slip_payment_account.id}, " + f"routing slip : {routing_slip.id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) has_errors = True continue diff --git a/jobs/payment-jobs/tasks/stale_payment_task.py b/jobs/payment-jobs/tasks/stale_payment_task.py index e3480821c..f971824d9 100644 --- a/jobs/payment-jobs/tasks/stale_payment_task.py +++ b/jobs/payment-jobs/tasks/stale_payment_task.py @@ -25,8 +25,7 @@ from pay_api.utils.enums import InvoiceReferenceStatus, PaymentStatus, TransactionStatus from requests import HTTPError - -STATUS_PAID = ('PAID', 'CMPLT') +STATUS_PAID = ("PAID", "CMPLT") class StalePaymentTask: # pylint: disable=too-few-public-methods @@ -35,7 +34,7 @@ class StalePaymentTask: # pylint: disable=too-few-public-methods @classmethod def update_stale_payments(cls): """Update stale payments.""" - current_app.logger.info(f'StalePaymentTask Ran at {datetime.datetime.now(tz=datetime.timezone.utc)}') + current_app.logger.info(f"StalePaymentTask Ran at {datetime.datetime.now(tz=datetime.timezone.utc)}") cls._update_stale_payments() cls._delete_marked_payments() cls._verify_created_direct_pay_invoices() @@ -49,31 +48,39 @@ def _update_stale_payments(cls): """ stale_transactions = PaymentTransactionModel.find_stale_records(minutes=30) # Find all payments which were failed due to service unavailable error. - service_unavailable_transactions = db.session.query(PaymentTransactionModel)\ - .join(PaymentModel, PaymentModel.id == PaymentTransactionModel.payment_id) \ - .filter(PaymentModel.payment_status_code == PaymentStatus.CREATED.value)\ - .filter(PaymentTransactionModel.pay_system_reason_code == Error.SERVICE_UNAVAILABLE.name)\ + service_unavailable_transactions = ( + db.session.query(PaymentTransactionModel) + .join(PaymentModel, PaymentModel.id == PaymentTransactionModel.payment_id) + .filter(PaymentModel.payment_status_code == PaymentStatus.CREATED.value) + .filter(PaymentTransactionModel.pay_system_reason_code == Error.SERVICE_UNAVAILABLE.name) .all() + ) if len(stale_transactions) == 0 and len(service_unavailable_transactions) == 0: - current_app.logger.info(f'Stale Transaction Job Ran at {datetime.datetime.now(tz=datetime.timezone.utc)}.' - 'But No records found!') + current_app.logger.info( + f"Stale Transaction Job Ran at {datetime.datetime.now(tz=datetime.timezone.utc)}." + "But No records found!" + ) for transaction in [*stale_transactions, *service_unavailable_transactions]: try: - current_app.logger.info(f'Stale Transaction Job found records.Payment Id: {transaction.payment_id}, ' - f'Transaction Id : {transaction.id}') + current_app.logger.info( + f"Stale Transaction Job found records.Payment Id: {transaction.payment_id}, " + f"Transaction Id : {transaction.id}" + ) TransactionService.update_transaction(transaction.id, pay_response_url=None) - current_app.logger.info(f'Stale Transaction Job Updated records.Payment Id: {transaction.payment_id}, ' - f'Transaction Id : {transaction.id}') + current_app.logger.info( + f"Stale Transaction Job Updated records.Payment Id: {transaction.payment_id}, " + f"Transaction Id : {transaction.id}" + ) except BusinessException as err: # just catch and continue .Don't stop # If the error is for COMPLETED PAYMENT, then mark the transaction as CANCELLED # as there would be COMPLETED transaction in place and continue. if err.code == Error.COMPLETED_PAYMENT.name: - current_app.logger.info('Completed payment, marking transaction as CANCELLED.') + current_app.logger.info("Completed payment, marking transaction as CANCELLED.") transaction.status_code = TransactionStatus.CANCELLED.value transaction.save() else: - current_app.logger.info('Stale Transaction Error on update_transaction') + current_app.logger.info("Stale Transaction Error on update_transaction") current_app.logger.info(err) @classmethod @@ -85,30 +92,31 @@ def _delete_marked_payments(cls): """ invoices_to_delete = InvoiceModel.find_invoices_marked_for_delete() if len(invoices_to_delete) == 0: - current_app.logger.info(f'Delete Invoice Job Ran at {datetime.datetime.now(tz=datetime.timezone.utc)}.' - 'But No records found!') + current_app.logger.info( + f"Delete Invoice Job Ran at {datetime.datetime.now(tz=datetime.timezone.utc)}." "But No records found!" + ) for invoice in invoices_to_delete: try: - current_app.logger.info(f'Delete Payment Job found records.Payment Id: {invoice.id}') + current_app.logger.info(f"Delete Payment Job found records.Payment Id: {invoice.id}") PaymentService.delete_invoice(invoice.id) - current_app.logger.info(f'Delete Payment Job Updated records.Payment Id: {invoice.id}') + current_app.logger.info(f"Delete Payment Job Updated records.Payment Id: {invoice.id}") except BusinessException as err: # just catch and continue .Don't stop - current_app.logger.warn('Error on delete_payment') + current_app.logger.warn("Error on delete_payment") current_app.logger.warn(err) @classmethod def _verify_created_direct_pay_invoices(cls): """Verify recent invoice with PAYBC.""" created_invoices = InvoiceModel.find_created_direct_pay_invoices(days=2) - current_app.logger.info(f'Found {len(created_invoices)} Created Invoices to be Verified.') + current_app.logger.info(f"Found {len(created_invoices)} Created Invoices to be Verified.") for invoice in created_invoices: try: - current_app.logger.info(f'Verify Invoice Job found records.Invoice Id: {invoice.id}') + current_app.logger.info(f"Verify Invoice Job found records.Invoice Id: {invoice.id}") paybc_invoice = DirectPayService.query_order_status(invoice, InvoiceReferenceStatus.ACTIVE.value) if paybc_invoice.paymentstatus in STATUS_PAID: - current_app.logger.debug('_update_active_transactions') + current_app.logger.debug("_update_active_transactions") transaction = TransactionService.find_active_by_invoice_id(invoice.id) if transaction: # check existing payment status in PayBC and save receipt @@ -116,7 +124,10 @@ def _verify_created_direct_pay_invoices(cls): except HTTPError as http_err: if http_err.response is None or http_err.response.status_code != 404: - current_app.logger.error(f'HTTPError on verifying invoice {invoice.id}: {http_err}', exc_info=True) - current_app.logger.info(f'Invoice not found (404) at PAYBC. Skipping invoice id: {invoice.id}') - except Exception as err: # NOQA # pylint: disable=broad-except - current_app.logger.error(f'Error verifying invoice {invoice.id}: {err}', exc_info=True) + current_app.logger.error( + f"HTTPError on verifying invoice {invoice.id}: {http_err}", + exc_info=True, + ) + current_app.logger.info(f"Invoice not found (404) at PAYBC. Skipping invoice id: {invoice.id}") + except Exception as err: # NOQA # pylint: disable=broad-except + current_app.logger.error(f"Error verifying invoice {invoice.id}: {err}", exc_info=True) diff --git a/jobs/payment-jobs/tasks/statement_notification_task.py b/jobs/payment-jobs/tasks/statement_notification_task.py index ead5242cc..8589fb4d6 100644 --- a/jobs/payment-jobs/tasks/statement_notification_task.py +++ b/jobs/payment-jobs/tasks/statement_notification_task.py @@ -20,15 +20,15 @@ from pay_api.models.payment import PaymentAccount as PaymentAccountModel from pay_api.models.statement import Statement as StatementModel from pay_api.models.statement_recipients import StatementRecipients as StatementRecipientsModel -from pay_api.services.oauth_service import OAuthService from pay_api.services import Statement as StatementService from pay_api.services.flags import flags +from pay_api.services.oauth_service import OAuthService from pay_api.utils.enums import AuthHeaderType, ContentType, NotificationStatus, PaymentMethod from utils.auth import get_token from utils.mailer import publish_statement_notification -ENV = Environment(loader=FileSystemLoader('.'), autoescape=True) +ENV = Environment(loader=FileSystemLoader("."), autoescape=True) class StatementNotificationTask: # pylint:disable=too-few-public-methods @@ -46,20 +46,20 @@ def send_notifications(cls): 4. Check status and update back """ statements_with_pending_notifications = StatementModel.find_all_statements_by_notification_status( - (NotificationStatus.PENDING.value,)) + (NotificationStatus.PENDING.value,) + ) if statement_len := len(statements_with_pending_notifications) < 1: - current_app.logger.info('No Statements with Pending notifications Found!') + current_app.logger.info("No Statements with Pending notifications Found!") return - current_app.logger.info(f'{statement_len} Statements with Pending notifications Found!') + current_app.logger.info(f"{statement_len} Statements with Pending notifications Found!") token = get_token() params = { - 'logo_url': - f"{current_app.config.get('AUTH_WEB_URL')}/{current_app.config.get('REGISTRIES_LOGO_IMAGE_NAME')}", - 'url': f"{current_app.config.get('AUTH_WEB_URL')}" + "logo_url": f"{current_app.config.get('AUTH_WEB_URL')}/{current_app.config.get('REGISTRIES_LOGO_IMAGE_NAME')}", + "url": f"{current_app.config.get('AUTH_WEB_URL')}", } - template = ENV.get_template('statement_notification.html') + template = ENV.get_template("statement_notification.html") for statement in statements_with_pending_notifications: statement.notification_status_code = NotificationStatus.PROCESSING.value statement.notification_date = datetime.now(tz=timezone.utc) @@ -67,41 +67,43 @@ def send_notifications(cls): payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(statement.payment_account_id) recipients = StatementRecipientsModel.find_all_recipients_for_payment_id(statement.payment_account_id) if len(recipients) < 1: - current_app.logger.info(f'No recipients found for statement: ' - f'{statement.payment_account_id}.Skipping sending') + current_app.logger.info( + f"No recipients found for statement: " f"{statement.payment_account_id}.Skipping sending" + ) statement.notification_status_code = NotificationStatus.SKIP.value statement.notification_date = datetime.now(tz=timezone.utc) statement.commit() continue - to_emails = ','.join([str(recipient.email) for recipient in recipients]) - current_app.logger.info(f'Recipients email Ids:{to_emails}') - params['org_name'] = payment_account.name - params['frequency'] = statement.frequency.lower() + to_emails = ",".join([str(recipient.email) for recipient in recipients]) + current_app.logger.info(f"Recipients email Ids:{to_emails}") + params["org_name"] = payment_account.name + params["frequency"] = statement.frequency.lower() # logic changed https://github.com/bcgov/entity/issues/4809 # params.update({'url': params['url'].replace('orgId', payment_account.auth_account_id)}) notification_success = True - eft_enabled = flags.is_on('enable-eft-payment-method', default=False) + eft_enabled = flags.is_on("enable-eft-payment-method", default=False) try: if not payment_account.payment_method == PaymentMethod.EFT.value: notification_success = cls.send_email(token, to_emails, template.render(params)) elif eft_enabled: # This statement template currently only used for EFT result = StatementService.get_summary(payment_account.auth_account_id) - notification_success = publish_statement_notification(payment_account, statement, - result['total_due'], to_emails) + notification_success = publish_statement_notification( + payment_account, statement, result["total_due"], to_emails + ) else: # EFT not enabled - mark skip - shouldn't happen, but safeguard for manual data injection statement.notification_status_code = NotificationStatus.SKIP.value statement.notification_date = datetime.now(tz=timezone.utc) statement.commit() continue - except Exception as e: # NOQA # pylint:disable=broad-except - current_app.logger.error(' 0 else None auth_account_override = arguments[1] if arguments and len(arguments) > 1 else None - target_time = get_local_time(datetime.now(tz=timezone.utc)) if date_override is None \ - else datetime.strptime(date_override, '%Y-%m-%d') + timedelta(days=1) + target_time = ( + get_local_time(datetime.now(tz=timezone.utc)) + if date_override is None + else datetime.strptime(date_override, "%Y-%m-%d") + timedelta(days=1) + ) cls.has_date_override = date_override is not None cls.has_account_override = auth_account_override is not None if date_override: - current_app.logger.debug(f'Generating statements for: {date_override} using date override.') + current_app.logger.debug(f"Generating statements for: {date_override} using date override.") if auth_account_override: - current_app.logger.debug(f'Generating statements for: {auth_account_override} using account override.') + current_app.logger.debug(f"Generating statements for: {auth_account_override} using account override.") # If today is sunday - generate all weekly statements for pervious week # If today is month beginning - generate all monthly statements for previous month # For every day generate all daily statements for previous day @@ -82,17 +89,19 @@ def _generate_gap_statements(cls, target_time, account_override): statement_from, _ = get_week_start_and_end_date(previous_day, index=0) statement_from = statement_from.date() statement_to = previous_day.date() - statement_settings = StatementSettingsService.find_accounts_settings_by_frequency(previous_day, - StatementFrequency.WEEKLY, - from_date=statement_from, - to_date=statement_to) + statement_settings = StatementSettingsService.find_accounts_settings_by_frequency( + previous_day, + StatementFrequency.WEEKLY, + from_date=statement_from, + to_date=statement_to, + ) if statement_from == statement_to or not statement_settings: return - current_app.logger.debug(f'Found {len(statement_settings)} accounts to generate GAP statements') + current_app.logger.debug(f"Found {len(statement_settings)} accounts to generate GAP statements") search_filter = { - 'dateFilter': { - 'startDate': statement_from.strftime('%Y-%m-%d'), - 'endDate': statement_to.strftime('%Y-%m-%d') + "dateFilter": { + "startDate": statement_from.strftime("%Y-%m-%d"), + "endDate": statement_to.strftime("%Y-%m-%d"), } } cls._create_statement_records(search_filter, statement_settings, account_override) @@ -101,13 +110,14 @@ def _generate_gap_statements(cls, target_time, account_override): def _generate_daily_statements(cls, target_time: datetime, account_override: str): """Generate daily statements for all accounts with settings to generate daily.""" previous_day = get_previous_day(target_time) - statement_settings = StatementSettingsService.find_accounts_settings_by_frequency(previous_day, - StatementFrequency.DAILY) - current_app.logger.debug(f'Found {len(statement_settings)} accounts to generate DAILY statements') + statement_settings = StatementSettingsService.find_accounts_settings_by_frequency( + previous_day, StatementFrequency.DAILY + ) + current_app.logger.debug(f"Found {len(statement_settings)} accounts to generate DAILY statements") search_filter = { - 'dateFilter': { - 'startDate': previous_day.strftime('%Y-%m-%d'), - 'endDate': previous_day.strftime('%Y-%m-%d') + "dateFilter": { + "startDate": previous_day.strftime("%Y-%m-%d"), + "endDate": previous_day.strftime("%Y-%m-%d"), } } cls._create_statement_records(search_filter, statement_settings, account_override) @@ -116,14 +126,15 @@ def _generate_daily_statements(cls, target_time: datetime, account_override: str def _generate_weekly_statements(cls, target_time: datetime, account_override: str): """Generate weekly statements for all accounts with settings to generate weekly.""" previous_day = get_previous_day(target_time) - statement_settings = StatementSettingsService.find_accounts_settings_by_frequency(previous_day, - StatementFrequency.WEEKLY) - current_app.logger.debug(f'Found {len(statement_settings)} accounts to generate WEEKLY statements') + statement_settings = StatementSettingsService.find_accounts_settings_by_frequency( + previous_day, StatementFrequency.WEEKLY + ) + current_app.logger.debug(f"Found {len(statement_settings)} accounts to generate WEEKLY statements") statement_from, statement_to = get_week_start_and_end_date(previous_day, index=1) search_filter = { - 'dateFilter': { - 'startDate': statement_from.strftime('%Y-%m-%d'), - 'endDate': statement_to.strftime('%Y-%m-%d') + "dateFilter": { + "startDate": statement_from.strftime("%Y-%m-%d"), + "endDate": statement_to.strftime("%Y-%m-%d"), } } @@ -133,16 +144,12 @@ def _generate_weekly_statements(cls, target_time: datetime, account_override: st def _generate_monthly_statements(cls, target_time: datetime, account_override: str): """Generate monthly statements for all accounts with settings to generate monthly.""" previous_day = get_previous_day(target_time) - statement_settings = StatementSettingsService.find_accounts_settings_by_frequency(previous_day, - StatementFrequency.MONTHLY) - current_app.logger.debug(f'Found {len(statement_settings)} accounts to generate MONTHLY statements') + statement_settings = StatementSettingsService.find_accounts_settings_by_frequency( + previous_day, StatementFrequency.MONTHLY + ) + current_app.logger.debug(f"Found {len(statement_settings)} accounts to generate MONTHLY statements") last_month, last_month_year = get_previous_month_and_year(target_time) - search_filter = { - 'monthFilter': { - 'month': last_month, - 'year': last_month_year - } - } + search_filter = {"monthFilter": {"month": last_month, "year": last_month_year}} cls._create_statement_records(search_filter, statement_settings, account_override) @@ -152,83 +159,93 @@ def _upsert_statements(cls, statement_settings, invoice_detail_tuple, reuse_stat statements = [] for setting, pay_account in statement_settings: existing_statement = next( - (statement for statement in reuse_statements - if statement.payment_account_id == pay_account.id and - statement.frequency == setting.frequency and - statement.from_date == cls.statement_from.date() and statement.to_date == cls.statement_to.date()), - None - ) - notification_status = NotificationStatus.PENDING.value \ - if pay_account.statement_notification_enabled is True and cls.has_date_override is False \ + ( + statement + for statement in reuse_statements + if statement.payment_account_id == pay_account.id + and statement.frequency == setting.frequency + and statement.from_date == cls.statement_from.date() + and statement.to_date == cls.statement_to.date() + ), + None, + ) + notification_status = ( + NotificationStatus.PENDING.value + if pay_account.statement_notification_enabled is True and cls.has_date_override is False else NotificationStatus.SKIP.value - payment_methods = StatementService.determine_payment_methods(invoice_detail_tuple, - pay_account, - existing_statement) + ) + payment_methods = StatementService.determine_payment_methods( + invoice_detail_tuple, pay_account, existing_statement + ) created_on = get_local_time(datetime.now(tz=timezone.utc)) if existing_statement: - current_app.logger.debug(f'Reusing existing statement already exists for {cls.statement_from.date()}') + current_app.logger.debug(f"Reusing existing statement already exists for {cls.statement_from.date()}") existing_statement.notification_status_code = notification_status existing_statement.payment_methods = payment_methods existing_statement.created_on = created_on statements.append(existing_statement) else: - statements.append(StatementModel( - frequency=setting.frequency, - statement_settings_id=setting.id, - payment_account_id=pay_account.id, - created_on=created_on, - from_date=cls.statement_from, - to_date=cls.statement_to, - notification_status_code=notification_status, - payment_methods=payment_methods - )) + statements.append( + StatementModel( + frequency=setting.frequency, + statement_settings_id=setting.id, + payment_account_id=pay_account.id, + created_on=created_on, + from_date=cls.statement_from, + to_date=cls.statement_to, + notification_status_code=notification_status, + payment_methods=payment_methods, + ) + ) return statements @classmethod def _create_statement_records(cls, search_filter, statement_settings, account_override: str): cls.statement_from = None cls.statement_to = None - if search_filter.get('dateFilter', None): - cls.statement_from = parse(search_filter.get('dateFilter').get('startDate')) - cls.statement_to = parse(search_filter.get('dateFilter').get('endDate')) + if search_filter.get("dateFilter", None): + cls.statement_from = parse(search_filter.get("dateFilter").get("startDate")) + cls.statement_to = parse(search_filter.get("dateFilter").get("endDate")) if cls.statement_from == cls.statement_to: - current_app.logger.debug(f'Statements for day: {cls.statement_from.date()}') + current_app.logger.debug(f"Statements for day: {cls.statement_from.date()}") else: - current_app.logger.debug(f'Statements for week: {cls.statement_from.date()} to ' - f'{cls.statement_to.date()}') - elif search_filter.get('monthFilter', None): + current_app.logger.debug( + f"Statements for week: {cls.statement_from.date()} to " f"{cls.statement_to.date()}" + ) + elif search_filter.get("monthFilter", None): cls.statement_from, cls.statement_to = get_first_and_last_dates_of_month( - search_filter.get('monthFilter').get('month'), search_filter.get('monthFilter').get('year')) - current_app.logger.debug(f'Statements for month: {cls.statement_from.date()} to {cls.statement_to.date()}') + search_filter.get("monthFilter").get("month"), + search_filter.get("monthFilter").get("year"), + ) + current_app.logger.debug(f"Statements for month: {cls.statement_from.date()} to {cls.statement_to.date()}") if cls.has_account_override: auth_account_ids = [account_override] statement_settings = cls._filter_settings_by_override(statement_settings, account_override) - current_app.logger.debug(f'Override Filtered to {len(statement_settings)} accounts to generate statements.') + current_app.logger.debug(f"Override Filtered to {len(statement_settings)} accounts to generate statements.") else: auth_account_ids = [pay_account.auth_account_id for _, pay_account in statement_settings] - search_filter['authAccountIds'] = auth_account_ids + search_filter["authAccountIds"] = auth_account_ids # Force match on these methods where if the payment method is in matchPaymentMethods, the invoice payment method # must match the account payment method. Used for EFT so the statements only show EFT invoices and interim # statement logic when transitioning payment methods - search_filter['matchPaymentMethods'] = True + search_filter["matchPaymentMethods"] = True invoice_detail_tuple = PaymentModel.get_invoices_and_payment_accounts_for_statements(search_filter) reuse_statements = [] if cls.has_date_override and statement_settings: reuse_statements = cls._clean_up_old_statements(statement_settings) - current_app.logger.debug('Upserting statements.') + current_app.logger.debug("Upserting statements.") statements = cls._upsert_statements(statement_settings, invoice_detail_tuple, reuse_statements) # Return defaults which returns the id. db.session.bulk_save_objects(statements, return_defaults=True) db.session.flush() - current_app.logger.debug('Inserting statement invoices.') + current_app.logger.debug("Inserting statement invoices.") statement_invoices = [] for statement, auth_account_id in zip(statements, auth_account_ids): invoices = [i for i in invoice_detail_tuple if i.auth_account_id == auth_account_id] - statement_invoices = statement_invoices + [StatementInvoicesModel( - statement_id=statement.id, - invoice_id=invoice.id - ) for invoice in invoices] + statement_invoices = statement_invoices + [ + StatementInvoicesModel(statement_id=statement.id, invoice_id=invoice.id) for invoice in invoices + ] db.session.bulk_save_objects(statement_invoices) @classmethod @@ -236,22 +253,32 @@ def _clean_up_old_statements(cls, statement_settings): """Clean up duplicate / old statements before generating.""" payment_account_ids = [pay_account.id for _, pay_account in statement_settings] payment_account_ids = select(func.unnest(cast(payment_account_ids, ARRAY(INTEGER)))) - existing_statements = db.session.query(StatementModel)\ + existing_statements = ( + db.session.query(StatementModel) .filter_by( frequency=statement_settings[0].StatementSettings.frequency, - from_date=cls.statement_from.date(), to_date=cls.statement_to.date(), - is_interim_statement=False)\ - .filter(StatementModel.payment_account_id.in_(payment_account_ids))\ + from_date=cls.statement_from.date(), + to_date=cls.statement_to.date(), + is_interim_statement=False, + ) + .filter(StatementModel.payment_account_id.in_(payment_account_ids)) .all() - current_app.logger.debug(f'Removing {len(existing_statements)} existing duplicate/stale statement invoices.') + ) + current_app.logger.debug(f"Removing {len(existing_statements)} existing duplicate/stale statement invoices.") remove_statements_ids = [statement.id for statement in existing_statements] - remove_statement_invoices = db.session.query(StatementInvoicesModel)\ - .filter(StatementInvoicesModel.statement_id.in_( - select(func.unnest(cast(remove_statements_ids, ARRAY(INTEGER))))))\ + remove_statement_invoices = ( + db.session.query(StatementInvoicesModel) + .filter( + StatementInvoicesModel.statement_id.in_( + select(func.unnest(cast(remove_statements_ids, ARRAY(INTEGER)))) + ) + ) .all() + ) statement_invoice_ids = [statement_invoice.id for statement_invoice in remove_statement_invoices] - delete_statement_invoice = delete(StatementInvoicesModel)\ - .where(StatementInvoicesModel.id.in_(select(func.unnest(cast(statement_invoice_ids, ARRAY(INTEGER)))))) + delete_statement_invoice = delete(StatementInvoicesModel).where( + StatementInvoicesModel.id.in_(select(func.unnest(cast(statement_invoice_ids, ARRAY(INTEGER))))) + ) db.session.execute(delete_statement_invoice) db.session.flush() return existing_statements @@ -259,6 +286,6 @@ def _clean_up_old_statements(cls, statement_settings): @classmethod def _filter_settings_by_override(cls, statement_settings, auth_account_id: str): """Return filtered Statement settings by payment account.""" - return [settings - for settings in statement_settings - if settings.PaymentAccount.auth_account_id == auth_account_id] + return [ + settings for settings in statement_settings if settings.PaymentAccount.auth_account_id == auth_account_id + ] diff --git a/jobs/payment-jobs/tasks/unpaid_invoice_notify_task.py b/jobs/payment-jobs/tasks/unpaid_invoice_notify_task.py index 8da43550b..7769d33d1 100644 --- a/jobs/payment-jobs/tasks/unpaid_invoice_notify_task.py +++ b/jobs/payment-jobs/tasks/unpaid_invoice_notify_task.py @@ -49,39 +49,62 @@ def _notify_for_ob(cls): # pylint: disable=too-many-locals """ unpaid_status = ( - InvoiceStatus.SETTLEMENT_SCHEDULED.value, InvoiceStatus.PARTIAL.value, InvoiceStatus.CREATED.value) - notification_date = datetime.now(tz=timezone.utc) - timedelta(days=current_app.config.get('NOTIFY_AFTER_DAYS')) + InvoiceStatus.SETTLEMENT_SCHEDULED.value, + InvoiceStatus.PARTIAL.value, + InvoiceStatus.CREATED.value, + ) + notification_date = datetime.now(tz=timezone.utc) - timedelta(days=current_app.config.get("NOTIFY_AFTER_DAYS")) # Get distinct accounts with pending invoices for that exact day - notification_pending_accounts = db.session.query(InvoiceModel.payment_account_id).distinct().filter(and_( - InvoiceModel.invoice_status_code.in_(unpaid_status), - InvoiceModel.payment_method_code == PaymentMethod.ONLINE_BANKING.value, - # cast is used to get the exact match stripping the timestamp from date - cast(InvoiceModel.created_on, Date) == notification_date.date() - )).all() - current_app.logger.debug(f'Found {len(notification_pending_accounts)} invoices to notify admins.') + notification_pending_accounts = ( + db.session.query(InvoiceModel.payment_account_id) + .distinct() + .filter( + and_( + InvoiceModel.invoice_status_code.in_(unpaid_status), + InvoiceModel.payment_method_code == PaymentMethod.ONLINE_BANKING.value, + # cast is used to get the exact match stripping the timestamp from date + cast(InvoiceModel.created_on, Date) == notification_date.date(), + ) + ) + .all() + ) + current_app.logger.debug(f"Found {len(notification_pending_accounts)} invoices to notify admins.") for payment_account in notification_pending_accounts: try: payment_account_id = payment_account[0] - total = db.session.query(func.sum(InvoiceModel.total).label('total')).filter(and_( - InvoiceModel.invoice_status_code.in_(unpaid_status), - InvoiceModel.payment_account_id == payment_account_id, - InvoiceModel.payment_method_code == PaymentMethod.ONLINE_BANKING.value - )).group_by(InvoiceModel.payment_account_id).all() - pay_account: PaymentAccountModel = \ - PaymentAccountModel.find_by_id(payment_account_id) + total = ( + db.session.query(func.sum(InvoiceModel.total).label("total")) + .filter( + and_( + InvoiceModel.invoice_status_code.in_(unpaid_status), + InvoiceModel.payment_account_id == payment_account_id, + InvoiceModel.payment_method_code == PaymentMethod.ONLINE_BANKING.value, + ) + ) + .group_by(InvoiceModel.payment_account_id) + .all() + ) + pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account_id, - PaymentMethod.ONLINE_BANKING.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method( + payment_account_id, PaymentMethod.ONLINE_BANKING.value + ) # emit account mailer event - addition_params_to_mailer = {'transactionAmount': float(total[0][0]), - 'cfsAccountId': cfs_account.cfs_account, - 'authAccountId': pay_account.auth_account_id, - } - mailer.publish_mailer_events(QueueMessageTypes.PAYMENT_PENDING.value, - pay_account, - addition_params_to_mailer) + addition_params_to_mailer = { + "transactionAmount": float(total[0][0]), + "cfsAccountId": cfs_account.cfs_account, + "authAccountId": pay_account.auth_account_id, + } + mailer.publish_mailer_events( + QueueMessageTypes.PAYMENT_PENDING.value, + pay_account, + addition_params_to_mailer, + ) except Exception as e: # NOQA # pylint: disable=broad-except - capture_message(f'Error on notifying mailer OB Pending invoice: account id={pay_account.id}, ' - f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') + capture_message( + f"Error on notifying mailer OB Pending invoice: account id={pay_account.id}, " + f"auth account : {pay_account.auth_account_id}, ERROR : {str(e)}", + level="error", + ) current_app.logger.error(e) diff --git a/jobs/payment-jobs/tests/jobs/__init__.py b/jobs/payment-jobs/tests/jobs/__init__.py index 21e84332a..00b0f1fb9 100644 --- a/jobs/payment-jobs/tests/jobs/__init__.py +++ b/jobs/payment-jobs/tests/jobs/__init__.py @@ -15,6 +15,5 @@ import os import sys - my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + '/../') +sys.path.insert(0, my_path + "/../") diff --git a/jobs/payment-jobs/tests/jobs/conftest.py b/jobs/payment-jobs/tests/jobs/conftest.py index c153014fc..ba7ec8685 100644 --- a/jobs/payment-jobs/tests/jobs/conftest.py +++ b/jobs/payment-jobs/tests/jobs/conftest.py @@ -31,6 +31,7 @@ @pytest.fixture(autouse=True) def mock_pub_sub_call(mocker): """Mock pub sub call.""" + class Expando(object): """Expando class.""" @@ -41,6 +42,7 @@ def __init__(self, *args, **kwargs): def result(): """Return true for mock.""" return True + self.result = result def publish(self, *args, **kwargs): @@ -49,35 +51,35 @@ def publish(self, *args, **kwargs): ex.result = self.result return ex - mocker.patch('google.cloud.pubsub_v1.PublisherClient', PublisherMock) + mocker.patch("google.cloud.pubsub_v1.PublisherClient", PublisherMock) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def app(): """Return a session-wide application configured in TEST mode.""" - return create_app('testing') + return create_app("testing") -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def app_request(): """Return a session-wide application configured in TEST mode.""" - return create_app('testing') + return create_app("testing") -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client(app): # pylint: disable=redefined-outer-name """Return a session-wide Flask test client.""" return app.test_client() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client_ctx(app): """Return session-wide Flask test client.""" with app.test_client() as _client: yield _client -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def db(app): # pylint: disable=redefined-outer-name, invalid-name """Return a session-wide initialised database.""" with app.app_context(): @@ -85,16 +87,16 @@ def db(app): # pylint: disable=redefined-outer-name, invalid-name drop_database(_db.engine.url) create_database(_db.engine.url) _db.session().execute(text('SET TIME ZONE "UTC";')) - pay_api_dir = os.path.abspath('..').replace('jobs', 'pay-api') - pay_api_dir = os.path.join(pay_api_dir, 'migrations') + pay_api_dir = os.path.abspath("..").replace("jobs", "pay-api") + pay_api_dir = os.path.join(pay_api_dir, "migrations") Migrate(app, _db, directory=pay_api_dir) upgrade() # Restore the logging, alembic and sqlalchemy have their own logging from alembic.ini. - setup_logging(os.path.abspath('logging.conf')) + setup_logging(os.path.abspath("logging.conf")) return _db -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def session(db, app): # pylint: disable=redefined-outer-name, invalid-name """Return a function-scoped session.""" with app.app_context(): @@ -107,7 +109,7 @@ def session(db, app): # pylint: disable=redefined-outer-name, invalid-name db.session.commit = nested.commit db.session.rollback = nested.rollback - @event.listens_for(sess, 'after_transaction_end') + @event.listens_for(sess, "after_transaction_end") def restart_savepoint(sess2, trans): # pylint: disable=unused-variable nonlocal nested if trans.nested: @@ -130,73 +132,74 @@ def restart_savepoint(sess2, trans): # pylint: disable=unused-variable transaction.rollback() -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def auto(docker_services, app): """Spin up docker instances.""" - if app.config['USE_DOCKER_MOCK']: - docker_services.start('keycloak') - docker_services.wait_for_service('keycloak', 8081) - docker_services.start('bcol') - docker_services.start('auth') - docker_services.start('paybc') - docker_services.start('reports') - docker_services.start('proxy') - docker_services.start('sftp') + if app.config["USE_DOCKER_MOCK"]: + docker_services.start("keycloak") + docker_services.wait_for_service("keycloak", 8081) + docker_services.start("bcol") + docker_services.start("auth") + docker_services.start("paybc") + docker_services.start("reports") + docker_services.start("proxy") + docker_services.start("sftp") time.sleep(2) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def docker_compose_files(pytestconfig): """Get the docker-compose.yml absolute path.""" - return [ - os.path.join(str(pytestconfig.rootdir), 'tests/docker', 'docker-compose.yml') - ] + return [os.path.join(str(pytestconfig.rootdir), "tests/docker", "docker-compose.yml")] @pytest.fixture() def admin_users_mock(monkeypatch): """Mock auth rest call to get org admins.""" + def get_account_admin_users(payment_account): return { - 'members': [ + "members": [ { - 'id': 4048, - 'membershipStatus': 'ACTIVE', - 'membershipTypeCode': 'ADMIN', - 'user': { - 'contacts': [ + "id": 4048, + "membershipStatus": "ACTIVE", + "membershipTypeCode": "ADMIN", + "user": { + "contacts": [ { - 'email': 'test@test.com', - 'phone': '(250) 111-2222', - 'phoneExtension': '' + "email": "test@test.com", + "phone": "(250) 111-2222", + "phoneExtension": "", } ], - 'firstname': 'FIRST', - 'id': 18, - 'lastname': 'LAST', - 'loginSource': 'BCSC' - } + "firstname": "FIRST", + "id": 18, + "lastname": "LAST", + "loginSource": "BCSC", + }, } ] } - monkeypatch.setattr('pay_api.services.auth.get_account_admin_users', - get_account_admin_users) + + monkeypatch.setattr("pay_api.services.auth.get_account_admin_users", get_account_admin_users) @pytest.fixture() def emails_with_keycloak_role_mock(monkeypatch): """Mock auth rest call to get org admins.""" + def get_emails_with_keycloak_role(role): - return 'test@email.com' + return "test@email.com" - monkeypatch.setattr('tasks.eft_overpayment_notification_task.get_emails_with_keycloak_role', - get_emails_with_keycloak_role) + monkeypatch.setattr( + "tasks.eft_overpayment_notification_task.get_emails_with_keycloak_role", + get_emails_with_keycloak_role, + ) @pytest.fixture() def send_email_mock(monkeypatch): """Mock send_email.""" send_email = Mock(return_value=True) - monkeypatch.setattr('tasks.eft_overpayment_notification_task.send_email', - send_email) + monkeypatch.setattr("tasks.eft_overpayment_notification_task.send_email", send_email) return send_email diff --git a/jobs/payment-jobs/tests/jobs/factory.py b/jobs/payment-jobs/tests/jobs/factory.py index d4c818d34..adb5e95fc 100644 --- a/jobs/payment-jobs/tests/jobs/factory.py +++ b/jobs/payment-jobs/tests/jobs/factory.py @@ -21,82 +21,125 @@ from random import randrange from pay_api.models import ( - CfsAccount, DistributionCode, DistributionCodeLink, EFTCredit, EFTCreditInvoiceLink, EFTFile, EFTRefund, - EFTShortnameLinks, EFTShortnames, EFTShortnamesHistorical, EFTTransaction, Invoice, InvoiceReference, Payment, - PaymentAccount, PaymentLineItem, Receipt, Refund, RefundsPartial, RoutingSlip, Statement, StatementInvoices, - StatementRecipients, StatementSettings) + CfsAccount, + DistributionCode, + DistributionCodeLink, + EFTCredit, + EFTCreditInvoiceLink, + EFTFile, + EFTRefund, + EFTShortnameLinks, + EFTShortnames, + EFTShortnamesHistorical, + EFTTransaction, + Invoice, + InvoiceReference, + Payment, + PaymentAccount, + PaymentLineItem, + Receipt, + Refund, + RefundsPartial, + RoutingSlip, + Statement, + StatementInvoices, + StatementRecipients, + StatementSettings, +) from pay_api.utils.enums import ( - CfsAccountStatus, DisbursementStatus, EFTHistoricalTypes, EFTProcessStatus, EFTShortnameStatus, EFTShortnameType, - InvoiceReferenceStatus, InvoiceStatus, LineItemStatus, PaymentMethod, PaymentStatus, PaymentSystem, - RoutingSlipStatus) - - -def factory_premium_payment_account(bcol_user_id='PB25020', bcol_account_id='1234567890', auth_account_id='1234'): + CfsAccountStatus, + DisbursementStatus, + EFTHistoricalTypes, + EFTProcessStatus, + EFTShortnameStatus, + EFTShortnameType, + InvoiceReferenceStatus, + InvoiceStatus, + LineItemStatus, + PaymentMethod, + PaymentStatus, + PaymentSystem, + RoutingSlipStatus, +) + + +def factory_premium_payment_account(bcol_user_id="PB25020", bcol_account_id="1234567890", auth_account_id="1234"): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - bcol_user_id=bcol_user_id, - bcol_account=bcol_account_id, - payment_method=PaymentMethod.DRAWDOWN.value - ).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + bcol_user_id=bcol_user_id, + bcol_account=bcol_account_id, + payment_method=PaymentMethod.DRAWDOWN.value, + ).save() return account -def factory_statement_recipient(auth_user_id: int, first_name: str, last_name: str, email: str, - payment_account_id: int): +def factory_statement_recipient( + auth_user_id: int, + first_name: str, + last_name: str, + email: str, + payment_account_id: int, +): """Return statement recipient model.""" return StatementRecipients( auth_user_id=auth_user_id, firstname=first_name, lastname=last_name, email=email, - payment_account_id=payment_account_id + payment_account_id=payment_account_id, ).save() -def factory_statement_invoices( - statement_id: str, - invoice_id: str): +def factory_statement_invoices(statement_id: str, invoice_id: str): """Return Factory.""" - return StatementInvoices(statement_id=statement_id, - invoice_id=invoice_id).save() + return StatementInvoices(statement_id=statement_id, invoice_id=invoice_id).save() def factory_statement( - frequency: str = 'WEEKLY', - payment_account_id: str = None, - from_date: datetime = datetime.now(tz=timezone.utc), - to_date: datetime = datetime.now(tz=timezone.utc), - statement_settings_id: str = None, - created_on: datetime = datetime.now(tz=timezone.utc), - payment_methods: str = PaymentMethod.EFT.value): + frequency: str = "WEEKLY", + payment_account_id: str = None, + from_date: datetime = datetime.now(tz=timezone.utc), + to_date: datetime = datetime.now(tz=timezone.utc), + statement_settings_id: str = None, + created_on: datetime = datetime.now(tz=timezone.utc), + payment_methods: str = PaymentMethod.EFT.value, +): """Return Factory.""" - return Statement(frequency=frequency, - statement_settings_id=statement_settings_id, - payment_account_id=payment_account_id, - from_date=from_date, - to_date=to_date, - created_on=created_on, - payment_methods=payment_methods).save() + return Statement( + frequency=frequency, + statement_settings_id=statement_settings_id, + payment_account_id=payment_account_id, + from_date=from_date, + to_date=to_date, + created_on=created_on, + payment_methods=payment_methods, + ).save() -def factory_statement_settings(pay_account_id: str, frequency='DAILY', from_date=datetime.now(tz=timezone.utc), - to_date=None) -> StatementSettings: +def factory_statement_settings( + pay_account_id: str, + frequency="DAILY", + from_date=datetime.now(tz=timezone.utc), + to_date=None, +) -> StatementSettings: """Return Factory.""" return StatementSettings( frequency=frequency, payment_account_id=pay_account_id, from_date=from_date, - to_date=to_date + to_date=to_date, ).save() def factory_payment( - payment_system_code: str = 'PAYBC', payment_method_code: str = 'CC', - payment_status_code: str = PaymentStatus.CREATED.value, - payment_date: datetime = datetime.now(tz=timezone.utc), - invoice_number: str = None, - payment_account_id: int = None, - invoice_amount: float = None, + payment_system_code: str = "PAYBC", + payment_method_code: str = "CC", + payment_status_code: str = PaymentStatus.CREATED.value, + payment_date: datetime = datetime.now(tz=timezone.utc), + invoice_number: str = None, + payment_account_id: int = None, + invoice_amount: float = None, ): """Return Factory.""" return Payment( @@ -106,20 +149,24 @@ def factory_payment( payment_date=payment_date, invoice_number=invoice_number, payment_account_id=payment_account_id, - invoice_amount=invoice_amount + invoice_amount=invoice_amount, ).save() -def factory_invoice(payment_account: PaymentAccount, status_code: str = InvoiceStatus.CREATED.value, - corp_type_code='CP', - business_identifier: str = 'CP0001234', - service_fees: float = 0.0, total=0, paid=0, - payment_method_code: str = PaymentMethod.DIRECT_PAY.value, - created_on: datetime = datetime.now(tz=timezone.utc), - cfs_account_id: int = 0, - routing_slip=None, - disbursement_status_code=None - ): +def factory_invoice( + payment_account: PaymentAccount, + status_code: str = InvoiceStatus.CREATED.value, + corp_type_code="CP", + business_identifier: str = "CP0001234", + service_fees: float = 0.0, + total=0, + paid=0, + payment_method_code: str = PaymentMethod.DIRECT_PAY.value, + created_on: datetime = datetime.now(tz=timezone.utc), + cfs_account_id: int = 0, + routing_slip=None, + disbursement_status_code=None, +): """Return Factory.""" status_code = InvoiceStatus.APPROVED.value if payment_method_code == PaymentMethod.PAD.value else status_code invoice = Invoice( @@ -127,18 +174,18 @@ def factory_invoice(payment_account: PaymentAccount, status_code: str = InvoiceS payment_account_id=payment_account.id, total=total, paid=paid, - created_by='test', + created_by="test", created_on=created_on, business_identifier=business_identifier, corp_type_code=corp_type_code, - folio_number='1234567890', + folio_number="1234567890", service_fees=service_fees, bcol_account=payment_account.bcol_account, payment_method_code=payment_method_code or payment_account.payment_method, routing_slip=routing_slip, disbursement_status_code=disbursement_status_code, payment_date=None, - refund_date=None + refund_date=None, ) if cfs_account_id != 0: invoice.cfs_account_id = cfs_account_id @@ -147,9 +194,15 @@ def factory_invoice(payment_account: PaymentAccount, status_code: str = InvoiceS return invoice -def factory_payment_line_item(invoice_id: str, fee_schedule_id: int, filing_fees: int = 10, total: int = 10, - service_fees: int = 0, status: str = LineItemStatus.ACTIVE.value, - fee_dist_id=None): +def factory_payment_line_item( + invoice_id: str, + fee_schedule_id: int, + filing_fees: int = 10, + total: int = 10, + service_fees: int = 0, + status: str = LineItemStatus.ACTIVE.value, + fee_dist_id=None, +): """Return Factory.""" if not fee_dist_id: fee_dist_id = DistributionCode.find_by_active_for_fee_schedule(fee_schedule_id).distribution_code_id @@ -160,68 +213,94 @@ def factory_payment_line_item(invoice_id: str, fee_schedule_id: int, filing_fees total=total, service_fees=service_fees, line_item_status_code=status, - fee_distribution_id=fee_dist_id + fee_distribution_id=fee_dist_id, ).save() -def factory_invoice_reference(invoice_id: int, invoice_number: str = '10021', - status_code=InvoiceReferenceStatus.ACTIVE.value, - is_consolidated=False): +def factory_invoice_reference( + invoice_id: int, + invoice_number: str = "10021", + status_code=InvoiceReferenceStatus.ACTIVE.value, + is_consolidated=False, +): """Return Factory.""" - return InvoiceReference(invoice_id=invoice_id, - status_code=status_code, - invoice_number=invoice_number, - is_consolidated=is_consolidated).save() + return InvoiceReference( + invoice_id=invoice_id, + status_code=status_code, + invoice_number=invoice_number, + is_consolidated=is_consolidated, + ).save() -def factory_create_online_banking_account(auth_account_id='1234', status=CfsAccountStatus.PENDING.value, - cfs_account='12356'): +def factory_create_online_banking_account( + auth_account_id="1234", status=CfsAccountStatus.PENDING.value, cfs_account="12356" +): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.ONLINE_BANKING.value, - name=f'Test {auth_account_id}').save() - CfsAccount(status=status, account_id=account.id, cfs_account=cfs_account, - payment_method=PaymentMethod.ONLINE_BANKING.value).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.ONLINE_BANKING.value, + name=f"Test {auth_account_id}", + ).save() + CfsAccount( + status=status, + account_id=account.id, + cfs_account=cfs_account, + payment_method=PaymentMethod.ONLINE_BANKING.value, + ).save() return account -def factory_create_pad_account(auth_account_id='1234', bank_number='001', bank_branch='004', bank_account='1234567890', - status=CfsAccountStatus.PENDING.value, payment_method=PaymentMethod.PAD.value, - confirmation_period: int = 3): +def factory_create_pad_account( + auth_account_id="1234", + bank_number="001", + bank_branch="004", + bank_account="1234567890", + status=CfsAccountStatus.PENDING.value, + payment_method=PaymentMethod.PAD.value, + confirmation_period: int = 3, +): """Return Factory.""" date_after_wait_period = datetime.now(tz=timezone.utc) + timedelta(confirmation_period) - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=payment_method, - pad_activation_date=date_after_wait_period, - name=f'Test {auth_account_id}').save() - CfsAccount(status=status, account_id=account.id, bank_number=bank_number, - bank_branch_number=bank_branch, bank_account_number=bank_account, - payment_method=PaymentMethod.PAD.value).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=payment_method, + pad_activation_date=date_after_wait_period, + name=f"Test {auth_account_id}", + ).save() + CfsAccount( + status=status, + account_id=account.id, + bank_number=bank_number, + bank_branch_number=bank_branch, + bank_account_number=bank_account, + payment_method=PaymentMethod.PAD.value, + ).save() return account -def factory_create_direct_pay_account(auth_account_id='1234', payment_method=PaymentMethod.DIRECT_PAY.value): +def factory_create_direct_pay_account(auth_account_id="1234", payment_method=PaymentMethod.DIRECT_PAY.value): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=payment_method, name=f'Test {auth_account_id}') + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=payment_method, + name=f"Test {auth_account_id}", + ) return account def factory_routing_slip_account( - number: str = '1234', - status: str = CfsAccountStatus.PENDING.value, - total: int = 0, - remaining_amount: int = 0, - routing_slip_date=datetime.now(tz=timezone.utc), - payment_method=PaymentMethod.CASH.value, - auth_account_id='1234', - routing_slip_status=RoutingSlipStatus.ACTIVE.value, - refund_amount=0 + number: str = "1234", + status: str = CfsAccountStatus.PENDING.value, + total: int = 0, + remaining_amount: int = 0, + routing_slip_date=datetime.now(tz=timezone.utc), + payment_method=PaymentMethod.CASH.value, + auth_account_id="1234", + routing_slip_status=RoutingSlipStatus.ACTIVE.value, + refund_amount=0, ): """Create routing slip and return payment account with it.""" - payment_account = PaymentAccount( - payment_method=payment_method, - name=f'Test {auth_account_id}') + payment_account = PaymentAccount(payment_method=payment_method, name=f"Test {auth_account_id}") payment_account.save() rs = RoutingSlip( @@ -230,46 +309,59 @@ def factory_routing_slip_account( status=routing_slip_status, total=total, remaining_amount=remaining_amount, - created_by='test', + created_by="test", routing_slip_date=routing_slip_date, - refund_amount=refund_amount + refund_amount=refund_amount, ).save() - Payment(payment_system_code=PaymentSystem.FAS.value, - payment_account_id=payment_account.id, - payment_method_code=PaymentMethod.CASH.value, - payment_status_code=PaymentStatus.COMPLETED.value, - receipt_number=number, - is_routing_slip=True, - paid_amount=rs.total, - created_by='TEST') + Payment( + payment_system_code=PaymentSystem.FAS.value, + payment_account_id=payment_account.id, + payment_method_code=PaymentMethod.CASH.value, + payment_status_code=PaymentStatus.COMPLETED.value, + receipt_number=number, + is_routing_slip=True, + paid_amount=rs.total, + created_by="TEST", + ) - CfsAccount(status=status, account_id=payment_account.id, payment_method=PaymentMethod.INTERNAL.value).save() + CfsAccount( + status=status, + account_id=payment_account.id, + payment_method=PaymentMethod.INTERNAL.value, + ).save() return payment_account -def factory_create_eft_account(auth_account_id='1234', status=CfsAccountStatus.PENDING.value): +def factory_create_eft_account(auth_account_id="1234", status=CfsAccountStatus.PENDING.value): """Return Factory.""" - payment_account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.EFT.value, - name=f'Test {auth_account_id}').save() - CfsAccount(status=status, account_id=payment_account.id, payment_method=PaymentMethod.EFT.value).save() + payment_account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.EFT.value, + name=f"Test {auth_account_id}", + ).save() + CfsAccount( + status=status, + account_id=payment_account.id, + payment_method=PaymentMethod.EFT.value, + ).save() return payment_account def factory_create_eft_shortname(short_name: str, short_name_type: str = EFTShortnameType.EFT.value): """Return Factory.""" - short_name = EFTShortnames( - short_name=short_name, - type=short_name_type - ).save() + short_name = EFTShortnames(short_name=short_name, type=short_name_type).save() return short_name -def factory_eft_shortname_link(short_name_id: int, auth_account_id: str = '1234', - updated_by: str = None, updated_on: datetime = datetime.now(tz=timezone.utc), - status_code: str = EFTShortnameStatus.LINKED.value): +def factory_eft_shortname_link( + short_name_id: int, + auth_account_id: str = "1234", + updated_by: str = None, + updated_on: datetime = datetime.now(tz=timezone.utc), + status_code: str = EFTShortnameStatus.LINKED.value, +): """Return an EFT short name link model.""" return EFTShortnameLinks( eft_short_name_id=short_name_id, @@ -277,7 +369,7 @@ def factory_eft_shortname_link(short_name_id: int, auth_account_id: str = '1234' status_code=status_code, updated_by=updated_by, updated_by_name=updated_by, - updated_on=updated_on + updated_on=updated_on, ).save() @@ -288,22 +380,23 @@ def factory_create_eft_credit(amount=100, remaining_amount=0, eft_file_id=1, sho remaining_amount=remaining_amount, eft_file_id=eft_file_id, short_name_id=short_name_id, - eft_transaction_id=eft_transaction_id + eft_transaction_id=eft_transaction_id, ).save() return eft_credit -def factory_create_eft_file(file_ref='test.txt', status_code=EFTProcessStatus.COMPLETED.value): +def factory_create_eft_file(file_ref="test.txt", status_code=EFTProcessStatus.COMPLETED.value): """Return Factory.""" - eft_file = EFTFile( - file_ref=file_ref, - status_code=status_code - ).save() + eft_file = EFTFile(file_ref=file_ref, status_code=status_code).save() return eft_file -def factory_create_eft_transaction(file_id=1, line_number=1, line_type='T', - status_code=EFTProcessStatus.COMPLETED.value): +def factory_create_eft_transaction( + file_id=1, + line_number=1, + line_type="T", + status_code=EFTProcessStatus.COMPLETED.value, +): """Return Factory.""" eft_transaction = EFTTransaction( file_id=file_id, @@ -314,27 +407,32 @@ def factory_create_eft_transaction(file_id=1, line_number=1, line_type='T', return eft_transaction -def factory_create_eft_credit_invoice_link(invoice_id=1, eft_credit_id=1, status_code='PENDING', amount=10, - link_group_id=1): +def factory_create_eft_credit_invoice_link( + invoice_id=1, eft_credit_id=1, status_code="PENDING", amount=10, link_group_id=1 +): """Return Factory.""" eft_credit_invoice_link = EFTCreditInvoiceLink( amount=amount, invoice_id=invoice_id, eft_credit_id=eft_credit_id, - receipt_number='1234', + receipt_number="1234", status_code=status_code, - link_group_id=link_group_id + link_group_id=link_group_id, ).save() return eft_credit_invoice_link -def factory_create_eft_shortname_historical(payment_account_id=1, related_group_link_id=1, short_name_id=1, - statement_number=123, - transaction_type=EFTHistoricalTypes.STATEMENT_PAID.value): +def factory_create_eft_shortname_historical( + payment_account_id=1, + related_group_link_id=1, + short_name_id=1, + statement_number=123, + transaction_type=EFTHistoricalTypes.STATEMENT_PAID.value, +): """Return Factory.""" eft_historical = EFTShortnamesHistorical( amount=100, - created_by='TEST USER', + created_by="TEST USER", credit_balance=100, hidden=True, is_processing=True, @@ -343,19 +441,19 @@ def factory_create_eft_shortname_historical(payment_account_id=1, related_group_ short_name_id=short_name_id, statement_number=statement_number, transaction_date=datetime.now(tz=timezone.utc), - transaction_type=transaction_type + transaction_type=transaction_type, ).save() return eft_historical def factory_create_eft_refund( - cas_supplier_number: str = '1234', - comment: str = 'Test Comment', + cas_supplier_number: str = "1234", + comment: str = "Test Comment", refund_amount: float = 100.0, - refund_email: str = '', + refund_email: str = "", short_name_id: int = 1, status: str = InvoiceStatus.APPROVED.value, - disbursement_status_code: str = DisbursementStatus.ACKNOWLEDGED.value + disbursement_status_code: str = DisbursementStatus.ACKNOWLEDGED.value, ): """Return Factory.""" eft_refund = EFTRefund( @@ -366,115 +464,129 @@ def factory_create_eft_refund( refund_email=refund_email, short_name_id=short_name_id, status=status, - created_on=datetime.now(tz=timezone.utc) + created_on=datetime.now(tz=timezone.utc), ) return eft_refund -def factory_create_account(auth_account_id: str = '1234', payment_method_code: str = PaymentMethod.DIRECT_PAY.value, - status: str = CfsAccountStatus.PENDING.value, statement_notification_enabled: bool = True): +def factory_create_account( + auth_account_id: str = "1234", + payment_method_code: str = PaymentMethod.DIRECT_PAY.value, + status: str = CfsAccountStatus.PENDING.value, + statement_notification_enabled: bool = True, +): """Return payment account model.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=payment_method_code, - name=f'Test {auth_account_id}', - statement_notification_enabled=statement_notification_enabled).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=payment_method_code, + name=f"Test {auth_account_id}", + statement_notification_enabled=statement_notification_enabled, + ).save() CfsAccount(status=status, account_id=account.id, payment_method=payment_method_code).save() return account -def factory_create_ejv_account(auth_account_id='1234', - client: str = '112', - resp_centre: str = '11111', - service_line: str = '11111', - stob: str = '1111', - project_code: str = '1111111'): +def factory_create_ejv_account( + auth_account_id="1234", + client: str = "112", + resp_centre: str = "11111", + service_line: str = "11111", + stob: str = "1111", + project_code: str = "1111111", +): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.EJV.value, - name=f'Test {auth_account_id}').save() - DistributionCode(name=account.name, - client=client, - responsibility_centre=resp_centre, - service_line=service_line, - stob=stob, - project_code=project_code, - account_id=account.id, - start_date=datetime.now(tz=timezone.utc).date(), - created_by='test').save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.EJV.value, + name=f"Test {auth_account_id}", + ).save() + DistributionCode( + name=account.name, + client=client, + responsibility_centre=resp_centre, + service_line=service_line, + stob=stob, + project_code=project_code, + account_id=account.id, + start_date=datetime.now(tz=timezone.utc).date(), + created_by="test", + ).save() return account -def factory_distribution(name: str, client: str = '111', reps_centre: str = '22222', service_line: str = '33333', - stob: str = '4444', project_code: str = '5555555', service_fee_dist_id: int = None, - disbursement_dist_id: int = None): +def factory_distribution( + name: str, + client: str = "111", + reps_centre: str = "22222", + service_line: str = "33333", + stob: str = "4444", + project_code: str = "5555555", + service_fee_dist_id: int = None, + disbursement_dist_id: int = None, +): """Return Factory.""" - return DistributionCode(name=name, - client=client, - responsibility_centre=reps_centre, - service_line=service_line, - stob=stob, - project_code=project_code, - service_fee_distribution_code_id=service_fee_dist_id, - disbursement_distribution_code_id=disbursement_dist_id, - start_date=datetime.now(tz=timezone.utc).date(), - created_by='test').save() + return DistributionCode( + name=name, + client=client, + responsibility_centre=reps_centre, + service_line=service_line, + stob=stob, + project_code=project_code, + service_fee_distribution_code_id=service_fee_dist_id, + disbursement_distribution_code_id=disbursement_dist_id, + start_date=datetime.now(tz=timezone.utc).date(), + created_by="test", + ).save() def factory_distribution_link(distribution_code_id: int, fee_schedule_id: int): """Return Factory.""" - return DistributionCodeLink(fee_schedule_id=fee_schedule_id, - distribution_code_id=distribution_code_id).save() + return DistributionCodeLink(fee_schedule_id=fee_schedule_id, distribution_code_id=distribution_code_id).save() def factory_receipt( - invoice_id: int, - receipt_number: str = 'TEST1234567890', - receipt_date: datetime = datetime.now(tz=timezone.utc), - receipt_amount: float = 10.0 + invoice_id: int, + receipt_number: str = "TEST1234567890", + receipt_date: datetime = datetime.now(tz=timezone.utc), + receipt_amount: float = 10.0, ): """Return Factory.""" return Receipt( invoice_id=invoice_id, receipt_number=receipt_number, receipt_date=receipt_date, - receipt_amount=receipt_amount + receipt_amount=receipt_amount, ) -def factory_refund( - routing_slip_id: int, - details={} -): +def factory_refund(routing_slip_id: int, details={}): """Return Factory.""" return Refund( routing_slip_id=routing_slip_id, requested_date=datetime.now(tz=timezone.utc), - reason='TEST', - requested_by='TEST', - details=details + reason="TEST", + requested_by="TEST", + details=details, ).save() -def factory_refund_invoice( - invoice_id: int, - details={} -): +def factory_refund_invoice(invoice_id: int, details={}): """Return Factory.""" return Refund( invoice_id=invoice_id, requested_date=datetime.now(tz=timezone.utc), - reason='TEST', - requested_by='TEST', - details=details + reason="TEST", + requested_by="TEST", + details=details, ).save() def factory_refund_partial( - payment_line_item_id: int, - refund_amount: float, - refund_type: str, - created_by='test', - created_on: datetime = datetime.now(tz=timezone.utc) + payment_line_item_id: int, + refund_amount: float, + refund_type: str, + created_by="test", + created_on: datetime = datetime.now(tz=timezone.utc), ): """Return Factory.""" return RefundsPartial( @@ -482,38 +594,37 @@ def factory_refund_partial( refund_amount=refund_amount, refund_type=refund_type, created_by=created_by, - created_on=created_on + created_on=created_on, ).save() -def factory_pad_account_payload(account_id: int = randrange(999999), bank_number: str = '001', - transit_number='999', - bank_account='1234567890'): +def factory_pad_account_payload( + account_id: int = randrange(999999), + bank_number: str = "001", + transit_number="999", + bank_account="1234567890", +): """Return a pad payment account object.""" return { - 'accountId': account_id, - 'accountName': 'Test Account', - 'paymentInfo': { - 'methodOfPayment': PaymentMethod.PAD.value, - 'billable': True, - 'bankTransitNumber': transit_number, - 'bankInstitutionNumber': bank_number, - 'bankAccountNumber': bank_account - } + "accountId": account_id, + "accountName": "Test Account", + "paymentInfo": { + "methodOfPayment": PaymentMethod.PAD.value, + "billable": True, + "bankTransitNumber": transit_number, + "bankInstitutionNumber": bank_number, + "bankAccountNumber": bank_account, + }, } -def factory_eft_account_payload(payment_method: str = PaymentMethod.EFT.value, - account_id: int = randrange(999999)): +def factory_eft_account_payload(payment_method: str = PaymentMethod.EFT.value, account_id: int = randrange(999999)): """Return a premium eft enable payment account object.""" return { - 'accountId': account_id, - 'accountName': 'Test Account', - 'bcolAccountNumber': '2000000', - 'bcolUserId': 'U100000', - 'eft_enable': False, - 'paymentInfo': { - 'methodOfPayment': payment_method, - 'billable': True - } + "accountId": account_id, + "accountName": "Test Account", + "bcolAccountNumber": "2000000", + "bcolUserId": "U100000", + "eft_enable": False, + "paymentInfo": {"methodOfPayment": payment_method, "billable": True}, } diff --git a/jobs/payment-jobs/tests/jobs/mocks.py b/jobs/payment-jobs/tests/jobs/mocks.py index 22c1088fa..b2d2fdc2f 100644 --- a/jobs/payment-jobs/tests/jobs/mocks.py +++ b/jobs/payment-jobs/tests/jobs/mocks.py @@ -17,38 +17,44 @@ def paybc_token_response(cls, *args): # pylint: disable=unused-argument; mocks of library methods """Mock paybc token response.""" - return Mock(status_code=201, json=lambda: { - 'access_token': '5945-534534554-43534535', - 'token_type': 'Basic', - 'expires_in': 3600 - }) + return Mock( + status_code=201, + json=lambda: { + "access_token": "5945-534534554-43534535", + "token_type": "Basic", + "expires_in": 3600, + }, + ) def refund_payload_response(cls, *args): # pylint: disable=unused-argument; mocks of library methods """Mock refund payload response.""" - return Mock(status_code=201, json=lambda: { - 'refundstatus': 'PAID', - 'revenue': [ + return Mock( + status_code=201, + json=lambda: { + "refundstatus": "PAID", + "revenue": [ { - 'linenumber': '1', - 'revenueaccount': '112.32041.35301.1278.3200000.000000.0000', - 'revenueamount': '130', - 'glstatus': 'PAID', - 'glerrormessage': None, - 'refundglstatus': 'RJCT', - 'refundglerrormessage': 'BAD' + "linenumber": "1", + "revenueaccount": "112.32041.35301.1278.3200000.000000.0000", + "revenueamount": "130", + "glstatus": "PAID", + "glerrormessage": None, + "refundglstatus": "RJCT", + "refundglerrormessage": "BAD", }, { - 'linenumber': '2', - 'revenueaccount': '112.32041.35301.1278.3200000.000000.0000', - 'revenueamount': '1.5', - 'glstatus': 'PAID', - 'glerrormessage': None, - 'refundglstatus': 'RJCT', - 'refundglerrormessage': 'BAD' - } - ] - }) + "linenumber": "2", + "revenueaccount": "112.32041.35301.1278.3200000.000000.0000", + "revenueamount": "1.5", + "glstatus": "PAID", + "glerrormessage": None, + "refundglstatus": "RJCT", + "refundglerrormessage": "BAD", + }, + ], + }, + ) def empty_refund_payload_response(cls, *args): # pylint: disable=unused-argument; mocks of library methods @@ -58,12 +64,15 @@ def empty_refund_payload_response(cls, *args): # pylint: disable=unused-argumen def mocked_invoice_response(cls, *args): # pylint: disable=unused-argument; mocks of library methods """Mock POST invoice 200 payload response.""" - return Mock(status_code=200, json=lambda: { - 'invoice_number': '123', - 'pbc_ref_number': '10007', - 'party_number': '104894', - 'account_number': '116225', - 'site_number': '179145', - 'total': '15', - 'amount_due': '15' - }) + return Mock( + status_code=200, + json=lambda: { + "invoice_number": "123", + "pbc_ref_number": "10007", + "party_number": "104894", + "account_number": "116225", + "site_number": "179145", + "total": "15", + "amount_due": "15", + }, + ) diff --git a/jobs/payment-jobs/tests/jobs/test_activate_pad_account_task.py b/jobs/payment-jobs/tests/jobs/test_activate_pad_account_task.py index 615381995..1a7f20581 100644 --- a/jobs/payment-jobs/tests/jobs/test_activate_pad_account_task.py +++ b/jobs/payment-jobs/tests/jobs/test_activate_pad_account_task.py @@ -38,49 +38,53 @@ def test_activate_pad_accounts(session): def test_activate_pad_accounts_with_time_check(session): """Test Activate account.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='1') + account = factory_create_pad_account(auth_account_id="1") CreateAccountTask.create_accounts() account: PaymentAccount = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) - assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, 'Created account has pending pad status' + assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, "Created account has pending pad status" assert account.payment_method == PaymentMethod.PAD.value ActivatePadAccountTask.activate_pad_accounts() cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) - assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, \ - 'Same day Job runs and shouldnt change anything.' + assert ( + cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value + ), "Same day Job runs and shouldnt change anything." - time_delay = current_app.config['PAD_CONFIRMATION_PERIOD_IN_DAYS'] + time_delay = current_app.config["PAD_CONFIRMATION_PERIOD_IN_DAYS"] with freeze_time(datetime.now(tz=timezone.utc) + timedelta(days=time_delay, minutes=1)): ActivatePadAccountTask.activate_pad_accounts() account: PaymentAccount = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) - assert cfs_account.status == CfsAccountStatus.ACTIVE.value, \ - 'After the confirmation period is over , status should be active' + assert ( + cfs_account.status == CfsAccountStatus.ACTIVE.value + ), "After the confirmation period is over , status should be active" assert account.payment_method == PaymentMethod.PAD.value def test_activate_bcol_change_to_pad(session): """Test Activate account.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='1', payment_method=PaymentMethod.DRAWDOWN.value) + account = factory_create_pad_account(auth_account_id="1", payment_method=PaymentMethod.DRAWDOWN.value) CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) - assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, 'Created account has pending pad status' + assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, "Created account has pending pad status" assert account.payment_method == PaymentMethod.DRAWDOWN.value ActivatePadAccountTask.activate_pad_accounts() cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) - assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, \ - 'Same day Job runs and shouldnt change anything.' + assert ( + cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value + ), "Same day Job runs and shouldnt change anything." account = PaymentAccount.find_by_id(account.id) assert account.payment_method == PaymentMethod.DRAWDOWN.value - time_delay = current_app.config['PAD_CONFIRMATION_PERIOD_IN_DAYS'] + time_delay = current_app.config["PAD_CONFIRMATION_PERIOD_IN_DAYS"] with freeze_time(datetime.now(tz=timezone.utc) + timedelta(days=time_delay, minutes=1)): ActivatePadAccountTask.activate_pad_accounts() - assert cfs_account.status == CfsAccountStatus.ACTIVE.value, \ - 'After the confirmation period is over , status should be active' + assert ( + cfs_account.status == CfsAccountStatus.ACTIVE.value + ), "After the confirmation period is over , status should be active" account = PaymentAccount.find_by_id(account.id) assert account.payment_method == PaymentMethod.PAD.value diff --git a/jobs/payment-jobs/tests/jobs/test_ap_task.py b/jobs/payment-jobs/tests/jobs/test_ap_task.py index adc79537b..bc914d447 100644 --- a/jobs/payment-jobs/tests/jobs/test_ap_task.py +++ b/jobs/payment-jobs/tests/jobs/test_ap_task.py @@ -21,15 +21,31 @@ from pay_api.models import FeeSchedule as FeeScheduleModel from pay_api.models import RoutingSlip from pay_api.utils.enums import ( - CfsAccountStatus, DisbursementStatus, EFTShortnameRefundStatus, InvoiceStatus, PaymentMethod, RoutingSlipStatus) + CfsAccountStatus, + DisbursementStatus, + EFTShortnameRefundStatus, + InvoiceStatus, + PaymentMethod, + RoutingSlipStatus, +) from tasks.ap_task import ApTask from .factory import ( - factory_create_eft_account, factory_create_eft_credit, factory_create_eft_credit_invoice_link, - factory_create_eft_file, factory_create_eft_refund, factory_create_eft_shortname, factory_create_eft_transaction, - factory_create_pad_account, factory_eft_shortname_link, factory_invoice, factory_payment_line_item, factory_refund, - factory_routing_slip_account) + factory_create_eft_account, + factory_create_eft_credit, + factory_create_eft_credit_invoice_link, + factory_create_eft_file, + factory_create_eft_refund, + factory_create_eft_shortname, + factory_create_eft_transaction, + factory_create_pad_account, + factory_eft_shortname_link, + factory_invoice, + factory_payment_line_item, + factory_refund, + factory_routing_slip_account, +) def test_eft_refunds(session, monkeypatch): @@ -39,23 +55,20 @@ def test_eft_refunds(session, monkeypatch): 1) Create an invoice with refund and status REFUNDED 2) Run the job and assert status """ - account = factory_create_eft_account( - auth_account_id='1', - status=CfsAccountStatus.ACTIVE.value - ) + account = factory_create_eft_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) invoice = factory_invoice( payment_account=account, payment_method_code=PaymentMethod.EFT.value, status_code=InvoiceStatus.PAID.value, total=100, ) - short_name = factory_create_eft_shortname('SHORTNAMETEST') + short_name = factory_create_eft_shortname("SHORTNAMETEST") eft_refund = factory_create_eft_refund( disbursement_status_code=DisbursementStatus.ACKNOWLEDGED.value, refund_amount=100, - refund_email='test@test.com', + refund_email="test@test.com", short_name_id=short_name.id, - status=EFTShortnameRefundStatus.APPROVED.value + status=EFTShortnameRefundStatus.APPROVED.value, ) eft_refund.save() eft_file = factory_create_eft_file() @@ -63,12 +76,12 @@ def test_eft_refunds(session, monkeypatch): eft_credit = factory_create_eft_credit( short_name_id=short_name.id, eft_transaction_id=eft_transaction.id, - eft_file_id=eft_file.id + eft_file_id=eft_file.id, ) factory_create_eft_credit_invoice_link(invoice_id=invoice.id, eft_credit_id=eft_credit.id) factory_eft_shortname_link(short_name_id=short_name.id) - with patch('pysftp.Connection.put') as mock_upload: + with patch("pysftp.Connection.put") as mock_upload: ApTask.create_ap_files() mock_upload.assert_called() @@ -80,29 +93,33 @@ def test_routing_slip_refunds(session, monkeypatch): 1) Create a routing slip with remaining_amount and status REFUND_AUTHORIZED 2) Run the job and assert status """ - rs_1 = 'RS0000001' + rs_1 = "RS0000001" factory_routing_slip_account( number=rs_1, status=CfsAccountStatus.ACTIVE.value, total=100, remaining_amount=0, - auth_account_id='1234', + auth_account_id="1234", routing_slip_status=RoutingSlipStatus.REFUND_AUTHORIZED.value, - refund_amount=100) + refund_amount=100, + ) routing_slip = RoutingSlip.find_by_number(rs_1) - factory_refund(routing_slip.id, { - 'name': 'TEST', - 'mailingAddress': { - 'city': 'Victoria', - 'region': 'BC', - 'street': '655 Douglas St', - 'country': 'CA', - 'postalCode': 'V8V 0B6', - 'streetAdditional': '' - } - }) - with patch('pysftp.Connection.put') as mock_upload: + factory_refund( + routing_slip.id, + { + "name": "TEST", + "mailingAddress": { + "city": "Victoria", + "region": "BC", + "street": "655 Douglas St", + "country": "CA", + "postalCode": "V8V 0B6", + "streetAdditional": "", + }, + }, + ) + with patch("pysftp.Connection.put") as mock_upload: ApTask.create_ap_files() mock_upload.assert_called() @@ -110,7 +127,7 @@ def test_routing_slip_refunds(session, monkeypatch): assert routing_slip.status == RoutingSlipStatus.REFUND_UPLOADED.value # Run again and assert nothing is uploaded - with patch('pysftp.Connection.put') as mock_upload: + with patch("pysftp.Connection.put") as mock_upload: ApTask.create_ap_files() mock_upload.assert_not_called() @@ -122,14 +139,14 @@ def test_ap_disbursement(session, monkeypatch): 1) Create invoices, payment line items with BCA corp type. 2) Run the job and assert status """ - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) invoice = factory_invoice( payment_account=account, status_code=InvoiceStatus.PAID.value, total=10, - corp_type_code='BCA' + corp_type_code="BCA", ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('BCA', 'OLAARTOQ') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("BCA", "OLAARTOQ") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -138,10 +155,10 @@ def test_ap_disbursement(session, monkeypatch): status_code=InvoiceStatus.REFUNDED.value, total=10, disbursement_status_code=DisbursementStatus.COMPLETED.value, - corp_type_code='BCA' + corp_type_code="BCA", ) line = factory_payment_line_item(refund_invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() - with patch('pysftp.Connection.put') as mock_upload: + with patch("pysftp.Connection.put") as mock_upload: ApTask.create_ap_files() mock_upload.assert_called() diff --git a/jobs/payment-jobs/tests/jobs/test_bcol_refund_confirmation_task.py b/jobs/payment-jobs/tests/jobs/test_bcol_refund_confirmation_task.py index d198d1c6f..1c6f06d80 100644 --- a/jobs/payment-jobs/tests/jobs/test_bcol_refund_confirmation_task.py +++ b/jobs/payment-jobs/tests/jobs/test_bcol_refund_confirmation_task.py @@ -14,63 +14,112 @@ """Tests to assure the BCOL Refund Confirmation Job.""" from decimal import Decimal +from unittest.mock import Mock import pytest from pay_api.models import Invoice from pay_api.utils.enums import CfsAccountStatus, InvoiceStatus, PaymentMethod, PaymentSystem -from unittest.mock import Mock from tasks.bcol_refund_confirmation_task import BcolRefundConfirmationTask from .factory import ( - factory_create_direct_pay_account, factory_create_pad_account, factory_invoice, factory_invoice_reference, - factory_payment) - + factory_create_direct_pay_account, + factory_create_pad_account, + factory_invoice, + factory_invoice_reference, + factory_payment, +) -@pytest.mark.parametrize('test_name, payment_method, invoice_total, refund_total, start_status, expected, mismatch', [ - ('drawdown_refund_full', PaymentMethod.DRAWDOWN.value, 31.5, -31.5, - InvoiceStatus.REFUND_REQUESTED.value, InvoiceStatus.REFUNDED.value, False), - ('drawdown_refund_partial', PaymentMethod.DRAWDOWN.value, 31.5, -10, - InvoiceStatus.REFUND_REQUESTED.value, InvoiceStatus.REFUND_REQUESTED.value, True), - ('drawdown_refund_none', PaymentMethod.DRAWDOWN.value, 31.5, 0, - InvoiceStatus.REFUND_REQUESTED.value, InvoiceStatus.REFUND_REQUESTED.value, False), - ('no_refund_requested', PaymentMethod.DRAWDOWN.value, 31.5, -31.5, - InvoiceStatus.APPROVED.value, InvoiceStatus.APPROVED.value, False), -]) +@pytest.mark.parametrize( + "test_name, payment_method, invoice_total, refund_total, start_status, expected, mismatch", + [ + ( + "drawdown_refund_full", + PaymentMethod.DRAWDOWN.value, + 31.5, + -31.5, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.REFUNDED.value, + False, + ), + ( + "drawdown_refund_partial", + PaymentMethod.DRAWDOWN.value, + 31.5, + -10, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.REFUND_REQUESTED.value, + True, + ), + ( + "drawdown_refund_none", + PaymentMethod.DRAWDOWN.value, + 31.5, + 0, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.REFUND_REQUESTED.value, + False, + ), + ( + "no_refund_requested", + PaymentMethod.DRAWDOWN.value, + 31.5, + -31.5, + InvoiceStatus.APPROVED.value, + InvoiceStatus.APPROVED.value, + False, + ), + ], +) def test_bcol_refund_confirmation( - session, monkeypatch, test_name, payment_method, invoice_total, refund_total, start_status, expected, mismatch): + session, + monkeypatch, + test_name, + payment_method, + invoice_total, + refund_total, + start_status, + expected, + mismatch, +): """Test bcol refund confirmation.""" - invoice_number = f'{test_name}000012345' + invoice_number = f"{test_name}000012345" # setup mocks - colin_bcol_records_mock = Mock(return_value={invoice_number: Decimal(refund_total)} if refund_total != 0 else {}) + colin_bcol_records_mock = Mock(return_value=({invoice_number: Decimal(refund_total)} if refund_total != 0 else {})) sentry_mock = Mock() monkeypatch.setattr( - 'tasks.bcol_refund_confirmation_task.BcolRefundConfirmationTask._get_colin_bcol_records_for_invoices', - colin_bcol_records_mock) - monkeypatch.setattr('tasks.bcol_refund_confirmation_task.capture_message', sentry_mock) + "tasks.bcol_refund_confirmation_task.BcolRefundConfirmationTask._get_colin_bcol_records_for_invoices", + colin_bcol_records_mock, + ) + monkeypatch.setattr("tasks.bcol_refund_confirmation_task.capture_message", sentry_mock) # setup invoice / invoice reference / payment pay_account = None if payment_method == PaymentMethod.PAD.value: - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - payment_method=PaymentMethod.PAD.value) + pay_account = factory_create_pad_account( + status=CfsAccountStatus.ACTIVE.value, payment_method=PaymentMethod.PAD.value + ) else: pay_account = factory_create_direct_pay_account(payment_method=PaymentMethod.DRAWDOWN.value) - invoice = factory_invoice(payment_account=pay_account, - payment_method_code=pay_account.payment_method, - status_code=InvoiceStatus.REFUND_REQUESTED.value, - total=invoice_total) + invoice = factory_invoice( + payment_account=pay_account, + payment_method_code=pay_account.payment_method, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + total=invoice_total, + ) # explicitly set status to starting value (factory method overwrites it for pad) invoice.invoice_status_code = start_status invoice.save() inv_ref = factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) - factory_payment(invoice_number=inv_ref.invoice_number, - payment_status_code='COMPLETED', - payment_system_code=PaymentSystem.BCOL.value) + factory_payment( + invoice_number=inv_ref.invoice_number, + payment_status_code="COMPLETED", + payment_system_code=PaymentSystem.BCOL.value, + ) # run job BcolRefundConfirmationTask.update_bcol_refund_invoices() diff --git a/jobs/payment-jobs/tests/jobs/test_cfs_create_account_task.py b/jobs/payment-jobs/tests/jobs/test_cfs_create_account_task.py index aaf8461e7..3e974eaee 100644 --- a/jobs/payment-jobs/tests/jobs/test_cfs_create_account_task.py +++ b/jobs/payment-jobs/tests/jobs/test_cfs_create_account_task.py @@ -40,7 +40,7 @@ def test_create_account_setup(session): def test_create_pad_account(session): """Test create account.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='1') + account = factory_create_pad_account(auth_account_id="1") CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) @@ -55,7 +55,7 @@ def test_create_pad_account(session): def test_create_eft_account(session): """Test create account.""" # Create a pending account first, then call the job - account = factory_create_eft_account(auth_account_id='1') + account = factory_create_eft_account(auth_account_id="1") CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.EFT.value) @@ -66,16 +66,16 @@ def test_create_eft_account(session): def test_create_pad_account_user_error(session): """Test create account.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='1') + account = factory_create_pad_account(auth_account_id="1") cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) assert cfs_account.status == CfsAccountStatus.PENDING.value mock_response = requests.models.Response() - mock_response.headers['CAS-Returned-Messages'] = '[Errors = [34] Bank Account Number is Invalid]' + mock_response.headers["CAS-Returned-Messages"] = "[Errors = [34] Bank Account Number is Invalid]" mock_response.status_code = 404 side_effect = HTTPError(response=mock_response) - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: - with patch('pay_api.services.CFSService.create_cfs_account', side_effect=side_effect): + with patch.object(mailer, "publish_mailer_events") as mock_mailer: + with patch("pay_api.services.CFSService.create_cfs_account", side_effect=side_effect): CreateAccountTask.create_accounts() mock_mailer.assert_called @@ -87,16 +87,16 @@ def test_create_pad_account_user_error(session): def test_create_pad_account_system_error(session): """Test create account.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='1') + account = factory_create_pad_account(auth_account_id="1") cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) assert cfs_account.status == CfsAccountStatus.PENDING.value mock_response = requests.models.Response() - mock_response.headers['CAS-Returned-Messages'] = '[CFS Down]' + mock_response.headers["CAS-Returned-Messages"] = "[CFS Down]" mock_response.status_code = 404 side_effect = HTTPError(response=mock_response) - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: - with patch('pay_api.services.CFSService.create_cfs_account', side_effect=side_effect): + with patch.object(mailer, "publish_mailer_events") as mock_mailer: + with patch("pay_api.services.CFSService.create_cfs_account", side_effect=side_effect): CreateAccountTask.create_accounts() mock_mailer.assert_not_called() @@ -108,7 +108,7 @@ def test_create_pad_account_system_error(session): def test_create_pad_account_no_confirmation_period(session): """Test create account.Arbitrary scenario when there is no confirmation period.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='1', confirmation_period=0) + account = factory_create_pad_account(auth_account_id="1", confirmation_period=0) CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) @@ -123,7 +123,7 @@ def test_create_pad_account_no_confirmation_period(session): def test_create_online_banking_account(session): """Test create account.""" # Create a pending account first, then call the job - account = factory_create_online_banking_account(auth_account_id='2') + account = factory_create_online_banking_account(auth_account_id="2") CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) @@ -138,13 +138,13 @@ def test_create_online_banking_account(session): def test_update_online_banking_account(session): """Test update account.""" # Create a pending account first, then call the job - account = factory_create_online_banking_account(auth_account_id='2') + account = factory_create_online_banking_account(auth_account_id="2") CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) # Update account, which shouldn't change any details - OnlineBankingService().update_account(name='Test', cfs_account=cfs_account, payment_info=None) + OnlineBankingService().update_account(name="Test", cfs_account=cfs_account, payment_info=None) updated_cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) assert updated_cfs_account.status == CfsAccountStatus.ACTIVE.value @@ -154,7 +154,7 @@ def test_update_online_banking_account(session): def test_update_pad_account(session): """Test update account.""" # Create a pending account first, then call the job - account = factory_create_pad_account(auth_account_id='2') + account = factory_create_pad_account(auth_account_id="2") CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) @@ -164,11 +164,11 @@ def test_update_pad_account(session): # Now update the account. new_payment_details = { - 'bankInstitutionNumber': '111', - 'bankTransitNumber': '222', - 'bankAccountNumber': '3333333333' + "bankInstitutionNumber": "111", + "bankTransitNumber": "222", + "bankAccountNumber": "3333333333", } - PadService().update_account(name='Test', cfs_account=cfs_account, payment_info=new_payment_details) + PadService().update_account(name="Test", cfs_account=cfs_account, payment_info=new_payment_details) cfs_account = CfsAccount.find_by_id(cfs_account.id) # Run the job again @@ -176,9 +176,9 @@ def test_update_pad_account(session): updated_cfs_account = CfsAccount.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) assert updated_cfs_account.id != cfs_account.id - assert updated_cfs_account.bank_account_number == new_payment_details.get('bankAccountNumber') - assert updated_cfs_account.bank_branch_number == new_payment_details.get('bankTransitNumber') - assert updated_cfs_account.bank_number == new_payment_details.get('bankInstitutionNumber') + assert updated_cfs_account.bank_account_number == new_payment_details.get("bankAccountNumber") + assert updated_cfs_account.bank_branch_number == new_payment_details.get("bankTransitNumber") + assert updated_cfs_account.bank_number == new_payment_details.get("bankInstitutionNumber") assert cfs_account.status == CfsAccountStatus.INACTIVE.value assert updated_cfs_account.status == CfsAccountStatus.ACTIVE.value diff --git a/jobs/payment-jobs/tests/jobs/test_cfs_create_invoice_task.py b/jobs/payment-jobs/tests/jobs/test_cfs_create_invoice_task.py index 159cd1acf..a1702769c 100644 --- a/jobs/payment-jobs/tests/jobs/test_cfs_create_invoice_task.py +++ b/jobs/payment-jobs/tests/jobs/test_cfs_create_invoice_task.py @@ -23,6 +23,7 @@ from pay_api.models import FeeSchedule as FeeScheduleModel from pay_api.models import Invoice as InvoiceModel from pay_api.models import InvoiceReference as InvoiceReferenceModel + # from pay_api.models import Payment as PaymentModel from pay_api.services import CFSService from pay_api.utils.enums import CfsAccountStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod @@ -32,8 +33,13 @@ from tasks.cfs_create_invoice_task import CreateInvoiceTask from .factory import ( - factory_create_eft_account, factory_create_online_banking_account, factory_create_pad_account, factory_invoice, - factory_payment_line_item, factory_routing_slip_account) + factory_create_eft_account, + factory_create_online_banking_account, + factory_create_pad_account, + factory_invoice, + factory_payment_line_item, + factory_routing_slip_account, +) def test_create_invoice(session): @@ -45,13 +51,18 @@ def test_create_invoice(session): def test_create_pad_invoice_single_transaction(session): """Assert PAD invoices are created.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -59,8 +70,9 @@ def test_create_pad_invoice_single_transaction(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -69,19 +81,27 @@ def test_create_pad_invoice_single_transaction(session): def test_create_pad_invoice_mixed_pli_values(session): """Assert PAD invoices are created with total = 0, service fees > 0.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=1.5, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=1.5, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('PPR', 'FSREG') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("PPR", "FSREG") dist_code = DistributionCodeModel.find_by_active_for_fee_schedule(fee_schedule.fee_schedule_id) # We need a dist code, if we're charging service fees. dist_code.service_fee_distribution_code_id = 1 dist_code.save() line = factory_payment_line_item( - invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id, total=0, service_fees=1.5 + invoice.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + total=0, + service_fees=1.5, ) line.save() assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -89,8 +109,9 @@ def test_create_pad_invoice_mixed_pli_values(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -99,23 +120,28 @@ def test_create_pad_invoice_mixed_pli_values(session): def test_create_rs_invoice_single_transaction(session): """Assert PAD invoices are created.""" # Create an account and an invoice for the account - rs_number = '123' + rs_number = "123" account = factory_routing_slip_account(number=rs_number, status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, - payment_method_code=PaymentMethod.INTERNAL.value, routing_slip=rs_number) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.INTERNAL.value, + routing_slip=rs_number, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() invoice_data = { - 'invoice_number': '123', - 'pbc_ref_number': '10005', - 'party_number': '11111', - 'party_name': 'invoice' + "invoice_number": "123", + "pbc_ref_number": "10005", + "party_number": "11111", + "party_name": "invoice", } assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -123,21 +149,26 @@ def test_create_rs_invoice_single_transaction(session): invoice_failed_res.status_code = 400 # Testing the flow where create_account_invoice already has an invoice. - with patch.object(CFSService, 'create_account_invoice', return_value=invoice_failed_res, side_effect=HTTPError()) \ - as mock_create_invoice: - with patch.object(CFSService, 'get_invoice', return_value=invoice_data) as mock_get_invoice: + with patch.object( + CFSService, + "create_account_invoice", + return_value=invoice_failed_res, + side_effect=HTTPError(), + ) as mock_create_invoice: + with patch.object(CFSService, "get_invoice", return_value=invoice_data) as mock_get_invoice: CreateInvoiceTask.create_invoices() mock_create_invoice.assert_called() mock_get_invoice.assert_called() # Regular flow where create_account_invoice succeeds. - with patch.object(CFSService, 'create_account_invoice', return_value=invoice_data) as mock_create_invoice: + with patch.object(CFSService, "create_account_invoice", return_value=invoice_data) as mock_create_invoice: CreateInvoiceTask.create_invoices() mock_create_invoice.assert_called() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.COMPLETED.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.COMPLETED.value + ) assert inv_ref assert updated_invoice.invoice_status_code == InvoiceStatus.PAID.value @@ -146,31 +177,41 @@ def test_create_rs_invoice_single_transaction(session): def test_create_pad_invoice_single_transaction_run_again(session): """Assert PAD invoices are created.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() - invoice_response = {'invoice_number': '10021', 'pbc_ref_number': '10005', 'party_number': '11111', - 'party_name': 'invoice'} + invoice_response = { + "invoice_number": "10021", + "pbc_ref_number": "10005", + "party_number": "11111", + "party_name": "invoice", + } assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value - with patch.object(CFSService, 'create_account_invoice', return_value=invoice_response) as mock_cfs: + with patch.object(CFSService, "create_account_invoice", return_value=invoice_response) as mock_cfs: CreateInvoiceTask.create_invoices() mock_cfs.assert_called() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value - with patch.object(CFSService, 'create_account_invoice', return_value=invoice_response) as mock_cfs: + with patch.object(CFSService, "create_account_invoice", return_value=invoice_response) as mock_cfs: CreateInvoiceTask.create_invoices() mock_cfs.assert_not_called() @@ -178,13 +219,18 @@ def test_create_pad_invoice_single_transaction_run_again(session): def test_create_pad_invoice_for_frozen_accounts(session): """Assert PAD invoices are created.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.FREEZE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.FREEZE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -193,8 +239,9 @@ def test_create_pad_invoice_for_frozen_accounts(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref is None assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -203,19 +250,29 @@ def test_create_pad_invoice_for_frozen_accounts(session): def test_create_pad_invoice_multiple_transactions(session): """Assert PAD invoices are created.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() # Create another invoice for this account - invoice2 = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) - fee_schedule2 = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTADD') + invoice2 = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) + fee_schedule2 = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTADD") line2 = factory_payment_line_item(invoice2.id, fee_schedule_id=fee_schedule2.fee_schedule_id) line2.save() @@ -228,13 +285,18 @@ def test_create_pad_invoice_multiple_transactions(session): def test_create_pad_invoice_before_cutoff(session): """Assert PAD invoices are created.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=2) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -243,8 +305,9 @@ def test_create_pad_invoice_before_cutoff(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref is not None # As PAD will be summed up for all outstanding invoices assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -253,12 +316,17 @@ def test_create_pad_invoice_before_cutoff(session): def test_create_online_banking_transaction(session): """Assert Online Banking invoices are created.""" # Create an account and an invoice for the account - account = factory_create_online_banking_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_online_banking_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, payment_method_code=None) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + payment_method_code=None, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -267,8 +335,9 @@ def test_create_online_banking_transaction(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref assert updated_invoice.invoice_status_code == InvoiceStatus.SETTLEMENT_SCHEDULED.value @@ -276,13 +345,18 @@ def test_create_online_banking_transaction(session): def test_create_eft_invoice(session): """Assert EFT invoice is created.""" - account = factory_create_eft_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_eft_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=PaymentMethod.EFT.value) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.EFT.value, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -290,8 +364,9 @@ def test_create_eft_invoice(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - invoice_reference: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + invoice_reference: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert invoice_reference assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -299,44 +374,59 @@ def test_create_eft_invoice(session): def test_create_eft_invoice_rerun(session): """Assert EFT invoice is created.""" - account = factory_create_eft_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_eft_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=PaymentMethod.EFT.value) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.EFT.value, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() - invoice_response = {'invoice_number': '10021', 'pbc_ref_number': '10005', 'party_number': '11111', - 'party_name': 'invoice'} + invoice_response = { + "invoice_number": "10021", + "pbc_ref_number": "10005", + "party_number": "11111", + "party_name": "invoice", + } assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value - with patch.object(CFSService, 'create_account_invoice', return_value=invoice_response) as mock_cfs: + with patch.object(CFSService, "create_account_invoice", return_value=invoice_response) as mock_cfs: CreateInvoiceTask.create_invoices() mock_cfs.assert_called() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value - with patch.object(CFSService, 'create_account_invoice', return_value=invoice_response) as mock_cfs: + with patch.object(CFSService, "create_account_invoice", return_value=invoice_response) as mock_cfs: CreateInvoiceTask.create_invoices() mock_cfs.assert_not_called() def test_create_eft_invoice_on_frozen_account(session): """Assert EFT invoice is created.""" - account = factory_create_eft_account(auth_account_id='1', status=CfsAccountStatus.FREEZE.value) + account = factory_create_eft_account(auth_account_id="1", status=CfsAccountStatus.FREEZE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=PaymentMethod.EFT.value) + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.EFT.value, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -345,8 +435,9 @@ def test_create_eft_invoice_on_frozen_account(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref is None assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value @@ -354,19 +445,29 @@ def test_create_eft_invoice_on_frozen_account(session): def test_create_eft_invoices(session): """Assert EFT invoices are created.""" - account = factory_create_eft_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_eft_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=PaymentMethod.EFT.value) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.EFT.value, + ) + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() # Create another invoice for this account - invoice2 = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=PaymentMethod.EFT.value) - fee_schedule2 = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTADD') + invoice2 = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.EFT.value, + ) + fee_schedule2 = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTADD") line2 = factory_payment_line_item(invoice2.id, fee_schedule_id=fee_schedule2.fee_schedule_id) line2.save() @@ -378,12 +479,17 @@ def test_create_eft_invoices(session): def test_create_eft_invoice_before_cutoff(session): """Assert EFT invoices are created.""" - account = factory_create_eft_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_eft_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=2) # Create an invoice for this account - invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=PaymentMethod.EFT.value) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + invoice = factory_invoice( + payment_account=account, + created_on=previous_day, + total=10, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=PaymentMethod.EFT.value, + ) + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -392,8 +498,9 @@ def test_create_eft_invoice_before_cutoff(session): CreateInvoiceTask.create_invoices() updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id) - inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \ - find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value) + inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status( + invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) assert inv_ref is not None # As EFT will be summed up for all outstanding invoices assert updated_invoice.invoice_status_code == InvoiceStatus.APPROVED.value diff --git a/jobs/payment-jobs/tests/jobs/test_cfs_create_routing_slip_account_task.py b/jobs/payment-jobs/tests/jobs/test_cfs_create_routing_slip_account_task.py index 8340835ad..daa26d418 100644 --- a/jobs/payment-jobs/tests/jobs/test_cfs_create_routing_slip_account_task.py +++ b/jobs/payment-jobs/tests/jobs/test_cfs_create_routing_slip_account_task.py @@ -19,6 +19,7 @@ from pay_api.models import CfsAccount from pay_api.utils.enums import CfsAccountStatus, PaymentMethod + from tasks.cfs_create_account_task import CreateAccountTask from .factory import factory_routing_slip_account diff --git a/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py b/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py index 6bae89034..66d01a6ab 100644 --- a/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py +++ b/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py @@ -14,6 +14,7 @@ """Tests for direct pay automated refund task.""" import datetime + from freezegun import freeze_time from pay_api.models import Refund as RefundModel from pay_api.utils.enums import InvoiceReferenceStatus, InvoiceStatus, PaymentStatus @@ -21,8 +22,12 @@ from tasks.direct_pay_automated_refund_task import DirectPayAutomatedRefundTask from .factory import ( - factory_create_direct_pay_account, factory_invoice, factory_invoice_reference, factory_payment, - factory_refund_invoice) + factory_create_direct_pay_account, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_refund_invoice, +) def test_automated_refund_task(session): @@ -34,25 +39,20 @@ def test_automated_refund_task(session): def test_successful_paid_refund(session, monkeypatch): """Bambora paid, but not GL complete.""" payment_account = factory_create_direct_pay_account() - invoice = factory_invoice(payment_account=payment_account, status_code=InvoiceStatus.REFUND_REQUESTED.value) + invoice = factory_invoice( + payment_account=payment_account, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + ) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value).save() - payment = factory_payment('PAYBC', invoice_number=invoice.id) + payment = factory_payment("PAYBC", invoice_number=invoice.id) refund = factory_refund_invoice(invoice.id) - def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods - return { - 'revenue': [ - { - 'refund_data': [ - { - 'refundglstatus': 'PAID', - 'refundglerrormessage': '' - } - ] - } - ] - } - target = 'tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status' + def payment_status( + cls, + ): # pylint: disable=unused-argument; mocks of library methods + return {"revenue": [{"refund_data": [{"refundglstatus": "PAID", "refundglerrormessage": ""}]}]} + + target = "tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status" monkeypatch.setattr(target, payment_status) DirectPayAutomatedRefundTask().process_cc_refunds() @@ -66,27 +66,20 @@ def test_successful_completed_refund(session, monkeypatch): """Test successful refund (GL complete).""" invoice = factory_invoice(factory_create_direct_pay_account(), status_code=InvoiceStatus.REFUNDED.value) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value).save() - payment = factory_payment('PAYBC', invoice_number=invoice.id) + payment = factory_payment("PAYBC", invoice_number=invoice.id) refund = factory_refund_invoice(invoice.id) - def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods - return { - 'revenue': [ - { - 'refund_data': [ - { - 'refundglstatus': 'CMPLT', - 'refundglerrormessage': '' - } - ] - } - ] - } - target = 'tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status' + def payment_status( + cls, + ): # pylint: disable=unused-argument; mocks of library methods + return {"revenue": [{"refund_data": [{"refundglstatus": "CMPLT", "refundglerrormessage": ""}]}]} + + target = "tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status" monkeypatch.setattr(target, payment_status) - with freeze_time(datetime.datetime.combine(datetime.datetime.now(tz=datetime.timezone.utc).date(), - datetime.time(6, 00))): + with freeze_time( + datetime.datetime.combine(datetime.datetime.now(tz=datetime.timezone.utc).date(), datetime.time(6, 00)) + ): DirectPayAutomatedRefundTask().process_cc_refunds() refund = RefundModel.find_by_invoice_id(invoice.id) assert invoice.invoice_status_code == InvoiceStatus.REFUNDED.value @@ -101,42 +94,36 @@ def test_bad_cfs_refund(session, monkeypatch): refund = factory_refund_invoice(invoice.id) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value).save() - def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods + def payment_status( + cls, + ): # pylint: disable=unused-argument; mocks of library methods return { - 'revenue': [ + "revenue": [ { - 'linenumber': '1', - 'revenueaccount': '112.32041.35301.1278.3200000.000000.0000', - 'revenueamount': '130', - 'glstatus': 'PAID', - 'glerrormessage': None, - 'refund_data': [ - { - 'refundglstatus': 'RJCT', - 'refundglerrormessage': 'BAD' - } - ] + "linenumber": "1", + "revenueaccount": "112.32041.35301.1278.3200000.000000.0000", + "revenueamount": "130", + "glstatus": "PAID", + "glerrormessage": None, + "refund_data": [{"refundglstatus": "RJCT", "refundglerrormessage": "BAD"}], }, { - 'linenumber': '2', - 'revenueaccount': '112.32041.35301.1278.3200000.000000.0000', - 'revenueamount': '1.5', - 'glstatus': 'PAID', - 'glerrormessage': None, - 'refund_data': [ - { - 'refundglstatus': 'RJCT', - 'refundglerrormessage': 'BAD' - } - ] - } + "linenumber": "2", + "revenueaccount": "112.32041.35301.1278.3200000.000000.0000", + "revenueamount": "1.5", + "glstatus": "PAID", + "glerrormessage": None, + "refund_data": [{"refundglstatus": "RJCT", "refundglerrormessage": "BAD"}], + }, ] } - target = 'tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status' + + target = "tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status" monkeypatch.setattr(target, payment_status) - with freeze_time(datetime.datetime.combine(datetime.datetime.now(tz=datetime.timezone.utc).date(), - datetime.time(6, 00))): + with freeze_time( + datetime.datetime.combine(datetime.datetime.now(tz=datetime.timezone.utc).date(), datetime.time(6, 00)) + ): DirectPayAutomatedRefundTask().process_cc_refunds() - assert refund.gl_error == 'BAD BAD' + assert refund.gl_error == "BAD BAD" assert refund.gl_posted is None diff --git a/jobs/payment-jobs/tests/jobs/test_distribution_task.py b/jobs/payment-jobs/tests/jobs/test_distribution_task.py index daffbd10e..b6fd90707 100644 --- a/jobs/payment-jobs/tests/jobs/test_distribution_task.py +++ b/jobs/payment-jobs/tests/jobs/test_distribution_task.py @@ -23,8 +23,16 @@ from tasks.distribution_task import DistributionTask from .factory import ( - factory_create_direct_pay_account, factory_create_ejv_account, factory_distribution, factory_distribution_link, - factory_invoice, factory_invoice_reference, factory_payment, factory_payment_line_item, factory_refund_invoice) + factory_create_direct_pay_account, + factory_create_ejv_account, + factory_distribution, + factory_distribution_link, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_payment_line_item, + factory_refund_invoice, +) from .mocks import empty_refund_payload_response, paybc_token_response, refund_payload_response @@ -36,31 +44,45 @@ def test_update_failed_distributions(session): def test_update_failed_distributions_refunds(session, monkeypatch): """Test failed distribution refunds.""" - invoice = factory_invoice(factory_create_direct_pay_account( - ), status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value) + invoice = factory_invoice( + factory_create_direct_pay_account(), + status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value, + ) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value) - service_fee_distribution = factory_distribution(name='VS Service Fee', client='112') + service_fee_distribution = factory_distribution(name="VS Service Fee", client="112") fee_distribution = factory_distribution( - 'Super Dist', service_fee_dist_id=service_fee_distribution.disbursement_distribution_code_id, client='112') + "Super Dist", + service_fee_dist_id=service_fee_distribution.disbursement_distribution_code_id, + client="112", + ) - corp_type: CorpTypeModel = CorpTypeModel.find_by_code('VS') - fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, 'WILLNOTICE') + corp_type: CorpTypeModel = CorpTypeModel.find_by_code("VS") + fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, "WILLNOTICE") factory_distribution_link(fee_distribution.distribution_code_id, fee_schedule.fee_schedule_id) - factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id, filing_fees=30, - total=31.5, service_fees=1.5, fee_dist_id=fee_distribution.distribution_code_id) + factory_payment_line_item( + invoice.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=30, + total=31.5, + service_fees=1.5, + fee_dist_id=fee_distribution.distribution_code_id, + ) - factory_payment('PAYBC', 'DIRECT_PAY', invoice_number=invoice.id) + factory_payment("PAYBC", "DIRECT_PAY", invoice_number=invoice.id) factory_refund_invoice(invoice.id) # Required, because mocking out the POST below (This uses the OAuthService POST). - monkeypatch.setattr('pay_api.services.direct_pay_service.DirectPayService.get_token', paybc_token_response) + monkeypatch.setattr( + "pay_api.services.direct_pay_service.DirectPayService.get_token", + paybc_token_response, + ) # Mock POST until obtain OAS spec from PayBC for updating GL. - monkeypatch.setattr('pay_api.services.oauth_service.OAuthService.post', lambda *args, **kwargs: None) + monkeypatch.setattr("pay_api.services.oauth_service.OAuthService.post", lambda *args, **kwargs: None) # Mock refund payload response. - monkeypatch.setattr('pay_api.services.oauth_service.OAuthService.get', refund_payload_response) + monkeypatch.setattr("pay_api.services.oauth_service.OAuthService.get", refund_payload_response) DistributionTask.update_failed_distributions() assert invoice.invoice_status_code == InvoiceStatus.REFUNDED.value @@ -68,29 +90,43 @@ def test_update_failed_distributions_refunds(session, monkeypatch): def test_update_failed_distribution_payments(session, monkeypatch): """Test failed distribution payments.""" - invoice = factory_invoice(factory_create_direct_pay_account( - ), status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value) + invoice = factory_invoice( + factory_create_direct_pay_account(), + status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value, + ) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value) - service_fee_distribution = factory_distribution(name='VS Service Fee', client='112') + service_fee_distribution = factory_distribution(name="VS Service Fee", client="112") fee_distribution = factory_distribution( - 'Super Dist', service_fee_dist_id=service_fee_distribution.disbursement_distribution_code_id, client='112') + "Super Dist", + service_fee_dist_id=service_fee_distribution.disbursement_distribution_code_id, + client="112", + ) - corp_type: CorpTypeModel = CorpTypeModel.find_by_code('VS') - fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, 'WILLNOTICE') + corp_type: CorpTypeModel = CorpTypeModel.find_by_code("VS") + fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, "WILLNOTICE") factory_distribution_link(fee_distribution.distribution_code_id, fee_schedule.fee_schedule_id) - factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id, filing_fees=30, - total=31.5, service_fees=1.5, fee_dist_id=fee_distribution.distribution_code_id) + factory_payment_line_item( + invoice.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=30, + total=31.5, + service_fees=1.5, + fee_dist_id=fee_distribution.distribution_code_id, + ) - factory_payment('PAYBC', 'DIRECT_PAY', invoice_number=invoice.id) + factory_payment("PAYBC", "DIRECT_PAY", invoice_number=invoice.id) factory_refund_invoice(invoice.id) # Required, because we're mocking out the POST below (This uses the OAuthService POST). - monkeypatch.setattr('pay_api.services.direct_pay_service.DirectPayService.get_token', paybc_token_response) + monkeypatch.setattr( + "pay_api.services.direct_pay_service.DirectPayService.get_token", + paybc_token_response, + ) # Mock POST until obtain OAS spec from PayBC for updating GL. - monkeypatch.setattr('pay_api.services.oauth_service.OAuthService.post', lambda *args, **kwargs: None) + monkeypatch.setattr("pay_api.services.oauth_service.OAuthService.post", lambda *args, **kwargs: None) DistributionTask.update_failed_distributions() assert invoice.invoice_status_code == InvoiceStatus.PAID.value @@ -98,16 +134,21 @@ def test_update_failed_distribution_payments(session, monkeypatch): def test_non_direct_pay_invoices(session, monkeypatch): """Test non DIRECT_PAY invoices.""" - invoice = factory_invoice(factory_create_ejv_account(), status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value) + invoice = factory_invoice( + factory_create_ejv_account(), + status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value, + ) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value) - factory_payment('PAYBC', 'EJV', invoice_number=invoice.id) + factory_payment("PAYBC", "EJV", invoice_number=invoice.id) DistributionTask.update_failed_distributions() assert invoice.invoice_status_code == InvoiceStatus.PAID.value - invoice = factory_invoice(factory_create_ejv_account( - ), status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value) + invoice = factory_invoice( + factory_create_ejv_account(), + status_code=InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value, + ) factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value) - factory_payment('PAYBC', 'EJV', invoice_number=invoice.id) + factory_payment("PAYBC", "EJV", invoice_number=invoice.id) factory_refund_invoice(invoice.id) DistributionTask.update_failed_distributions() assert invoice.invoice_status_code == InvoiceStatus.REFUNDED.value @@ -116,5 +157,5 @@ def test_non_direct_pay_invoices(session, monkeypatch): def test_no_response_pay_bc(session, monkeypatch): """Test no response from PayBC.""" invoice = factory_invoice(factory_create_direct_pay_account(), status_code=InvoiceStatus.PAID.value) - monkeypatch.setattr('pay_api.services.oauth_service.OAuthService.get', empty_refund_payload_response) + monkeypatch.setattr("pay_api.services.oauth_service.OAuthService.get", empty_refund_payload_response) assert invoice.invoice_status_code == InvoiceStatus.PAID.value diff --git a/jobs/payment-jobs/tests/jobs/test_eft_overpayment_notification_task.py b/jobs/payment-jobs/tests/jobs/test_eft_overpayment_notification_task.py index 674a1d138..47d45bfd0 100644 --- a/jobs/payment-jobs/tests/jobs/test_eft_overpayment_notification_task.py +++ b/jobs/payment-jobs/tests/jobs/test_eft_overpayment_notification_task.py @@ -26,15 +26,19 @@ from tasks.eft_overpayment_notification_task import EFTOverpaymentNotificationTask from .factory import ( - factory_create_eft_credit, factory_create_eft_file, factory_create_eft_shortname, factory_create_eft_transaction, - factory_eft_shortname_link) + factory_create_eft_credit, + factory_create_eft_file, + factory_create_eft_shortname, + factory_create_eft_transaction, + factory_eft_shortname_link, +) def create_unlinked_short_names_data(created_on: datetime): """Create seed data for unlinked short names.""" eft_file = factory_create_eft_file() eft_transaction = factory_create_eft_transaction(file_id=eft_file.id) - unlinked_with_credit = factory_create_eft_shortname('UNLINKED_WITH_CREDIT') + unlinked_with_credit = factory_create_eft_shortname("UNLINKED_WITH_CREDIT") unlinked_with_credit.created_on = created_on unlinked_with_credit.save() factory_create_eft_credit( @@ -42,10 +46,10 @@ def create_unlinked_short_names_data(created_on: datetime): eft_transaction_id=eft_transaction.id, eft_file_id=eft_file.id, amount=100, - remaining_amount=100 + remaining_amount=100, ) - inactive_link_with_credit = factory_create_eft_shortname('INACTIVE_LINK_WITH_CREDIT') + inactive_link_with_credit = factory_create_eft_shortname("INACTIVE_LINK_WITH_CREDIT") inactive_link_with_credit.created_on = created_on inactive_link_with_credit.save() factory_create_eft_credit( @@ -53,13 +57,15 @@ def create_unlinked_short_names_data(created_on: datetime): eft_transaction_id=eft_transaction.id, eft_file_id=eft_file.id, amount=10, - remaining_amount=10 + remaining_amount=10, + ) + factory_eft_shortname_link( + short_name_id=inactive_link_with_credit.id, + status_code=EFTShortnameStatus.INACTIVE.value, ) - factory_eft_shortname_link(short_name_id=inactive_link_with_credit.id, - status_code=EFTShortnameStatus.INACTIVE.value) # Create unlinked short name that is not 30 days old yet - unlinked_not_included = factory_create_eft_shortname('UNLINKED_NOT_INCLUDED') + unlinked_not_included = factory_create_eft_shortname("UNLINKED_NOT_INCLUDED") unlinked_not_included.created_on = created_on + timedelta(days=1) unlinked_not_included.save() factory_create_eft_credit( @@ -67,7 +73,7 @@ def create_unlinked_short_names_data(created_on: datetime): eft_transaction_id=eft_transaction.id, eft_file_id=eft_file.id, amount=100, - remaining_amount=100 + remaining_amount=100, ) return unlinked_with_credit, inactive_link_with_credit, unlinked_not_included @@ -77,25 +83,25 @@ def create_linked_short_names_data(created_on: datetime): """Create seed data for linked short names.""" eft_file = factory_create_eft_file() eft_transaction = factory_create_eft_transaction(file_id=eft_file.id) - linked_with_credit = factory_create_eft_shortname('LINKED_WITH_CREDIT') + linked_with_credit = factory_create_eft_shortname("LINKED_WITH_CREDIT") credit_1 = factory_create_eft_credit( short_name_id=linked_with_credit.id, eft_transaction_id=eft_transaction.id, eft_file_id=eft_file.id, amount=100, - remaining_amount=100 + remaining_amount=100, ) credit_1.created_on = created_on credit_1.save() factory_eft_shortname_link(short_name_id=linked_with_credit.id) - linked_with_no_credit = factory_create_eft_shortname('LINKED_WITH_NO_CREDIT') + linked_with_no_credit = factory_create_eft_shortname("LINKED_WITH_NO_CREDIT") credit_2 = factory_create_eft_credit( short_name_id=linked_with_no_credit.id, eft_transaction_id=eft_transaction.id, eft_file_id=eft_file.id, amount=100, - remaining_amount=0 + remaining_amount=0, ) credit_2.created_on = created_on credit_2.save() @@ -105,50 +111,66 @@ def create_linked_short_names_data(created_on: datetime): def test_over_payment_notification_not_sent(session): """Assert notification is not being sent.""" - with patch('tasks.eft_overpayment_notification_task.send_email') as mock_mailer: + with patch("tasks.eft_overpayment_notification_task.send_email") as mock_mailer: EFTOverpaymentNotificationTask.process_overpayment_notification() mock_mailer.assert_not_called() -@pytest.mark.parametrize('test_name, created_on_date, execution_date, assert_calls_override', [ - ('has-notifications', datetime(2024, 10, 1, 5), - datetime(2024, 10, 2, 5), []), - ('no-notifications', datetime(2024, 10, 1, 5), - datetime(2024, 10, 31, 5), None) -]) -def test_over_payment_notification_unlinked(session, test_name, created_on_date, execution_date, assert_calls_override, - emails_with_keycloak_role_mock, send_email_mock): +@pytest.mark.parametrize( + "test_name, created_on_date, execution_date, assert_calls_override", + [ + ("has-notifications", datetime(2024, 10, 1, 5), datetime(2024, 10, 2, 5), []), + ("no-notifications", datetime(2024, 10, 1, 5), datetime(2024, 10, 31, 5), None), + ], +) +def test_over_payment_notification_unlinked( + session, + test_name, + created_on_date, + execution_date, + assert_calls_override, + emails_with_keycloak_role_mock, + send_email_mock, +): """Assert notification is being sent for unlinked accounts.""" - unlinked_shortname, inactive_link_shortname, _ = create_unlinked_short_names_data( - created_on_date) - expected_email = 'test@email.com' - expected_subject = 'Pending Unsettled Amount for Short Name' + unlinked_shortname, inactive_link_shortname, _ = create_unlinked_short_names_data(created_on_date) + expected_email = "test@email.com" + expected_subject = "Pending Unsettled Amount for Short Name" with freeze_time(execution_date): EFTOverpaymentNotificationTask.process_overpayment_notification() - expected_calls = [ - call(recipients=expected_email, - subject=f'{expected_subject} {unlinked_shortname.short_name}', - body=ANY), - call(recipients=expected_email, - subject=f'{expected_subject} {inactive_link_shortname.short_name}', - body=ANY), - ] if assert_calls_override is None else [] + expected_calls = ( + [ + call( + recipients=expected_email, + subject=f"{expected_subject} {unlinked_shortname.short_name}", + body=ANY, + ), + call( + recipients=expected_email, + subject=f"{expected_subject} {inactive_link_shortname.short_name}", + body=ANY, + ), + ] + if assert_calls_override is None + else [] + ) send_email_mock.assert_has_calls(expected_calls, any_order=True) assert send_email_mock.call_count == len(expected_calls) def test_over_payment_notification_linked(session, emails_with_keycloak_role_mock, send_email_mock): """Assert notification is being sent for linked accounts.""" - linked_shortname, _ = create_linked_short_names_data( - datetime(2024, 10, 1, 5)) - expected_email = 'test@email.com' - expected_subject = 'Pending Unsettled Amount for Short Name' + linked_shortname, _ = create_linked_short_names_data(datetime(2024, 10, 1, 5)) + expected_email = "test@email.com" + expected_subject = "Pending Unsettled Amount for Short Name" with freeze_time(datetime(2024, 10, 1, 7)): EFTOverpaymentNotificationTask.process_overpayment_notification() expected_calls = [ - call(recipients=expected_email, - subject=f'{expected_subject} {linked_shortname.short_name}', - body=ANY), + call( + recipients=expected_email, + subject=f"{expected_subject} {linked_shortname.short_name}", + body=ANY, + ), ] send_email_mock.assert_has_calls(expected_calls, any_order=True) assert send_email_mock.call_count == len(expected_calls) @@ -156,15 +178,16 @@ def test_over_payment_notification_linked(session, emails_with_keycloak_role_moc def test_over_payment_notification_override(session, emails_with_keycloak_role_mock, send_email_mock): """Assert notification is being sent with date override.""" - linked_shortname, _ = create_linked_short_names_data( - datetime(2024, 10, 1, 5)) - expected_email = 'test@email.com' - expected_subject = 'Pending Unsettled Amount for Short Name' - EFTOverpaymentNotificationTask.process_overpayment_notification(date_override='2024-10-01') + linked_shortname, _ = create_linked_short_names_data(datetime(2024, 10, 1, 5)) + expected_email = "test@email.com" + expected_subject = "Pending Unsettled Amount for Short Name" + EFTOverpaymentNotificationTask.process_overpayment_notification(date_override="2024-10-01") expected_calls = [ - call(recipients=expected_email, - subject=f'{expected_subject} {linked_shortname.short_name}', - body=ANY), + call( + recipients=expected_email, + subject=f"{expected_subject} {linked_shortname.short_name}", + body=ANY, + ), ] send_email_mock.assert_has_calls(expected_calls, any_order=True) assert send_email_mock.call_count == len(expected_calls) diff --git a/jobs/payment-jobs/tests/jobs/test_eft_statement_due_task.py b/jobs/payment-jobs/tests/jobs/test_eft_statement_due_task.py index f6a356a36..f0669757d 100644 --- a/jobs/payment-jobs/tests/jobs/test_eft_statement_due_task.py +++ b/jobs/payment-jobs/tests/jobs/test_eft_statement_due_task.py @@ -18,10 +18,10 @@ """ import decimal from datetime import datetime -from dateutil.relativedelta import relativedelta from unittest.mock import patch import pytest +from dateutil.relativedelta import relativedelta from faker import Faker from flask import Flask from freezegun import freeze_time @@ -32,15 +32,18 @@ from pay_api.utils.util import current_local_time import config -from tasks.statement_task import StatementTask from tasks.eft_statement_due_task import EFTStatementDueTask +from tasks.statement_task import StatementTask from utils.enums import StatementNotificationAction from utils.mailer import StatementNotificationInfo from .factory import ( - factory_create_account, factory_invoice, factory_invoice_reference, factory_statement_recipient, - factory_statement_settings) - + factory_create_account, + factory_invoice, + factory_invoice_reference, + factory_statement_recipient, + factory_statement_settings, +) fake = Faker() app = None @@ -52,29 +55,37 @@ def setup(): """Initialize app with test env for testing.""" global app app = Flask(__name__) - app.env = 'testing' - app.config.from_object(config.CONFIGURATION['testing']) + app.env = "testing" + app.config.from_object(config.CONFIGURATION["testing"]) -def create_test_data(payment_method_code: str, payment_date: datetime, - statement_frequency: str, invoice_total: decimal = 0.00, - invoice_paid: decimal = 0.00): +def create_test_data( + payment_method_code: str, + payment_date: datetime, + statement_frequency: str, + invoice_total: decimal = 0.00, + invoice_paid: decimal = 0.00, +): """Create seed data for tests.""" - account = factory_create_account(auth_account_id='1', payment_method_code=payment_method_code) - invoice = factory_invoice(payment_account=account, created_on=payment_date, - payment_method_code=payment_method_code, status_code=InvoiceStatus.APPROVED.value, - total=invoice_total) + account = factory_create_account(auth_account_id="1", payment_method_code=payment_method_code) + invoice = factory_invoice( + payment_account=account, + created_on=payment_date, + payment_method_code=payment_method_code, + status_code=InvoiceStatus.APPROVED.value, + total=invoice_total, + ) inv_ref = factory_invoice_reference(invoice_id=invoice.id) - statement_recipient = factory_statement_recipient(auth_user_id=account.auth_account_id, - first_name=fake.first_name(), - last_name=fake.last_name(), - email=fake.email(), - payment_account_id=account.id) + statement_recipient = factory_statement_recipient( + auth_user_id=account.auth_account_id, + first_name=fake.first_name(), + last_name=fake.last_name(), + email=fake.email(), + payment_account_id=account.id, + ) statement_settings = factory_statement_settings( - pay_account_id=account.id, - from_date=payment_date, - frequency=statement_frequency + pay_account_id=account.id, from_date=payment_date, frequency=statement_frequency ) return account, invoice, inv_ref, statement_recipient, statement_settings @@ -85,18 +96,22 @@ def create_test_data(payment_method_code: str, payment_date: datetime, # 3. 7 day reminder Feb 21th (due date - 7) # 4. Final reminder Feb 28th (due date client should be told to pay by this time) # 5. Overdue Date and account locked March 15th -@pytest.mark.parametrize('test_name, action_on, action', [ - ('reminder', datetime(2023, 2, 21, 8), StatementNotificationAction.REMINDER), - ('due', datetime(2023, 2, 28, 8), StatementNotificationAction.DUE), - ('overdue', datetime(2023, 3, 15, 8), StatementNotificationAction.OVERDUE) -]) +@pytest.mark.parametrize( + "test_name, action_on, action", + [ + ("reminder", datetime(2023, 2, 21, 8), StatementNotificationAction.REMINDER), + ("due", datetime(2023, 2, 28, 8), StatementNotificationAction.DUE), + ("overdue", datetime(2023, 3, 15, 8), StatementNotificationAction.OVERDUE), + ], +) def test_send_unpaid_statement_notification(setup, session, test_name, action_on, action): """Assert payment reminder event is being sent.""" - account, invoice, _, \ - statement_recipient, _ = create_test_data(PaymentMethod.EFT.value, - datetime(2023, 1, 1, 8), # Hour 0 doesnt work for CI - StatementFrequency.MONTHLY.value, - 351.50) + account, invoice, _, statement_recipient, _ = create_test_data( + PaymentMethod.EFT.value, + datetime(2023, 1, 1, 8), # Hour 0 doesnt work for CI + StatementFrequency.MONTHLY.value, + 351.50, + ) assert invoice.payment_method_code == PaymentMethod.EFT.value assert invoice.overdue_date assert account.payment_method == PaymentMethod.EFT.value @@ -114,10 +129,10 @@ def test_send_unpaid_statement_notification(setup, session, test_name, action_on assert invoices[0].invoice_id == invoice.id summary = StatementService.get_summary(account.auth_account_id, statements[0][0].id) - total_amount_owing = summary['total_due'] + total_amount_owing = summary["total_due"] - with patch('utils.auth_event.AuthEvent.publish_lock_account_event') as mock_auth_event: - with patch('tasks.eft_statement_due_task.publish_payment_notification') as mock_mailer: + with patch("utils.auth_event.AuthEvent.publish_lock_account_event") as mock_auth_event: + with patch("tasks.eft_statement_due_task.publish_payment_notification") as mock_mailer: with freeze_time(action_on): # Statement due task looks at the month before. EFTStatementDueTask.process_unpaid_statements(statement_date_override=datetime(2023, 2, 1, 0)) @@ -128,19 +143,23 @@ def test_send_unpaid_statement_notification(setup, session, test_name, action_on assert account.has_overdue_invoices else: due_date = statements[0][0].to_date + relativedelta(months=1) - mock_mailer.assert_called_with(StatementNotificationInfo(auth_account_id=account.auth_account_id, - statement=statements[0][0], - action=action, - due_date=due_date, - emails=statement_recipient.email, - total_amount_owing=total_amount_owing, - short_name_links_count=0)) + mock_mailer.assert_called_with( + StatementNotificationInfo( + auth_account_id=account.auth_account_id, + statement=statements[0][0], + action=action, + due_date=due_date, + emails=statement_recipient.email, + total_amount_owing=total_amount_owing, + short_name_links_count=0, + ) + ) def test_unpaid_statement_notification_not_sent(setup, session): """Assert payment reminder event is not being sent.""" # Assert notification was published to the mailer queue - with patch('tasks.eft_statement_due_task.publish_payment_notification') as mock_mailer: + with patch("tasks.eft_statement_due_task.publish_payment_notification") as mock_mailer: # Freeze time to 10th of the month - should not trigger any notification with freeze_time(current_local_time().replace(day=10)): EFTStatementDueTask.process_unpaid_statements() @@ -150,17 +169,19 @@ def test_unpaid_statement_notification_not_sent(setup, session): def test_overdue_invoices_updated(setup, session): """Assert invoices are transitioned to overdue status.""" invoice_date = current_local_time() - relativedelta(months=2, days=15) - account, invoice, _, \ - _, _ = create_test_data(PaymentMethod.EFT.value, - invoice_date, - StatementFrequency.MONTHLY.value, - 351.50) + account, invoice, _, _, _ = create_test_data( + PaymentMethod.EFT.value, invoice_date, StatementFrequency.MONTHLY.value, 351.50 + ) assert invoice.payment_method_code == PaymentMethod.EFT.value assert invoice.invoice_status_code == InvoiceStatus.APPROVED.value - invoice2 = factory_invoice(payment_account=account, created_on=current_local_time().date() + relativedelta(hours=1), - payment_method_code=PaymentMethod.EFT.value, status_code=InvoiceStatus.APPROVED.value, - total=10.50) + invoice2 = factory_invoice( + payment_account=account, + created_on=current_local_time().date() + relativedelta(hours=1), + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.APPROVED.value, + total=10.50, + ) assert invoice2.payment_method_code == PaymentMethod.EFT.value assert invoice2.invoice_status_code == InvoiceStatus.APPROVED.value @@ -171,18 +192,22 @@ def test_overdue_invoices_updated(setup, session): assert invoice2.invoice_status_code == InvoiceStatus.APPROVED.value -@pytest.mark.parametrize('test_name, date_override, action', [ - ('reminder', '2023-02-21', StatementNotificationAction.REMINDER), - ('due', '2023-02-28', StatementNotificationAction.DUE), - ('overdue', '2023-03-15', StatementNotificationAction.OVERDUE) -]) +@pytest.mark.parametrize( + "test_name, date_override, action", + [ + ("reminder", "2023-02-21", StatementNotificationAction.REMINDER), + ("due", "2023-02-28", StatementNotificationAction.DUE), + ("overdue", "2023-03-15", StatementNotificationAction.OVERDUE), + ], +) def test_statement_due_overrides(setup, session, test_name, date_override, action): """Assert payment reminder event is being sent.""" - account, invoice, _, \ - statement_recipient, _ = create_test_data(PaymentMethod.EFT.value, - datetime(2023, 1, 1, 8), # Hour 0 doesnt work for CI - StatementFrequency.MONTHLY.value, - 351.50) + account, invoice, _, statement_recipient, _ = create_test_data( + PaymentMethod.EFT.value, + datetime(2023, 1, 1, 8), # Hour 0 doesnt work for CI + StatementFrequency.MONTHLY.value, + 351.50, + ) assert invoice.payment_method_code == PaymentMethod.EFT.value assert invoice.overdue_date assert account.payment_method == PaymentMethod.EFT.value @@ -200,17 +225,15 @@ def test_statement_due_overrides(setup, session, test_name, date_override, actio assert invoices[0].invoice_id == invoice.id summary = StatementService.get_summary(account.auth_account_id, statements[0][0].id) - total_amount_owing = summary['total_due'] + total_amount_owing = summary["total_due"] - with patch('utils.auth_event.AuthEvent.publish_lock_account_event') as mock_auth_event: - with patch('tasks.eft_statement_due_task.publish_payment_notification') as mock_mailer: + with patch("utils.auth_event.AuthEvent.publish_lock_account_event") as mock_auth_event: + with patch("tasks.eft_statement_due_task.publish_payment_notification") as mock_mailer: # Statement due task looks at the month before. - if test_name == 'overdue': - EFTStatementDueTask.process_unpaid_statements(action_override='OVERDUE', - date_override=date_override) + if test_name == "overdue": + EFTStatementDueTask.process_unpaid_statements(action_override="OVERDUE", date_override=date_override) - EFTStatementDueTask.process_unpaid_statements(action_override='NOTIFICATION', - date_override=date_override) + EFTStatementDueTask.process_unpaid_statements(action_override="NOTIFICATION", date_override=date_override) if action == StatementNotificationAction.OVERDUE: mock_auth_event.assert_called() assert statements[0][0].overdue_notification_date @@ -218,10 +241,14 @@ def test_statement_due_overrides(setup, session, test_name, date_override, actio assert account.has_overdue_invoices else: due_date = statements[0][0].to_date + relativedelta(months=1) - mock_mailer.assert_called_with(StatementNotificationInfo(auth_account_id=account.auth_account_id, - statement=statements[0][0], - action=action, - due_date=due_date, - emails=statement_recipient.email, - total_amount_owing=total_amount_owing, - short_name_links_count=0)) + mock_mailer.assert_called_with( + StatementNotificationInfo( + auth_account_id=account.auth_account_id, + statement=statements[0][0], + action=action, + due_date=due_date, + emails=statement_recipient.email, + total_amount_owing=total_amount_owing, + short_name_links_count=0, + ) + ) diff --git a/jobs/payment-jobs/tests/jobs/test_eft_task.py b/jobs/payment-jobs/tests/jobs/test_eft_task.py index 98a6711a3..bdf31d619 100644 --- a/jobs/payment-jobs/tests/jobs/test_eft_task.py +++ b/jobs/payment-jobs/tests/jobs/test_eft_task.py @@ -20,116 +20,245 @@ from unittest.mock import patch import pytest - from pay_api.models import CfsAccount as CfsAccountModel from pay_api.models import FeeSchedule as FeeScheduleModel from pay_api.models import Receipt as ReceiptModel from pay_api.utils.enums import ( - CfsAccountStatus, DisbursementStatus, EFTCreditInvoiceStatus, EFTHistoricalTypes, InvoiceReferenceStatus, - InvoiceStatus, PaymentMethod) + CfsAccountStatus, + DisbursementStatus, + EFTCreditInvoiceStatus, + EFTHistoricalTypes, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, +) from tasks.eft_task import EFTTask from .factory import ( - factory_create_eft_account, factory_create_eft_credit, factory_create_eft_credit_invoice_link, - factory_create_eft_file, factory_create_eft_shortname, factory_create_eft_shortname_historical, - factory_create_eft_transaction, factory_invoice, factory_invoice_reference, factory_payment, - factory_payment_line_item, factory_receipt) + factory_create_eft_account, + factory_create_eft_credit, + factory_create_eft_credit_invoice_link, + factory_create_eft_file, + factory_create_eft_shortname, + factory_create_eft_shortname_historical, + factory_create_eft_transaction, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_payment_line_item, + factory_receipt, +) def setup_eft_credit_invoice_links_test(): """Initiate test data.""" - auth_account_id = '1234' + auth_account_id = "1234" eft_file = factory_create_eft_file() eft_transaction_id = factory_create_eft_transaction(file_id=eft_file.id).id - short_name_id = factory_create_eft_shortname('heyhey').id + short_name_id = factory_create_eft_shortname("heyhey").id return auth_account_id, eft_file, short_name_id, eft_transaction_id tests = [ - ('invoice_refund_flow', PaymentMethod.EFT.value, [InvoiceStatus.REFUND_REQUESTED.value], - [EFTCreditInvoiceStatus.CANCELLED.value], [None], 0, 0), - ('insufficient_amount_on_links', PaymentMethod.EFT.value, [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], - [EFTCreditInvoiceStatus.PENDING.value, EFTCreditInvoiceStatus.PENDING_REFUND.value], [None], 0, 0), - ('happy_flow_multiple_links', PaymentMethod.EFT.value, [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], - [EFTCreditInvoiceStatus.PENDING.value, EFTCreditInvoiceStatus.PENDING_REFUND.value], - [None, DisbursementStatus.COMPLETED.value], 1, 2), - ('happy_flow', PaymentMethod.EFT.value, [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], - [EFTCreditInvoiceStatus.PENDING.value, - EFTCreditInvoiceStatus.PENDING_REFUND.value], [None, DisbursementStatus.COMPLETED.value], 1, 2), - ('duplicate_active_cfs_account', PaymentMethod.EFT.value, [ - InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], [EFTCreditInvoiceStatus.PENDING.value, - EFTCreditInvoiceStatus.PENDING_REFUND.value], - [None], 1, 1), - ('no_cfs_active', PaymentMethod.EFT.value, [ - InvoiceStatus.APPROVED.value], [EFTCreditInvoiceStatus.PENDING.value], [None], 0, 0), - ('wrong_payment_method', PaymentMethod.PAD.value, [ - InvoiceStatus.CREATED.value], [EFTCreditInvoiceStatus.PENDING.value], [None], 0, 0), - ('credit_invoice_link_status_incorrect', PaymentMethod.EFT.value, [ - InvoiceStatus.APPROVED.value], [EFTCreditInvoiceStatus.COMPLETED.value, EFTCreditInvoiceStatus.REFUNDED.value], - [None], 0, 0), - ('wrong_disbursement', PaymentMethod.EFT.value, [ - InvoiceStatus.APPROVED.value], [EFTCreditInvoiceStatus.PENDING.value], [DisbursementStatus.UPLOADED.value], 0, 0), - ('wrong_invoice_status', PaymentMethod.EFT.value, [ - InvoiceStatus.CREDITED.value, InvoiceStatus.PARTIAL.value, InvoiceStatus.CREATED.value], - [EFTCreditInvoiceStatus.PENDING.value], [None], 0, 0), - ('no_invoice_reference', PaymentMethod.EFT.value, [ - InvoiceStatus.APPROVED.value], [EFTCreditInvoiceStatus.PENDING.value], [None], 0, 0), + ( + "invoice_refund_flow", + PaymentMethod.EFT.value, + [InvoiceStatus.REFUND_REQUESTED.value], + [EFTCreditInvoiceStatus.CANCELLED.value], + [None], + 0, + 0, + ), + ( + "insufficient_amount_on_links", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], + [ + EFTCreditInvoiceStatus.PENDING.value, + EFTCreditInvoiceStatus.PENDING_REFUND.value, + ], + [None], + 0, + 0, + ), + ( + "happy_flow_multiple_links", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], + [ + EFTCreditInvoiceStatus.PENDING.value, + EFTCreditInvoiceStatus.PENDING_REFUND.value, + ], + [None, DisbursementStatus.COMPLETED.value], + 1, + 2, + ), + ( + "happy_flow", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], + [ + EFTCreditInvoiceStatus.PENDING.value, + EFTCreditInvoiceStatus.PENDING_REFUND.value, + ], + [None, DisbursementStatus.COMPLETED.value], + 1, + 2, + ), + ( + "duplicate_active_cfs_account", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value, InvoiceStatus.PAID.value], + [ + EFTCreditInvoiceStatus.PENDING.value, + EFTCreditInvoiceStatus.PENDING_REFUND.value, + ], + [None], + 1, + 1, + ), + ( + "no_cfs_active", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value], + [EFTCreditInvoiceStatus.PENDING.value], + [None], + 0, + 0, + ), + ( + "wrong_payment_method", + PaymentMethod.PAD.value, + [InvoiceStatus.CREATED.value], + [EFTCreditInvoiceStatus.PENDING.value], + [None], + 0, + 0, + ), + ( + "credit_invoice_link_status_incorrect", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value], + [EFTCreditInvoiceStatus.COMPLETED.value, EFTCreditInvoiceStatus.REFUNDED.value], + [None], + 0, + 0, + ), + ( + "wrong_disbursement", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value], + [EFTCreditInvoiceStatus.PENDING.value], + [DisbursementStatus.UPLOADED.value], + 0, + 0, + ), + ( + "wrong_invoice_status", + PaymentMethod.EFT.value, + [ + InvoiceStatus.CREDITED.value, + InvoiceStatus.PARTIAL.value, + InvoiceStatus.CREATED.value, + ], + [EFTCreditInvoiceStatus.PENDING.value], + [None], + 0, + 0, + ), + ( + "no_invoice_reference", + PaymentMethod.EFT.value, + [InvoiceStatus.APPROVED.value], + [EFTCreditInvoiceStatus.PENDING.value], + [None], + 0, + 0, + ), ] -@pytest.mark.parametrize('test_name, payment_method, invoice_status_codes, eft_credit_invoice_statuses,' + - 'disbursement_status_codes, pending_count, pending_refund_count', tests) -def test_eft_credit_invoice_links_by_status(session, test_name, payment_method, invoice_status_codes, - eft_credit_invoice_statuses, disbursement_status_codes, - pending_count, pending_refund_count): +@pytest.mark.parametrize( + "test_name, payment_method, invoice_status_codes, eft_credit_invoice_statuses," + + "disbursement_status_codes, pending_count, pending_refund_count", + tests, +) +def test_eft_credit_invoice_links_by_status( + session, + test_name, + payment_method, + invoice_status_codes, + eft_credit_invoice_statuses, + disbursement_status_codes, + pending_count, + pending_refund_count, +): """Tests multiple scenarios for EFT credit invoice links.""" auth_account_id, eft_file, short_name_id, eft_transaction_id = setup_eft_credit_invoice_links_test() payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value) max_cfs_account_id = 0 match test_name: - case 'duplicate_active_cfs_account': - max_cfs_account_id = CfsAccountModel(status=CfsAccountStatus.ACTIVE.value, - account_id=payment_account.id, - payment_method=PaymentMethod.EFT.value).save().id - case 'no_cfs_active': + case "duplicate_active_cfs_account": + max_cfs_account_id = ( + CfsAccountModel( + status=CfsAccountStatus.ACTIVE.value, + account_id=payment_account.id, + payment_method=PaymentMethod.EFT.value, + ) + .save() + .id + ) + case "no_cfs_active": CfsAccountModel.find_by_account_id(payment_account.id)[0].status = CfsAccountStatus.INACTIVE.value eft_credit = factory_create_eft_credit( - amount=100, remaining_amount=0, eft_file_id=eft_file.id, short_name_id=short_name_id, - eft_transaction_id=eft_transaction_id) + amount=100, + remaining_amount=0, + eft_file_id=eft_file.id, + short_name_id=short_name_id, + eft_transaction_id=eft_transaction_id, + ) for invoice_status in invoice_status_codes: for disbursement_status in disbursement_status_codes: for eft_credit_invoice_status in eft_credit_invoice_statuses: - invoice = factory_invoice(payment_account=payment_account, - payment_method_code=payment_method, - status_code=invoice_status, - disbursement_status_code=disbursement_status) - if test_name != 'no_invoice_reference': + invoice = factory_invoice( + payment_account=payment_account, + payment_method_code=payment_method, + status_code=invoice_status, + disbursement_status_code=disbursement_status, + ) + if test_name != "no_invoice_reference": factory_invoice_reference(invoice_id=invoice.id) match test_name: - case 'happy_flow_multiple_links': + case "happy_flow_multiple_links": factory_create_eft_credit_invoice_link( - invoice_id=invoice.id, - eft_credit_id=eft_credit.id, - status_code=eft_credit_invoice_status, - amount=invoice.total / 2) + invoice_id=invoice.id, + eft_credit_id=eft_credit.id, + status_code=eft_credit_invoice_status, + amount=invoice.total / 2, + ) factory_create_eft_credit_invoice_link( - invoice_id=invoice.id, - eft_credit_id=eft_credit.id, - status_code=eft_credit_invoice_status, - amount=invoice.total / 2) - case 'insufficient_amount_on_links': + invoice_id=invoice.id, + eft_credit_id=eft_credit.id, + status_code=eft_credit_invoice_status, + amount=invoice.total / 2, + ) + case "insufficient_amount_on_links": factory_create_eft_credit_invoice_link( - invoice_id=invoice.id, - eft_credit_id=eft_credit.id, - status_code=eft_credit_invoice_status, - amount=invoice.total - 1) + invoice_id=invoice.id, + eft_credit_id=eft_credit.id, + status_code=eft_credit_invoice_status, + amount=invoice.total - 1, + ) case _: factory_create_eft_credit_invoice_link( - invoice_id=invoice.id, eft_credit_id=eft_credit.id, status_code=eft_credit_invoice_status, - amount=invoice.total) + invoice_id=invoice.id, + eft_credit_id=eft_credit.id, + status_code=eft_credit_invoice_status, + amount=invoice.total, + ) results = EFTTask.get_eft_credit_invoice_links_by_status(EFTCreditInvoiceStatus.PENDING.value) if max_cfs_account_id: @@ -138,77 +267,97 @@ def test_eft_credit_invoice_links_by_status(session, test_name, payment_method, assert len(results) == pending_count results = EFTTask.get_eft_credit_invoice_links_by_status(EFTCreditInvoiceStatus.PENDING_REFUND.value) assert len(results) == pending_refund_count - if test_name == 'invoice_refund_flow': + if test_name == "invoice_refund_flow": results = EFTTask.get_eft_credit_invoice_links_by_status(EFTCreditInvoiceStatus.CANCELLED.value) assert len(results) == 1 -@pytest.mark.parametrize('test_name', ('happy_path', 'consolidated_happy', 'consolidated_mismatch', - 'normal_invoice_missing')) +@pytest.mark.parametrize( + "test_name", + ( + "happy_path", + "consolidated_happy", + "consolidated_mismatch", + "normal_invoice_missing", + ), +) def test_link_electronic_funds_transfers(session, test_name): """Test link electronic funds transfers.""" auth_account_id, eft_file, short_name_id, eft_transaction_id = setup_eft_credit_invoice_links_test() payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value) - invoice = factory_invoice(payment_account=payment_account, payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.APPROVED.value, total=10) + invoice = factory_invoice( + payment_account=payment_account, + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.APPROVED.value, + total=10, + ) invoice_reference = factory_invoice_reference(invoice_id=invoice.id) - factory_payment(payment_account_id=payment_account.id, payment_method_code=PaymentMethod.EFT.value, - invoice_amount=351.50) + factory_payment( + payment_account_id=payment_account.id, + payment_method_code=PaymentMethod.EFT.value, + invoice_amount=351.50, + ) eft_credit = factory_create_eft_credit( - amount=100, remaining_amount=0, eft_file_id=eft_file.id, short_name_id=short_name_id, - eft_transaction_id=eft_transaction_id) - credit_invoice_link = factory_create_eft_credit_invoice_link(invoice_id=invoice.id, eft_credit_id=eft_credit.id, - link_group_id=1, amount=5) - credit_invoice_link2 = factory_create_eft_credit_invoice_link(invoice_id=invoice.id, eft_credit_id=eft_credit.id, - link_group_id=1, amount=5) + amount=100, + remaining_amount=0, + eft_file_id=eft_file.id, + short_name_id=short_name_id, + eft_transaction_id=eft_transaction_id, + ) + credit_invoice_link = factory_create_eft_credit_invoice_link( + invoice_id=invoice.id, eft_credit_id=eft_credit.id, link_group_id=1, amount=5 + ) + credit_invoice_link2 = factory_create_eft_credit_invoice_link( + invoice_id=invoice.id, eft_credit_id=eft_credit.id, link_group_id=1, amount=5 + ) eft_historical = factory_create_eft_shortname_historical( payment_account_id=payment_account.id, short_name_id=short_name_id, - related_group_link_id=1 + related_group_link_id=1, ) assert eft_historical.hidden assert eft_historical.is_processing - cfs_account = CfsAccountModel.find_effective_by_payment_method( - payment_account.id, PaymentMethod.EFT.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, PaymentMethod.EFT.value) return_value = {} original_invoice_reference = None match test_name: - case 'consolidated_happy' | 'consolidated_mismatch': + case "consolidated_happy" | "consolidated_mismatch": invoice_reference.is_consolidated = True invoice_reference.save() - original_invoice_reference = factory_invoice_reference(invoice_id=invoice.id, - is_consolidated=False, - status_code=InvoiceReferenceStatus.CANCELLED.value) \ - .save() - return_value = {'total': 10.00} - if test_name == 'consolidated_mismatch': - return_value = {'total': 10.01} - case 'normal_invoice_missing': + original_invoice_reference = factory_invoice_reference( + invoice_id=invoice.id, + is_consolidated=False, + status_code=InvoiceReferenceStatus.CANCELLED.value, + ).save() + return_value = {"total": 10.00} + if test_name == "consolidated_mismatch": + return_value = {"total": 10.01} + case "normal_invoice_missing": invoice_reference.is_consolidated = True invoice_reference.save() case _: pass - if test_name in ['consolidated_mismatch', 'normal_invoice_missing']: - with patch('pay_api.services.CFSService.get_invoice', return_value=return_value) as mock_get_invoice: + if test_name in ["consolidated_mismatch", "normal_invoice_missing"]: + with patch("pay_api.services.CFSService.get_invoice", return_value=return_value) as mock_get_invoice: EFTTask.link_electronic_funds_transfers_cfs() # No change, the amount didn't match or normal invoice was missing. assert invoice_reference.status_code == InvoiceReferenceStatus.ACTIVE.value return - with patch('pay_api.services.CFSService.reverse_invoice') as mock_reverse_invoice: - with patch('pay_api.services.CFSService.create_cfs_receipt') as mock_create_receipt: - with patch('pay_api.services.CFSService.get_invoice', return_value=return_value) as mock_get_invoice: + with patch("pay_api.services.CFSService.reverse_invoice") as mock_reverse_invoice: + with patch("pay_api.services.CFSService.create_cfs_receipt") as mock_create_receipt: + with patch("pay_api.services.CFSService.get_invoice", return_value=return_value) as mock_get_invoice: EFTTask.link_electronic_funds_transfers_cfs() - if test_name == 'consolidated_happy': + if test_name == "consolidated_happy": mock_reverse_invoice.assert_called() mock_get_invoice.assert_called() mock_create_receipt.assert_called() assert cfs_account.status == CfsAccountStatus.ACTIVE.value - if test_name == 'consolidated_happy': + if test_name == "consolidated_happy": assert invoice_reference.status_code == InvoiceReferenceStatus.CANCELLED.value assert original_invoice_reference.status_code == InvoiceReferenceStatus.COMPLETED.value else: @@ -230,73 +379,107 @@ def test_link_electronic_funds_transfers(session, test_name): def test_reverse_electronic_funds_transfers(session): """Test reverse electronic funds transfers.""" auth_account_id, eft_file, short_name_id, eft_transaction_id = setup_eft_credit_invoice_links_test() - receipt_number = '1111R' - invoice_number = '1234' + receipt_number = "1111R" + invoice_number = "1234" payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value) - invoice = factory_invoice(payment_account=payment_account, total=30, - status_code=InvoiceStatus.PAID.value, - payment_method_code=PaymentMethod.EFT.value) + invoice = factory_invoice( + payment_account=payment_account, + total=30, + status_code=InvoiceStatus.PAID.value, + payment_method_code=PaymentMethod.EFT.value, + ) - factory_payment(payment_account_id=payment_account.id, payment_method_code=PaymentMethod.EFT.value, - invoice_amount=351.50, invoice_number=invoice_number) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + factory_payment( + payment_account_id=payment_account.id, + payment_method_code=PaymentMethod.EFT.value, + invoice_amount=351.50, + invoice_number=invoice_number, + ) + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") factory_payment_line_item(invoice_id=invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) - invoice_reference = factory_invoice_reference(invoice_id=invoice.id, - status_code=InvoiceReferenceStatus.COMPLETED.value, - invoice_number=invoice_number) + invoice_reference = factory_invoice_reference( + invoice_id=invoice.id, + status_code=InvoiceReferenceStatus.COMPLETED.value, + invoice_number=invoice_number, + ) eft_credit = factory_create_eft_credit( - amount=100, remaining_amount=0, eft_file_id=eft_file.id, short_name_id=short_name_id, - eft_transaction_id=eft_transaction_id) - cil = factory_create_eft_credit_invoice_link(invoice_id=invoice.id, - status_code=EFTCreditInvoiceStatus.PENDING_REFUND.value, - eft_credit_id=eft_credit.id, - amount=30) + amount=100, + remaining_amount=0, + eft_file_id=eft_file.id, + short_name_id=short_name_id, + eft_transaction_id=eft_transaction_id, + ) + cil = factory_create_eft_credit_invoice_link( + invoice_id=invoice.id, + status_code=EFTCreditInvoiceStatus.PENDING_REFUND.value, + eft_credit_id=eft_credit.id, + amount=30, + ) factory_receipt(invoice.id, receipt_number) - refund_requested_invoice = factory_invoice(payment_account=payment_account, total=30, - status_code=InvoiceStatus.REFUND_REQUESTED.value, - payment_method_code=PaymentMethod.EFT.value) - - cil2 = factory_create_eft_credit_invoice_link(invoice_id=refund_requested_invoice.id, - status_code=EFTCreditInvoiceStatus.CANCELLED.value, - eft_credit_id=eft_credit.id, - amount=30) - invoice_reference2 = factory_invoice_reference(invoice_id=refund_requested_invoice.id, - status_code=InvoiceReferenceStatus.ACTIVE.value, - invoice_number=invoice_number) - refund_requested_invoice2 = factory_invoice(payment_account=payment_account, total=30, - status_code=InvoiceStatus.REFUND_REQUESTED.value, - payment_method_code=PaymentMethod.EFT.value) - - cil3 = factory_create_eft_credit_invoice_link(invoice_id=refund_requested_invoice2.id, - status_code=EFTCreditInvoiceStatus.PENDING_REFUND.value, - eft_credit_id=eft_credit.id, - amount=30) - invoice_reference3 = factory_invoice_reference(invoice_id=refund_requested_invoice2.id, - status_code=InvoiceReferenceStatus.COMPLETED.value, - invoice_number=invoice_number) - - refund_requested_no_ref = factory_invoice(payment_account=payment_account, total=30, - status_code=InvoiceStatus.REFUND_REQUESTED.value, - payment_method_code=PaymentMethod.EFT.value) - cil4 = factory_create_eft_credit_invoice_link(invoice_id=refund_requested_no_ref.id, - status_code=EFTCreditInvoiceStatus.CANCELLED.value, - eft_credit_id=eft_credit.id, - amount=30) + refund_requested_invoice = factory_invoice( + payment_account=payment_account, + total=30, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=PaymentMethod.EFT.value, + ) + + cil2 = factory_create_eft_credit_invoice_link( + invoice_id=refund_requested_invoice.id, + status_code=EFTCreditInvoiceStatus.CANCELLED.value, + eft_credit_id=eft_credit.id, + amount=30, + ) + invoice_reference2 = factory_invoice_reference( + invoice_id=refund_requested_invoice.id, + status_code=InvoiceReferenceStatus.ACTIVE.value, + invoice_number=invoice_number, + ) + refund_requested_invoice2 = factory_invoice( + payment_account=payment_account, + total=30, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=PaymentMethod.EFT.value, + ) + + cil3 = factory_create_eft_credit_invoice_link( + invoice_id=refund_requested_invoice2.id, + status_code=EFTCreditInvoiceStatus.PENDING_REFUND.value, + eft_credit_id=eft_credit.id, + amount=30, + ) + invoice_reference3 = factory_invoice_reference( + invoice_id=refund_requested_invoice2.id, + status_code=InvoiceReferenceStatus.COMPLETED.value, + invoice_number=invoice_number, + ) + + refund_requested_no_ref = factory_invoice( + payment_account=payment_account, + total=30, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=PaymentMethod.EFT.value, + ) + cil4 = factory_create_eft_credit_invoice_link( + invoice_id=refund_requested_no_ref.id, + status_code=EFTCreditInvoiceStatus.CANCELLED.value, + eft_credit_id=eft_credit.id, + amount=30, + ) eft_historical = factory_create_eft_shortname_historical( payment_account_id=payment_account.id, short_name_id=short_name_id, related_group_link_id=1, - transaction_type=EFTHistoricalTypes.STATEMENT_REVERSE.value + transaction_type=EFTHistoricalTypes.STATEMENT_REVERSE.value, ) assert eft_historical.hidden assert eft_historical.is_processing session.commit() - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_reverse: - with patch('pay_api.services.CFSService.reverse_invoice') as mock_invoice: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_reverse: + with patch("pay_api.services.CFSService.reverse_invoice") as mock_invoice: EFTTask.reverse_electronic_funds_transfers_cfs() mock_invoice.assert_called() mock_reverse.assert_called() @@ -326,23 +509,35 @@ def test_unlock_overdue_accounts(session): auth_account_id, eft_file, short_name_id, eft_transaction_id = setup_eft_credit_invoice_links_test() payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value) payment_account.has_overdue_invoices = datetime.now(tz=timezone.utc) - invoice_1 = factory_invoice(payment_account=payment_account, payment_method_code=PaymentMethod.EFT.value, total=10) + invoice_1 = factory_invoice( + payment_account=payment_account, + payment_method_code=PaymentMethod.EFT.value, + total=10, + ) invoice_1.invoice_status_code = InvoiceStatus.OVERDUE.value invoice_1.save() factory_invoice_reference(invoice_id=invoice_1.id) eft_credit = factory_create_eft_credit( - amount=100, remaining_amount=0, eft_file_id=eft_file.id, short_name_id=short_name_id, - eft_transaction_id=eft_transaction_id) + amount=100, + remaining_amount=0, + eft_file_id=eft_file.id, + short_name_id=short_name_id, + eft_transaction_id=eft_transaction_id, + ) factory_create_eft_credit_invoice_link(invoice_id=invoice_1.id, eft_credit_id=eft_credit.id, amount=10) # Create second overdue invoice and confirm unlock is not double called on a payment account - invoice_2 = factory_invoice(payment_account=payment_account, payment_method_code=PaymentMethod.EFT.value, total=10) + invoice_2 = factory_invoice( + payment_account=payment_account, + payment_method_code=PaymentMethod.EFT.value, + total=10, + ) invoice_2.invoice_status_code = InvoiceStatus.OVERDUE.value invoice_2.save() factory_invoice_reference(invoice_id=invoice_2.id) factory_create_eft_credit_invoice_link(invoice_id=invoice_2.id, eft_credit_id=eft_credit.id, amount=10) - with patch('utils.auth_event.AuthEvent.publish_unlock_account_event') as mock_unlock: + with patch("utils.auth_event.AuthEvent.publish_unlock_account_event") as mock_unlock: EFTTask.link_electronic_funds_transfers_cfs() assert payment_account.has_overdue_invoices is None mock_unlock.assert_called_once() @@ -353,19 +548,35 @@ def test_handle_unlinked_refund_requested_invoices(session): """Test handle unlinked refund requested invoices.""" auth_account_id, eft_file, short_name_id, eft_transaction_id = setup_eft_credit_invoice_links_test() eft_credit = factory_create_eft_credit( - amount=100, remaining_amount=0, eft_file_id=eft_file.id, short_name_id=short_name_id, - eft_transaction_id=eft_transaction_id) + amount=100, + remaining_amount=0, + eft_file_id=eft_file.id, + short_name_id=short_name_id, + eft_transaction_id=eft_transaction_id, + ) payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value) - invoice_1 = factory_invoice(payment_account=payment_account, status_code=InvoiceStatus.REFUND_REQUESTED.value, - payment_method_code=PaymentMethod.EFT.value, total=10).save() + invoice_1 = factory_invoice( + payment_account=payment_account, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=PaymentMethod.EFT.value, + total=10, + ).save() factory_invoice_reference(invoice_id=invoice_1.id).save() factory_create_eft_credit_invoice_link(invoice_id=invoice_1.id, eft_credit_id=eft_credit.id, amount=10) - invoice_2 = factory_invoice(payment_account=payment_account, status_code=InvoiceStatus.REFUND_REQUESTED.value, - payment_method_code=PaymentMethod.EFT.value, total=10).save() + invoice_2 = factory_invoice( + payment_account=payment_account, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=PaymentMethod.EFT.value, + total=10, + ).save() invoice_ref_2 = factory_invoice_reference(invoice_id=invoice_2.id).save() - invoice_3 = factory_invoice(payment_account=payment_account, status_code=InvoiceStatus.REFUND_REQUESTED.value, - payment_method_code=PaymentMethod.EFT.value, total=10).save() - with patch('pay_api.services.CFSService.reverse_invoice') as mock_invoice: + invoice_3 = factory_invoice( + payment_account=payment_account, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=PaymentMethod.EFT.value, + total=10, + ).save() + with patch("pay_api.services.CFSService.reverse_invoice") as mock_invoice: EFTTask.handle_unlinked_refund_requested_invoices() mock_invoice.assert_called() # Has CIL so it's excluded @@ -383,15 +594,19 @@ def test_rollback_consolidated_invoice(): """Ensure we can't rollback a consolidated invoice.""" payment_account = factory_create_eft_account(status=CfsAccountStatus.ACTIVE.value) invoice_1 = factory_invoice(payment_account=payment_account).save() - invoice_reference = factory_invoice_reference(invoice_id=invoice_1.id, - status_code=InvoiceReferenceStatus.COMPLETED.value, - is_consolidated=True).save() + invoice_reference = factory_invoice_reference( + invoice_id=invoice_1.id, + status_code=InvoiceReferenceStatus.COMPLETED.value, + is_consolidated=True, + ).save() with pytest.raises(Exception) as excinfo: - EFTTask._rollback_receipt_and_invoice(None, # pylint: disable=protected-access - invoice_1, - None, - cil_status_code=EFTCreditInvoiceStatus.PENDING_REFUND.value) - assert 'Cannot reverse a consolidated invoice' in excinfo.value.args + EFTTask._rollback_receipt_and_invoice( + None, # pylint: disable=protected-access + invoice_1, + None, + cil_status_code=EFTCreditInvoiceStatus.PENDING_REFUND.value, + ) + assert "Cannot reverse a consolidated invoice" in excinfo.value.args with pytest.raises(Exception) as excinfo: EFTTask._handle_invoice_refund(None, invoice_reference) # pylint: disable=protected-access - assert 'Cannot reverse a consolidated invoice' in excinfo.value.args + assert "Cannot reverse a consolidated invoice" in excinfo.value.args diff --git a/jobs/payment-jobs/tests/jobs/test_ejv_partner_distribution_task.py b/jobs/payment-jobs/tests/jobs/test_ejv_partner_distribution_task.py index d3dcdfb3f..b8463ccbd 100644 --- a/jobs/payment-jobs/tests/jobs/test_ejv_partner_distribution_task.py +++ b/jobs/payment-jobs/tests/jobs/test_ejv_partner_distribution_task.py @@ -30,11 +30,18 @@ from tasks.ejv_partner_distribution_task import EjvPartnerDistributionTask from .factory import ( - factory_create_pad_account, factory_distribution, factory_distribution_link, factory_invoice, - factory_invoice_reference, factory_payment, factory_payment_line_item, factory_receipt) - - -@pytest.mark.parametrize('client_code, batch_type', [('112', 'GA'), ('113', 'GI'), ('ABC', 'GI')]) + factory_create_pad_account, + factory_distribution, + factory_distribution_link, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_payment_line_item, + factory_receipt, +) + + +@pytest.mark.parametrize("client_code, batch_type", [("112", "GA"), ("113", "GI"), ("ABC", "GI")]) def test_disbursement_for_partners(session, monkeypatch, client_code, batch_type): """Test disbursement for partners. @@ -43,58 +50,72 @@ def test_disbursement_for_partners(session, monkeypatch, client_code, batch_type 2) Create paid invoices for these partners. 3) Run the job and assert results. """ - monkeypatch.setattr('pysftp.Connection.put', lambda *args, **kwargs: None) - corp_type: CorpTypeModel = CorpTypeModel.find_by_code('VS') + monkeypatch.setattr("pysftp.Connection.put", lambda *args, **kwargs: None) + corp_type: CorpTypeModel = CorpTypeModel.find_by_code("VS") corp_type.has_partner_disbursements = True corp_type.save() - pad_account = factory_create_pad_account(auth_account_id='1234', - bank_number='001', - bank_branch='004', - bank_account='1234567890', - status=CfsAccountStatus.ACTIVE.value, - payment_method=PaymentMethod.PAD.value) + pad_account = factory_create_pad_account( + auth_account_id="1234", + bank_number="001", + bank_branch="004", + bank_account="1234567890", + status=CfsAccountStatus.ACTIVE.value, + payment_method=PaymentMethod.PAD.value, + ) # GI - Create 3 distribution code records. 1 for VS stat fee, 1 for service fee and 1 for disbursement. - disbursement_distribution: DistributionCode = factory_distribution(name='VS Disbursement', client=client_code) + disbursement_distribution: DistributionCode = factory_distribution(name="VS Disbursement", client=client_code) - service_fee_distribution: DistributionCode = factory_distribution(name='VS Service Fee', client='112') + service_fee_distribution: DistributionCode = factory_distribution(name="VS Service Fee", client="112") fee_distribution: DistributionCode = factory_distribution( - name='VS Fee distribution', client='112', service_fee_dist_id=service_fee_distribution.distribution_code_id, - disbursement_dist_id=disbursement_distribution.distribution_code_id + name="VS Fee distribution", + client="112", + service_fee_dist_id=service_fee_distribution.distribution_code_id, + disbursement_dist_id=disbursement_distribution.distribution_code_id, ) - fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, 'WILLNOTICE') + fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, "WILLNOTICE") factory_distribution_link(fee_distribution.distribution_code_id, fee_schedule.fee_schedule_id) - invoice = factory_invoice(payment_account=pad_account, corp_type_code=corp_type.code, total=11.5, - status_code='PAID') + invoice = factory_invoice( + payment_account=pad_account, + corp_type_code=corp_type.code, + total=11.5, + status_code="PAID", + ) - factory_payment_line_item(invoice_id=invoice.id, - fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=10, - total=10, - service_fees=1.5, - fee_dist_id=fee_distribution.distribution_code_id) + factory_payment_line_item( + invoice_id=invoice.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=10, + total=10, + service_fees=1.5, + fee_dist_id=fee_distribution.distribution_code_id, + ) inv_ref = factory_invoice_reference(invoice_id=invoice.id) - factory_payment(invoice_number=inv_ref.invoice_number, payment_status_code='COMPLETED') + factory_payment(invoice_number=inv_ref.invoice_number, payment_status_code="COMPLETED") factory_receipt(invoice_id=invoice.id, receipt_date=datetime.now(tz=timezone.utc)).save() - eft_invoice = factory_invoice(payment_account=pad_account, - corp_type_code=corp_type.code, - total=11.5, - payment_method_code=PaymentMethod.EFT.value, - status_code='PAID') + eft_invoice = factory_invoice( + payment_account=pad_account, + corp_type_code=corp_type.code, + total=11.5, + payment_method_code=PaymentMethod.EFT.value, + status_code="PAID", + ) - factory_payment_line_item(invoice_id=eft_invoice.id, - fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=10, - total=10, - service_fees=1.5, - fee_dist_id=fee_distribution.distribution_code_id) + factory_payment_line_item( + invoice_id=eft_invoice.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=10, + total=10, + service_fees=1.5, + fee_dist_id=fee_distribution.distribution_code_id, + ) inv_ref = factory_invoice_reference(invoice_id=eft_invoice.id) - factory_payment(invoice_number=inv_ref.invoice_number, payment_status_code='COMPLETED') + factory_payment(invoice_number=inv_ref.invoice_number, payment_status_code="COMPLETED") factory_receipt(invoice_id=eft_invoice.id, receipt_date=datetime.now(tz=timezone.utc)).save() partner_disbursement = PartnerDisbursementsModel( amount=10, @@ -102,7 +123,7 @@ def test_disbursement_for_partners(session, monkeypatch, client_code, batch_type partner_code=eft_invoice.corp_type_code, status_code=DisbursementStatus.WAITING_FOR_RECEIPT.value, target_id=eft_invoice.id, - target_type=EJVLinkType.INVOICE.value + target_type=EJVLinkType.INVOICE.value, ).save() EjvPartnerDistributionTask.create_ejv_file() @@ -111,8 +132,9 @@ def test_disbursement_for_partners(session, monkeypatch, client_code, batch_type invoice = Invoice.find_by_id(invoice.id) assert invoice.disbursement_status_code is None - day_after_time_delay = datetime.now(tz=timezone.utc) + \ - timedelta(days=(current_app.config.get('DISBURSEMENT_DELAY_IN_DAYS') + 1)) + day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta( + days=(current_app.config.get("DISBURSEMENT_DELAY_IN_DAYS") + 1) + ) with freeze_time(day_after_time_delay): EjvPartnerDistributionTask.create_ejv_file() # Lookup invoice and assert disbursement status @@ -128,7 +150,7 @@ def test_disbursement_for_partners(session, monkeypatch, client_code, batch_type ejv_file = EjvFile.find_by_id(ejv_header.ejv_file_id) assert ejv_file - assert ejv_file.disbursement_status_code == DisbursementStatus.UPLOADED.value, f'{batch_type}' + assert ejv_file.disbursement_status_code == DisbursementStatus.UPLOADED.value, f"{batch_type}" assert partner_disbursement.status_code == DisbursementStatus.UPLOADED.value assert partner_disbursement.processed_on diff --git a/jobs/payment-jobs/tests/jobs/test_ejv_partner_partial_refund_distribution_task.py b/jobs/payment-jobs/tests/jobs/test_ejv_partner_partial_refund_distribution_task.py index 442530de7..fe6465bc9 100644 --- a/jobs/payment-jobs/tests/jobs/test_ejv_partner_partial_refund_distribution_task.py +++ b/jobs/payment-jobs/tests/jobs/test_ejv_partner_partial_refund_distribution_task.py @@ -16,9 +16,9 @@ Test-Suite to ensure that the CgiEjvJob is working as expected. """ -import pytest from datetime import datetime, timedelta, timezone +import pytest from flask import current_app from freezegun import freeze_time from pay_api.models import CorpType as CorpTypeModel @@ -28,46 +28,70 @@ from tasks.ejv_partner_distribution_task import EjvPartnerDistributionTask from .factory import ( - factory_create_direct_pay_account, factory_distribution, factory_distribution_link, factory_invoice, - factory_invoice_reference, factory_payment, factory_payment_line_item, factory_refund_partial) - - -@pytest.mark.skip(reason='Will be fixed in future ticket') + factory_create_direct_pay_account, + factory_distribution, + factory_distribution_link, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_payment_line_item, + factory_refund_partial, +) + + +@pytest.mark.skip(reason="Will be fixed in future ticket") def test_partial_refund_disbursement(session, monkeypatch): """Test partial refund disbursement.""" - monkeypatch.setattr('pysftp.Connection.put', lambda *args, **kwargs: None) - corp_type: CorpTypeModel = CorpTypeModel.find_by_code('VS') + monkeypatch.setattr("pysftp.Connection.put", lambda *args, **kwargs: None) + corp_type: CorpTypeModel = CorpTypeModel.find_by_code("VS") pay_account = factory_create_direct_pay_account() - disbursement_distribution: DistributionCode = factory_distribution(name='VS Disbursement', client='112') - service_fee_distribution: DistributionCode = factory_distribution(name='VS Service Fee', client='112') + disbursement_distribution: DistributionCode = factory_distribution(name="VS Disbursement", client="112") + service_fee_distribution: DistributionCode = factory_distribution(name="VS Service Fee", client="112") fee_distribution: DistributionCode = factory_distribution( - name='VS Fee distribution', client='112', service_fee_dist_id=service_fee_distribution.distribution_code_id, - disbursement_dist_id=disbursement_distribution.distribution_code_id + name="VS Fee distribution", + client="112", + service_fee_dist_id=service_fee_distribution.distribution_code_id, + disbursement_dist_id=disbursement_distribution.distribution_code_id, ) - fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, 'WILLNOTICE') + fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type.code, "WILLNOTICE") factory_distribution_link(fee_distribution.distribution_code_id, fee_schedule.fee_schedule_id) - invoice = factory_invoice(payment_account=pay_account, disbursement_status_code=DisbursementStatus.COMPLETED.value, - corp_type_code=corp_type.code, total=21.5, status_code='PAID') - pli = factory_payment_line_item(invoice_id=invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=0, total=1.5, service_fees=1.5, - fee_dist_id=fee_distribution.distribution_code_id) + invoice = factory_invoice( + payment_account=pay_account, + disbursement_status_code=DisbursementStatus.COMPLETED.value, + corp_type_code=corp_type.code, + total=21.5, + status_code="PAID", + ) + pli = factory_payment_line_item( + invoice_id=invoice.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=0, + total=1.5, + service_fees=1.5, + fee_dist_id=fee_distribution.distribution_code_id, + ) inv_ref = factory_invoice_reference(invoice_id=invoice.id) - factory_payment(invoice_number=inv_ref.invoice_number, payment_status_code='COMPLETED') + factory_payment(invoice_number=inv_ref.invoice_number, payment_status_code="COMPLETED") - refund_partial = factory_refund_partial(pli.id, refund_amount=1.5, created_by='test', - refund_type=RefundsPartialType.SERVICE_FEES.value) + refund_partial = factory_refund_partial( + pli.id, + refund_amount=1.5, + created_by="test", + refund_type=RefundsPartialType.SERVICE_FEES.value, + ) assert refund_partial.disbursement_status_code is None # Lookup refund_partial_link refund_partial_link = EjvLink.find_ejv_link_by_link_id(refund_partial.id) assert refund_partial_link is None - day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=( - current_app.config.get('DISBURSEMENT_DELAY_IN_DAYS') + 1)) + day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta( + days=(current_app.config.get("DISBURSEMENT_DELAY_IN_DAYS") + 1) + ) with freeze_time(day_after_time_delay): EjvPartnerDistributionTask.create_ejv_file() diff --git a/jobs/payment-jobs/tests/jobs/test_ejv_payment_task.py b/jobs/payment-jobs/tests/jobs/test_ejv_payment_task.py index 9c1a67363..2d24bab45 100644 --- a/jobs/payment-jobs/tests/jobs/test_ejv_payment_task.py +++ b/jobs/payment-jobs/tests/jobs/test_ejv_payment_task.py @@ -34,60 +34,70 @@ def test_payments_for_gov_accounts(session, monkeypatch): 4) Create some transactions for these accounts 5) Run the job and assert results. """ - monkeypatch.setattr('pysftp.Connection.put', lambda *args, **kwargs: None) + monkeypatch.setattr("pysftp.Connection.put", lambda *args, **kwargs: None) - corp_type = 'BEN' - filing_type = 'BCINC' + corp_type = "BEN" + filing_type = "BCINC" # Find fee schedule which have service fees. fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type, filing_type) # Create a service fee distribution code - service_fee_dist_code = factory_distribution(name='service fee', client='112', reps_centre='99999', - service_line='99999', - stob='9999', project_code='9999999') + service_fee_dist_code = factory_distribution( + name="service fee", + client="112", + reps_centre="99999", + service_line="99999", + stob="9999", + project_code="9999999", + ) service_fee_dist_code.save() dist_code: DistributionCode = DistributionCode.find_by_active_for_fee_schedule(fee_schedule.fee_schedule_id) # Update fee dist code to match the requirement. - dist_code.client = '112' - dist_code.responsibility_centre = '22222' - dist_code.service_line = '33333' - dist_code.stob = '4444' - dist_code.project_code = '5555555' + dist_code.client = "112" + dist_code.responsibility_centre = "22222" + dist_code.service_line = "33333" + dist_code.stob = "4444" + dist_code.project_code = "5555555" dist_code.service_fee_distribution_code_id = service_fee_dist_code.distribution_code_id dist_code.save() # GA - jv_account_1 = factory_create_ejv_account(auth_account_id='1') - jv_account_2 = factory_create_ejv_account(auth_account_id='2') + jv_account_1 = factory_create_ejv_account(auth_account_id="1") + jv_account_2 = factory_create_ejv_account(auth_account_id="2") # GI - jv_account_3 = factory_create_ejv_account(auth_account_id='3', client='111') - jv_account_4 = factory_create_ejv_account(auth_account_id='4', client='111') + jv_account_3 = factory_create_ejv_account(auth_account_id="3", client="111") + jv_account_4 = factory_create_ejv_account(auth_account_id="4", client="111") jv_accounts = [jv_account_1, jv_account_2, jv_account_3, jv_account_4] inv_ids = [] for jv_acc in jv_accounts: - inv = factory_invoice(payment_account=jv_acc, corp_type_code=corp_type, total=101.5, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) - factory_payment_line_item(invoice_id=inv.id, - fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=100, - total=100, - service_fees=1.5, - fee_dist_id=dist_code.distribution_code_id) + inv = factory_invoice( + payment_account=jv_acc, + corp_type_code=corp_type, + total=101.5, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) + factory_payment_line_item( + invoice_id=inv.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=100, + total=100, + service_fees=1.5, + fee_dist_id=dist_code.distribution_code_id, + ) inv_ids.append(inv.id) EjvPaymentTask.create_ejv_file() # Lookup invoice and assert invoice status for inv_id in inv_ids: - invoice_ref = InvoiceReference.find_by_invoice_id_and_status(inv_id, - InvoiceReferenceStatus.ACTIVE.value) + invoice_ref = InvoiceReference.find_by_invoice_id_and_status(inv_id, InvoiceReferenceStatus.ACTIVE.value) assert invoice_ref - ejv_inv_link: EjvLink = db.session.query(EjvLink)\ - .filter(EjvLink.link_id == inv_id).first() + ejv_inv_link: EjvLink = db.session.query(EjvLink).filter(EjvLink.link_id == inv_id).first() assert ejv_inv_link ejv_header = db.session.query(EjvHeader).filter(EjvHeader.id == ejv_inv_link.ejv_header_id).first() @@ -108,8 +118,7 @@ def test_payments_for_gov_accounts(session, monkeypatch): # Mark invoice as REFUND_REQUESTED and run a JV job again. for inv_id in inv_ids: # Set invoice ref status as COMPLETED, as that would be the status when the payment is reconciled. - invoice_ref = InvoiceReference.find_by_invoice_id_and_status(inv_id, - InvoiceReferenceStatus.ACTIVE.value) + invoice_ref = InvoiceReference.find_by_invoice_id_and_status(inv_id, InvoiceReferenceStatus.ACTIVE.value) invoice_ref.status_code = InvoiceReferenceStatus.COMPLETED.value # Set invoice status for Refund requested. @@ -122,12 +131,15 @@ def test_payments_for_gov_accounts(session, monkeypatch): # Lookup invoice and assert invoice status for inv_id in inv_ids: - invoice_ref = InvoiceReference.find_by_invoice_id_and_status(inv_id, - InvoiceReferenceStatus.ACTIVE.value) + invoice_ref = InvoiceReference.find_by_invoice_id_and_status(inv_id, InvoiceReferenceStatus.ACTIVE.value) assert invoice_ref - ejv_inv_link = db.session.query(EjvLink).filter(EjvLink.link_id == inv_id)\ - .filter(EjvLink.disbursement_status_code == DisbursementStatus.UPLOADED.value).first() + ejv_inv_link = ( + db.session.query(EjvLink) + .filter(EjvLink.link_id == inv_id) + .filter(EjvLink.disbursement_status_code == DisbursementStatus.UPLOADED.value) + .first() + ) assert ejv_inv_link ejv_header = db.session.query(EjvHeader).filter(EjvHeader.id == ejv_inv_link.ejv_header_id).first() diff --git a/jobs/payment-jobs/tests/jobs/test_routing_slip_task.py b/jobs/payment-jobs/tests/jobs/test_routing_slip_task.py index 8aa44f962..4b2fad023 100644 --- a/jobs/payment-jobs/tests/jobs/test_routing_slip_task.py +++ b/jobs/payment-jobs/tests/jobs/test_routing_slip_task.py @@ -26,20 +26,33 @@ from pay_api.models import PaymentAccount as PaymentAccountModel from pay_api.models import Receipt as ReceiptModel from pay_api.models import RoutingSlip as RoutingSlipModel -from pay_api.utils.enums import ( - CfsAccountStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, ReverseOperation, RoutingSlipStatus) from pay_api.services import CFSService +from pay_api.utils.enums import ( + CfsAccountStatus, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, + ReverseOperation, + RoutingSlipStatus, +) + from tasks.routing_slip_task import RoutingSlipTask from .factory import ( - factory_distribution, factory_distribution_link, factory_invoice, factory_invoice_reference, - factory_payment_line_item, factory_receipt, factory_routing_slip_account) + factory_distribution, + factory_distribution_link, + factory_invoice, + factory_invoice_reference, + factory_payment_line_item, + factory_receipt, + factory_routing_slip_account, +) def test_link_rs(session): """Test link routing slip.""" - child_rs_number = '1234' - parent_rs_number = '89799' + child_rs_number = "1234" + parent_rs_number = "89799" factory_routing_slip_account(number=child_rs_number, status=CfsAccountStatus.ACTIVE.value) factory_routing_slip_account(number=parent_rs_number, status=CfsAccountStatus.ACTIVE.value) child_rs = RoutingSlipModel.find_by_number(child_rs_number) @@ -48,15 +61,13 @@ def test_link_rs(session): child_rs.status = RoutingSlipStatus.LINKED.value child_rs.parent_number = parent_rs.number child_rs.save() - payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( - child_rs.payment_account_id) + payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(child_rs.payment_account_id) - cfs_account = CfsAccountModel.find_effective_by_payment_method( - payment_account.id, PaymentMethod.INTERNAL.value) + cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, PaymentMethod.INTERNAL.value) - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_cfs_reverse: - with patch('pay_api.services.CFSService.create_cfs_receipt') as mock_create_cfs: - with patch.object(CFSService, 'get_receipt') as mock_get_receipt: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_cfs_reverse: + with patch("pay_api.services.CFSService.create_cfs_receipt") as mock_create_cfs: + with patch.object(CFSService, "get_receipt") as mock_get_receipt: RoutingSlipTask.link_routing_slips() mock_cfs_reverse.assert_called() mock_cfs_reverse.assert_called_with(cfs_account, child_rs.number, ReverseOperation.LINK.value) @@ -71,8 +82,8 @@ def test_link_rs(session): assert cfs_account.status == CfsAccountStatus.INACTIVE.value # make sure next invocation doesnt fetch any records - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_cfs_reverse: - with patch('pay_api.services.CFSService.create_cfs_receipt') as mock_create_cfs: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_cfs_reverse: + with patch("pay_api.services.CFSService.create_cfs_receipt") as mock_create_cfs: RoutingSlipTask.link_routing_slips() mock_cfs_reverse.assert_not_called() mock_create_cfs.assert_not_called() @@ -82,9 +93,9 @@ def test_process_nsf(session): """Test process NSF.""" # 1. Link 2 child routing slips with parent. # 2. Mark the parent as NSF and run job. - child_1 = '123456789' - child_2 = '987654321' - parent = '111111111' + child_1 = "123456789" + child_2 = "987654321" + parent = "111111111" factory_routing_slip_account(number=child_1, status=CfsAccountStatus.ACTIVE.value, total=10) factory_routing_slip_account(number=child_2, status=CfsAccountStatus.ACTIVE.value, total=10) pay_account = factory_routing_slip_account(number=parent, status=CfsAccountStatus.ACTIVE.value, total=10) @@ -107,18 +118,21 @@ def test_process_nsf(session): parent_rs.save() # Create an invoice record against this routing slip. - invoice = factory_invoice(payment_account=pay_account, total=30, - status_code=InvoiceStatus.PAID.value, - payment_method_code=PaymentMethod.INTERNAL.value, - routing_slip=parent_rs.number) + invoice = factory_invoice( + payment_account=pay_account, + total=30, + status_code=InvoiceStatus.PAID.value, + payment_method_code=PaymentMethod.INTERNAL.value, + routing_slip=parent_rs.number, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() # Create a distribution for NSF -> As this is a manual step once in each env. - nsf_fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('BCR', 'NSF') - distribution = factory_distribution('NSF') + nsf_fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("BCR", "NSF") + distribution = factory_distribution("NSF") factory_distribution_link(distribution.distribution_code_id, nsf_fee_schedule.fee_schedule_id) # Create invoice @@ -129,7 +143,7 @@ def test_process_nsf(session): factory_receipt(invoice.id, child_1_rs.number) factory_receipt(invoice.id, child_2_rs.number) - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_cfs_reverse: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_cfs_reverse: RoutingSlipTask.process_nsf() mock_cfs_reverse.assert_called() @@ -142,7 +156,7 @@ def test_process_nsf(session): assert not ReceiptModel.find_all_receipts_for_invoice(invoice.id) assert float(RoutingSlipModel.find_by_number(parent_rs.number).remaining_amount) == -60 # Including NSF Fee - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_cfs_reverse_2: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_cfs_reverse_2: RoutingSlipTask.process_nsf() mock_cfs_reverse_2.assert_not_called() @@ -151,9 +165,9 @@ def test_process_void(session): """Test Routing slip set to VOID.""" # 1. Link 2 child routing slips with parent. # 2. Mark the parent as VOID and run job. - child_1 = '123456789' - child_2 = '987654321' - parent = '111111111' + child_1 = "123456789" + child_2 = "987654321" + parent = "111111111" factory_routing_slip_account(number=child_1, status=CfsAccountStatus.ACTIVE.value, total=10) factory_routing_slip_account(number=child_2, status=CfsAccountStatus.ACTIVE.value, total=10) factory_routing_slip_account(number=parent, status=CfsAccountStatus.ACTIVE.value, total=10) @@ -174,30 +188,33 @@ def test_process_void(session): parent_rs.status = RoutingSlipStatus.VOID.value parent_rs.save() - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_cfs_reverse: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_cfs_reverse: RoutingSlipTask.process_void() mock_cfs_reverse.assert_called() # Assert the records. assert float(RoutingSlipModel.find_by_number(parent_rs.number).remaining_amount) == 0 - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_cfs_reverse_2: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_cfs_reverse_2: RoutingSlipTask.process_void() mock_cfs_reverse_2.assert_not_called() def test_process_correction(session): """Test Routing slip set to CORRECTION.""" - number = '1111111' + number = "1111111" pay_account = factory_routing_slip_account(number=number, status=CfsAccountStatus.ACTIVE.value, total=10) # Create an invoice for the routing slip # Create an invoice record against this routing slip. - invoice = factory_invoice(payment_account=pay_account, total=30, - status_code=InvoiceStatus.PAID.value, - payment_method_code=PaymentMethod.INTERNAL.value, - routing_slip=number) + invoice = factory_invoice( + payment_account=pay_account, + total=30, + status_code=InvoiceStatus.PAID.value, + payment_method_code=PaymentMethod.INTERNAL.value, + routing_slip=number, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -214,9 +231,9 @@ def test_process_correction(session): session.commit() - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_reverse: - with patch('pay_api.services.CFSService.create_cfs_receipt') as mock_create_receipt: - with patch('pay_api.services.CFSService.get_invoice') as mock_get_invoice: + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs") as mock_reverse: + with patch("pay_api.services.CFSService.create_cfs_receipt") as mock_create_receipt: + with patch("pay_api.services.CFSService.get_invoice") as mock_get_invoice: RoutingSlipTask.process_correction() mock_reverse.assert_called() mock_get_invoice.assert_called() @@ -228,8 +245,8 @@ def test_process_correction(session): def test_link_to_nsf_rs(session): """Test routing slip with NSF as parent.""" - child_rs_number = '1234' - parent_rs_number = '89799' + child_rs_number = "1234" + parent_rs_number = "89799" factory_routing_slip_account(number=child_rs_number, status=CfsAccountStatus.ACTIVE.value) pay_account = factory_routing_slip_account(number=parent_rs_number, status=CfsAccountStatus.ACTIVE.value) child_rs = RoutingSlipModel.find_by_number(child_rs_number) @@ -244,18 +261,21 @@ def test_link_to_nsf_rs(session): # Create an invoice for the routing slip # Create an invoice record against this routing slip. - invoice = factory_invoice(payment_account=pay_account, total=30, - status_code=InvoiceStatus.PAID.value, - payment_method_code=PaymentMethod.INTERNAL.value, - routing_slip=parent_rs.number) + invoice = factory_invoice( + payment_account=pay_account, + total=30, + status_code=InvoiceStatus.PAID.value, + payment_method_code=PaymentMethod.INTERNAL.value, + routing_slip=parent_rs.number, + ) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("CP", "OTANN") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() # Create a distribution for NSF -> As this is a manual step once in each env. - nsf_fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('BCR', 'NSF') - distribution = factory_distribution('NSF') + nsf_fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("BCR", "NSF") + distribution = factory_distribution("NSF") factory_distribution_link(distribution.distribution_code_id, nsf_fee_schedule.fee_schedule_id) # Create invoice reference @@ -270,7 +290,7 @@ def test_link_to_nsf_rs(session): RoutingSlipTask.process_nsf() # Now create another RS and link it to the NSF RS, and assert status - child_rs_2_number = '8888' + child_rs_2_number = "8888" factory_routing_slip_account(number=child_rs_2_number, status=CfsAccountStatus.ACTIVE.value) child_2_rs = RoutingSlipModel.find_by_number(child_rs_2_number) child_2_rs.status = RoutingSlipStatus.LINKED.value @@ -278,7 +298,7 @@ def test_link_to_nsf_rs(session): child_2_rs.save() # Run link process - with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs'): + with patch("pay_api.services.CFSService.reverse_rs_receipt_in_cfs"): RoutingSlipTask.link_routing_slips() # Now the invoice status should be PAID as RS has recovered. @@ -287,16 +307,24 @@ def test_link_to_nsf_rs(session): assert RoutingSlipModel.find_by_number(parent_rs.number).status == RoutingSlipStatus.ACTIVE.value -@pytest.mark.parametrize('rs_status', [ - RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value, RoutingSlipStatus.REFUND_AUTHORIZED.value -]) +@pytest.mark.parametrize( + "rs_status", + [ + RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value, + RoutingSlipStatus.REFUND_AUTHORIZED.value, + ], +) def test_receipt_adjustments(session, rs_status): """Test routing slip adjustments.""" - child_rs_number = '1234' - parent_rs_number = '89799' + child_rs_number = "1234" + parent_rs_number = "89799" factory_routing_slip_account(number=child_rs_number, status=CfsAccountStatus.ACTIVE.value) - factory_routing_slip_account(number=parent_rs_number, status=CfsAccountStatus.ACTIVE.value, total=10, - remaining_amount=10) + factory_routing_slip_account( + number=parent_rs_number, + status=CfsAccountStatus.ACTIVE.value, + total=10, + remaining_amount=10, + ) child_rs = RoutingSlipModel.find_by_number(child_rs_number) parent_rs = RoutingSlipModel.find_by_number(parent_rs_number) # Do Link @@ -307,14 +335,14 @@ def test_receipt_adjustments(session, rs_status): parent_rs.status = rs_status # Test exception path first. - with patch('pay_api.services.CFSService.adjust_receipt_to_zero') as mock: - mock.side_effect = Exception('ERROR!') + with patch("pay_api.services.CFSService.adjust_receipt_to_zero") as mock: + mock.side_effect = Exception("ERROR!") RoutingSlipTask.adjust_routing_slips() parent_rs = RoutingSlipModel.find_by_number(parent_rs.number) assert parent_rs.remaining_amount == 10 - with patch('pay_api.services.CFSService.adjust_receipt_to_zero'): + with patch("pay_api.services.CFSService.adjust_receipt_to_zero"): RoutingSlipTask.adjust_routing_slips() parent_rs = RoutingSlipModel.find_by_number(parent_rs.number) diff --git a/jobs/payment-jobs/tests/jobs/test_statement_notification_task.py b/jobs/payment-jobs/tests/jobs/test_statement_notification_task.py index 7837870b9..7a7fa3389 100644 --- a/jobs/payment-jobs/tests/jobs/test_statement_notification_task.py +++ b/jobs/payment-jobs/tests/jobs/test_statement_notification_task.py @@ -33,9 +33,13 @@ from tasks.statement_notification_task import StatementNotificationTask from tasks.statement_task import StatementTask from tests.jobs.factory import ( - factory_create_account, factory_invoice, factory_invoice_reference, factory_payment, factory_statement_recipient, - factory_statement_settings) - + factory_create_account, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_statement_recipient, + factory_statement_settings, +) fake = Faker() app = None @@ -46,30 +50,38 @@ def setup(): """Initialize app with test env for testing.""" global app app = Flask(__name__) - app.env = 'testing' - app.config.from_object(config.CONFIGURATION['testing']) + app.env = "testing" + app.config.from_object(config.CONFIGURATION["testing"]) -def create_test_data(payment_method_code: str, payment_date: datetime, - statement_frequency: str, invoice_total: decimal = 0.00, - invoice_paid: decimal = 0.00): +def create_test_data( + payment_method_code: str, + payment_date: datetime, + statement_frequency: str, + invoice_total: decimal = 0.00, + invoice_paid: decimal = 0.00, +): """Create seed data for tests.""" - account = factory_create_account(auth_account_id='1', payment_method_code=payment_method_code) - invoice = factory_invoice(payment_account=account, created_on=payment_date, - payment_method_code=payment_method_code, status_code=InvoiceStatus.OVERDUE.value, - total=invoice_total) + account = factory_create_account(auth_account_id="1", payment_method_code=payment_method_code) + invoice = factory_invoice( + payment_account=account, + created_on=payment_date, + payment_method_code=payment_method_code, + status_code=InvoiceStatus.OVERDUE.value, + total=invoice_total, + ) inv_ref = factory_invoice_reference(invoice_id=invoice.id) payment = factory_payment(payment_date=payment_date, invoice_number=inv_ref.invoice_number) - statement_recipient = factory_statement_recipient(auth_user_id=account.auth_account_id, - first_name=fake.first_name(), - last_name=fake.last_name(), - email=fake.email(), - payment_account_id=account.id) + statement_recipient = factory_statement_recipient( + auth_user_id=account.auth_account_id, + first_name=fake.first_name(), + last_name=fake.last_name(), + email=fake.email(), + payment_account_id=account.id, + ) statement_settings = factory_statement_settings( - pay_account_id=account.id, - from_date=payment_date, - frequency=statement_frequency + pay_account_id=account.id, from_date=payment_date, frequency=statement_frequency ) return account, invoice, inv_ref, payment, statement_recipient, statement_settings @@ -81,25 +93,27 @@ def test_send_notifications(session): assert True -@pytest.mark.parametrize('payment_method_code', [ - PaymentMethod.CASH.value, - PaymentMethod.CC.value, - PaymentMethod.DRAWDOWN.value, - PaymentMethod.EJV.value, - PaymentMethod.INTERNAL.value, - PaymentMethod.ONLINE_BANKING.value, - PaymentMethod.PAD.value -]) +@pytest.mark.parametrize( + "payment_method_code", + [ + PaymentMethod.CASH.value, + PaymentMethod.CC.value, + PaymentMethod.DRAWDOWN.value, + PaymentMethod.EJV.value, + PaymentMethod.INTERNAL.value, + PaymentMethod.ONLINE_BANKING.value, + PaymentMethod.PAD.value, + ], +) def test_send_monthly_notifications(setup, session, payment_method_code): # pylint: disable=unused-argument """Test send monthly statement notifications.""" # create statement, invoice, payment data for previous month last_month, last_year = get_previous_month_and_year() previous_month_year = datetime(last_year, last_month, 5) - account, invoice, inv_ref, payment, \ - statement_recipient, statement_settings = create_test_data(payment_method_code, - previous_month_year, - StatementFrequency.MONTHLY.value) + account, invoice, inv_ref, payment, statement_recipient, statement_settings = create_test_data( + payment_method_code, previous_month_year, StatementFrequency.MONTHLY.value + ) assert invoice.payment_method_code == payment_method_code assert account.payment_method == payment_method_code @@ -118,9 +132,9 @@ def test_send_monthly_notifications(setup, session, payment_method_code): # pyl assert invoices[0].invoice_id == invoice.id # Assert notification send_email was invoked - with patch.object(StatementNotificationTask, 'send_email', return_value=True) as mock_mailer: - with patch('tasks.statement_notification_task.get_token') as mock_get_token: - mock_get_token.return_value = 'mock_token' + with patch.object(StatementNotificationTask, "send_email", return_value=True) as mock_mailer: + with patch("tasks.statement_notification_task.get_token") as mock_get_token: + mock_get_token.return_value = "mock_token" StatementNotificationTask.send_notifications() mock_get_token.assert_called_once() # Assert token and email recipient - mock any for HTML generated @@ -132,25 +146,27 @@ def test_send_monthly_notifications(setup, session, payment_method_code): # pyl assert statement.notification_status_code == NotificationStatus.SUCCESS.value -@pytest.mark.parametrize('payment_method_code', [ - PaymentMethod.CASH.value, - PaymentMethod.CC.value, - PaymentMethod.DRAWDOWN.value, - PaymentMethod.EJV.value, - PaymentMethod.INTERNAL.value, - PaymentMethod.ONLINE_BANKING.value, - PaymentMethod.PAD.value -]) +@pytest.mark.parametrize( + "payment_method_code", + [ + PaymentMethod.CASH.value, + PaymentMethod.CC.value, + PaymentMethod.DRAWDOWN.value, + PaymentMethod.EJV.value, + PaymentMethod.INTERNAL.value, + PaymentMethod.ONLINE_BANKING.value, + PaymentMethod.PAD.value, + ], +) def test_send_monthly_notifications_failed(setup, session, payment_method_code): # pylint: disable=unused-argument """Test send monthly statement notifications failure.""" # create statement, invoice, payment data for previous month last_month, last_year = get_previous_month_and_year() previous_month_year = datetime(last_year, last_month, 5) - account, invoice, inv_ref, payment, \ - statement_recipient, statement_settings = create_test_data(payment_method_code, - previous_month_year, - StatementFrequency.MONTHLY.value) + account, invoice, inv_ref, payment, statement_recipient, statement_settings = create_test_data( + payment_method_code, previous_month_year, StatementFrequency.MONTHLY.value + ) assert invoice.payment_method_code == payment_method_code assert account.payment_method == payment_method_code @@ -169,9 +185,9 @@ def test_send_monthly_notifications_failed(setup, session, payment_method_code): assert invoices[0].invoice_id == invoice.id # Assert notification send_email was invoked - with patch.object(StatementNotificationTask, 'send_email', return_value=False) as mock_mailer: - with patch('tasks.statement_notification_task.get_token') as mock_get_token: - mock_get_token.return_value = 'mock_token' + with patch.object(StatementNotificationTask, "send_email", return_value=False) as mock_mailer: + with patch("tasks.statement_notification_task.get_token") as mock_get_token: + mock_get_token.return_value = "mock_token" StatementNotificationTask.send_notifications() mock_get_token.assert_called_once() # Assert token and email recipient - mock any for HTML generated @@ -188,11 +204,12 @@ def test_send_eft_notifications(setup, session): # pylint: disable=unused-argum # create statement, invoice, payment data for previous month last_month, last_year = get_previous_month_and_year() previous_month_year = datetime(last_year, last_month, 5) - account, invoice, inv_ref, payment, \ - statement_recipient, statement_settings = create_test_data(PaymentMethod.EFT.value, - previous_month_year, - StatementFrequency.MONTHLY.value, - 351.50) + account, invoice, inv_ref, payment, statement_recipient, statement_settings = create_test_data( + PaymentMethod.EFT.value, + previous_month_year, + StatementFrequency.MONTHLY.value, + 351.50, + ) assert invoice.payment_method_code == PaymentMethod.EFT.value assert account.payment_method == PaymentMethod.EFT.value @@ -211,9 +228,9 @@ def test_send_eft_notifications(setup, session): # pylint: disable=unused-argum assert invoices[0].invoice_id == invoice.id # Assert notification was published to the mailer queue - with patch('tasks.statement_notification_task.publish_statement_notification') as mock_mailer: - with patch('tasks.statement_notification_task.get_token') as mock_get_token: - mock_get_token.return_value = 'mock_token' + with patch("tasks.statement_notification_task.publish_statement_notification") as mock_mailer: + with patch("tasks.statement_notification_task.get_token") as mock_get_token: + mock_get_token.return_value = "mock_token" StatementNotificationTask.send_notifications() mock_get_token.assert_called_once() mock_mailer.assert_called_once_with(account, statements[0][0], 351.5, statement_recipient.email) @@ -229,11 +246,12 @@ def test_send_eft_notifications_failure(setup, session): # pylint: disable=unus # create statement, invoice, payment data for previous month last_month, last_year = get_previous_month_and_year() previous_month_year = datetime(last_year, last_month, 5) - account, invoice, inv_ref, payment, \ - statement_recipient, statement_settings = create_test_data(PaymentMethod.EFT.value, - previous_month_year, - StatementFrequency.MONTHLY.value, - 351.50) + account, invoice, inv_ref, payment, statement_recipient, statement_settings = create_test_data( + PaymentMethod.EFT.value, + previous_month_year, + StatementFrequency.MONTHLY.value, + 351.50, + ) assert invoice.payment_method_code == PaymentMethod.EFT.value assert account.payment_method == PaymentMethod.EFT.value @@ -252,10 +270,10 @@ def test_send_eft_notifications_failure(setup, session): # pylint: disable=unus assert invoices[0].invoice_id == invoice.id # Assert notification was published to the mailer queue - with patch('tasks.statement_notification_task.publish_statement_notification') as mock_mailer: - mock_mailer.side_effect = Exception('Mock Exception') - with patch('tasks.statement_notification_task.get_token') as mock_get_token: - mock_get_token.return_value = 'mock_token' + with patch("tasks.statement_notification_task.publish_statement_notification") as mock_mailer: + mock_mailer.side_effect = Exception("Mock Exception") + with patch("tasks.statement_notification_task.get_token") as mock_get_token: + mock_get_token.return_value = "mock_token" StatementNotificationTask.send_notifications() mock_get_token.assert_called_once() mock_mailer.assert_called_once_with(account, statements[0][0], 351.5, statement_recipient.email) @@ -271,11 +289,12 @@ def test_send_eft_notifications_ff_disabled(setup, session): # pylint: disable= # create statement, invoice, payment data for previous month last_month, last_year = get_previous_month_and_year() previous_month_year = datetime(last_year, last_month, 5) - account, invoice, inv_ref, payment, \ - statement_recipient, statement_settings = create_test_data(PaymentMethod.EFT.value, - previous_month_year, - StatementFrequency.MONTHLY.value, - 351.50) + account, invoice, inv_ref, payment, statement_recipient, statement_settings = create_test_data( + PaymentMethod.EFT.value, + previous_month_year, + StatementFrequency.MONTHLY.value, + 351.50, + ) assert invoice.payment_method_code == PaymentMethod.EFT.value assert account.payment_method == PaymentMethod.EFT.value @@ -294,10 +313,10 @@ def test_send_eft_notifications_ff_disabled(setup, session): # pylint: disable= assert invoices[0].invoice_id == invoice.id # Assert notification was published to the mailer queue - with patch('tasks.statement_notification_task.publish_statement_notification') as mock_mailer: - with patch('tasks.statement_notification_task.get_token') as mock_get_token: - with patch('tasks.statement_notification_task.flags.is_on', return_value=False): - mock_get_token.return_value = 'mock_token' + with patch("tasks.statement_notification_task.publish_statement_notification") as mock_mailer: + with patch("tasks.statement_notification_task.get_token") as mock_get_token: + with patch("tasks.statement_notification_task.flags.is_on", return_value=False): + mock_get_token.return_value = "mock_token" StatementNotificationTask.send_notifications() mock_get_token.assert_called_once() mock_mailer.assert_not_called() diff --git a/jobs/payment-jobs/tests/jobs/test_statements_task.py b/jobs/payment-jobs/tests/jobs/test_statements_task.py index f4b94a0aa..6198331dc 100644 --- a/jobs/payment-jobs/tests/jobs/test_statements_task.py +++ b/jobs/payment-jobs/tests/jobs/test_statements_task.py @@ -18,8 +18,8 @@ """ from datetime import datetime, timedelta, timezone -import pytz import pytest +import pytz from freezegun import freeze_time from pay_api.models import Invoice as InvoiceModel from pay_api.models import Statement, StatementInvoices, StatementSettings, db @@ -33,11 +33,18 @@ from tasks.statement_task import StatementTask from .factory import ( - factory_create_account, factory_eft_account_payload, factory_invoice, factory_invoice_reference, - factory_pad_account_payload, factory_payment, factory_premium_payment_account, factory_statement_settings) - - -@freeze_time('2023-01-02 12:00:00T08:00:00') + factory_create_account, + factory_eft_account_payload, + factory_invoice, + factory_invoice_reference, + factory_pad_account_payload, + factory_payment, + factory_premium_payment_account, + factory_statement_settings, +) + + +@freeze_time("2023-01-02 12:00:00T08:00:00") def test_statements(session): """Test daily statement generation works. @@ -53,25 +60,18 @@ def test_statements(session): inv_ref = factory_invoice_reference(invoice_id=invoice.id) factory_payment(payment_date=previous_day, invoice_number=inv_ref.invoice_number) - factory_statement_settings( - pay_account_id=bcol_account.id, - from_date=previous_day, - frequency='DAILY' - ) + factory_statement_settings(pay_account_id=bcol_account.id, from_date=previous_day, frequency="DAILY") factory_statement_settings( pay_account_id=bcol_account.id, from_date=get_previous_day(previous_day), - frequency='DAILY' - ) - factory_statement_settings( - pay_account_id=bcol_account.id, - from_date=datetime.utcnow(), - frequency='DAILY' + frequency="DAILY", ) + factory_statement_settings(pay_account_id=bcol_account.id, from_date=datetime.utcnow(), frequency="DAILY") StatementTask.generate_statements() - statements = StatementService.get_account_statements(auth_account_id=bcol_account.auth_account_id, - page=1, limit=100) + statements = StatementService.get_account_statements( + auth_account_id=bcol_account.auth_account_id, page=1, limit=100 + ) assert statements is not None first_statement_id = statements[0][0].id invoices = StatementInvoices.find_all_invoices_for_statement(first_statement_id) @@ -80,10 +80,11 @@ def test_statements(session): # Test date override. # Override computes for the target date, not the previous date like above. - StatementTask.generate_statements([(datetime.utcnow() - timedelta(days=1)).strftime('%Y-%m-%d')]) + StatementTask.generate_statements([(datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%d")]) - statements = StatementService.get_account_statements(auth_account_id=bcol_account.auth_account_id, - page=1, limit=100) + statements = StatementService.get_account_statements( + auth_account_id=bcol_account.auth_account_id, page=1, limit=100 + ) assert statements is not None invoices = StatementInvoices.find_all_invoices_for_statement(statements[0][0].id) assert invoices is not None @@ -107,17 +108,14 @@ def test_statements_for_empty_results(session): bcol_account = factory_premium_payment_account() invoice = factory_invoice(payment_account=bcol_account, created_on=day_before_yday) inv_ref = factory_invoice_reference(invoice_id=invoice.id) - factory_statement_settings( - pay_account_id=bcol_account.id, - from_date=day_before_yday, - frequency='DAILY' - ) + factory_statement_settings(pay_account_id=bcol_account.id, from_date=day_before_yday, frequency="DAILY") factory_payment(payment_date=day_before_yday, invoice_number=inv_ref.invoice_number) StatementTask.generate_statements() - statements = StatementService.get_account_statements(auth_account_id=bcol_account.auth_account_id, - page=1, limit=100) + statements = StatementService.get_account_statements( + auth_account_id=bcol_account.auth_account_id, page=1, limit=100 + ) assert statements is not None invoices = StatementInvoices.find_all_invoices_for_statement(statements[0][0].id) assert len(invoices) == 0 @@ -128,15 +126,18 @@ def test_bcol_weekly_to_eft_statement(session): # Account set up account_create_date = datetime(2023, 10, 1, 12, 0) with freeze_time(account_create_date): - account = factory_create_account(auth_account_id='1', payment_method_code=PaymentMethod.EFT.value) + account = factory_create_account(auth_account_id="1", payment_method_code=PaymentMethod.EFT.value) assert account is not None # Setup previous payment method interim statement data invoice_create_date = localize_date(datetime(2023, 10, 9, 12, 0)) - weekly_invoice = factory_invoice(payment_account=account, created_on=invoice_create_date, - payment_method_code=PaymentMethod.DRAWDOWN.value, - status_code=InvoiceStatus.APPROVED.value, - total=50) + weekly_invoice = factory_invoice( + payment_account=account, + created_on=invoice_create_date, + payment_method_code=PaymentMethod.DRAWDOWN.value, + status_code=InvoiceStatus.APPROVED.value, + total=50, + ) assert weekly_invoice is not None @@ -148,13 +149,15 @@ def test_bcol_weekly_to_eft_statement(session): pay_account_id=account.id, from_date=statement_from_date, to_date=statement_to_date, - frequency=StatementFrequency.WEEKLY.value + frequency=StatementFrequency.WEEKLY.value, ).save() generate_date = localize_date(datetime(2023, 10, 12, 12, 0)) with freeze_time(generate_date): - weekly_statement = StatementService.generate_interim_statement(auth_account_id=account.auth_account_id, - new_frequency=StatementFrequency.MONTHLY.value) + weekly_statement = StatementService.generate_interim_statement( + auth_account_id=account.auth_account_id, + new_frequency=StatementFrequency.MONTHLY.value, + ) # Validate weekly interim invoice is correct weekly_invoices = StatementInvoices.find_all_invoices_for_statement(weekly_statement.id) @@ -177,15 +180,18 @@ def test_bcol_weekly_to_eft_statement(session): # Set up and EFT invoice # Using the same invoice create date as the weekly to test invoices on the same day with different payment methods - monthly_invoice = factory_invoice(payment_account=account, created_on=invoice_create_date, - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.APPROVED.value, - total=50) + monthly_invoice = factory_invoice( + payment_account=account, + created_on=invoice_create_date, + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.APPROVED.value, + total=50, + ) assert monthly_invoice is not None # Regenerate monthly statement using date override - it will clean up the previous empty monthly statement first - StatementTask.generate_statements([(generate_date - timedelta(days=1)).strftime('%Y-%m-%d')]) + StatementTask.generate_statements([(generate_date - timedelta(days=1)).strftime("%Y-%m-%d")]) statements = StatementService.get_account_statements(auth_account_id=account.auth_account_id, page=1, limit=100) @@ -203,21 +209,27 @@ def test_bcol_monthly_to_eft_statement(session): # Account set up account_create_date = datetime(2023, 10, 1, 12, 0) with freeze_time(account_create_date): - account = factory_create_account(auth_account_id='1', payment_method_code=PaymentMethod.EFT.value) + account = factory_create_account(auth_account_id="1", payment_method_code=PaymentMethod.EFT.value) assert account is not None # Setup previous payment method interim statement data invoice_create_date = localize_date(datetime(2023, 10, 9, 12, 0)) - bcol_invoice = factory_invoice(payment_account=account, created_on=invoice_create_date, - payment_method_code=PaymentMethod.DRAWDOWN.value, - status_code=InvoiceStatus.APPROVED.value, - total=50) + bcol_invoice = factory_invoice( + payment_account=account, + created_on=invoice_create_date, + payment_method_code=PaymentMethod.DRAWDOWN.value, + status_code=InvoiceStatus.APPROVED.value, + total=50, + ) assert bcol_invoice is not None - direct_pay_invoice = factory_invoice(payment_account=account, created_on=invoice_create_date, - payment_method_code=PaymentMethod.DIRECT_PAY.value, - status_code=InvoiceStatus.APPROVED.value, - total=50) + direct_pay_invoice = factory_invoice( + payment_account=account, + created_on=invoice_create_date, + payment_method_code=PaymentMethod.DIRECT_PAY.value, + status_code=InvoiceStatus.APPROVED.value, + total=50, + ) assert direct_pay_invoice statement_from_date = localize_date(datetime(2023, 10, 1, 12, 0)) @@ -228,14 +240,15 @@ def test_bcol_monthly_to_eft_statement(session): pay_account_id=account.id, from_date=statement_from_date, to_date=statement_to_date, - frequency=StatementFrequency.MONTHLY.value + frequency=StatementFrequency.MONTHLY.value, ).save() generate_date = localize_date(datetime(2023, 10, 12, 12, 0)) with freeze_time(generate_date): - bcol_monthly_statement = StatementService\ - .generate_interim_statement(auth_account_id=account.auth_account_id, - new_frequency=StatementFrequency.MONTHLY.value) + bcol_monthly_statement = StatementService.generate_interim_statement( + auth_account_id=account.auth_account_id, + new_frequency=StatementFrequency.MONTHLY.value, + ) account.payment_method_code = PaymentMethod.EFT.value account.save() @@ -258,28 +271,36 @@ def test_bcol_monthly_to_eft_statement(session): first_statement_id = statements[0][0].id # Test invoices existing and payment_account.payment_method_code fallback. assert statements[0][0].payment_methods == PaymentMethod.EFT.value - assert statements[0][1].payment_methods in [f'{PaymentMethod.DIRECT_PAY.value},{PaymentMethod.DRAWDOWN.value}', - f'{PaymentMethod.DRAWDOWN.value},{PaymentMethod.DIRECT_PAY.value}'] + assert statements[0][1].payment_methods in [ + f"{PaymentMethod.DIRECT_PAY.value},{PaymentMethod.DRAWDOWN.value}", + f"{PaymentMethod.DRAWDOWN.value},{PaymentMethod.DIRECT_PAY.value}", + ] monthly_invoices = StatementInvoices.find_all_invoices_for_statement(first_statement_id) assert len(monthly_invoices) == 0 # Set up and EFT invoice # Using the same invoice create date as the weekly to test invoices on the same day with different payment methods - monthly_invoice = factory_invoice(payment_account=account, created_on=invoice_create_date, - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.APPROVED.value, - total=50) + monthly_invoice = factory_invoice( + payment_account=account, + created_on=invoice_create_date, + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.APPROVED.value, + total=50, + ) assert monthly_invoice is not None # This should get ignored. - monthly_invoice_2 = factory_invoice(payment_account=account, created_on=invoice_create_date, - payment_method_code=PaymentMethod.DIRECT_PAY.value, - status_code=InvoiceStatus.APPROVED.value, - total=50) + monthly_invoice_2 = factory_invoice( + payment_account=account, + created_on=invoice_create_date, + payment_method_code=PaymentMethod.DIRECT_PAY.value, + status_code=InvoiceStatus.APPROVED.value, + total=50, + ) assert monthly_invoice_2 # Regenerate monthly statement using date override - it will clean up the previous empty monthly statement first - StatementTask.generate_statements([(generate_date - timedelta(days=1)).strftime('%Y-%m-%d')]) + StatementTask.generate_statements([(generate_date - timedelta(days=1)).strftime("%Y-%m-%d")]) statements = StatementService.get_account_statements(auth_account_id=account.auth_account_id, page=1, limit=100) @@ -292,7 +313,7 @@ def test_bcol_monthly_to_eft_statement(session): assert monthly_invoices[0].invoice_id == monthly_invoice.id # This should be EFT only, because there's a filter in the jobs that looks only for EFT invoices if # payment_account is set to EFT. - assert statements[0][0].payment_methods == f'{PaymentMethod.EFT.value}' + assert statements[0][0].payment_methods == f"{PaymentMethod.EFT.value}" # Validate bcol monthly interim invoice is correct bcol_invoices = StatementInvoices.find_all_invoices_for_statement(bcol_monthly_statement.id) @@ -304,149 +325,160 @@ def test_bcol_monthly_to_eft_statement(session): def test_many_statements(): """Ensure many statements work over 65535 statements.""" - account = factory_create_account(auth_account_id='1') + account = factory_create_account(auth_account_id="1") factory_statement_settings( pay_account_id=account.id, from_date=datetime(2024, 1, 1, 8), to_date=datetime(2024, 1, 4, 8), - frequency=StatementFrequency.DAILY.value + frequency=StatementFrequency.DAILY.value, ).save() invoice = factory_invoice(account) statement_list = [] for _ in range(0, 70000): - statement_list.append({'created_on': '2024-01-01', - 'from_date': '2024-01-01 08:00:00', - 'to_date': '2024-01-04 08:00:00', - 'payment_account_id': f'{account.id}', - 'frequency': StatementFrequency.DAILY.value - }) + statement_list.append( + { + "created_on": "2024-01-01", + "from_date": "2024-01-01 08:00:00", + "to_date": "2024-01-04 08:00:00", + "payment_account_id": f"{account.id}", + "frequency": StatementFrequency.DAILY.value, + } + ) db.session.execute(insert(Statement), statement_list) statement = db.session.query(Statement).first() statement_invoices_list = [] for _ in range(0, 70000): - statement_invoices_list.append({ - 'statement_id': statement.id, - 'invoice_id': invoice.id - }) + statement_invoices_list.append({"statement_id": statement.id, "invoice_id": invoice.id}) db.session.execute(insert(StatementInvoices), statement_invoices_list) - StatementTask.generate_statements([datetime(2024, 1, 1, 8).strftime('%Y-%m-%d')]) + StatementTask.generate_statements([datetime(2024, 1, 1, 8).strftime("%Y-%m-%d")]) assert True -@pytest.mark.parametrize('test_name', [('interim_overlap'), ('non_interim'), ('pad_to_eft'), ('eft_to_pad')]) +@pytest.mark.parametrize("test_name", [("interim_overlap"), ("non_interim"), ("pad_to_eft"), ("eft_to_pad")]) def test_gap_statements(session, test_name, admin_users_mock): """Ensure gap statements are generated for weekly to monthly.""" account_create_date = datetime(2024, 1, 1, 8) - if test_name == 'interim_overlap': + if test_name == "interim_overlap": account_create_date = datetime(2024, 8, 18, 15, 0) account = None invoice_ids = [] with freeze_time(account_create_date): match test_name: - case 'eft_to_pad': - account = factory_create_account(auth_account_id='1', payment_method_code=PaymentMethod.EFT.value) + case "eft_to_pad": + account = factory_create_account(auth_account_id="1", payment_method_code=PaymentMethod.EFT.value) from_date = (localize_date(account_create_date)).date() - case 'interim_overlap': - account = factory_create_account(auth_account_id='1', payment_method_code=PaymentMethod.PAD.value) + case "interim_overlap": + account = factory_create_account(auth_account_id="1", payment_method_code=PaymentMethod.PAD.value) from_date = (localize_date(datetime(2024, 8, 22, 15, 0))).date() case _: - account = factory_create_account(auth_account_id='1', payment_method_code=PaymentMethod.PAD.value) + account = factory_create_account(auth_account_id="1", payment_method_code=PaymentMethod.PAD.value) from_date = (localize_date(account_create_date)).date() StatementInvoices.query.delete() Statement.query.delete() StatementSettings.query.delete() InvoiceModel.query.delete() - frequency = StatementFrequency.MONTHLY.value if test_name == 'eft_to_pad' else StatementFrequency.WEEKLY.value - factory_statement_settings(pay_account_id=account.id, - frequency=frequency, - from_date=from_date - ).save() + frequency = StatementFrequency.MONTHLY.value if test_name == "eft_to_pad" else StatementFrequency.WEEKLY.value + factory_statement_settings(pay_account_id=account.id, frequency=frequency, from_date=from_date).save() match test_name: - case 'interim_overlap': - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.PAD.value, - status_code=InvoiceStatus.PAID.value, - total=31.50, - created_on=datetime(2024, 8, 19, 15, 15)).save() + case "interim_overlap": + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.PAD.value, + status_code=InvoiceStatus.PAID.value, + total=31.50, + created_on=datetime(2024, 8, 19, 15, 15), + ).save() invoice_ids.append(inv.id) - case 'non_interim': + case "non_interim": for i in range(0, 31): - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.PAD.value, - status_code=InvoiceStatus.PAID.value, - total=50, - created_on=account_create_date + timedelta(i)) \ - .save() + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.PAD.value, + status_code=InvoiceStatus.PAID.value, + total=50, + created_on=account_create_date + timedelta(i), + ).save() invoice_ids.append(inv.id) - case 'pad_to_eft': + case "pad_to_eft": for i in range(0, 28): - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.PAD.value, - status_code=InvoiceStatus.PAID.value, - total=50, - created_on=account_create_date + timedelta(i)) \ - .save() + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.PAD.value, + status_code=InvoiceStatus.PAID.value, + total=50, + created_on=account_create_date + timedelta(i), + ).save() invoice_ids.append(inv.id) # Overlap an EFT invoice and PAD on the same day. for i in range(28, 31): - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.PAID.value, - total=50, - created_on=account_create_date + timedelta(i)) \ - .save() + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.PAID.value, + total=50, + created_on=account_create_date + timedelta(i), + ).save() invoice_ids.append(inv.id) - case 'eft_to_pad': + case "eft_to_pad": for i in range(0, 28): - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.PAID.value, - total=50, - created_on=account_create_date + timedelta(i)) \ - .save() + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.PAID.value, + total=50, + created_on=account_create_date + timedelta(i), + ).save() invoice_ids.append(inv.id) # Overlap an EFT invoice and PAD on the same day. for i in range(28, 31): - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.PAD.value, - status_code=InvoiceStatus.PAID.value, - total=50, - created_on=account_create_date + timedelta(i)) \ - .save() + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.PAD.value, + status_code=InvoiceStatus.PAID.value, + total=50, + created_on=account_create_date + timedelta(i), + ).save() invoice_ids.append(inv.id) match test_name: - case 'interim_overlap': + case "interim_overlap": with freeze_time(localize_date(datetime(2024, 8, 22, 15))): - payload = factory_eft_account_payload(payment_method=PaymentMethod.EFT.value, - account_id=account.auth_account_id) + payload = factory_eft_account_payload( + payment_method=PaymentMethod.EFT.value, + account_id=account.auth_account_id, + ) PaymentAccountService.update(account.auth_account_id, payload) - inv = factory_invoice(payment_account=account, - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.PAID.value, - total=31.50, - created_on=datetime(2024, 8, 22, 15, 15)).save() + inv = factory_invoice( + payment_account=account, + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.PAID.value, + total=31.50, + created_on=datetime(2024, 8, 22, 15, 15), + ).save() invoice_ids.append(inv.id) # Intentional to generate the statements after the interim statement already exists. generate_statements(0, 32, override_start=datetime(2024, 8, 1, 15)) - case 'non_interim': + case "non_interim": with freeze_time(datetime(2024, 1, 1, 8)): # This should create a gap between 28th Sunday and 31st Wednesday, this is a gap statement. - StatementSettingsService.update_statement_settings(account.auth_account_id, - StatementFrequency.MONTHLY.value) + StatementSettingsService.update_statement_settings( + account.auth_account_id, StatementFrequency.MONTHLY.value + ) generate_statements(0, 32) - case 'pad_to_eft': + case "pad_to_eft": # Note: This will work even if we start off as MONTHLY or change to MONTHLY from weekly. # Generate up to the 28th before the interm statment. generate_statements(0, 28) with freeze_time(localize_date(datetime(2024, 1, 28, 8))): - payload = factory_eft_account_payload(payment_method=PaymentMethod.EFT.value, - account_id=account.auth_account_id) + payload = factory_eft_account_payload( + payment_method=PaymentMethod.EFT.value, + account_id=account.auth_account_id, + ) PaymentAccountService.update(account.auth_account_id, payload) generate_statements(29, 32) - case 'eft_to_pad': + case "eft_to_pad": # Generate up to the 28th before the interm statment. generate_statements(0, 28) with freeze_time(localize_date(datetime(2024, 1, 28, 8))): @@ -460,8 +492,8 @@ def test_gap_statements(session, test_name, admin_users_mock): statements = Statement.query.all() for statement in statements: - assert statement.payment_methods != 'PAD,EFT' - assert statement.payment_methods != 'EFT,PAD' + assert statement.payment_methods != "PAD,EFT" + assert statement.payment_methods != "EFT,PAD" weekly_statements = Statement.query.filter(StatementFrequency.WEEKLY.value == Statement.frequency).all() sorted_statements = sorted(weekly_statements, key=lambda x: x.from_date) @@ -469,14 +501,15 @@ def test_gap_statements(session, test_name, admin_users_mock): # Monthly can overlap with weekly, think of switching from PAD -> EFT on the Jan 24th. # we'd still need to generate EFT for the entire month of January 1 -> 31st. # Ensure weekly doesn't overlap with other weekly (interim or gap or weekly). - assert prev.to_date < current.from_date, \ - f'Overlap detected between weekly/gap/interim statements {prev.id} - {current.id}' + assert ( + prev.to_date < current.from_date + ), f"Overlap detected between weekly/gap/interim statements {prev.id} - {current.id}" monthly_statements = Statement.query.filter(StatementFrequency.MONTHLY.value == Statement.frequency).all() sorted_statements = sorted(monthly_statements, key=lambda x: x.from_date) for prev, current in zip(sorted_statements, sorted_statements[1:]): # Monthly should never overlap with monthly. - assert prev.to_date < current.from_date, f'Overlap detected between monthly statements {prev.id} - {current.id}' + assert prev.to_date < current.from_date, f"Overlap detected between monthly statements {prev.id} - {current.id}" generated_invoice_ids = [inv.invoice_id for inv in StatementInvoices.query.all()] @@ -486,12 +519,12 @@ def test_gap_statements(session, test_name, admin_users_mock): def generate_statements(start, end, override_start=None): """Generate statements helper.""" for r in range(start, end): - target = (override_start or datetime(2024, 1, 1, 8)) - override_date = localize_date(target + timedelta(days=r - 1)).strftime('%Y-%m-%d') + target = override_start or datetime(2024, 1, 1, 8) + override_date = localize_date(target + timedelta(days=r - 1)).strftime("%Y-%m-%d") StatementTask.generate_statements([override_date]) def localize_date(date: datetime): """Localize date object by adding timezone information.""" - pst = pytz.timezone('America/Vancouver') + pst = pytz.timezone("America/Vancouver") return pst.localize(date) diff --git a/jobs/payment-jobs/tests/jobs/test_unpaid_invoice_notifytask.py b/jobs/payment-jobs/tests/jobs/test_unpaid_invoice_notifytask.py index ef338ce45..0fcfbb411 100644 --- a/jobs/payment-jobs/tests/jobs/test_unpaid_invoice_notifytask.py +++ b/jobs/payment-jobs/tests/jobs/test_unpaid_invoice_notifytask.py @@ -34,40 +34,46 @@ def test_unpaid_one_invoice(session): """Assert events are being sent.""" # Create an account and an invoice for the account - account = factory_create_online_banking_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value, - cfs_account='1111') + account = factory_create_online_banking_account( + auth_account_id="1", status=CfsAccountStatus.ACTIVE.value, cfs_account="1111" + ) # Create an invoice for this account cfs_account = CfsAccountModel.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) - invoice = factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=10, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + invoice = factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=10, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) assert invoice.invoice_status_code == InvoiceStatus.CREATED.value # invoke today ;no mail - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() mock_mailer.assert_not_called() - time_delay = current_app.config['NOTIFY_AFTER_DAYS'] + time_delay = current_app.config["NOTIFY_AFTER_DAYS"] # invoke one day before the time delay ;shud be no mail day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=(time_delay - 1)) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() mock_mailer.assert_not_called() # exact day , mail shud be invoked day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() mock_mailer.assert_called() # after the time delay day ;shud not get sent day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay + 1) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() mock_mailer.assert_not_called() @@ -75,34 +81,50 @@ def test_unpaid_one_invoice(session): def test_unpaid_multiple_invoice(session): """Assert events are being sent.""" # Create an account and an invoice for the account - account = factory_create_online_banking_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value, - cfs_account='1111') + account = factory_create_online_banking_account( + auth_account_id="1", status=CfsAccountStatus.ACTIVE.value, cfs_account="1111" + ) # Create an invoice for this account cfs_account = CfsAccountModel.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) - invoice = factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=10, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + invoice = factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=10, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) assert invoice.invoice_status_code == InvoiceStatus.CREATED.value - factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=200, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=200, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) - factory_invoice(payment_account=account, created_on=previous_day, total=2000, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + factory_invoice( + payment_account=account, + created_on=previous_day, + total=2000, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) # created two invoices ; so two events - time_delay = current_app.config['NOTIFY_AFTER_DAYS'] + time_delay = current_app.config["NOTIFY_AFTER_DAYS"] day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() assert mock_mailer.call_count == 1 # created one invoice yesterday ; so assert one day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay - 1) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() assert mock_mailer.call_count == 1 @@ -110,19 +132,23 @@ def test_unpaid_multiple_invoice(session): def test_unpaid_invoice_pad(session): """Assert events are being sent.""" # Create an account and an invoice for the account - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) # Create an invoice for this account cfs_account = CfsAccountModel.find_effective_by_payment_method(account.id, PaymentMethod.PAD.value) - invoice = factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=10, - cfs_account_id=cfs_account.id) + invoice = factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=10, + cfs_account_id=cfs_account.id, + ) assert invoice.invoice_status_code == InvoiceStatus.CREATED.value # invoke today ;no mail - time_delay = current_app.config['NOTIFY_AFTER_DAYS'] + time_delay = current_app.config["NOTIFY_AFTER_DAYS"] day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() mock_mailer.assert_not_called() @@ -130,44 +156,56 @@ def test_unpaid_invoice_pad(session): def test_unpaid_single_invoice_total(session): """Assert events are being sent.""" # Create an account and an invoice for the account - account = factory_create_online_banking_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value, - cfs_account='1111') + account = factory_create_online_banking_account( + auth_account_id="1", status=CfsAccountStatus.ACTIVE.value, cfs_account="1111" + ) # Create an invoice for this account cfs_account = CfsAccountModel.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) # invoice amount total_invoice1 = 100 total_invoice2 = 200 - invoice = factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=total_invoice1, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + invoice = factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=total_invoice1, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) assert invoice.invoice_status_code == InvoiceStatus.CREATED.value previous_day = datetime.now(tz=timezone.utc) - timedelta(days=1) - factory_invoice(payment_account=account, created_on=previous_day, total=total_invoice2, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + factory_invoice( + payment_account=account, + created_on=previous_day, + total=total_invoice2, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) # created two invoices ; so two events - time_delay = current_app.config['NOTIFY_AFTER_DAYS'] + time_delay = current_app.config["NOTIFY_AFTER_DAYS"] day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() - assert mock_mailer.call_args.args[2].get('transactionAmount') == total_invoice1 + total_invoice2 + assert mock_mailer.call_args.args[2].get("transactionAmount") == total_invoice1 + total_invoice2 # created one invoice yesterday ; so assert one day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay - 1) with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() assert mock_mailer.call_count == 1 - assert mock_mailer.call_args.args[2].get('transactionAmount') == total_invoice1 + total_invoice2 + assert mock_mailer.call_args.args[2].get("transactionAmount") == total_invoice1 + total_invoice2 def test_unpaid_multiple_invoice_total(session): """Assert events are being sent.""" # Create an account and an invoice for the account - account = factory_create_online_banking_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value, - cfs_account='1111') + account = factory_create_online_banking_account( + auth_account_id="1", status=CfsAccountStatus.ACTIVE.value, cfs_account="1111" + ) # Create an invoice for this account cfs_account = CfsAccountModel.find_effective_by_payment_method(account.id, PaymentMethod.ONLINE_BANKING.value) # invoice amount @@ -175,32 +213,46 @@ def test_unpaid_multiple_invoice_total(session): total_invoice2 = 200 total_invoice3 = 300 - invoice = factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=total_invoice1, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + invoice = factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=total_invoice1, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) assert invoice.invoice_status_code == InvoiceStatus.CREATED.value - factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc), total=total_invoice2, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc), + total=total_invoice2, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) # this is future invoice - factory_invoice(payment_account=account, created_on=datetime.now(tz=timezone.utc) + timedelta(days=1), - total=total_invoice3, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, cfs_account_id=cfs_account.id) + factory_invoice( + payment_account=account, + created_on=datetime.now(tz=timezone.utc) + timedelta(days=1), + total=total_invoice3, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + cfs_account_id=cfs_account.id, + ) # created two invoices ; so two events - time_delay = current_app.config['NOTIFY_AFTER_DAYS'] + time_delay = current_app.config["NOTIFY_AFTER_DAYS"] day_after_time_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay) total_amount = total_invoice1 + total_invoice2 + total_invoice3 # total amount is the same for any day invocation with freeze_time(day_after_time_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() assert mock_mailer.call_count == 1 - assert mock_mailer.call_args.args[2].get('transactionAmount') == total_amount + assert mock_mailer.call_args.args[2].get("transactionAmount") == total_amount one_more_day_delay = datetime.now(tz=timezone.utc) + timedelta(days=time_delay + 1) with freeze_time(one_more_day_delay): - with patch.object(mailer, 'publish_mailer_events') as mock_mailer: + with patch.object(mailer, "publish_mailer_events") as mock_mailer: UnpaidInvoiceNotifyTask.notify_unpaid_invoices() assert mock_mailer.call_count == 1 - assert mock_mailer.call_args.args[2].get('transactionAmount') == total_amount + assert mock_mailer.call_args.args[2].get("transactionAmount") == total_amount diff --git a/jobs/payment-jobs/tests/services/test_flags.py b/jobs/payment-jobs/tests/services/test_flags.py index ca96cfd5b..252f355d6 100644 --- a/jobs/payment-jobs/tests/services/test_flags.py +++ b/jobs/payment-jobs/tests/services/test_flags.py @@ -15,7 +15,6 @@ """Test-Suite to ensure that the Flag Service is working as expected.""" import pytest from flask import Flask - from pay_api.services import Flags app = None @@ -26,7 +25,7 @@ def setup(): """Initialize app with test env for testing.""" global app app = Flask(__name__) - app.env = 'testing' + app.env = "testing" def test_flags_constructor_no_app(setup): @@ -40,67 +39,70 @@ def test_flags_constructor_with_app(setup): with app.app_context(): flags = Flags(app) assert flags - assert app.extensions['featureflags'] + assert app.extensions["featureflags"] def test_init_app_dev_with_key(setup): """Ensure that extension can be initialized with a key in dev.""" - app.config['PAY_LD_SDK_KEY'] = 'https://no.flag/avail' + app.config["PAY_LD_SDK_KEY"] = "https://no.flag/avail" with app.app_context(): flags = Flags() flags.init_app(app) assert flags - assert app.extensions['featureflags'] - assert app.extensions['featureflags'].get_sdk_key() == 'https://no.flag/avail' + assert app.extensions["featureflags"] + assert app.extensions["featureflags"].get_sdk_key() == "https://no.flag/avail" def test_init_app_dev_no_key(setup): """Ensure that extension can be initialized with no key in dev.""" - app.config['PAY_LD_SDK_KEY'] = None + app.config["PAY_LD_SDK_KEY"] = None with app.app_context(): flags = Flags() flags.init_app(app) assert flags - assert app.extensions['featureflags'] + assert app.extensions["featureflags"] def test_init_app_prod_with_key(setup): """Ensure that extension can be initialized with a key in prod.""" - app.env = 'production' - app.config['PAY_LD_SDK_KEY'] = 'https://no.flag/avail' + app.env = "production" + app.config["PAY_LD_SDK_KEY"] = "https://no.flag/avail" with app.app_context(): flags = Flags() flags.init_app(app) assert flags - assert app.extensions['featureflags'] - assert app.extensions['featureflags'].get_sdk_key() == 'https://no.flag/avail' + assert app.extensions["featureflags"] + assert app.extensions["featureflags"].get_sdk_key() == "https://no.flag/avail" def test_init_app_prod_no_key(setup): """Ensure that extension can be initialized with no key in prod.""" - app.env = 'production' - app.config['PAY_LD_SDK_KEY'] = None + app.env = "production" + app.config["PAY_LD_SDK_KEY"] = None with app.app_context(): flags = Flags() flags.init_app(app) with pytest.raises(KeyError): - client = app.extensions['featureflags'] + client = app.extensions["featureflags"] assert not client assert flags -@pytest.mark.parametrize('test_name,flag_name,expected', [ - ('boolean flag', 'bool-flag', True), - ('string flag', 'string-flag', 'a string value'), - ('integer flag', 'integer-flag', 10), -]) +@pytest.mark.parametrize( + "test_name,flag_name,expected", + [ + ("boolean flag", "bool-flag", True), + ("string flag", "string-flag", "a string value"), + ("integer flag", "integer-flag", 10), + ], +) def test_flags_read_from_json(setup, test_name, flag_name, expected): """Ensure that is_on is TRUE when reading flags from local JSON file.""" - app.config['PAY_LD_SDK_KEY'] = 'https://no.flag/avail' + app.config["PAY_LD_SDK_KEY"] = "https://no.flag/avail" with app.app_context(): flags = Flags() @@ -111,24 +113,27 @@ def test_flags_read_from_json(setup, test_name, flag_name, expected): def test_flags_read_from_json_missing_flag(setup): """Ensure that is_on is FALSE when reading a flag that doesn't exist from local JSON file.""" - app.config['PAY_LD_SDK_KEY'] = 'https://no.flag/avail' + app.config["PAY_LD_SDK_KEY"] = "https://no.flag/avail" with app.app_context(): flags = Flags() flags.init_app(app) - flag_on = flags.is_on('missing flag') + flag_on = flags.is_on("missing flag") assert not flag_on -@pytest.mark.parametrize('test_name,flag_name,expected', [ - ('boolean flag', 'bool-flag', True), - ('string flag', 'string-flag', 'a string value'), - ('integer flag', 'integer-flag', 10), -]) +@pytest.mark.parametrize( + "test_name,flag_name,expected", + [ + ("boolean flag", "bool-flag", True), + ("string flag", "string-flag", "a string value"), + ("integer flag", "integer-flag", 10), + ], +) def test_flags_read_flag_values_from_json(setup, test_name, flag_name, expected): """Ensure that values read from JSON == expected values when no user is passed.""" - app.config['PAY_LD_SDK_KEY'] = 'https://no.flag/avail' + app.config["PAY_LD_SDK_KEY"] = "https://no.flag/avail" with app.app_context(): flags = Flags() diff --git a/jobs/payment-jobs/utils/auth.py b/jobs/payment-jobs/utils/auth.py index 081b06abc..a52b8b058 100644 --- a/jobs/payment-jobs/utils/auth.py +++ b/jobs/payment-jobs/utils/auth.py @@ -20,15 +20,26 @@ def get_token(): - issuer_url = current_app.config.get('JWT_OIDC_ISSUER') + issuer_url = current_app.config.get("JWT_OIDC_ISSUER") - token_url = issuer_url + '/protocol/openid-connect/token' # https://sso-dev.pathfinder.gov.bc.ca/auth/realms/fcf0kpqr/protocol/openid-connect/token + token_url = ( + issuer_url + "/protocol/openid-connect/token" + ) # https://sso-dev.pathfinder.gov.bc.ca/auth/realms/fcf0kpqr/protocol/openid-connect/token basic_auth_encoded = base64.b64encode( - bytes(current_app.config.get('KEYCLOAK_SERVICE_ACCOUNT_ID') + ':' + current_app.config.get( - 'KEYCLOAK_SERVICE_ACCOUNT_SECRET'), - 'utf-8')).decode('utf-8') - data = 'grant_type=client_credentials' - token_response = OAuthService.post(token_url, basic_auth_encoded, AuthHeaderType.BASIC, - ContentType.FORM_URL_ENCODED, data) - token = token_response.json().get('access_token') + bytes( + current_app.config.get("KEYCLOAK_SERVICE_ACCOUNT_ID") + + ":" + + current_app.config.get("KEYCLOAK_SERVICE_ACCOUNT_SECRET"), + "utf-8", + ) + ).decode("utf-8") + data = "grant_type=client_credentials" + token_response = OAuthService.post( + token_url, + basic_auth_encoded, + AuthHeaderType.BASIC, + ContentType.FORM_URL_ENCODED, + data, + ) + token = token_response.json().get("access_token") return token diff --git a/jobs/payment-jobs/utils/auth_event.py b/jobs/payment-jobs/utils/auth_event.py index 0d776fd16..b769a84c5 100644 --- a/jobs/payment-jobs/utils/auth_event.py +++ b/jobs/payment-jobs/utils/auth_event.py @@ -1,4 +1,5 @@ """Common code that sends AUTH events.""" + from flask import current_app from pay_api.models import PaymentAccount as PaymentAccountModel from pay_api.services import gcp_queue_publisher @@ -12,7 +13,7 @@ class AuthEvent: """Publishes to the auth-queue as an auth event though PUBSUB, this message gets sent to account-mailer after.""" @staticmethod - def publish_lock_account_event(pay_account: PaymentAccountModel, additional_emails=''): + def publish_lock_account_event(pay_account: PaymentAccountModel, additional_emails=""): """Publish NSF lock account event to the auth queue.""" try: payload = AuthEvent._create_event_payload(pay_account, additional_emails) @@ -21,44 +22,54 @@ def publish_lock_account_event(pay_account: PaymentAccountModel, additional_emai source=QueueSources.PAY_JOBS.value, message_type=QueueMessageTypes.NSF_LOCK_ACCOUNT.value, payload=payload, - topic=current_app.config.get('AUTH_EVENT_TOPIC') + topic=current_app.config.get("AUTH_EVENT_TOPIC"), ) ) except Exception: # NOQA pylint: disable=broad-except - current_app.logger.error('Error publishing lock event:', exc_info=True) - current_app.logger.warning(f'Notification to Queue failed for the Account { - pay_account.auth_account_id} - {pay_account.name}') - capture_message(f'Notification to Queue failed for the Account { - pay_account.auth_account_id}, {payload}.', level='error') + current_app.logger.error("Error publishing lock event:", exc_info=True) + current_app.logger.warning( + f"Notification to Queue failed for the Account { + pay_account.auth_account_id} - {pay_account.name}" + ) + capture_message( + f"Notification to Queue failed for the Account { + pay_account.auth_account_id}, {payload}.", + level="error", + ) @staticmethod def publish_unlock_account_event(payment_account: PaymentAccountModel): """Publish NSF unlock event to the auth queue.""" try: unlock_payload = { - 'accountId': payment_account.auth_account_id, - 'skipNotification': True + "accountId": payment_account.auth_account_id, + "skipNotification": True, } gcp_queue_publisher.publish_to_queue( QueueMessage( source=QueueSources.PAY_JOBS.value, message_type=QueueMessageTypes.NSF_UNLOCK_ACCOUNT.value, payload=unlock_payload, - topic=current_app.config.get('AUTH_EVENT_TOPIC') + topic=current_app.config.get("AUTH_EVENT_TOPIC"), ) ) except Exception: # NOQA pylint: disable=broad-except - current_app.logger.error('Error publishing NSF unlock event:', exc_info=True) - current_app.logger.warning(f'Notification to Queue failed for the Account { - payment_account.auth_account_id} - {payment_account.name}') - capture_message(f'Notification to Queue failed for the Account { - payment_account.auth_account_id}, {unlock_payload}.', level='error') + current_app.logger.error("Error publishing NSF unlock event:", exc_info=True) + current_app.logger.warning( + f"Notification to Queue failed for the Account { + payment_account.auth_account_id} - {payment_account.name}" + ) + capture_message( + f"Notification to Queue failed for the Account { + payment_account.auth_account_id}, {unlock_payload}.", + level="error", + ) @staticmethod - def _create_event_payload(pay_account, additional_emails=''): + def _create_event_payload(pay_account, additional_emails=""): return { - 'accountId': pay_account.auth_account_id, - 'paymentMethod': PaymentMethod.EFT.value, - 'suspensionReasonCode': SuspensionReasonCodes.OVERDUE_EFT.value, - 'additionalEmails': additional_emails + "accountId": pay_account.auth_account_id, + "paymentMethod": PaymentMethod.EFT.value, + "suspensionReasonCode": SuspensionReasonCodes.OVERDUE_EFT.value, + "additionalEmails": additional_emails, } diff --git a/jobs/payment-jobs/utils/enums.py b/jobs/payment-jobs/utils/enums.py index e81012484..4cf7e4c8b 100644 --- a/jobs/payment-jobs/utils/enums.py +++ b/jobs/payment-jobs/utils/enums.py @@ -4,6 +4,6 @@ class StatementNotificationAction(Enum): """Enum for the action to take for a statement.""" - DUE = 'due' - OVERDUE = 'overdue' - REMINDER = 'reminder' + DUE = "due" + OVERDUE = "overdue" + REMINDER = "reminder" diff --git a/jobs/payment-jobs/utils/logger.py b/jobs/payment-jobs/utils/logger.py index 8568f87dd..d593fae51 100755 --- a/jobs/payment-jobs/utils/logger.py +++ b/jobs/payment-jobs/utils/logger.py @@ -21,6 +21,6 @@ def setup_logging(conf): """Create the services logger.""" if conf and path.isfile(conf): logging.config.fileConfig(conf) - print(f'Configure logging, from conf:{conf}', file=sys.stdout) + print(f"Configure logging, from conf:{conf}", file=sys.stdout) else: - print(f'Unable to configure logging, attempted conf:{conf}', file=sys.stderr) + print(f"Unable to configure logging, attempted conf:{conf}", file=sys.stderr) diff --git a/jobs/payment-jobs/utils/mailer.py b/jobs/payment-jobs/utils/mailer.py index 30af5d151..82bc417e7 100644 --- a/jobs/payment-jobs/utils/mailer.py +++ b/jobs/payment-jobs/utils/mailer.py @@ -44,12 +44,13 @@ def publish_mailer_events(message_type: str, pay_account: PaymentAccountModel, a """Publish payment message to the mailer queue.""" # Publish message to the Queue, saying account has been activated. Using the event spec. - fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type(corp_type_code='BCR', - filing_type_code='NSF') + fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type( + corp_type_code="BCR", filing_type_code="NSF" + ) payload = { - 'accountId': pay_account.auth_account_id, - 'nsfFee': float(fee_schedule.fee.amount), - **(additional_params or {}) + "accountId": pay_account.auth_account_id, + "nsfFee": float(fee_schedule.fee.amount), + **(additional_params or {}), } try: gcp_queue_publisher.publish_to_queue( @@ -57,29 +58,39 @@ def publish_mailer_events(message_type: str, pay_account: PaymentAccountModel, a source=QueueSources.PAY_JOBS.value, message_type=message_type, payload=payload, - topic=current_app.config.get('ACCOUNT_MAILER_TOPIC') + topic=current_app.config.get("ACCOUNT_MAILER_TOPIC"), ) ) except Exception as e: # pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Account Mailer %s - %s', - pay_account.auth_account_id, - payload) - capture_message('Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.'.format( - auth_account_id=pay_account.auth_account_id, msg=payload), level='error') + current_app.logger.warning( + "Notification to Queue failed for the Account Mailer %s - %s", + pay_account.auth_account_id, + payload, + ) + capture_message( + "Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.".format( + auth_account_id=pay_account.auth_account_id, msg=payload + ), + level="error", + ) -def publish_statement_notification(pay_account: PaymentAccountModel, statement: StatementModel, - total_amount_owing: float, emails: str) -> bool: +def publish_statement_notification( + pay_account: PaymentAccountModel, + statement: StatementModel, + total_amount_owing: float, + emails: str, +) -> bool: """Publish payment statement notification message to the mailer queue.""" message_type = QueueMessageTypes.STATEMENT_NOTIFICATION.value payload = { - 'emailAddresses': emails, - 'accountId': pay_account.auth_account_id, - 'fromDate': f'{statement.from_date}', - 'toDate': f'{statement.to_date}', - 'statementFrequency': statement.frequency, - 'totalAmountOwing': total_amount_owing + "emailAddresses": emails, + "accountId": pay_account.auth_account_id, + "fromDate": f"{statement.from_date}", + "toDate": f"{statement.to_date}", + "statementFrequency": statement.frequency, + "totalAmountOwing": total_amount_owing, } try: gcp_queue_publisher.publish_to_queue( @@ -87,16 +98,22 @@ def publish_statement_notification(pay_account: PaymentAccountModel, statement: source=QueueSources.PAY_JOBS.value, message_type=message_type, payload=payload, - topic=current_app.config.get('ACCOUNT_MAILER_TOPIC') + topic=current_app.config.get("ACCOUNT_MAILER_TOPIC"), ) ) except Exception as e: # pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Account Mailer %s - %s', - pay_account.auth_account_id, - payload) - capture_message('Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.'.format( - auth_account_id=pay_account.auth_account_id, msg=payload), level='error') + current_app.logger.warning( + "Notification to Queue failed for the Account Mailer %s - %s", + pay_account.auth_account_id, + payload, + ) + capture_message( + "Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.".format( + auth_account_id=pay_account.auth_account_id, msg=payload + ), + level="error", + ) return False @@ -105,19 +122,21 @@ def publish_statement_notification(pay_account: PaymentAccountModel, statement: def publish_payment_notification(info: StatementNotificationInfo) -> bool: """Publish payment notification message to the mailer queue.""" - message_type = QueueMessageTypes.PAYMENT_DUE_NOTIFICATION.value \ - if info.action in [StatementNotificationAction.DUE, StatementNotificationAction.OVERDUE] \ + message_type = ( + QueueMessageTypes.PAYMENT_DUE_NOTIFICATION.value + if info.action in [StatementNotificationAction.DUE, StatementNotificationAction.OVERDUE] else QueueMessageTypes.PAYMENT_REMINDER_NOTIFICATION.value + ) payload = { - 'emailAddresses': info.emails, - 'accountId': info.auth_account_id, - 'dueDate': f'{info.due_date}', - 'statementFrequency': info.statement.frequency, - 'statementMonth': info.statement.from_date.strftime('%B'), - 'statementNumber': info.statement.id, - 'totalAmountOwing': info.total_amount_owing, - 'shortNameLinksCount': info.short_name_links_count + "emailAddresses": info.emails, + "accountId": info.auth_account_id, + "dueDate": f"{info.due_date}", + "statementFrequency": info.statement.frequency, + "statementMonth": info.statement.from_date.strftime("%B"), + "statementNumber": info.statement.id, + "totalAmountOwing": info.total_amount_owing, + "shortNameLinksCount": info.short_name_links_count, } try: gcp_queue_publisher.publish_to_queue( @@ -125,16 +144,22 @@ def publish_payment_notification(info: StatementNotificationInfo) -> bool: source=QueueSources.PAY_JOBS.value, message_type=message_type, payload=payload, - topic=current_app.config.get('ACCOUNT_MAILER_TOPIC') + topic=current_app.config.get("ACCOUNT_MAILER_TOPIC"), ) ) except Exception as e: # pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Account Mailer %s - %s', - info.auth_account_id, - payload) - capture_message('Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.'.format( - auth_account_id=info.auth_account_id, msg=payload), level='error') + current_app.logger.warning( + "Notification to Queue failed for the Account Mailer %s - %s", + info.auth_account_id, + payload, + ) + capture_message( + "Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.".format( + auth_account_id=info.auth_account_id, msg=payload + ), + level="error", + ) return False diff --git a/jobs/payment-jobs/utils/minio.py b/jobs/payment-jobs/utils/minio.py index 1b42584e2..c9c7c6d9f 100644 --- a/jobs/payment-jobs/utils/minio.py +++ b/jobs/payment-jobs/utils/minio.py @@ -21,16 +21,25 @@ def put_object(value_as_bytes, file_name: str, file_size: int = 0): """Return a pre-signed URL for new doc upload.""" - current_app.logger.debug(f'Creating pre-signed URL for {file_name}') + current_app.logger.debug(f"Creating pre-signed URL for {file_name}") minio_client: Minio = _get_client() value_as_stream = io.BytesIO(value_as_bytes) - minio_client.put_object(current_app.config.get('MINIO_BUCKET_NAME'), file_name, value_as_stream, file_size) + minio_client.put_object( + current_app.config.get("MINIO_BUCKET_NAME"), + file_name, + value_as_stream, + file_size, + ) def _get_client() -> Minio: """Return a minio client.""" - minio_endpoint = current_app.config.get('MINIO_ENDPOINT') - minio_key = current_app.config.get('MINIO_ACCESS_KEY') - minio_secret = current_app.config.get('MINIO_ACCESS_SECRET') - return Minio(minio_endpoint, access_key=minio_key, secret_key=minio_secret, - secure=current_app.config.get('MINIO_SECURE')) \ No newline at end of file + minio_endpoint = current_app.config.get("MINIO_ENDPOINT") + minio_key = current_app.config.get("MINIO_ACCESS_KEY") + minio_secret = current_app.config.get("MINIO_ACCESS_SECRET") + return Minio( + minio_endpoint, + access_key=minio_key, + secret_key=minio_secret, + secure=current_app.config.get("MINIO_SECURE"), + ) diff --git a/jobs/payment-jobs/utils/sftp.py b/jobs/payment-jobs/utils/sftp.py index daa112bf6..42bcf57ad 100644 --- a/jobs/payment-jobs/utils/sftp.py +++ b/jobs/payment-jobs/utils/sftp.py @@ -16,39 +16,39 @@ import paramiko from flask import current_app -from pysftp import Connection, CnOpts +from pysftp import CnOpts, Connection def upload_to_ftp(local_file_path: str, trg_file_path: str): """Upload files to sftp.""" config = current_app.config - sftp_host: str = config.get('CGI_SFTP_HOST') + sftp_host: str = config.get("CGI_SFTP_HOST") cnopts = CnOpts() # only for local development set this to false . - if config.get('CGI_SFTP_VERIFY_HOST').lower() == 'false': + if config.get("CGI_SFTP_VERIFY_HOST").lower() == "false": cnopts.hostkeys = None else: - ftp_host_key_data = config.get('CGI_SFTP_HOST_KEY').encode() + ftp_host_key_data = config.get("CGI_SFTP_HOST_KEY").encode() key = paramiko.RSAKey(data=decodebytes(ftp_host_key_data)) - cnopts.hostkeys.add(sftp_host, 'ssh-rsa', key) + cnopts.hostkeys.add(sftp_host, "ssh-rsa", key) - sftp_port: int = config.get('CGI_SFTP_PORT') + sftp_port: int = config.get("CGI_SFTP_PORT") sftp_credentials = { - 'username': config.get('CGI_SFTP_USERNAME'), + "username": config.get("CGI_SFTP_USERNAME"), # private_key should be the absolute path to where private key file lies since sftp - 'private_key': config.get('BCREG_CGI_FTP_PRIVATE_KEY_LOCATION'), - 'private_key_pass': config.get('BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE') + "private_key": config.get("BCREG_CGI_FTP_PRIVATE_KEY_LOCATION"), + "private_key_pass": config.get("BCREG_CGI_FTP_PRIVATE_KEY_PASSPHRASE"), } # to support local testing. SFTP CAS server should run in private key mode - if password := config.get('CGI_SFTP_PASSWORD'): - sftp_credentials['password'] = password + if password := config.get("CGI_SFTP_PASSWORD"): + sftp_credentials["password"] = password with Connection(host=sftp_host, **sftp_credentials, cnopts=cnopts, port=sftp_port) as sftp_connection: - current_app.logger.debug('sftp_connection successful') - with sftp_connection.cd(config.get('CGI_SFTP_DIRECTORY')): + current_app.logger.debug("sftp_connection successful") + with sftp_connection.cd(config.get("CGI_SFTP_DIRECTORY")): sftp_connection.put(local_file_path) # Now upload trg file sftp_connection.put(trg_file_path) sftp_connection.close() - current_app.logger.debug('File upload complete') + current_app.logger.debug("File upload complete") From d1291c29bb315d61df2a46c76a3514eae33dff4f Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:16:42 -0700 Subject: [PATCH 04/10] pay-jobs --- jobs/payment-jobs/tasks/statement_notification_task.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jobs/payment-jobs/tasks/statement_notification_task.py b/jobs/payment-jobs/tasks/statement_notification_task.py index 8589fb4d6..730aaaed2 100644 --- a/jobs/payment-jobs/tasks/statement_notification_task.py +++ b/jobs/payment-jobs/tasks/statement_notification_task.py @@ -55,8 +55,9 @@ def send_notifications(cls): current_app.logger.info(f"{statement_len} Statements with Pending notifications Found!") token = get_token() + image_name = current_app.config.get("REGISTRIES_LOGO_IMAGE_NAME") params = { - "logo_url": f"{current_app.config.get('AUTH_WEB_URL')}/{current_app.config.get('REGISTRIES_LOGO_IMAGE_NAME')}", + "logo_url": f"{current_app.config.get('AUTH_WEB_URL')}/{image_name}", "url": f"{current_app.config.get('AUTH_WEB_URL')}", } template = ENV.get_template("statement_notification.html") From 9a8ea2d1b43c62d690d507c1c9d50b6d92e41784 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:18:48 -0700 Subject: [PATCH 05/10] pay-admin --- pay-admin/Makefile | 18 ++- pay-admin/admin/__init__.py | 23 ++-- pay-admin/admin/config.py | 75 +++++++------ pay-admin/admin/keycloak.py | 13 ++- pay-admin/admin/version.py | 2 +- pay-admin/admin/views/code.py | 7 +- pay-admin/admin/views/corp_type.py | 45 ++++---- pay-admin/admin/views/distribution_code.py | 81 +++++++++----- pay-admin/admin/views/fee_code.py | 6 +- pay-admin/admin/views/fee_schedule.py | 71 ++++++++---- pay-admin/admin/views/index.py | 1 + pay-admin/admin/views/secured_view.py | 37 +++++-- pay-admin/gunicorn_config.py | 9 +- pay-admin/poetry.lock | 84 +++++++++++++- pay-admin/pyproject.toml | 123 +++++++++++++++++++++ pay-admin/setup.py | 5 +- pay-admin/tests/conftest.py | 6 +- pay-admin/tests/fake_oidc.py | 6 +- pay-admin/tests/unit/test_views.py | 17 +-- pay-admin/wsgi.py | 4 +- 20 files changed, 463 insertions(+), 170 deletions(-) diff --git a/pay-admin/Makefile b/pay-admin/Makefile index ecefc600b..0f086ab65 100755 --- a/pay-admin/Makefile +++ b/pay-admin/Makefile @@ -45,15 +45,27 @@ install: clean ################################################################################# # COMMANDS - CI # ################################################################################# -ci: lint flake8 test ## CI flow +ci: isort-ci black-ci lint flake8 test ## CI flow + +isort: + poetry run isort . + +isort-ci: + poetry run isort --check . + +black: ## Linting with black + poetry run black . + +black-ci: + poetry run black --check . pylint: ## Linting with pylint - poetry run pylint --rcfile=setup.cfg $(PROJECT_NAME) + poetry run pylint $(PROJECT_NAME) flake8: ## Linting with flake8 poetry run flake8 $(PROJECT_NAME) tests -lint: pylint flake8 ## run all lint type scripts +lint: isort black pylint flake8 ## run all lint type scripts test: ## Unit testing poetry run pytest diff --git a/pay-admin/admin/__init__.py b/pay-admin/admin/__init__.py index feb0c1fb0..767e55a78 100755 --- a/pay-admin/admin/__init__.py +++ b/pay-admin/admin/__init__.py @@ -31,42 +31,41 @@ from .keycloak import Keycloak +setup_logging(os.path.join(_Config.PROJECT_ROOT, "logging.conf")) -setup_logging(os.path.join(_Config.PROJECT_ROOT, 'logging.conf')) - -def create_app(run_mode=os.getenv('DEPLOYMENT_ENV', 'production')): +def create_app(run_mode=os.getenv("DEPLOYMENT_ENV", "production")): """Return a configured Flask App using the Factory method.""" app = Flask(__name__) app.config.from_object(config.CONFIGURATION[run_mode]) - app.logger.info('init db.') + app.logger.info("init db.") db.init_app(app) ma.init_app(app) - app.logger.info('init flask admin.') + app.logger.info("init flask admin.") init_flask_admin(app) - app.logger.info('init cache.') + app.logger.info("init cache.") Cache(app) - app.logger.info('init session.') + app.logger.info("init session.") Session(app) - app.logger.info('init keycloak.') + app.logger.info("init keycloak.") Keycloak(app) - @app.route('/') + @app.route("/") def index(): - return redirect('/admin/feecode/') + return redirect("/admin/feecode/") - app.logger.info('create_app is complete.') + app.logger.info("create_app is complete.") return app def init_flask_admin(app): """Initialize flask admin and it's views.""" - flask_admin = Admin(app, name='Fee Admin', template_mode='bootstrap4', index_view=IndexView()) + flask_admin = Admin(app, name="Fee Admin", template_mode="bootstrap4", index_view=IndexView()) flask_admin.add_view(FeeCodeView) flask_admin.add_view(CorpTypeView) flask_admin.add_view(CodeConfig(FilingType, db.session)) diff --git a/pay-admin/admin/config.py b/pay-admin/admin/config.py index a4aa3a7c7..afc91f229 100755 --- a/pay-admin/admin/config.py +++ b/pay-admin/admin/config.py @@ -25,28 +25,27 @@ from cachelib.file import FileSystemCache from dotenv import find_dotenv, load_dotenv - # this will load all the envars from a .env file located in the project root (api) load_dotenv(find_dotenv()) CONFIGURATION = { - 'development': 'admin.config.DevConfig', - 'testing': 'admin.config.TestConfig', - 'production': 'admin.config.ProdConfig', - 'default': 'admin.config.ProdConfig' + "development": "admin.config.DevConfig", + "testing": "admin.config.TestConfig", + "production": "admin.config.ProdConfig", + "default": "admin.config.ProdConfig", } -def get_named_config(config_name: str = 'production'): +def get_named_config(config_name: str = "production"): """Return the configuration object based on the name. :raise: KeyError: if an unknown configuration is requested """ - if config_name in ['production', 'staging', 'default']: + if config_name in ["production", "staging", "default"]: config = ProdConfig() - elif config_name == 'testing': + elif config_name == "testing": config = TestConfig() - elif config_name == 'development': + elif config_name == "development": config = DevConfig() else: raise KeyError(f"Unknown configuration '{config_name}'") @@ -55,48 +54,48 @@ def get_named_config(config_name: str = 'production'): def _get_config(config_key: str, **kwargs): """Get the config from environment, and throw error if there are no default values and if the value is None.""" - if 'default' in kwargs: - value = os.getenv(config_key, kwargs.get('default')) + if "default" in kwargs: + value = os.getenv(config_key, kwargs.get("default")) else: value = os.getenv(config_key) return value -class _Config(): # pylint: disable=too-few-public-methods +class _Config: # pylint: disable=too-few-public-methods """Base class configuration that should set reasonable defaults for all the other configurations.""" PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - SECRET_KEY = 'my secret' + SECRET_KEY = "my secret" SQLALCHEMY_TRACK_MODIFICATIONS = False # POSTGRESQL - DB_USER = _get_config('DATABASE_USERNAME') - DB_PASSWORD = _get_config('DATABASE_PASSWORD') - DB_NAME = _get_config('DATABASE_NAME') - DB_HOST = _get_config('DATABASE_HOST') - DB_PORT = _get_config('DATABASE_PORT', default='5432') - if DB_UNIX_SOCKET := os.getenv('DATABASE_UNIX_SOCKET', None): + DB_USER = _get_config("DATABASE_USERNAME") + DB_PASSWORD = _get_config("DATABASE_PASSWORD") + DB_NAME = _get_config("DATABASE_NAME") + DB_HOST = _get_config("DATABASE_HOST") + DB_PORT = _get_config("DATABASE_PORT", default="5432") + if DB_UNIX_SOCKET := os.getenv("DATABASE_UNIX_SOCKET", None): SQLALCHEMY_DATABASE_URI = ( - f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?unix_sock={DB_UNIX_SOCKET}/.s.PGSQL.5432' + f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?unix_sock={DB_UNIX_SOCKET}/.s.PGSQL.5432" ) else: - SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' - SQLALCHEMY_ECHO = _get_config('SQLALCHEMY_ECHO', default='False').lower() == 'true' + SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}" + SQLALCHEMY_ECHO = _get_config("SQLALCHEMY_ECHO", default="False").lower() == "true" # Normal Keycloak parameters. - OIDC_CLIENT_SECRETS = os.getenv('PAY_OIDC_CLIENT_SECRETS', 'secrets/keycloak.json') - OIDC_SCOPES = ['openid', 'email', 'profile'] + OIDC_CLIENT_SECRETS = os.getenv("PAY_OIDC_CLIENT_SECRETS", "secrets/keycloak.json") + OIDC_SCOPES = ["openid", "email", "profile"] # Undocumented Keycloak parameter: allows sending cookies without the secure flag, which we need for the local # non-TLS HTTP server. Set this to non-"True" for local development, and use the default everywhere else. - OIDC_ID_TOKEN_COOKIE_SECURE = os.getenv('PAY_OIDC_ID_TOKEN_COOKIE_SECURE', 'True').lower() == 'true' + OIDC_ID_TOKEN_COOKIE_SECURE = os.getenv("PAY_OIDC_ID_TOKEN_COOKIE_SECURE", "True").lower() == "true" - PREFERRED_URL_SCHEME = 'https' - SESSION_TYPE = 'cachelib' - SESSION_SERIALIZATION_FORMAT = 'json' - SESSION_CACHELIB = FileSystemCache(threshold=500, cache_dir='/tmp/sessions') - CACHE_TYPE = 'simple' + PREFERRED_URL_SCHEME = "https" + SESSION_TYPE = "cachelib" + SESSION_SERIALIZATION_FORMAT = "json" + SESSION_CACHELIB = FileSystemCache(threshold=500, cache_dir="/tmp/sessions") + CACHE_TYPE = "simple" TESTING = False DEBUG = True @@ -116,22 +115,22 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods TESTING = True # POSTGRESQL - DB_USER = _get_config('DATABASE_TEST_USERNAME', default='postgres') - DB_PASSWORD = _get_config('DATABASE_TEST_PASSWORD', default='postgres') - DB_NAME = _get_config('DATABASE_TEST_NAME', default='paytestdb') - DB_HOST = _get_config('DATABASE_TEST_HOST', default='localhost') - DB_PORT = _get_config('DATABASE_TEST_PORT', default='5432') - SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + DB_USER = _get_config("DATABASE_TEST_USERNAME", default="postgres") + DB_PASSWORD = _get_config("DATABASE_TEST_PASSWORD", default="postgres") + DB_NAME = _get_config("DATABASE_TEST_NAME", default="paytestdb") + DB_HOST = _get_config("DATABASE_TEST_HOST", default="localhost") + DB_PORT = _get_config("DATABASE_TEST_PORT", default="5432") + SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}" class ProdConfig(_Config): # pylint: disable=too-few-public-methods """Production environment configuration.""" - SECRET_KEY = _get_config('SECRET_KEY', default=None) + SECRET_KEY = _get_config("SECRET_KEY", default=None) if not SECRET_KEY: SECRET_KEY = os.urandom(24) - print('WARNING: SECRET_KEY being set as a one-shot', file=sys.stderr) + print("WARNING: SECRET_KEY being set as a one-shot", file=sys.stderr) TESTING = False DEBUG = False diff --git a/pay-admin/admin/keycloak.py b/pay-admin/admin/keycloak.py index bae017e8b..74d4a9faf 100644 --- a/pay-admin/admin/keycloak.py +++ b/pay-admin/admin/keycloak.py @@ -12,8 +12,9 @@ See the License for the specific language governing permissions and limitations under the License. """ -from flask import redirect, request, session, url_for + import flask_oidc +from flask import redirect, request, session, url_for class Keycloak: @@ -38,15 +39,15 @@ def is_logged_in(self) -> bool: """Determine whether or not the user is logged in.""" return self._oidc.user_loggedin - def has_access(self, role='admin_view') -> bool: + def has_access(self, role="admin_view") -> bool: """Determine whether or not the user is authorized to use the application. True if the user have role.""" if not self._oidc.get_access_token(): return False - if not session['oidc_auth_profile']['roles']: + if not session["oidc_auth_profile"]["roles"]: return False - roles_ = session['oidc_auth_profile']['roles'] + roles_ = session["oidc_auth_profile"]["roles"] access = role in roles_ return access @@ -57,8 +58,8 @@ def get_redirect_url(self) -> str: :rtype: object """ - return redirect(url_for('oidc_auth.login', next=request.url)) + return redirect(url_for("oidc_auth.login", next=request.url)) def get_username(self) -> str: """Get the username for the currently logged in user.""" - return self._oidc.user_getfield('preferred_username') + return self._oidc.user_getfield("preferred_username") diff --git a/pay-admin/admin/version.py b/pay-admin/admin/version.py index 6f57debc9..eadba756b 100755 --- a/pay-admin/admin/version.py +++ b/pay-admin/admin/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = '1.0.0' # pylint: disable=invalid-name +__version__ = "1.0.0" # pylint: disable=invalid-name diff --git a/pay-admin/admin/views/code.py b/pay-admin/admin/views/code.py index f41ed220e..19ff338cc 100644 --- a/pay-admin/admin/views/code.py +++ b/pay-admin/admin/views/code.py @@ -12,17 +12,18 @@ See the License for the specific language governing permissions and limitations under the License. """ + from .secured_view import SecuredView class CodeConfig(SecuredView): """Code config for all generic code tables.""" - column_list = form_columns = column_searchable_list = ('code', 'description') + column_list = form_columns = column_searchable_list = ("code", "description") # Keep everything sorted, although realistically also we need to sort the values within a row before it is saved. - column_default_sort = 'code' + column_default_sort = "code" def on_form_prefill(self, form, id): # pylint:disable=redefined-builtin """Set code as readonly.""" - form.code.render_kw = {'readonly': True} + form.code.render_kw = {"readonly": True} diff --git a/pay-admin/admin/views/corp_type.py b/pay-admin/admin/views/corp_type.py index 5f7493a27..744c4129d 100644 --- a/pay-admin/admin/views/corp_type.py +++ b/pay-admin/admin/views/corp_type.py @@ -21,35 +21,40 @@ class CorpTypeConfig(SecuredView): """Corp Type config.""" - column_list = ['code', 'description'] + column_list = ["code", "description"] column_labels = { - 'code': 'Code', - 'description': 'Description', - 'bcol_code_full_service_fee': 'BCOL Fee Code used for Account transactions - ' - 'Service Fee ($1.50 or $1.05 for ESRA)', - 'bcol_code_no_service_fee': 'BCOL Fee Code used for Account transactions - Service Fee ($0)', - 'bcol_code_partial_service_fee': 'BCOL Fee Code used for Account transactions - Service Fee ($1.00)', - 'bcol_staff_fee_code': "BCOL Fee Code used for Staff transactions. (starts with 'C')", - 'is_online_banking_allowed': 'Is Online Banking allowed', - 'product': 'Product to map in account products' + "code": "Code", + "description": "Description", + "bcol_code_full_service_fee": "BCOL Fee Code used for Account transactions - " + "Service Fee ($1.50 or $1.05 for ESRA)", + "bcol_code_no_service_fee": "BCOL Fee Code used for Account transactions - Service Fee ($0)", + "bcol_code_partial_service_fee": "BCOL Fee Code used for Account transactions - Service Fee ($1.00)", + "bcol_staff_fee_code": "BCOL Fee Code used for Staff transactions. (starts with 'C')", + "is_online_banking_allowed": "Is Online Banking allowed", + "product": "Product to map in account products", } - column_searchable_list = ('code',) - column_sortable_list = ('code',) + column_searchable_list = ("code",) + column_sortable_list = ("code",) - column_default_sort = 'code' + column_default_sort = "code" - form_choices = { - } + form_choices = {} - form_columns = edit_columns = ['code', 'description', 'bcol_code_full_service_fee', - 'bcol_code_no_service_fee', 'bcol_code_partial_service_fee', 'bcol_staff_fee_code', - 'is_online_banking_allowed', - 'product'] + form_columns = edit_columns = [ + "code", + "description", + "bcol_code_full_service_fee", + "bcol_code_no_service_fee", + "bcol_code_partial_service_fee", + "bcol_staff_fee_code", + "is_online_banking_allowed", + "product", + ] def on_form_prefill(self, form, id): # pylint:disable=redefined-builtin """Prefill overrides.""" - form.code.render_kw = {'readonly': True} + form.code.render_kw = {"readonly": True} # If this view is going to be displayed for only special roles, do like below diff --git a/pay-admin/admin/views/distribution_code.py b/pay-admin/admin/views/distribution_code.py index faec1daaf..26fafcf2b 100644 --- a/pay-admin/admin/views/distribution_code.py +++ b/pay-admin/admin/views/distribution_code.py @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from pay_api.models import DistributionCode, PaymentAccount, db from .secured_view import SecuredView @@ -20,46 +21,72 @@ class DistributionCodeConfig(SecuredView): """Distribution Code config.""" - column_list = ['name', 'client', 'responsibility_centre', 'service_line', 'stob', 'project_code', 'start_date', - 'end_date'] + column_list = [ + "name", + "client", + "responsibility_centre", + "service_line", + "stob", + "project_code", + "start_date", + "end_date", + ] column_labels = { - 'name': 'Name', - 'client': 'Client Code', - 'responsibility_centre': 'Responsibility Centre', - 'service_line': 'Service Line', - 'stob': 'STOB', - 'project_code': 'Project Code', - 'start_date': 'Effective start date', - 'end_date': 'Effective end date', - 'stop_ejv': 'Suspend EJV', - 'service_fee_distribution_code': 'Service Fee Distribution Code', - 'disbursement_distribution_code': 'Disbursement Distribution Code', - 'account': 'Account (For ministry government accounts)' + "name": "Name", + "client": "Client Code", + "responsibility_centre": "Responsibility Centre", + "service_line": "Service Line", + "stob": "STOB", + "project_code": "Project Code", + "start_date": "Effective start date", + "end_date": "Effective end date", + "stop_ejv": "Suspend EJV", + "service_fee_distribution_code": "Service Fee Distribution Code", + "disbursement_distribution_code": "Disbursement Distribution Code", + "account": "Account (For ministry government accounts)", } column_searchable_list = ( - 'name', 'stop_ejv', 'client', 'responsibility_centre', 'service_line', 'stob', 'project_code') - column_sortable_list = ('name',) - - column_default_sort = 'name' + "name", + "stop_ejv", + "client", + "responsibility_centre", + "service_line", + "stob", + "project_code", + ) + column_sortable_list = ("name",) + + column_default_sort = "name" form_args = { - 'account': { - 'query_factory': lambda: db.session.query(PaymentAccount) - .filter(PaymentAccount.payment_method == 'EJV').all() + "account": { + "query_factory": lambda: db.session.query(PaymentAccount) + .filter(PaymentAccount.payment_method == "EJV") + .all() } } form_columns = edit_columns = [ - 'name', 'stop_ejv', 'client', 'responsibility_centre', 'service_line', 'stob', 'project_code', - 'start_date', 'end_date', 'stop_ejv', 'service_fee_distribution_code', 'disbursement_distribution_code', - 'account' + "name", + "stop_ejv", + "client", + "responsibility_centre", + "service_line", + "stob", + "project_code", + "start_date", + "end_date", + "stop_ejv", + "service_fee_distribution_code", + "disbursement_distribution_code", + "account", ] def edit_form(self, obj=None): """Edit form overrides.""" form = super().edit_form(obj) - form.account.render_kw = {'disabled': True} + form.account.render_kw = {"disabled": True} return form @@ -70,9 +97,9 @@ def create_form(self, obj=None): def on_model_change(self, form, model, is_created): """Trigger on model change.""" - model.created_by = model.created_by or 'SYSTEM' + model.created_by = model.created_by or "SYSTEM" if is_created: - model.updated_by = 'SYSTEM' + model.updated_by = "SYSTEM" # If this view is going to be displayed for only special roles, do like below diff --git a/pay-admin/admin/views/fee_code.py b/pay-admin/admin/views/fee_code.py index d8eb490ad..4ca144462 100644 --- a/pay-admin/admin/views/fee_code.py +++ b/pay-admin/admin/views/fee_code.py @@ -21,14 +21,14 @@ class FeeCodeConfig(SecuredView): """Fee code config.""" - column_list = form_columns = column_searchable_list = ('code', 'amount') + column_list = form_columns = column_searchable_list = ("code", "amount") # Keep everything sorted, although realistically also we need to sort the values within a row before it is saved. - column_default_sort = 'code' + column_default_sort = "code" def on_form_prefill(self, form, id): # pylint:disable=redefined-builtin """Prefill overrides.""" - form.code.render_kw = {'readonly': True} + form.code.render_kw = {"readonly": True} # If this view is going to be displayed for only special roles, do like below diff --git a/pay-admin/admin/views/fee_schedule.py b/pay-admin/admin/views/fee_schedule.py index 6b9e646d9..8b043f95c 100644 --- a/pay-admin/admin/views/fee_schedule.py +++ b/pay-admin/admin/views/fee_schedule.py @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from pay_api.models import FeeSchedule, db from .secured_view import SecuredView @@ -20,36 +21,58 @@ class FeeScheduleConfig(SecuredView): """Fee Schedule config.""" - column_list = ['corp_type_code', 'filing_type_code', 'fee', - 'future_effective_fee', 'priority_fee', 'service_fee', 'variable'] + column_list = [ + "corp_type_code", + "filing_type_code", + "fee", + "future_effective_fee", + "priority_fee", + "service_fee", + "variable", + ] column_labels = { - 'corp_type': 'Corp Type', - 'corp_type_code': 'Corp Type', - 'filing_type': 'Filing Type', - 'filing_type_code': 'Filing Type', - 'fee': 'Filing Fee', - 'fee_start_date': 'Fee effective start date', - 'fee_end_date': 'Fee End Date', - 'future_effective_fee': 'Future Effective Fee', - 'priority_fee': 'Priority Fee', - 'service_fee': 'Service Fee', - 'distribution_codes': 'Distribution Code', - 'variable': 'Variable Fee Flag' + "corp_type": "Corp Type", + "corp_type_code": "Corp Type", + "filing_type": "Filing Type", + "filing_type_code": "Filing Type", + "fee": "Filing Fee", + "fee_start_date": "Fee effective start date", + "fee_end_date": "Fee End Date", + "future_effective_fee": "Future Effective Fee", + "priority_fee": "Priority Fee", + "service_fee": "Service Fee", + "distribution_codes": "Distribution Code", + "variable": "Variable Fee Flag", } - column_searchable_list = ('corp_type_code', 'filing_type_code') - column_sortable_list = ('corp_type_code',) + column_searchable_list = ("corp_type_code", "filing_type_code") + column_sortable_list = ("corp_type_code",) - column_default_sort = 'corp_type_code' + column_default_sort = "corp_type_code" form_args = {} - form_columns = ['corp_type', 'filing_type', 'fee', 'fee_start_date', - 'fee_end_date', 'future_effective_fee', 'priority_fee', 'service_fee', - 'distribution_codes', 'variable'] - edit_columns = ['corp_type', 'filing_type', 'fee_start_date', - 'fee_end_date', 'priority_fee', 'service_fee', - 'distribution_codes'] + form_columns = [ + "corp_type", + "filing_type", + "fee", + "fee_start_date", + "fee_end_date", + "future_effective_fee", + "priority_fee", + "service_fee", + "distribution_codes", + "variable", + ] + edit_columns = [ + "corp_type", + "filing_type", + "fee_start_date", + "fee_end_date", + "priority_fee", + "service_fee", + "distribution_codes", + ] @staticmethod def _change_labels(form): @@ -57,7 +80,7 @@ def _change_labels(form): form.future_effective_fee.label.text = "Future Effective Fee (Starts with 'FUT')" form.priority_fee.label.text = "Priority Fee (Starts with 'PRI')" form.service_fee.label.text = "Service Fee (Starts with 'TRF')" - form.distribution_codes.label.text = 'Distribution Code (Mandatory for non-zero fees)' + form.distribution_codes.label.text = "Distribution Code (Mandatory for non-zero fees)" def edit_form(self, obj=None): """Edit form overrides.""" diff --git a/pay-admin/admin/views/index.py b/pay-admin/admin/views/index.py index d2d678a30..816891a39 100644 --- a/pay-admin/admin/views/index.py +++ b/pay-admin/admin/views/index.py @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from flask_admin import AdminIndexView diff --git a/pay-admin/admin/views/secured_view.py b/pay-admin/admin/views/secured_view.py index 18e9be8ad..2ece5d821 100644 --- a/pay-admin/admin/views/secured_view.py +++ b/pay-admin/admin/views/secured_view.py @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from flask_admin.contrib import sqla from admin import keycloak @@ -40,14 +41,34 @@ def can_edit(self): """Return if user can edit.""" return self._has_role(self.edit_role) - def __init__(self, model, session, # pylint: disable=too-many-arguments - name=None, category=None, endpoint=None, url=None, static_folder=None, - menu_class_name=None, menu_icon_type=None, menu_icon_value=None, - view_role: str = 'admin_view', edit_role: str = 'admin_edit'): + def __init__( # pylint: disable=too-many-arguments + self, + model, + session, + name=None, + category=None, + endpoint=None, + url=None, + static_folder=None, + menu_class_name=None, + menu_icon_type=None, + menu_icon_value=None, + view_role: str = "admin_view", + edit_role: str = "admin_edit", + ): """Initialize.""" - super().__init__(model, session, - name, category, endpoint, url, static_folder, - menu_class_name, menu_icon_type, menu_icon_value) + super().__init__( + model, + session, + name, + category, + endpoint, + url, + static_folder, + menu_class_name, + menu_icon_type, + menu_icon_value, + ) self.connected = False self.view_role = view_role self.edit_role = edit_role @@ -70,4 +91,4 @@ def inaccessible_callback(self, name, **kwargs): kc = keycloak.Keycloak(None) return kc.get_redirect_url() - return 'not authorized' + return "not authorized" diff --git a/pay-admin/gunicorn_config.py b/pay-admin/gunicorn_config.py index 9cb75dd26..ef109bd97 100755 --- a/pay-admin/gunicorn_config.py +++ b/pay-admin/gunicorn_config.py @@ -17,9 +17,8 @@ import os +workers = int(os.environ.get("GUNICORN_PROCESSES", "1")) # pylint: disable=invalid-name +threads = int(os.environ.get("GUNICORN_THREADS", "1")) # pylint: disable=invalid-name -workers = int(os.environ.get('GUNICORN_PROCESSES', '1')) # pylint: disable=invalid-name -threads = int(os.environ.get('GUNICORN_THREADS', '1')) # pylint: disable=invalid-name - -forwarded_allow_ips = '*' # pylint: disable=invalid-name -secure_scheme_headers = {'X-Forwarded-Proto': 'https'} # pylint: disable=invalid-name +forwarded_allow_ips = "*" # pylint: disable=invalid-name +secure_scheme_headers = {"X-Forwarded-Proto": "https"} # pylint: disable=invalid-name diff --git a/pay-admin/poetry.lock b/pay-admin/poetry.lock index a9cfb8ee5..aa2499a87 100644 --- a/pay-admin/poetry.lock +++ b/pay-admin/poetry.lock @@ -209,6 +209,50 @@ files = [ [package.dependencies] pycodestyle = ">=2.12.0" +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "blinker" version = "1.7.0" @@ -724,6 +768,22 @@ isort = ">=5.0.0,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + [[package]] name = "flake8-quotes" version = "3.4.0" @@ -1816,6 +1876,17 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "opentracing" version = "2.4.0" @@ -1840,6 +1911,17 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pay-api" version = "0.1.0" @@ -3028,4 +3110,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "38bc83b0ef6e49b3daf7158017b8b348365ee00bd72642608bccb6f20bc2f728" +content-hash = "449884ee1d86b60ab1c69ddf067d25f1e8a887a71ca16efc2feec4bbc9e97d76" diff --git a/pay-admin/pyproject.toml b/pay-admin/pyproject.toml index 7d325556e..f38c129ec 100644 --- a/pay-admin/pyproject.toml +++ b/pay-admin/pyproject.toml @@ -42,6 +42,129 @@ pylint = "^3.1.0" pylint-flask = "^0.6" pydocstyle = "^6.3.0" isort = "^5.13.2" +black = "^24.10.0" +flake8-pyproject = "^1.2.3" + +[tool.flake8] +ignore = ["F401","E402", "Q000", "E203", "W503"] +exclude = [ + ".venv", + "./venv", + ".git", + ".history", + "devops", + "*migrations*", +] +per-file-ignores = [ + "__init__.py:F401", + "*.py:B902" +] +max-line-length = 120 +docstring-min-length=10 +count = true + +[tool.zimports] +black-line-length = 120 +keep-unused-type-checking = true + +[tool.black] +target-version = ["py310", "py311", "py312"] +line-length = 120 +include = '\.pyi?$' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + migrations + | devops + | .history +)/ +''' + +[tool.isort] +atomic = true +profile = "black" +line_length = 120 +skip_gitignore = true +skip_glob = ["migrations", "devops"] + +[tool.pylint.main] +fail-under = 10 +max-line-length = 120 +ignore = [ "migrations", "devops", "tests"] +ignore-patterns = ["^\\.#"] +ignored-modules= ["flask_sqlalchemy", "sqlalchemy", "SQLAlchemy" , "alembic", "scoped_session"] +ignored-classes= "scoped_session" +ignore-long-lines = "^\\s*(# )??$" +extension-pkg-whitelist = "pydantic" +notes = ["FIXME","XXX","TODO"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] +disable = "C0209,C0301,W0511,W0613,W0703,W1514,W1203,R0801,R0902,R0903,R0911,R0401,R1705,R1718,W3101" +argument-naming-style = "snake_case" +attr-naming-style = "snake_case" +class-attribute-naming-style = "any" +class-const-naming-style = "UPPER_CASE" +class-naming-style = "PascalCase" +const-naming-style = "UPPER_CASE" +function-naming-style = "snake_case" +inlinevar-naming-style = "any" +method-naming-style = "snake_case" +module-naming-style = "any" +variable-naming-style = "snake_case" +docstring-min-length = -1 +good-names = ["i", "j", "k", "ex", "Run", "_"] +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] +valid-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/auth_api", +] +omit = [ + "wsgi.py", + "gunicorn_config.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core"] diff --git a/pay-admin/setup.py b/pay-admin/setup.py index 2cad61c85..514a2e9c8 100755 --- a/pay-admin/setup.py +++ b/pay-admin/setup.py @@ -16,7 +16,4 @@ from setuptools import find_packages, setup -setup( - name="admin", - packages=find_packages() -) +setup(name="admin", packages=find_packages()) diff --git a/pay-admin/tests/conftest.py b/pay-admin/tests/conftest.py index 50235bdaa..b9de1bf09 100644 --- a/pay-admin/tests/conftest.py +++ b/pay-admin/tests/conftest.py @@ -2,8 +2,8 @@ Fixtures for testing. """ -import pytest +import pytest from flask_sqlalchemy import SQLAlchemy from admin import create_app @@ -11,11 +11,11 @@ from tests.fake_oidc import FakeOidc -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def db(): """DB.""" Keycloak._oidc = FakeOidc() # pylint-disable=protected-access - app = create_app(run_mode='testing') + app = create_app(run_mode="testing") with app.app_context(): _db = SQLAlchemy() _db.app = app diff --git a/pay-admin/tests/fake_oidc.py b/pay-admin/tests/fake_oidc.py index a3ad01b85..d447212b9 100644 --- a/pay-admin/tests/fake_oidc.py +++ b/pay-admin/tests/fake_oidc.py @@ -11,7 +11,7 @@ class FakeOidc: def user_getfield(self, key): """Get user.""" - return 'Joe' + return "Joe" def user_role(self): """Get user role.""" @@ -23,8 +23,8 @@ def has_access(self): def get_access_token(self): """Get access token.""" - return 'any' + return "any" def _get_token_info(self, token): """Get token info.""" - return {'roles': ['admin_view']} + return {"roles": ["admin_view"]} diff --git a/pay-admin/tests/unit/test_views.py b/pay-admin/tests/unit/test_views.py index dc60c0ae7..c8fee8d55 100644 --- a/pay-admin/tests/unit/test_views.py +++ b/pay-admin/tests/unit/test_views.py @@ -25,13 +25,16 @@ from admin.views.fee_schedule import FeeSchedule, FeeScheduleConfig -@pytest.mark.parametrize('model, config', [ - (FeeCode, FeeCodeConfig), - (CorpType, CorpTypeConfig), - (FilingType, CodeConfig), - (DistributionCode, DistributionCodeConfig), - (FeeSchedule, FeeScheduleConfig) -]) +@pytest.mark.parametrize( + "model, config", + [ + (FeeCode, FeeCodeConfig), + (CorpType, CorpTypeConfig), + (FilingType, CodeConfig), + (DistributionCode, DistributionCodeConfig), + (FeeSchedule, FeeScheduleConfig), + ], +) def test_view_configs(db, model, config): """Test view configs.""" view = config(model, db.session) diff --git a/pay-admin/wsgi.py b/pay-admin/wsgi.py index e12a72ee3..b29acbd54 100755 --- a/pay-admin/wsgi.py +++ b/pay-admin/wsgi.py @@ -13,15 +13,15 @@ # limitations under the License. """Provides the WSGI entry point for running the application """ -from admin import create_app import sys +from admin import create_app # Openshift s2i expects a lower case name of application application = create_app() # pylint: disable=invalid-name if __name__ == "__main__": - port = '8080' + port = "8080" if len(sys.argv) > 1: port = sys.argv[1] application.run(port=int(port)) From 72af1ad5e9b6aa7e628449afce5006eaeb8e5aed Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:23:40 -0700 Subject: [PATCH 06/10] Fix pay-queue --- pay-queue/Makefile | 18 +- pay-queue/app.py | 8 +- pay-queue/gunicorn_config.py | 12 +- pay-queue/poetry.lock | 84 +- pay-queue/pyproject.toml | 123 ++ pay-queue/setup.py | 38 +- pay-queue/src/pay_queue/__init__.py | 14 +- pay-queue/src/pay_queue/config.py | 138 +- pay-queue/src/pay_queue/enums.py | 74 +- pay-queue/src/pay_queue/external/gcp_auth.py | 16 +- pay-queue/src/pay_queue/minio.py | 16 +- pay-queue/src/pay_queue/resources/__init__.py | 2 +- pay-queue/src/pay_queue/resources/worker.py | 18 +- .../pay_queue/services/cgi_reconciliations.py | 208 +- .../src/pay_queue/services/eft/eft_base.py | 4 +- .../src/pay_queue/services/eft/eft_enums.py | 12 +- .../src/pay_queue/services/eft/eft_errors.py | 26 +- .../src/pay_queue/services/eft/eft_header.py | 6 +- .../services/eft/eft_reconciliation.py | 257 ++- .../src/pay_queue/services/eft/eft_record.py | 27 +- .../src/pay_queue/services/email_service.py | 51 +- .../pay_queue/services/identifier_updater.py | 8 +- .../services/payment_reconciliations.py | 565 +++-- pay-queue/src/pay_queue/version.py | 2 +- pay-queue/tests/__init__.py | 3 +- pay-queue/tests/conftest.py | 68 +- pay-queue/tests/integration/__init__.py | 72 +- pay-queue/tests/integration/factory.py | 431 ++-- .../integration/test_cgi_reconciliations.py | 1858 +++++++++-------- .../integration/test_eft_reconciliation.py | 857 +++++--- .../test_payment_reconciliations.py | 912 +++++--- .../tests/integration/test_worker_queue.py | 15 +- pay-queue/tests/integration/utils.py | 110 +- pay-queue/tests/unit/test_eft_file_parser.py | 625 +++--- pay-queue/tests/utilities/factory_utils.py | 63 +- 35 files changed, 4067 insertions(+), 2674 deletions(-) diff --git a/pay-queue/Makefile b/pay-queue/Makefile index ff9f7a854..d39bcdb6a 100644 --- a/pay-queue/Makefile +++ b/pay-queue/Makefile @@ -44,15 +44,27 @@ install: clean ################################################################################# # COMMANDS - CI # ################################################################################# -ci: lint flake8 test ## CI flow +ci: isort-ci black-ci lint flake8 test ## CI flow + +isort: + poetry run isort . + +isort-ci: + poetry run isort --check . + +black: ## Linting with black + poetry run black . + +black-ci: + poetry run black --check . pylint: ## Linting with pylint - poetry run pylint --rcfile=setup.cfg src/$(PROJECT_NAME) + poetry run pylint src/$(PROJECT_NAME) flake8: ## Linting with flake8 poetry run flake8 src/$(PROJECT_NAME) tests -lint: pylint flake8 ## run all lint type scripts +lint: isort black pylint flake8 ## run all lint type scripts test: ## Unit testing poetry run pytest diff --git a/pay-queue/app.py b/pay-queue/app.py index a1f9fdbe5..eea5f3d2a 100755 --- a/pay-queue/app.py +++ b/pay-queue/app.py @@ -17,9 +17,11 @@ """Initialize Flask app.""" import os + from pay_queue import create_app + app = create_app() -if __name__ == '__main__': - server_port = os.environ.get('PORT', '5001') - app.run(debug=False, port=server_port, host='0.0.0.0') +if __name__ == "__main__": + server_port = os.environ.get("PORT", "5001") + app.run(debug=False, port=server_port, host="0.0.0.0") diff --git a/pay-queue/gunicorn_config.py b/pay-queue/gunicorn_config.py index d52a64e76..17c107427 100644 --- a/pay-queue/gunicorn_config.py +++ b/pay-queue/gunicorn_config.py @@ -18,10 +18,10 @@ import os # https://docs.gunicorn.org/en/stable/settings.html#workers -workers = int(os.environ.get('GUNICORN_PROCESSES', '1')) # gunicorn default - 1 -worker_class = os.environ.get('GUNICORN_WORKER_CLASS', 'sync') # gunicorn default - sync -worker_connections = int(os.environ.get('GUNICORN_WORKER_CONNECIONS', '1000')) # gunicorn default - 1000 -threads = int(os.environ.get('GUNICORN_THREADS', '4')) # gunicorn default - 1 -timeout = int(os.environ.get('GUNICORN_TIMEOUT', '100')) # gunicorn default - 30 -keepalive = int(os.environ.get('GUNICORN_KEEPALIVE', '2')) # gunicorn default - 2 +workers = int(os.environ.get("GUNICORN_PROCESSES", "1")) # gunicorn default - 1 +worker_class = os.environ.get("GUNICORN_WORKER_CLASS", "sync") # gunicorn default - sync +worker_connections = int(os.environ.get("GUNICORN_WORKER_CONNECIONS", "1000")) # gunicorn default - 1000 +threads = int(os.environ.get("GUNICORN_THREADS", "4")) # gunicorn default - 1 +timeout = int(os.environ.get("GUNICORN_TIMEOUT", "100")) # gunicorn default - 30 +keepalive = int(os.environ.get("GUNICORN_KEEPALIVE", "2")) # gunicorn default - 2 # WHEN MIGRATING TO GCP - GUNICORN_THREADS = 8, GUNICORN_TIMEOUT = 0, GUNICORN_PROCESSES = 1 diff --git a/pay-queue/poetry.lock b/pay-queue/poetry.lock index 5eb67f19e..9e5c1beee 100644 --- a/pay-queue/poetry.lock +++ b/pay-queue/poetry.lock @@ -252,6 +252,50 @@ files = [ [package.dependencies] pycodestyle = ">=2.12.0" +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "blinker" version = "1.7.0" @@ -807,6 +851,22 @@ isort = ">=5.0.0,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + [[package]] name = "flake8-quotes" version = "3.4.0" @@ -1870,6 +1930,17 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "opentracing" version = "2.4.0" @@ -1894,6 +1965,17 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pay-api" version = "0.1.0" @@ -3104,4 +3186,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "04de57c69ecf9b5e5112cc584e3912723d6be73f05f3216ed09262f625bb1c5d" +content-hash = "6f6bbae22c7f261d99596df24c3e9dd621d97b8dd00956357dfdc3ec420cba59" diff --git a/pay-queue/pyproject.toml b/pay-queue/pyproject.toml index 8203b408c..74830de62 100644 --- a/pay-queue/pyproject.toml +++ b/pay-queue/pyproject.toml @@ -45,6 +45,129 @@ pydocstyle = "^6.3.0" isort = "^5.13.2" lovely-pytest-docker = "^0.3.1" pytest-asyncio = "0.18.3" +black = "^24.10.0" +flake8-pyproject = "^1.2.3" + +[tool.flake8] +ignore = ["F401","E402", "Q000", "E203", "W503"] +exclude = [ + ".venv", + "./venv", + ".git", + ".history", + "devops", + "*migrations*", +] +per-file-ignores = [ + "__init__.py:F401", + "*.py:B902" +] +max-line-length = 120 +docstring-min-length=10 +count = true + +[tool.zimports] +black-line-length = 120 +keep-unused-type-checking = true + +[tool.black] +target-version = ["py310", "py311", "py312"] +line-length = 120 +include = '\.pyi?$' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + migrations + | devops + | .history +)/ +''' + +[tool.isort] +atomic = true +profile = "black" +line_length = 120 +skip_gitignore = true +skip_glob = ["migrations", "devops"] + +[tool.pylint.main] +fail-under = 10 +max-line-length = 120 +ignore = [ "migrations", "devops", "tests"] +ignore-patterns = ["^\\.#"] +ignored-modules= ["flask_sqlalchemy", "sqlalchemy", "SQLAlchemy" , "alembic", "scoped_session"] +ignored-classes= "scoped_session" +ignore-long-lines = "^\\s*(# )??$" +extension-pkg-whitelist = "pydantic" +notes = ["FIXME","XXX","TODO"] +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] +disable = "C0209,C0301,W0511,W0613,W0703,W1514,W1203,R0801,R0902,R0903,R0911,R0401,R1705,R1718,W3101" +argument-naming-style = "snake_case" +attr-naming-style = "snake_case" +class-attribute-naming-style = "any" +class-const-naming-style = "UPPER_CASE" +class-naming-style = "PascalCase" +const-naming-style = "UPPER_CASE" +function-naming-style = "snake_case" +inlinevar-naming-style = "any" +method-naming-style = "snake_case" +module-naming-style = "any" +variable-naming-style = "snake_case" +docstring-min-length = -1 +good-names = ["i", "j", "k", "ex", "Run", "_"] +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] +valid-classmethod-first-arg = ["cls"] +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +minversion = "2.0" +testpaths = [ + "tests", +] +addopts = "--verbose --strict -p no:warnings --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml" +python_files = [ + "test*.py" +] +norecursedirs = [ + ".git", ".tox", "venv*", "requirements*", "build", +] +log_cli = true +log_cli_level = "1" +filterwarnings = [ + "ignore::UserWarning" +] +markers = [ + "slow", + "serial", +] + +[tool.coverage.run] +branch = true +source = [ + "src/auth_api", +] +omit = [ + "wsgi.py", + "gunicorn_config.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "from", + "import", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + 'if __name__ == "__main__":', +] [build-system] requires = ["poetry-core"] diff --git a/pay-queue/setup.py b/pay-queue/setup.py index 9bc1ac212..0080379eb 100644 --- a/pay-queue/setup.py +++ b/pay-queue/setup.py @@ -20,12 +20,12 @@ from setuptools import find_packages, setup +_version_re = re.compile(r"__version__\s+=\s+(.*)") # pylint: disable=invalid-name -_version_re = re.compile(r'__version__\s+=\s+(.*)') # pylint: disable=invalid-name - -with open('src/pay_queue/version.py', 'rb') as f: - version = str(ast.literal_eval(_version_re.search( # pylint: disable=invalid-name - f.read().decode('utf-8')).group(1))) +with open("src/pay_queue/version.py", "rb") as f: + version = str( + ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1)) # pylint: disable=invalid-name + ) def read_requirements(filename): @@ -34,9 +34,9 @@ def read_requirements(filename): the requirements.txt file. :return: Python requirements """ - with open(filename, 'r') as req: + with open(filename, "r") as req: requirements = req.readlines() - install_requires = [r.strip() for r in requirements if (r.find('git+') != 0 and r.find('-e git+') != 0)] + install_requires = [r.strip() for r in requirements if (r.find("git+") != 0 and r.find("-e git+") != 0)] return install_requires @@ -46,25 +46,29 @@ def read(filepath): :param str filepath: path to the file to be read :return: file contents """ - with open(filepath, 'r') as file_handle: + with open(filepath, "r") as file_handle: content = file_handle.read() return content -REQUIREMENTS = read_requirements('requirements.txt') +REQUIREMENTS = read_requirements("requirements.txt") setup( name="pay_queue", version=version, - author_email='', - packages=find_packages('src'), - package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + author_email="", + packages=find_packages("src"), + package_dir={"": "src"}, + py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], include_package_data=True, - license=read('LICENSE'), - long_description=read('README.md'), + license=read("LICENSE"), + long_description=read("README.md"), zip_safe=False, install_requires=REQUIREMENTS, - setup_requires=["pytest-runner", ], - tests_require=["pytest", ], + setup_requires=[ + "pytest-runner", + ], + tests_require=[ + "pytest", + ], ) diff --git a/pay-queue/src/pay_queue/__init__.py b/pay-queue/src/pay_queue/__init__.py index 1fe3ebdcc..4b9a4da46 100644 --- a/pay-queue/src/pay_queue/__init__.py +++ b/pay-queue/src/pay_queue/__init__.py @@ -34,22 +34,21 @@ from .resources import register_endpoints +setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging.conf")) # important to do this first -setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first - -def create_app(run_mode=os.getenv('DEPLOYMENT_ENV', 'production')) -> Flask: +def create_app(run_mode=os.getenv("DEPLOYMENT_ENV", "production")) -> Flask: """Return a configured Flask App using the Factory method.""" app = Flask(__name__) app.env = run_mode app.config.from_object(config.CONFIGURATION[run_mode]) # Configure Sentry - if dsn := app.config.get('SENTRY_DSN', None): + if dsn := app.config.get("SENTRY_DSN", None): sentry_sdk.init( dsn=dsn, integrations=[FlaskIntegration()], - release=f'pay-queue@{get_run_version()}', + release=f"pay-queue@{get_run_version()}", send_default_pii=False, ) @@ -68,10 +67,11 @@ def build_cache(app): cache.init_app(app) with app.app_context(): cache.clear() - if not app.config.get('TESTING', False): + if not app.config.get("TESTING", False): try: from pay_api.services.code import Code as CodeService # pylint: disable=import-outside-toplevel + CodeService.build_all_codes_cache() except Exception as e: # NOQA pylint:disable=broad-except - app.logger.error('Error on caching ') + app.logger.error("Error on caching ") app.logger.error(e) diff --git a/pay-queue/src/pay_queue/config.py b/pay-queue/src/pay_queue/config.py index 2f05ba22e..d91fd6da9 100644 --- a/pay-queue/src/pay_queue/config.py +++ b/pay-queue/src/pay_queue/config.py @@ -23,98 +23,97 @@ from dotenv import find_dotenv, load_dotenv - # this will load all the envars from a .env file located in the project root (api) load_dotenv(find_dotenv()) CONFIGURATION = { - 'development': 'pay_queue.config.DevConfig', - 'testing': 'pay_queue.config.TestConfig', - 'production': 'pay_queue.config.ProdConfig', - 'default': 'pay_queue.config.ProdConfig' + "development": "pay_queue.config.DevConfig", + "testing": "pay_queue.config.TestConfig", + "production": "pay_queue.config.ProdConfig", + "default": "pay_queue.config.ProdConfig", } -def get_named_config(config_name: str = 'production'): +def get_named_config(config_name: str = "production"): """Return the configuration object based on the name. :raise: KeyError: if an unknown configuration is requested """ - if config_name in ['production', 'staging', 'default']: + if config_name in ["production", "staging", "default"]: app_config = ProdConfig() - elif config_name == 'testing': + elif config_name == "testing": app_config = TestConfig() - elif config_name == 'development': + elif config_name == "development": app_config = DevConfig() else: - raise KeyError(f'Unknown configuration: {config_name}') + raise KeyError(f"Unknown configuration: {config_name}") return app_config -class _Config(): # pylint: disable=too-few-public-methods,protected-access +class _Config: # pylint: disable=too-few-public-methods,protected-access """Base class configuration that should set reasonable defaults. Used as the base for all the other configurations. """ PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - PAY_LD_SDK_KEY = os.getenv('PAY_LD_SDK_KEY', None) - LEGISLATIVE_TIMEZONE = os.getenv('LEGISLATIVE_TIMEZONE', 'America/Vancouver') + PAY_LD_SDK_KEY = os.getenv("PAY_LD_SDK_KEY", None) + LEGISLATIVE_TIMEZONE = os.getenv("LEGISLATIVE_TIMEZONE", "America/Vancouver") - SENTRY_ENABLE = os.getenv('SENTRY_ENABLE', 'False') - SENTRY_DSN = os.getenv('SENTRY_DSN', None) + SENTRY_ENABLE = os.getenv("SENTRY_ENABLE", "False") + SENTRY_DSN = os.getenv("SENTRY_DSN", None) SQLALCHEMY_TRACK_MODIFICATIONS = False # POSTGRESQL - DB_USER = os.getenv('DATABASE_USERNAME', '') - DB_PASSWORD = os.getenv('DATABASE_PASSWORD', '') - DB_NAME = os.getenv('DATABASE_NAME', '') - DB_HOST = os.getenv('DATABASE_HOST', '') - DB_PORT = os.getenv('DATABASE_PORT', '5432') - if DB_UNIX_SOCKET := os.getenv('DATABASE_UNIX_SOCKET', None): + DB_USER = os.getenv("DATABASE_USERNAME", "") + DB_PASSWORD = os.getenv("DATABASE_PASSWORD", "") + DB_NAME = os.getenv("DATABASE_NAME", "") + DB_HOST = os.getenv("DATABASE_HOST", "") + DB_PORT = os.getenv("DATABASE_PORT", "5432") + if DB_UNIX_SOCKET := os.getenv("DATABASE_UNIX_SOCKET", None): SQLALCHEMY_DATABASE_URI = ( - f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?unix_sock={DB_UNIX_SOCKET}/.s.PGSQL.5432' + f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?unix_sock={DB_UNIX_SOCKET}/.s.PGSQL.5432" ) else: - SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + SQLALCHEMY_DATABASE_URI = f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}" # Minio configuration values - MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT') - MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY') - MINIO_ACCESS_SECRET = os.getenv('MINIO_ACCESS_SECRET') - MINIO_SECURE = os.getenv('MINIO_SECURE', 'True').lower() == 'true' + MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT") + MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") + MINIO_ACCESS_SECRET = os.getenv("MINIO_ACCESS_SECRET") + MINIO_SECURE = os.getenv("MINIO_SECURE", "True").lower() == "true" # CFS API Settings - CFS_BASE_URL = os.getenv('CFS_BASE_URL') - CFS_CLIENT_ID = os.getenv('CFS_CLIENT_ID') - CFS_CLIENT_SECRET = os.getenv('CFS_CLIENT_SECRET') - CONNECT_TIMEOUT = int(os.getenv('CONNECT_TIMEOUT', '10')) + CFS_BASE_URL = os.getenv("CFS_BASE_URL") + CFS_CLIENT_ID = os.getenv("CFS_CLIENT_ID") + CFS_CLIENT_SECRET = os.getenv("CFS_CLIENT_SECRET") + CONNECT_TIMEOUT = int(os.getenv("CONNECT_TIMEOUT", "10")) # EFT Config - EFT_TDI17_LOCATION_ID = os.getenv('EFT_TDI17_LOCATION_ID') + EFT_TDI17_LOCATION_ID = os.getenv("EFT_TDI17_LOCATION_ID") # Secret key for encrypting bank account - ACCOUNT_SECRET_KEY = os.getenv('ACCOUNT_SECRET_KEY') + ACCOUNT_SECRET_KEY = os.getenv("ACCOUNT_SECRET_KEY") - KEYCLOAK_SERVICE_ACCOUNT_ID = os.getenv('SBC_AUTH_ADMIN_CLIENT_ID') - KEYCLOAK_SERVICE_ACCOUNT_SECRET = os.getenv('SBC_AUTH_ADMIN_CLIENT_SECRET') - JWT_OIDC_ISSUER = os.getenv('JWT_OIDC_ISSUER') - NOTIFY_API_URL = os.getenv('NOTIFY_API_URL', '') - NOTIFY_API_VERSION = os.getenv('NOTIFY_API_VERSION', '') - NOTIFY_API_ENDPOINT = f'{NOTIFY_API_URL + NOTIFY_API_VERSION}/' - IT_OPS_EMAIL = os.getenv('IT_OPS_EMAIL', 'SBC_ITOperationsSupport@gov.bc.ca') + KEYCLOAK_SERVICE_ACCOUNT_ID = os.getenv("SBC_AUTH_ADMIN_CLIENT_ID") + KEYCLOAK_SERVICE_ACCOUNT_SECRET = os.getenv("SBC_AUTH_ADMIN_CLIENT_SECRET") + JWT_OIDC_ISSUER = os.getenv("JWT_OIDC_ISSUER") + NOTIFY_API_URL = os.getenv("NOTIFY_API_URL", "") + NOTIFY_API_VERSION = os.getenv("NOTIFY_API_VERSION", "") + NOTIFY_API_ENDPOINT = f"{NOTIFY_API_URL + NOTIFY_API_VERSION}/" + IT_OPS_EMAIL = os.getenv("IT_OPS_EMAIL", "SBC_ITOperationsSupport@gov.bc.ca") - DISABLE_EJV_ERROR_EMAIL = os.getenv('DISABLE_EJV_ERROR_EMAIL', 'true').lower() == 'true' - DISABLE_CSV_ERROR_EMAIL = os.getenv('DISABLE_CSV_ERROR_EMAIL', 'true').lower() == 'true' + DISABLE_EJV_ERROR_EMAIL = os.getenv("DISABLE_EJV_ERROR_EMAIL", "true").lower() == "true" + DISABLE_CSV_ERROR_EMAIL = os.getenv("DISABLE_CSV_ERROR_EMAIL", "true").lower() == "true" # PUB/SUB - PUB: account-mailer-dev, auth-event-dev, SUB to ftp-poller-payment-reconciliation-dev, business-events - ACCOUNT_MAILER_TOPIC = os.getenv('ACCOUNT_MAILER_TOPIC', 'account-mailer-dev') - AUTH_EVENT_TOPIC = os.getenv('AUTH_EVENT_TOPIC', 'auth-event-dev') - GCP_AUTH_KEY = os.getenv('AUTHPAY_GCP_AUTH_KEY', None) + ACCOUNT_MAILER_TOPIC = os.getenv("ACCOUNT_MAILER_TOPIC", "account-mailer-dev") + AUTH_EVENT_TOPIC = os.getenv("AUTH_EVENT_TOPIC", "auth-event-dev") + GCP_AUTH_KEY = os.getenv("AUTHPAY_GCP_AUTH_KEY", None) # If blank in PUBSUB, this should match the https endpoint the subscription is pushing to. - PAY_AUDIENCE_SUB = os.getenv('PAY_AUDIENCE_SUB', None) - VERIFY_PUBSUB_EMAILS = f'{os.getenv("AUTHPAY_SERVICE_ACCOUNT")},{os.getenv("BUSINESS_SERVICE_ACCOUNT")}'.split(',') + PAY_AUDIENCE_SUB = os.getenv("PAY_AUDIENCE_SUB", None) + VERIFY_PUBSUB_EMAILS = f'{os.getenv("AUTHPAY_SERVICE_ACCOUNT")},{os.getenv("BUSINESS_SERVICE_ACCOUNT")}'.split(",") class DevConfig(_Config): # pylint: disable=too-few-public-methods @@ -133,38 +132,45 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods DEBUG = True TESTING = True # POSTGRESQL - DB_USER = os.getenv('DATABASE_TEST_USERNAME', '') - DB_PASSWORD = os.getenv('DATABASE_TEST_PASSWORD', '') - DB_NAME = os.getenv('DATABASE_TEST_NAME', '') - DB_HOST = os.getenv('DATABASE_TEST_HOST', '') - DB_PORT = os.getenv('DATABASE_TEST_PORT', '5432') + DB_USER = os.getenv("DATABASE_TEST_USERNAME", "") + DB_PASSWORD = os.getenv("DATABASE_TEST_PASSWORD", "") + DB_NAME = os.getenv("DATABASE_TEST_NAME", "") + DB_HOST = os.getenv("DATABASE_TEST_HOST", "") + DB_PORT = os.getenv("DATABASE_TEST_PORT", "5432") SQLALCHEMY_DATABASE_URI = os.getenv( - 'DATABASE_TEST_URL', - default=f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + "DATABASE_TEST_URL", + default=f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}", ) - USE_DOCKER_MOCK = os.getenv('USE_DOCKER_MOCK', None) + USE_DOCKER_MOCK = os.getenv("USE_DOCKER_MOCK", None) # Minio variables - MINIO_ENDPOINT = 'localhost:9000' - MINIO_ACCESS_KEY = 'minio' - MINIO_ACCESS_SECRET = 'minio123' - MINIO_BUCKET_NAME = 'payment-sftp' + MINIO_ENDPOINT = "localhost:9000" + MINIO_ACCESS_KEY = "minio" + MINIO_ACCESS_SECRET = "minio123" + MINIO_BUCKET_NAME = "payment-sftp" MINIO_SECURE = False - CFS_BASE_URL = 'http://localhost:8080/paybc-api' - CFS_CLIENT_ID = 'TEST' - CFS_CLIENT_SECRET = 'TEST' + CFS_BASE_URL = "http://localhost:8080/paybc-api" + CFS_CLIENT_ID = "TEST" + CFS_CLIENT_SECRET = "TEST" # Secret key for encrypting bank account - ACCOUNT_SECRET_KEY = os.getenv('ACCOUNT_SECRET_KEY', 'test') + ACCOUNT_SECRET_KEY = os.getenv("ACCOUNT_SECRET_KEY", "test") # Secrets for integration tests - TEST_GCP_PROJECT_NAME = 'pay-queue-dev' + TEST_GCP_PROJECT_NAME = "pay-queue-dev" # Needs to have ftp-poller-dev in it. - TEST_GCP_TOPICS = ['account-mailer-dev', 'ftp-poller-dev', 'business-identifier-update-pay-dev'] + TEST_GCP_TOPICS = [ + "account-mailer-dev", + "ftp-poller-dev", + "business-identifier-update-pay-dev", + ] TEST_PUSH_ENDPOINT_PORT = 5020 - TEST_PUSH_ENDPOINT = os.getenv('TEST_PUSH_ENDPOINT', f'http://host.docker.internal:{str(TEST_PUSH_ENDPOINT_PORT)}/') + TEST_PUSH_ENDPOINT = os.getenv( + "TEST_PUSH_ENDPOINT", + f"http://host.docker.internal:{str(TEST_PUSH_ENDPOINT_PORT)}/", + ) GCP_AUTH_KEY = None DISABLE_EJV_ERROR_EMAIL = False DISABLE_CSV_ERROR_EMAIL = False diff --git a/pay-queue/src/pay_queue/enums.py b/pay-queue/src/pay_queue/enums.py index f23c76288..6b2ae29b5 100644 --- a/pay-queue/src/pay_queue/enums.py +++ b/pay-queue/src/pay_queue/enums.py @@ -18,60 +18,60 @@ class SourceTransaction(Enum): """Source Transaction types.""" - PAD = 'BCR-PAD Daily' - ONLINE_BANKING = 'BCR Online Banking Payments' - CREDIT_MEMO = 'CM' - ADJUSTMENT = 'BCR-ADJ' - EFT_WIRE = 'BC REG EFT Wire Cheque' + PAD = "BCR-PAD Daily" + ONLINE_BANKING = "BCR Online Banking Payments" + CREDIT_MEMO = "CM" + ADJUSTMENT = "BCR-ADJ" + EFT_WIRE = "BC REG EFT Wire Cheque" class RecordType(Enum): """Record types.""" - PAD = 'PADP' - PAYR = 'PAYR' - BOLP = 'BOLP' - CMAP = 'CMAP' - ADJS = 'ADJS' - ONAC = 'ONAC' - ONAP = 'ONAP' - EFTP = 'EFTP' - PADR = 'PADR' - DRWP = 'DRWP' + PAD = "PADP" + PAYR = "PAYR" + BOLP = "BOLP" + CMAP = "CMAP" + ADJS = "ADJS" + ONAC = "ONAC" + ONAP = "ONAP" + EFTP = "EFTP" + PADR = "PADR" + DRWP = "DRWP" class Column(Enum): """Column Types.""" - RECORD_TYPE = 'Record type' - SOURCE_TXN = 'Source transaction type' - SOURCE_TXN_NO = 'Source Transaction Number' - APP_ID = 'Application Id' - APP_DATE = 'Application Date' - APP_AMOUNT = 'Application amount' - CUSTOMER_ACC = 'Customer Account' - TARGET_TXN = 'Target transaction type' - TARGET_TXN_NO = 'Target transaction Number' - TARGET_TXN_ORIGINAL = 'Target Transaction Original amount' - TARGET_TXN_OUTSTANDING = 'Target Transaction Outstanding Amount' - TARGET_TXN_STATUS = 'Target transaction status' - REVERSAL_REASON_CODE = 'Reversal Reason code' - REVERSAL_REASON_DESC = 'Reversal reason desc' + RECORD_TYPE = "Record type" + SOURCE_TXN = "Source transaction type" + SOURCE_TXN_NO = "Source Transaction Number" + APP_ID = "Application Id" + APP_DATE = "Application Date" + APP_AMOUNT = "Application amount" + CUSTOMER_ACC = "Customer Account" + TARGET_TXN = "Target transaction type" + TARGET_TXN_NO = "Target transaction Number" + TARGET_TXN_ORIGINAL = "Target Transaction Original amount" + TARGET_TXN_OUTSTANDING = "Target Transaction Outstanding Amount" + TARGET_TXN_STATUS = "Target transaction status" + REVERSAL_REASON_CODE = "Reversal Reason code" + REVERSAL_REASON_DESC = "Reversal reason desc" class Status(Enum): """Target Transaction Status.""" - PAID = 'Fully PAID' - NOT_PAID = 'Not PAID' - ON_ACC = 'On Account' - PARTIAL = 'Partially PAID' + PAID = "Fully PAID" + NOT_PAID = "Not PAID" + ON_ACC = "On Account" + PARTIAL = "Partially PAID" class TargetTransaction(Enum): """Target Transaction.""" - INV = 'INV' - DEBIT_MEMO = 'DM' - CREDIT_MEMO = 'CM' - RECEIPT = 'RECEIPT' + INV = "INV" + DEBIT_MEMO = "DM" + CREDIT_MEMO = "CM" + RECEIPT = "RECEIPT" diff --git a/pay-queue/src/pay_queue/external/gcp_auth.py b/pay-queue/src/pay_queue/external/gcp_auth.py index 398b9ca5f..eead6782b 100644 --- a/pay-queue/src/pay_queue/external/gcp_auth.py +++ b/pay-queue/src/pay_queue/external/gcp_auth.py @@ -15,28 +15,30 @@ def verify_jwt(session): """Check token is valid with the correct audience and email claims for configured email address.""" try: - jwt_token = request.headers.get('Authorization', '').split()[1] + jwt_token = request.headers.get("Authorization", "").split()[1] claims = id_token.verify_oauth2_token( jwt_token, Request(session=session), - audience=current_app.config.get('PAY_AUDIENCE_SUB') + audience=current_app.config.get("PAY_AUDIENCE_SUB"), ) - required_emails = current_app.config.get('VERIFY_PUBSUB_EMAILS') - if claims.get('email_verified') and claims.get('email') in required_emails: + required_emails = current_app.config.get("VERIFY_PUBSUB_EMAILS") + if claims.get("email_verified") and claims.get("email") in required_emails: return None else: - return 'Email not verified or does not match', 401 + return "Email not verified or does not match", 401 except Exception as e: - current_app.logger.info(f'Invalid token {e}') - return f'Invalid token: {e}', 400 + current_app.logger.info(f"Invalid token {e}") + return f"Invalid token: {e}", 400 def ensure_authorized_queue_user(f): """Ensures the user is authorized to use the queue.""" + @functools.wraps(f) def decorated_function(*args, **kwargs): # Use CacheControl to avoid re-fetching certificates for every request. if verify_jwt(CacheControl(Session())): abort(HTTPStatus.UNAUTHORIZED) return f(*args, **kwargs) + return decorated_function diff --git a/pay-queue/src/pay_queue/minio.py b/pay-queue/src/pay_queue/minio.py index d8e8f4eec..30e4aab76 100644 --- a/pay-queue/src/pay_queue/minio.py +++ b/pay-queue/src/pay_queue/minio.py @@ -20,12 +20,16 @@ def get_object(bucket_name: str, file_name: str) -> HTTPResponse: """Return a pre-signed URL for new doc upload.""" - current_app.logger.debug(f'Creating pre-signed URL for {file_name}') - minio_endpoint = current_app.config['MINIO_ENDPOINT'] - minio_key = current_app.config['MINIO_ACCESS_KEY'] - minio_secret = current_app.config['MINIO_ACCESS_SECRET'] + current_app.logger.debug(f"Creating pre-signed URL for {file_name}") + minio_endpoint = current_app.config["MINIO_ENDPOINT"] + minio_key = current_app.config["MINIO_ACCESS_KEY"] + minio_secret = current_app.config["MINIO_ACCESS_SECRET"] - minio_client: Minio = Minio(minio_endpoint, access_key=minio_key, secret_key=minio_secret, - secure=current_app.config['MINIO_SECURE']) + minio_client: Minio = Minio( + minio_endpoint, + access_key=minio_key, + secret_key=minio_secret, + secure=current_app.config["MINIO_SECURE"], + ) return minio_client.get_object(bucket_name, file_name) diff --git a/pay-queue/src/pay_queue/resources/__init__.py b/pay-queue/src/pay_queue/resources/__init__.py index 5fa222686..fca5f3457 100644 --- a/pay-queue/src/pay_queue/resources/__init__.py +++ b/pay-queue/src/pay_queue/resources/__init__.py @@ -24,7 +24,7 @@ def register_endpoints(app: Flask): app.url_map.strict_slashes = False app.register_blueprint( - url_prefix='/', + url_prefix="/", blueprint=worker_endpoint, ) app.register_blueprint(ops_bp) diff --git a/pay-queue/src/pay_queue/resources/worker.py b/pay-queue/src/pay_queue/resources/worker.py index 1d1b30e74..cbcb3092f 100644 --- a/pay-queue/src/pay_queue/resources/worker.py +++ b/pay-queue/src/pay_queue/resources/worker.py @@ -27,11 +27,10 @@ from pay_queue.services.eft.eft_reconciliation import reconcile_eft_payments from pay_queue.services.payment_reconciliations import reconcile_payments +bp = Blueprint("worker", __name__) -bp = Blueprint('worker', __name__) - -@bp.route('/', methods=('POST',)) +@bp.route("/", methods=("POST",)) @ensure_authorized_queue_user def worker(): """Worker to handle incoming queue pushes.""" @@ -40,7 +39,7 @@ def worker(): return {}, HTTPStatus.OK try: - current_app.logger.info('Event Message Received: %s ', json.dumps(dataclasses.asdict(ce))) + current_app.logger.info("Event Message Received: %s ", json.dumps(dataclasses.asdict(ce))) if ce.type == QueueMessageTypes.CAS_MESSAGE_TYPE.value: reconcile_payments(ce) elif ce.type == QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value: @@ -49,13 +48,16 @@ def worker(): reconcile_distributions(ce.data, is_feedback=True) elif ce.type == QueueMessageTypes.EFT_FILE_UPLOADED.value: reconcile_eft_payments(ce) - elif ce.type in [QueueMessageTypes.INCORPORATION.value, QueueMessageTypes.REGISTRATION.value]: + elif ce.type in [ + QueueMessageTypes.INCORPORATION.value, + QueueMessageTypes.REGISTRATION.value, + ]: update_temporary_identifier(ce.data) else: - current_app.logger.warning('Invalid queue message type: %s', ce.type) + current_app.logger.warning("Invalid queue message type: %s", ce.type) return {}, HTTPStatus.OK - except Exception: # NOQA # pylint: disable=broad-except + except Exception: # NOQA # pylint: disable=broad-except # Catch Exception so that any error is still caught and the message is removed from the queue - current_app.logger.error('Error processing event:', exc_info=True) + current_app.logger.error("Error processing event:", exc_info=True) return {}, HTTPStatus.OK diff --git a/pay-queue/src/pay_queue/services/cgi_reconciliations.py b/pay-queue/src/pay_queue/services/cgi_reconciliations.py index b625588c8..cea4a553f 100644 --- a/pay-queue/src/pay_queue/services/cgi_reconciliations.py +++ b/pay-queue/src/pay_queue/services/cgi_reconciliations.py @@ -35,16 +35,25 @@ from pay_api.services import gcp_queue_publisher from pay_api.services.gcp_queue_publisher import QueueMessage from pay_api.utils.enums import ( - DisbursementStatus, EFTShortnameRefundStatus, EjvFileType, EJVLinkType, InvoiceReferenceStatus, InvoiceStatus, - PaymentMethod, PaymentStatus, PaymentSystem, QueueSources, RoutingSlipStatus) + DisbursementStatus, + EFTShortnameRefundStatus, + EjvFileType, + EJVLinkType, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, + PaymentStatus, + PaymentSystem, + QueueSources, + RoutingSlipStatus, +) from sbc_common_components.utils.enums import QueueMessageTypes from sentry_sdk import capture_message from pay_queue import config from pay_queue.minio import get_object - -APP_CONFIG = config.get_named_config(os.getenv('DEPLOYMENT_ENV', 'production')) +APP_CONFIG = config.get_named_config(os.getenv("DEPLOYMENT_ENV", "production")) def reconcile_distributions(msg: Dict[str, any], is_feedback: bool = False): @@ -61,27 +70,27 @@ def reconcile_distributions(msg: Dict[str, any], is_feedback: bool = False): def _update_acknowledgement(msg: Dict[str, any]): """Log the ack file, we don't know which batch it's for.""" - current_app.logger.info('Ack file received: %s', msg.get('fileName')) + current_app.logger.info("Ack file received: %s", msg.get("fileName")) def _update_feedback(msg: Dict[str, any]): # pylint:disable=too-many-locals, too-many-statements # Read the file and find records from the database, and update status. - file_name: str = msg.get('fileName') - minio_location: str = msg.get('location') + file_name: str = msg.get("fileName") + minio_location: str = msg.get("location") file = get_object(minio_location, file_name) - content = file.data.decode('utf-8-sig') + content = file.data.decode("utf-8-sig") group_batches: List[str] = _group_batches(content) - if _is_processed_or_processing(group_batches['EJV'], file_name): + if _is_processed_or_processing(group_batches["EJV"], file_name): return - has_errors = _process_ejv_feedback(group_batches['EJV']) - has_errors = _process_ap_feedback(group_batches['AP']) or has_errors + has_errors = _process_ejv_feedback(group_batches["EJV"]) + has_errors = _process_ap_feedback(group_batches["AP"]) or has_errors if has_errors and not APP_CONFIG.DISABLE_EJV_ERROR_EMAIL: _publish_mailer_events(file_name, minio_location) - current_app.logger.info('Feedback file processing completed.') + current_app.logger.info("Feedback file processing completed.") def _is_processed_or_processing(group_batches, file_name) -> bool: @@ -89,14 +98,16 @@ def _is_processed_or_processing(group_batches, file_name) -> bool: for group_batch in group_batches: ejv_file: Optional[EjvFileModel] = None for line in group_batch.splitlines(): - is_batch_group: bool = line[2:4] == 'BG' + is_batch_group: bool = line[2:4] == "BG" if is_batch_group: batch_number = int(line[15:24]) ejv_file = EjvFileModel.find_by_id(batch_number) if ejv_file.feedback_file_ref: current_app.logger.info( - 'EJV file id %s with feedback file %s is already processing or has been processed. Skipping.', - batch_number, file_name) + "EJV file id %s with feedback file %s is already processing or has been processed. Skipping.", + batch_number, + file_name, + ) return True ejv_file.feedback_file_ref = file_name ejv_file.save() @@ -111,10 +122,10 @@ def _process_ejv_feedback(group_batches) -> bool: # pylint:disable=too-many-loc receipt_number: Optional[str] = None for line in group_batch.splitlines(): # For all these indexes refer the sharepoint docs refer : https://github.com/bcgov/entity/issues/6226 - is_batch_group = line[2:4] == 'BG' - is_batch_header = line[2:4] == 'BH' - is_jv_header = line[2:4] == 'JH' - is_jv_detail = line[2:4] == 'JD' + is_batch_group = line[2:4] == "BG" + is_batch_header = line[2:4] == "BH" + is_jv_header = line[2:4] == "JH" + is_jv_detail = line[2:4] == "JD" if is_batch_group: batch_number = int(line[15:24]) ejv_file = EjvFileModel.find_by_id(batch_number) @@ -169,11 +180,11 @@ def _process_jv_details_feedback(ejv_file, has_errors, line, receipt_number) -> details = _build_jv_details(line, receipt_number) # If the JV process failed, then mark the GL code against the invoice to be stopped # for further JV process for the credit GL. - current_app.logger.info('Is Credit or Debit %s - %s', line[104:105], ejv_file.file_type) + current_app.logger.info("Is Credit or Debit %s - %s", line[104:105], ejv_file.file_type) credit_or_debit_line = details.line[104:105] - if credit_or_debit_line == 'C' and ejv_file.file_type == EjvFileType.DISBURSEMENT.value: + if credit_or_debit_line == "C" and ejv_file.file_type == EjvFileType.DISBURSEMENT.value: has_errors = _handle_jv_disbursement_feedback(details, has_errors) - elif credit_or_debit_line == 'D' and ejv_file.file_type == EjvFileType.PAYMENT.value: + elif credit_or_debit_line == "D" and ejv_file.file_type == EjvFileType.PAYMENT.value: has_errors = _handle_jv_payment_feedback(details, has_errors) return has_errors @@ -188,20 +199,23 @@ def _build_jv_details(line, receipt_number) -> JVDetailsFeedback: flowthrough=line[205:315].strip(), invoice_return_code=line[315:319], invoice_return_message=line[319:469], - receipt_number=receipt_number + receipt_number=receipt_number, ) - if '-' in details.flowthrough: - invoice_id = int(details.flowthrough.split('-')[0]) - partner_disbursement_id = int(details.flowthrough.split('-')[1]) + if "-" in details.flowthrough: + invoice_id = int(details.flowthrough.split("-")[0]) + partner_disbursement_id = int(details.flowthrough.split("-")[1]) details.partner_disbursement = PartnerDisbursementsModel.find_by_id(partner_disbursement_id) else: invoice_id = int(details.flowthrough) - current_app.logger.info('Invoice id - %s', invoice_id) + current_app.logger.info("Invoice id - %s", invoice_id) details.invoice = InvoiceModel.find_by_id(invoice_id) - details.invoice_link = db.session.query(EjvLinkModel).filter( - EjvLinkModel.ejv_header_id == details.ejv_header_model_id).filter( - EjvLinkModel.link_id == invoice_id).filter( - EjvLinkModel.link_type == EJVLinkType.INVOICE.value).one_or_none() + details.invoice_link = ( + db.session.query(EjvLinkModel) + .filter(EjvLinkModel.ejv_header_id == details.ejv_header_model_id) + .filter(EjvLinkModel.link_id == invoice_id) + .filter(EjvLinkModel.link_type == EJVLinkType.INVOICE.value) + .one_or_none() + ) return details @@ -209,7 +223,7 @@ def _handle_jv_disbursement_feedback(details: JVDetailsFeedback, has_errors: boo disbursement_status = _get_disbursement_status(details.invoice_return_code) details.invoice_link.disbursement_status_code = disbursement_status details.invoice_link.message = details.invoice_return_message.strip() - current_app.logger.info('disbursement_status %s', disbursement_status) + current_app.logger.info("disbursement_status %s", disbursement_status) if disbursement_status == DisbursementStatus.ERRORED.value: has_errors = True if details.partner_disbursement: @@ -219,13 +233,13 @@ def _handle_jv_disbursement_feedback(details: JVDetailsFeedback, has_errors: boo line_items: List[PaymentLineItemModel] = details.invoice.payment_line_items for line_item in line_items: # Line debit distribution - debit_distribution: DistributionCodeModel = DistributionCodeModel \ - .find_by_id(line_item.fee_distribution_id) - credit_distribution: DistributionCodeModel = DistributionCodeModel \ - .find_by_id(debit_distribution.disbursement_distribution_code_id) + debit_distribution: DistributionCodeModel = DistributionCodeModel.find_by_id(line_item.fee_distribution_id) + credit_distribution: DistributionCodeModel = DistributionCodeModel.find_by_id( + debit_distribution.disbursement_distribution_code_id + ) credit_distribution.stop_ejv = True else: - effective_date = datetime.strptime(details.line[22:30], '%Y%m%d') + effective_date = datetime.strptime(details.line[22:30], "%Y%m%d") _update_invoice_disbursement_status(details.invoice, effective_date, details.partner_disbursement) return has_errors @@ -234,25 +248,30 @@ def _handle_jv_payment_feedback(details: JVDetailsFeedback, has_errors: bool) -> # This is for gov account payment JV. details.invoice_link.disbursement_status_code = _get_disbursement_status(details.invoice_return_code) details.invoice_link.message = details.invoice_return_message.strip() - current_app.logger.info('Invoice ID %s', details.invoice.id) + current_app.logger.info("Invoice ID %s", details.invoice.id) inv_ref = InvoiceReferenceModel.find_by_invoice_id_and_status( - details.invoice.id, InvoiceReferenceStatus.ACTIVE.value) - current_app.logger.info('invoice_link.disbursement_status_code %s', details.invoice_link.disbursement_status_code) + details.invoice.id, InvoiceReferenceStatus.ACTIVE.value + ) + current_app.logger.info( + "invoice_link.disbursement_status_code %s", + details.invoice_link.disbursement_status_code, + ) if details.invoice_link.disbursement_status_code == DisbursementStatus.ERRORED.value: has_errors = True # Cancel the invoice reference. if inv_ref: inv_ref.status_code = InvoiceReferenceStatus.CANCELLED.value # Find the distribution code and set the stop_ejv flag to TRUE - dist_code = DistributionCodeModel.find_by_active_for_account( - details.invoice.payment_account_id) + dist_code = DistributionCodeModel.find_by_active_for_account(details.invoice.payment_account_id) dist_code.stop_ejv = True elif details.invoice_link.disbursement_status_code == DisbursementStatus.COMPLETED.value: # Set the invoice status as REFUNDED if it's a JV reversal, else mark as PAID - effective_date = datetime.strptime(details.line[22:30], '%Y%m%d') + effective_date = datetime.strptime(details.line[22:30], "%Y%m%d") # No need for credited here as these are just for EJV payments, which are never credited. is_reversal = details.invoice.invoice_status_code in ( - InvoiceStatus.REFUNDED.value, InvoiceStatus.REFUND_REQUESTED.value) + InvoiceStatus.REFUNDED.value, + InvoiceStatus.REFUND_REQUESTED.value, + ) _set_invoice_jv_reversal(details.invoice, effective_date, is_reversal) # Mark the invoice reference as COMPLETED, create a receipt @@ -260,14 +279,18 @@ def _handle_jv_payment_feedback(details: JVDetailsFeedback, has_errors: bool) -> inv_ref.status_code = InvoiceReferenceStatus.COMPLETED.value # Find receipt and add total to it, as single invoice can be multiple rows in the file if not is_reversal: - receipt = ReceiptModel.find_by_invoice_id_and_receipt_number(invoice_id=details.invoice.id, - receipt_number=details.receipt_number) + receipt = ReceiptModel.find_by_invoice_id_and_receipt_number( + invoice_id=details.invoice.id, receipt_number=details.receipt_number + ) if receipt: receipt.receipt_amount += float(details.line[89:104]) else: - ReceiptModel(invoice_id=details.invoice.id, receipt_number=details.receipt_number, - receipt_date=datetime.now(tz=timezone.utc), - receipt_amount=float(details.line[89:104])).flush() + ReceiptModel( + invoice_id=details.invoice.id, + receipt_number=details.receipt_number, + receipt_date=datetime.now(tz=timezone.utc), + receipt_amount=float(details.line[89:104]), + ).flush() return has_errors @@ -285,9 +308,9 @@ def _set_invoice_jv_reversal(invoice: InvoiceModel, effective_date: datetime, is def _fix_invoice_line(line): """Work around for CAS, they said fix the feedback files.""" # Check for zeros within 300->315 range. Bump them over with spaces. - if (zero_position := line[300:315].find('0')) > -1: + if (zero_position := line[300:315].find("0")) > -1: spaces_to_insert = 15 - zero_position - return line[:300 + zero_position] + (' ' * spaces_to_insert) + line[300 + zero_position:] + return line[: 300 + zero_position] + (" " * spaces_to_insert) + line[300 + zero_position :] return line @@ -300,13 +323,18 @@ def _update_partner_disbursement(partner_disbursement, status_code, effective_da partner_disbursement.feedback_on = effective_date -def _update_invoice_disbursement_status(invoice: InvoiceModel, - effective_date: datetime, - partner_disbursement: PartnerDisbursementsModel): +def _update_invoice_disbursement_status( + invoice: InvoiceModel, + effective_date: datetime, + partner_disbursement: PartnerDisbursementsModel, +): """Update status to reversed if its a refund, else to completed.""" # Look up partner disbursements table and update the status. - if invoice.invoice_status_code in (InvoiceStatus.REFUNDED.value, InvoiceStatus.REFUND_REQUESTED.value, - InvoiceStatus.CREDITED.value): + if invoice.invoice_status_code in ( + InvoiceStatus.REFUNDED.value, + InvoiceStatus.REFUND_REQUESTED.value, + InvoiceStatus.CREDITED.value, + ): _update_partner_disbursement(partner_disbursement, DisbursementStatus.REVERSED.value, effective_date) invoice.disbursement_status_code = DisbursementStatus.REVERSED.value invoice.disbursement_reversal_date = effective_date @@ -326,55 +354,57 @@ def _create_payment_record(amount, ejv_header, receipt_number): receipt_number=receipt_number, invoice_amount=amount, paid_amount=amount, - payment_date=datetime.now()).flush() + payment_date=datetime.now(), + ).flush() def _group_batches(content: str) -> Dict[str, List]: """Group batches based on the group and trailer.""" # A batch starts from BG to BT. - group_batches: Dict[str, List] = {'EJV': [], 'AP': []} - batch_content: str = '' + group_batches: Dict[str, List] = {"EJV": [], "AP": []} + batch_content: str = "" is_ejv = True for line in content.splitlines(): - if line[:4] in ('GABG', 'GIBG', 'APBG'): # batch starts from GIBG or GABG for JV - is_ejv = line[:4] in ('GABG', 'GIBG') + if line[:4] in ( + "GABG", + "GIBG", + "APBG", + ): # batch starts from GIBG or GABG for JV + is_ejv = line[:4] in ("GABG", "GIBG") batch_content = line else: batch_content = batch_content + os.linesep + line - if line[2:4] == 'BT': # batch ends with BT + if line[2:4] == "BT": # batch ends with BT if is_ejv: - group_batches['EJV'].append(batch_content) + group_batches["EJV"].append(batch_content) else: - group_batches['AP'].append(batch_content) + group_batches["AP"].append(batch_content) return group_batches def _get_disbursement_status(return_code: str) -> str: """Return disbursement status from return code.""" - if return_code == '0000': + if return_code == "0000": return DisbursementStatus.COMPLETED.value return DisbursementStatus.ERRORED.value def _publish_mailer_events(file_name: str, minio_location: str): """Publish payment message to the mailer queue.""" - payload = { - 'fileName': file_name, - 'minioLocation': minio_location - } + payload = {"fileName": file_name, "minioLocation": minio_location} try: gcp_queue_publisher.publish_to_queue( QueueMessage( source=QueueSources.PAY_QUEUE.value, message_type=QueueMessageTypes.EJV_FAILED.value, payload=payload, - topic=current_app.config.get('ACCOUNT_MAILER_TOPIC') + topic=current_app.config.get("ACCOUNT_MAILER_TOPIC"), ) ) except Exception as e: # NOQA pylint: disable=broad-except current_app.logger.error(e) - capture_message('EJV Failed message error', level='error') + capture_message("EJV Failed message error", level="error") def _process_ap_feedback(group_batches) -> bool: # pylint:disable=too-many-locals @@ -384,9 +414,9 @@ def _process_ap_feedback(group_batches) -> bool: # pylint:disable=too-many-loca ejv_file: Optional[EjvFileModel] = None for line in group_batch.splitlines(): # For all these indexes refer the sharepoint docs refer : https://github.com/bcgov/entity/issues/6226 - is_batch_group: bool = line[2:4] == 'BG' - is_batch_header: bool = line[2:4] == 'BH' - is_ap_header: bool = line[2:4] == 'IH' + is_batch_group: bool = line[2:4] == "BG" + is_batch_header: bool = line[2:4] == "BH" + is_ap_header: bool = line[2:4] == "IH" if is_batch_group: batch_number = int(line[15:24]) ejv_file = EjvFileModel.find_by_id(batch_number) @@ -424,8 +454,10 @@ def _process_ap_header_routing_slips(line) -> bool: if _get_disbursement_status(ap_header_return_code) == DisbursementStatus.ERRORED.value: has_errors = True routing_slip.status = RoutingSlipStatus.REFUND_REJECTED.value - capture_message(f'Refund failed for {routing_slip_number}, reason : {ap_header_error_message}', - level='error') + capture_message( + f"Refund failed for {routing_slip_number}, reason : {ap_header_error_message}", + level="error", + ) else: routing_slip.status = RoutingSlipStatus.REFUND_COMPLETED.value refund = RefundModel.find_by_routing_slip_id(routing_slip.id) @@ -444,7 +476,10 @@ def _process_ap_header_eft(line) -> bool: has_errors = True eft_refund.status = EFTShortnameRefundStatus.ERRORED.value eft_refund.disbursement_status_code = DisbursementStatus.ERRORED.value - capture_message(f'EFT Refund failed for {eft_refund_id}, reason : {ap_header_error_message}', level='error') + capture_message( + f"EFT Refund failed for {eft_refund_id}, reason : {ap_header_error_message}", + level="error", + ) else: eft_refund.status = EFTShortnameRefundStatus.COMPLETED.value eft_refund.disbursement_status_code = DisbursementStatus.COMPLETED.value @@ -460,19 +495,24 @@ def _process_ap_header_non_gov_disbursement(line, ejv_file: EjvFileModel) -> boo ap_header_return_code = line[414:418] ap_header_error_message = line[418:568] disbursement_status = _get_disbursement_status(ap_header_return_code) - invoice_link = db.session.query(EjvLinkModel)\ - .join(EjvHeaderModel).join(EjvFileModel)\ - .filter(EjvFileModel.id == ejv_file.id)\ - .filter(EjvLinkModel.link_id == invoice_id)\ - .filter(EjvLinkModel.link_type == EJVLinkType.INVOICE.value) \ + invoice_link = ( + db.session.query(EjvLinkModel) + .join(EjvHeaderModel) + .join(EjvFileModel) + .filter(EjvFileModel.id == ejv_file.id) + .filter(EjvLinkModel.link_id == invoice_id) + .filter(EjvLinkModel.link_type == EJVLinkType.INVOICE.value) .one_or_none() + ) invoice_link.disbursement_status_code = disbursement_status invoice_link.message = ap_header_error_message.strip() if disbursement_status == DisbursementStatus.ERRORED.value: invoice.disbursement_status_code = disbursement_status has_errors = True - capture_message(f'AP - NON-GOV - Disbursement failed for {invoice_id}, reason : {ap_header_error_message}', - level='error') + capture_message( + f"AP - NON-GOV - Disbursement failed for {invoice_id}, reason : {ap_header_error_message}", + level="error", + ) else: # TODO - Fix this on BC Assessment launch, so the effective date reads from the feedback. _update_invoice_disbursement_status(invoice, effective_date=datetime.now(), partner_disbursement=None) diff --git a/pay-queue/src/pay_queue/services/eft/eft_base.py b/pay-queue/src/pay_queue/services/eft/eft_base.py index 13f396db7..0026d7b4c 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_base.py +++ b/pay-queue/src/pay_queue/services/eft/eft_base.py @@ -100,8 +100,8 @@ def parse_decimal(self, value: str, error: EFTError) -> decimal: """Try to parse decimal value from a string, return None if it fails and add an error.""" try: # ends with blank or minus sign, handle the minus sign situation - if value.endswith('-'): - value = '-' + value[:-1] + if value.endswith("-"): + value = "-" + value[:-1] result = decimal.Decimal(str(value)) except (ValueError, TypeError, decimal.InvalidOperation): diff --git a/pay-queue/src/pay_queue/services/eft/eft_enums.py b/pay-queue/src/pay_queue/services/eft/eft_enums.py index 86ae39f6e..e47be6a24 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_enums.py +++ b/pay-queue/src/pay_queue/services/eft/eft_enums.py @@ -19,16 +19,16 @@ class EFTConstants(Enum): """EFT constants.""" # Currency - CURRENCY_CAD = 'CAD' + CURRENCY_CAD = "CAD" # Record Type - HEADER_RECORD_TYPE = '1' - TRANSACTION_RECORD_TYPE = '2' - TRAILER_RECORD_TYPE = '7' + HEADER_RECORD_TYPE = "1" + TRANSACTION_RECORD_TYPE = "2" + TRAILER_RECORD_TYPE = "7" # Formats - DATE_TIME_FORMAT = '%Y%m%d%H%M' - DATE_FORMAT = '%Y%m%d' + DATE_TIME_FORMAT = "%Y%m%d%H%M" + DATE_FORMAT = "%Y%m%d" # Lengths EXPECTED_LINE_LENGTH = 140 diff --git a/pay-queue/src/pay_queue/services/eft/eft_errors.py b/pay-queue/src/pay_queue/services/eft/eft_errors.py index 21b5906ad..7abf7566a 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_errors.py +++ b/pay-queue/src/pay_queue/services/eft/eft_errors.py @@ -19,16 +19,16 @@ class EFTError(Enum): """EFT Error Enum.""" - INVALID_LINE_LENGTH = 'Invalid EFT file line length.' - INVALID_RECORD_TYPE = 'Invalid Record Type.' - INVALID_CREATION_DATETIME = 'Invalid header creation date time.' - INVALID_DEPOSIT_START_DATE = 'Invalid header deposit start date.' - INVALID_DEPOSIT_END_DATE = 'Invalid header deposit end date.' - INVALID_NUMBER_OF_DETAILS = 'Invalid trailer number of details value.' - INVALID_TOTAL_DEPOSIT_AMOUNT = 'Invalid trailer total deposit amount.' - INVALID_DEPOSIT_AMOUNT = 'Invalid transaction deposit amount.' - INVALID_EXCHANGE_ADJ_AMOUNT = 'Invalid transaction exchange adjustment amount.' - INVALID_DEPOSIT_AMOUNT_CAD = 'Invalid transaction deposit amount CAD.' - INVALID_TRANSACTION_DATE = 'Invalid transaction date.' - INVALID_DEPOSIT_DATETIME = 'Invalid transaction deposit date time' - ACCOUNT_SHORTNAME_REQUIRED = 'Account shortname is missing from the transaction description.' + INVALID_LINE_LENGTH = "Invalid EFT file line length." + INVALID_RECORD_TYPE = "Invalid Record Type." + INVALID_CREATION_DATETIME = "Invalid header creation date time." + INVALID_DEPOSIT_START_DATE = "Invalid header deposit start date." + INVALID_DEPOSIT_END_DATE = "Invalid header deposit end date." + INVALID_NUMBER_OF_DETAILS = "Invalid trailer number of details value." + INVALID_TOTAL_DEPOSIT_AMOUNT = "Invalid trailer total deposit amount." + INVALID_DEPOSIT_AMOUNT = "Invalid transaction deposit amount." + INVALID_EXCHANGE_ADJ_AMOUNT = "Invalid transaction exchange adjustment amount." + INVALID_DEPOSIT_AMOUNT_CAD = "Invalid transaction deposit amount CAD." + INVALID_TRANSACTION_DATE = "Invalid transaction date." + INVALID_DEPOSIT_DATETIME = "Invalid transaction deposit date time" + ACCOUNT_SHORTNAME_REQUIRED = "Account shortname is missing from the transaction description." diff --git a/pay-queue/src/pay_queue/services/eft/eft_header.py b/pay-queue/src/pay_queue/services/eft/eft_header.py index 9244eafa3..03aaef916 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_header.py +++ b/pay-queue/src/pay_queue/services/eft/eft_header.py @@ -44,8 +44,10 @@ def _process(self): self.validate_record_type(EFTConstants.HEADER_RECORD_TYPE.value) # Confirm valid file creation datetime - self.creation_datetime = self.parse_datetime(self.extract_value(16, 24) + self.extract_value(41, 45), - EFTError.INVALID_CREATION_DATETIME) + self.creation_datetime = self.parse_datetime( + self.extract_value(16, 24) + self.extract_value(41, 45), + EFTError.INVALID_CREATION_DATETIME, + ) # Confirm valid deposit dates self.starting_deposit_date = self.parse_date(self.extract_value(69, 77), EFTError.INVALID_DEPOSIT_START_DATE) diff --git a/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py b/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py index 1795f4fbe..02f5bdf87 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py +++ b/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py @@ -39,25 +39,24 @@ def __init__(self, ce): """:param ce: The cloud event object containing relevant data.""" self.ce = ce self.msg = ce.data - self.file_name: str = self.msg.get('fileName') - self.minio_location: str = self.msg.get('location') + self.file_name: str = self.msg.get("fileName") + self.minio_location: str = self.msg.get("location") self.error_messages: List[Dict[str, any]] = [] - def eft_error_handling(self, row, error_msg: str, - capture_error: bool = True, table_name: str = None): + def eft_error_handling(self, row, error_msg: str, capture_error: bool = True, table_name: str = None): """Handle EFT errors by logging, capturing messages, and optionally sending an email.""" if capture_error: current_app.logger.error(error_msg, exc_info=True) - capture_message(error_msg, level='error') - self.error_messages.append({'error': error_msg, 'row': row}) + capture_message(error_msg, level="error") + self.error_messages.append({"error": error_msg, "row": row}) if table_name is not None: email_service_params = EmailParams( - subject='EFT TDI17 Reconciliation Failure', + subject="EFT TDI17 Reconciliation Failure", file_name=self.file_name, minio_location=self.minio_location, error_messages=self.error_messages, ce=self.ce, - table_name=table_name + table_name=table_name, ) send_error_email(email_service_params) @@ -79,25 +78,28 @@ def reconcile_eft_payments(ce): # pylint: disable=too-many-locals context = EFTReconciliation(ce) # Used to filter transactions by location id to isolate EFT specific transactions from the TDI17 - eft_location_id = current_app.config.get('EFT_TDI17_LOCATION_ID') + eft_location_id = current_app.config.get("EFT_TDI17_LOCATION_ID") if eft_location_id is None: - context.eft_error_handling('N/A', 'Missing EFT_TDI17_LOCATION_ID configuration') + context.eft_error_handling("N/A", "Missing EFT_TDI17_LOCATION_ID configuration") return # Fetch EFT File file = get_object(context.minio_location, context.file_name) - file_content = file.data.decode('utf-8-sig') + file_content = file.data.decode("utf-8-sig") # Split into lines lines = file_content.splitlines() # Check if there is an existing EFT File record - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == context.file_name).one_or_none() - - if eft_file_model and eft_file_model.status_code in \ - [EFTProcessStatus.IN_PROGRESS.value, EFTProcessStatus.COMPLETED.value]: - current_app.logger.info('File: %s already %s.', context.file_name, str(eft_file_model.status_code)) + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == context.file_name).one_or_none() + ) + + if eft_file_model and eft_file_model.status_code in [ + EFTProcessStatus.IN_PROGRESS.value, + EFTProcessStatus.COMPLETED.value, + ]: + current_app.logger.info("File: %s already %s.", context.file_name, str(eft_file_model.status_code)) return # There is no existing EFT File record - instantiate one @@ -115,11 +117,10 @@ def reconcile_eft_payments(ce): # pylint: disable=too-many-locals # If header and/or trailer has errors do not proceed if not (eft_header_valid and eft_trailer_valid): - error_msg = f'Failed to process file {context.file_name} with an invalid header or trailer.' + error_msg = f"Failed to process file {context.file_name} with an invalid header or trailer." eft_file_model.status_code = EFTProcessStatus.FAILED.value eft_file_model.save() - context.eft_error_handling('N/A', error_msg, - table_name=eft_file_model.__tablename__) + context.eft_error_handling("N/A", error_msg, table_name=eft_file_model.__tablename__) return has_eft_transaction_errors = False @@ -131,19 +132,26 @@ def reconcile_eft_payments(ce): # pylint: disable=too-many-locals for eft_transaction in eft_transactions: if eft_transaction.has_errors(): # Flag any instance of an error - will indicate file is partially processed has_eft_transaction_errors = True - context.eft_error_handling(eft_transaction.index, eft_transaction.errors[0].message, capture_error=False) + context.eft_error_handling( + eft_transaction.index, + eft_transaction.errors[0].message, + capture_error=False, + ) _save_eft_transaction(eft_record=eft_transaction, eft_file_model=eft_file_model, is_error=True) else: # Save TDI17 transaction record - _save_eft_transaction(eft_record=eft_transaction, eft_file_model=eft_file_model, is_error=False) + _save_eft_transaction( + eft_record=eft_transaction, + eft_file_model=eft_file_model, + is_error=False, + ) # EFT Transactions have parsing errors - stop and FAIL transactions # We want a full file to be parseable as we want to get a full accurate balance before applying them to invoices if has_eft_transaction_errors: - error_msg = f'Failed to process file {context.file_name} has transaction parsing errors.' + error_msg = f"Failed to process file {context.file_name} has transaction parsing errors." _update_transactions_to_fail(eft_file_model) - context.eft_error_handling('N/A', error_msg, - table_name=eft_file_model.__tablename__) + context.eft_error_handling("N/A", error_msg, table_name=eft_file_model.__tablename__) return # Generate dictionary with shortnames and total deposits @@ -156,9 +164,8 @@ def reconcile_eft_payments(ce): # pylint: disable=too-many-locals if has_eft_transaction_errors or has_eft_credits_error: db.session.rollback() _update_transactions_to_fail(eft_file_model) - error_msg = f'Failed to process file {context.file_name} due to transaction errors.' - context.eft_error_handling('N/A', error_msg, - table_name=eft_file_model.__tablename__) + error_msg = f"Failed to process file {context.file_name} due to transaction errors." + context.eft_error_handling("N/A", error_msg, table_name=eft_file_model.__tablename__) return _finalize_process_state(eft_file_model) @@ -186,31 +193,32 @@ def _parse_tdi17_lines(eft_lines: List[str]): def _apply_eft_pending_payments(context: EFTReconciliation, shortname_balance): """Apply payments to short name links.""" for shortname in shortname_balance.keys(): - short_name_type = shortname_balance[shortname]['short_name_type'] + short_name_type = shortname_balance[shortname]["short_name_type"] eft_short_name = _get_shortname(shortname, short_name_type) eft_credit_balance = EFTCreditModel.get_eft_credit_balance(eft_short_name.id) - shortname_links = EFTShortnamesService.get_shortname_links(eft_short_name.id).get('items', []) + shortname_links = EFTShortnamesService.get_shortname_links(eft_short_name.id).get("items", []) for shortname_link in shortname_links: # We are expecting pending payments to have been cleared since this runs after the # eft task job. Something may have gone wrong, we will skip this link. - if shortname_link.get('has_pending_payment'): - error_msg = f'Unexpected pending payment on link: {shortname_link.id}' - context.eft_error_handling('N/A', error_msg, - table_name=eft_short_name.__tablename__) + if shortname_link.get("has_pending_payment"): + error_msg = f"Unexpected pending payment on link: {shortname_link.id}" + context.eft_error_handling("N/A", error_msg, table_name=eft_short_name.__tablename__) continue - amount_owing = shortname_link.get('amount_owing') - auth_account_id = shortname_link.get('account_id') + amount_owing = shortname_link.get("amount_owing") + auth_account_id = shortname_link.get("account_id") if 0 < amount_owing <= eft_credit_balance: try: - payload = {'action': EFTPaymentActions.APPLY_CREDITS.value, - 'accountId': auth_account_id} + payload = { + "action": EFTPaymentActions.APPLY_CREDITS.value, + "accountId": auth_account_id, + } EFTShortnamesService.process_payment_action(eft_short_name.id, payload) except Exception as exception: # NOQA # pylint: disable=broad-except # EFT Short name service handles commit and rollback when the action fails, we just need to make # sure we log the error here - error_msg = 'error in _apply_eft_pending_payments.' - context.eft_error_handling('N/A', error_msg, ex=exception) + error_msg = "error in _apply_eft_pending_payments." + context.eft_error_handling("N/A", error_msg, ex=exception) def _finalize_process_state(eft_file_model: EFTFileModel): @@ -226,7 +234,7 @@ def _finalize_process_state(eft_file_model: EFTFileModel): def _process_eft_header(eft_header: EFTHeader, eft_file_model: EFTFileModel) -> bool: """Process the EFT Header.""" if eft_header is None: - current_app.logger.error('Failed to process file %s with an invalid header.', eft_file_model.file_ref) + current_app.logger.error("Failed to process file %s with an invalid header.", eft_file_model.file_ref) return False # Populate header and trailer data on EFT File record - values will return None if parsing failed @@ -243,7 +251,10 @@ def _process_eft_header(eft_header: EFTHeader, eft_file_model: EFTFileModel) -> def _process_eft_trailer(eft_trailer: EFTTrailer, eft_file_model: EFTFileModel) -> bool: """Process the EFT Trailer.""" if eft_trailer is None: - current_app.logger.error('Failed to process file %s with an invalid trailer.', eft_file_model.file_ref) + current_app.logger.error( + "Failed to process file %s with an invalid trailer.", + eft_file_model.file_ref, + ) return False # Populate header and trailer data on EFT File record - values will return None if parsing failed @@ -262,23 +273,25 @@ def _process_eft_credits(shortname_balance, eft_file_id): has_credit_errors = False for shortname in shortname_balance.keys(): try: - short_name_type = shortname_balance[shortname]['short_name_type'] + short_name_type = shortname_balance[shortname]["short_name_type"] eft_short_name = _get_shortname(shortname, short_name_type) - eft_transactions = shortname_balance[shortname]['transactions'] + eft_transactions = shortname_balance[shortname]["transactions"] for eft_transaction in eft_transactions: # Check if there is an existing eft credit for this file and transaction - eft_credit_model = db.session.query(EFTCreditModel) \ - .filter(EFTCreditModel.eft_file_id == eft_file_id) \ - .filter(EFTCreditModel.short_name_id == eft_short_name.id) \ - .filter(EFTCreditModel.eft_transaction_id == eft_transaction['id']) \ + eft_credit_model = ( + db.session.query(EFTCreditModel) + .filter(EFTCreditModel.eft_file_id == eft_file_id) + .filter(EFTCreditModel.short_name_id == eft_short_name.id) + .filter(EFTCreditModel.eft_transaction_id == eft_transaction["id"]) .one_or_none() + ) if eft_credit_model is None: eft_credit_model = EFTCreditModel() # Skip if there is no deposit amount - deposit_amount = eft_transaction['deposit_amount'] + deposit_amount = eft_transaction["deposit_amount"] if not deposit_amount > 0: continue @@ -286,39 +299,45 @@ def _process_eft_credits(shortname_balance, eft_file_id): eft_credit_model.short_name_id = eft_short_name.id eft_credit_model.amount = deposit_amount eft_credit_model.remaining_amount = deposit_amount - eft_credit_model.eft_transaction_id = eft_transaction['id'] + eft_credit_model.eft_transaction_id = eft_transaction["id"] eft_credit_model.flush() credit_balance = EFTCreditModel.get_eft_credit_balance(eft_credit_model.short_name_id) - EFTHistoryService.create_funds_received(EFTHistory(short_name_id=eft_credit_model.short_name_id, - amount=deposit_amount, - credit_balance=credit_balance)).flush() + EFTHistoryService.create_funds_received( + EFTHistory( + short_name_id=eft_credit_model.short_name_id, + amount=deposit_amount, + credit_balance=credit_balance, + ) + ).flush() except Exception as e: # NOQA pylint: disable=broad-exception-caught has_credit_errors = True current_app.logger.error(e, exc_info=True) - capture_message('EFT Failed to set EFT balance.', level='error') + capture_message("EFT Failed to set EFT balance.", level="error") return has_credit_errors def _set_eft_header_on_file(eft_header: EFTHeader, eft_file_model: EFTFileModel): """Set EFT Header information on EFTFile model.""" - eft_file_model.file_creation_date = getattr(eft_header, 'creation_datetime', None) - eft_file_model.deposit_from_date = getattr(eft_header, 'starting_deposit_date', None) - eft_file_model.deposit_to_date = getattr(eft_header, 'ending_deposit_date', None) + eft_file_model.file_creation_date = getattr(eft_header, "creation_datetime", None) + eft_file_model.deposit_from_date = getattr(eft_header, "starting_deposit_date", None) + eft_file_model.deposit_to_date = getattr(eft_header, "ending_deposit_date", None) def _set_eft_trailer_on_file(eft_trailer: EFTTrailer, eft_file_model: EFTFileModel): """Set EFT Trailer information on EFTFile model.""" - eft_file_model.number_of_details = getattr(eft_trailer, 'number_of_details', None) - eft_file_model.total_deposit_cents = getattr(eft_trailer, 'total_deposit_amount', None) + eft_file_model.number_of_details = getattr(eft_trailer, "number_of_details", None) + eft_file_model.total_deposit_cents = getattr(eft_trailer, "total_deposit_amount", None) -def _set_eft_base_error(line_type: str, index: int, - eft_file_id: int, error_messages: [str]) -> EFTTransactionModel: +def _set_eft_base_error(line_type: str, index: int, eft_file_id: int, error_messages: [str]) -> EFTTransactionModel: """Instantiate EFT Transaction model error record.""" - eft_transaction_model = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_id) \ - .filter(EFTTransactionModel.line_type == line_type).one_or_none() + eft_transaction_model = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_id) + .filter(EFTTransactionModel.line_type == line_type) + .one_or_none() + ) if eft_transaction_model is None: eft_transaction_model = EFTTransactionModel() @@ -334,19 +353,23 @@ def _set_eft_base_error(line_type: str, index: int, def _save_eft_header_error(eft_header: EFTHeader, eft_file_model: EFTFileModel): """Save or update EFT Header error record.""" - eft_transaction_model = _set_eft_base_error(line_type=EFTFileLineType.HEADER.value, - index=eft_header.index, - eft_file_id=eft_file_model.id, - error_messages=eft_header.get_error_messages()) + eft_transaction_model = _set_eft_base_error( + line_type=EFTFileLineType.HEADER.value, + index=eft_header.index, + eft_file_id=eft_file_model.id, + error_messages=eft_header.get_error_messages(), + ) eft_transaction_model.save() def _save_eft_trailer_error(eft_trailer: EFTTrailer, eft_file_model: EFTFileModel): """Save or update EFT Trailer error record.""" - eft_transaction_model = _set_eft_base_error(line_type=EFTFileLineType.TRAILER.value, - index=eft_trailer.index, - eft_file_id=eft_file_model.id, - error_messages=eft_trailer.get_error_messages()) + eft_transaction_model = _set_eft_base_error( + line_type=EFTFileLineType.TRAILER.value, + index=eft_trailer.index, + eft_file_id=eft_file_model.id, + error_messages=eft_trailer.get_error_messages(), + ) eft_transaction_model.save() @@ -355,17 +378,21 @@ def _save_eft_transaction(eft_record: EFTRecord, eft_file_model: EFTFileModel, i line_type = EFTFileLineType.TRANSACTION.value status_code = EFTProcessStatus.FAILED.value if is_error else EFTProcessStatus.IN_PROGRESS.value - eft_transaction_model = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_number == eft_record.index) \ - .filter(EFTTransactionModel.line_type == line_type).one_or_none() + eft_transaction_model = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_number == eft_record.index) + .filter(EFTTransactionModel.line_type == line_type) + .one_or_none() + ) if eft_transaction_model is None: eft_transaction_model = EFTTransactionModel() if eft_record.transaction_description and eft_record.short_name_type: - eft_short_name: EFTShortnameModel = _get_shortname(eft_record.transaction_description, - eft_record.short_name_type) + eft_short_name: EFTShortnameModel = _get_shortname( + eft_record.transaction_description, eft_record.short_name_type + ) eft_transaction_model.short_name_id = eft_short_name.id eft_transaction_model.line_type = line_type @@ -373,13 +400,13 @@ def _save_eft_transaction(eft_record: EFTRecord, eft_file_model: EFTFileModel, i eft_transaction_model.file_id = eft_file_model.id eft_transaction_model.status_code = status_code eft_transaction_model.error_messages = eft_record.get_error_messages() - eft_transaction_model.batch_number = getattr(eft_record, 'batch_number', None) - eft_transaction_model.sequence_number = getattr(eft_record, 'transaction_sequence', None) - eft_transaction_model.jv_type = getattr(eft_record, 'jv_type', None) - eft_transaction_model.jv_number = getattr(eft_record, 'jv_number', None) - deposit_amount_cad = getattr(eft_record, 'deposit_amount_cad', None) - eft_transaction_model.deposit_date = getattr(eft_record, 'deposit_datetime') - eft_transaction_model.transaction_date = getattr(eft_record, 'transaction_date') + eft_transaction_model.batch_number = getattr(eft_record, "batch_number", None) + eft_transaction_model.sequence_number = getattr(eft_record, "transaction_sequence", None) + eft_transaction_model.jv_type = getattr(eft_record, "jv_type", None) + eft_transaction_model.jv_number = getattr(eft_record, "jv_number", None) + deposit_amount_cad = getattr(eft_record, "deposit_amount_cad", None) + eft_transaction_model.deposit_date = getattr(eft_record, "deposit_datetime") + eft_transaction_model.transaction_date = getattr(eft_record, "transaction_date") eft_transaction_model.deposit_amount_cents = deposit_amount_cad eft_transaction_model.save() @@ -388,10 +415,17 @@ def _save_eft_transaction(eft_record: EFTRecord, eft_file_model: EFTFileModel, i def _update_transactions_to_fail(eft_file_model: EFTFileModel) -> int: """Set EFT transactions to fail status.""" - result = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id, - EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) \ - .update({EFTTransactionModel.status_code: EFTProcessStatus.FAILED.value}, synchronize_session='fetch') + result = ( + db.session.query(EFTTransactionModel) + .filter( + EFTTransactionModel.file_id == eft_file_model.id, + EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value, + ) + .update( + {EFTTransactionModel.status_code: EFTProcessStatus.FAILED.value}, + synchronize_session="fetch", + ) + ) eft_file_model.status_code = EFTProcessStatus.FAILED.value eft_file_model.save() @@ -401,10 +435,15 @@ def _update_transactions_to_fail(eft_file_model: EFTFileModel) -> int: def _update_transactions_to_complete(eft_file_model: EFTFileModel) -> int: """Set EFT transactions to complete status if they are currently in progress.""" - result = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.status_code == EFTProcessStatus.IN_PROGRESS.value) \ - .update({EFTTransactionModel.status_code: EFTProcessStatus.COMPLETED.value}, synchronize_session='fetch') + result = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.status_code == EFTProcessStatus.IN_PROGRESS.value) + .update( + {EFTTransactionModel.status_code: EFTProcessStatus.COMPLETED.value}, + synchronize_session="fetch", + ) + ) db.session.commit() return result @@ -412,10 +451,12 @@ def _update_transactions_to_complete(eft_file_model: EFTFileModel) -> int: def _get_shortname(short_name: str, short_name_type: str) -> EFTShortnameModel: """Save short name if it doesn't exist.""" - eft_short_name = db.session.query(EFTShortnameModel) \ - .filter(EFTShortnameModel.short_name == short_name) \ - .filter(EFTShortnameModel.type == short_name_type) \ + eft_short_name = ( + db.session.query(EFTShortnameModel) + .filter(EFTShortnameModel.short_name == short_name) + .filter(EFTShortnameModel.type == short_name_type) .one_or_none() + ) if eft_short_name is None: eft_short_name = EFTShortnameModel() @@ -438,12 +479,12 @@ def _shortname_balance_as_dict(eft_transactions: List[EFTRecord]) -> Dict: short_name = eft_transaction.transaction_description shortname_type = eft_transaction.short_name_type deposit_amount = eft_transaction.deposit_amount_cad / 100 - transaction = {'id': eft_transaction.id, 'deposit_amount': deposit_amount} + transaction = {"id": eft_transaction.id, "deposit_amount": deposit_amount} - shortname_balance.setdefault(short_name, {'balance': 0}) - shortname_balance[short_name]['short_name_type'] = shortname_type - shortname_balance[short_name]['balance'] += deposit_amount - shortname_balance[short_name].setdefault('transactions', []).append(transaction) + shortname_balance.setdefault(short_name, {"balance": 0}) + shortname_balance[short_name]["short_name_type"] = shortname_type + shortname_balance[short_name]["balance"] += deposit_amount + shortname_balance[short_name].setdefault("transactions", []).append(transaction) return shortname_balance @@ -451,10 +492,14 @@ def _shortname_balance_as_dict(eft_transactions: List[EFTRecord]) -> Dict: def _filter_eft_transactions(eft_transactions: List[EFTRecord], eft_location_id: str) -> List[EFTRecord]: """Filter down EFT Transactions.""" eft_transactions = [ - transaction for transaction in eft_transactions - if (transaction.has_errors() or (transaction.short_name_type in [ - EFTShortnameType.EFT.value, - EFTShortnameType.WIRE.value - ] and transaction.location_id == eft_location_id)) + transaction + for transaction in eft_transactions + if ( + transaction.has_errors() + or ( + transaction.short_name_type in [EFTShortnameType.EFT.value, EFTShortnameType.WIRE.value] + and transaction.location_id == eft_location_id + ) + ) ] return eft_transactions diff --git a/pay-queue/src/pay_queue/services/eft/eft_record.py b/pay-queue/src/pay_queue/services/eft/eft_record.py index 76a0e2765..4e0bd2ffb 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_record.py +++ b/pay-queue/src/pay_queue/services/eft/eft_record.py @@ -26,9 +26,9 @@ class EFTRecord(EFTBase): """Defines the structure of the transaction record of a received EFT file.""" - PAD_DESCRIPTION_PATTERN = 'MISC PAYMENT BCONLINE' - EFT_DESCRIPTION_PATTERN = 'MISC PAYMENT' - WIRE_DESCRIPTION_PATTERN = 'FUNDS TRANSFER CR TT' + PAD_DESCRIPTION_PATTERN = "MISC PAYMENT BCONLINE" + EFT_DESCRIPTION_PATTERN = "MISC PAYMENT" + WIRE_DESCRIPTION_PATTERN = "FUNDS TRANSFER CR TT" ministry_code: str program_code: str @@ -75,10 +75,11 @@ def _process(self): self.program_code = self.extract_value(3, 7) deposit_time = self.extract_value(20, 24) - deposit_time = '0000' if len(deposit_time) == 0 else deposit_time # default to 0000 if time not provided + deposit_time = "0000" if len(deposit_time) == 0 else deposit_time # default to 0000 if time not provided - self.deposit_datetime = self.parse_datetime(self.extract_value(7, 15) + deposit_time, - EFTError.INVALID_DEPOSIT_DATETIME) + self.deposit_datetime = self.parse_datetime( + self.extract_value(7, 15) + deposit_time, EFTError.INVALID_DEPOSIT_DATETIME + ) self.location_id = self.extract_value(15, 20) self.transaction_sequence = self.extract_value(24, 27) @@ -99,8 +100,9 @@ def _process(self): # transaction date is optional - parse if there is a value transaction_date = self.extract_value(131, 139) - self.transaction_date = None if len(transaction_date) == 0 \ - else self.parse_date(transaction_date, EFTError.INVALID_TRANSACTION_DATE) + self.transaction_date = ( + None if len(transaction_date) == 0 else self.parse_date(transaction_date, EFTError.INVALID_TRANSACTION_DATE) + ) def parse_transaction_description(self): """Determine if the transaction is an EFT/Wire and parse it.""" @@ -109,11 +111,12 @@ def parse_transaction_description(self): if self.transaction_description.startswith(self.WIRE_DESCRIPTION_PATTERN): self.short_name_type = EFTShortnameType.WIRE.value - self.transaction_description = self.transaction_description[len(self.WIRE_DESCRIPTION_PATTERN):].strip() + self.transaction_description = self.transaction_description[len(self.WIRE_DESCRIPTION_PATTERN) :].strip() return # Check if this a PAD or EFT Transaction - if self.transaction_description.startswith(self.EFT_DESCRIPTION_PATTERN) \ - and not self.transaction_description.startswith(self.PAD_DESCRIPTION_PATTERN): + if self.transaction_description.startswith( + self.EFT_DESCRIPTION_PATTERN + ) and not self.transaction_description.startswith(self.PAD_DESCRIPTION_PATTERN): self.short_name_type = EFTShortnameType.EFT.value - self.transaction_description = self.transaction_description[len(self.EFT_DESCRIPTION_PATTERN):].strip() + self.transaction_description = self.transaction_description[len(self.EFT_DESCRIPTION_PATTERN) :].strip() diff --git a/pay-queue/src/pay_queue/services/email_service.py b/pay-queue/src/pay_queue/services/email_service.py index ce3132da2..dedae1aac 100644 --- a/pay-queue/src/pay_queue/services/email_service.py +++ b/pay-queue/src/pay_queue/services/email_service.py @@ -29,7 +29,7 @@ class EmailParams: """Params required to send error email.""" - subject: Optional[str] = '' + subject: Optional[str] = "" file_name: Optional[str] = None minio_location: Optional[str] = None error_messages: Optional[List[Dict[str, Any]]] = dataclasses.field(default_factory=list) @@ -39,58 +39,55 @@ class EmailParams: def send_error_email(params: EmailParams): """Send the email asynchronously, using the given details.""" - recipient = current_app.config.get('IT_OPS_EMAIL') + recipient = current_app.config.get("IT_OPS_EMAIL") current_dir = os.path.dirname(os.path.abspath(__file__)) project_root_dir = os.path.dirname(current_dir) - templates_dir = os.path.join(project_root_dir, 'templates') + templates_dir = os.path.join(project_root_dir, "templates") env = Environment(loader=FileSystemLoader(templates_dir), autoescape=True) - template = env.get_template('payment_reconciliation_failed_email.html') + template = env.get_template("payment_reconciliation_failed_email.html") email_params = { - 'fileName': params.file_name, - 'errorMessages': params.error_messages, - 'minioLocation': params.minio_location, - 'payload': json.dumps(dataclasses.asdict(params.ce)), - 'tableName': params.table_name + "fileName": params.file_name, + "errorMessages": params.error_messages, + "minioLocation": params.minio_location, + "payload": json.dumps(dataclasses.asdict(params.ce)), + "tableName": params.table_name, } html_body = template.render(email_params) - send_email_service( - recipients=[recipient], - subject=params.subject, - html_body=html_body - ) + send_email_service(recipients=[recipient], subject=params.subject, html_body=html_body) def send_email_service(recipients: list, subject: str, html_body: str): """Send the email notification.""" # Refactor this common code to PAY-API. token = get_service_account_token() - current_app.logger.info(f'>send_email to recipients: {recipients}') - notify_url = current_app.config.get('NOTIFY_API_ENDPOINT') + 'notify/' + current_app.logger.info(f">send_email to recipients: {recipients}") + notify_url = current_app.config.get("NOTIFY_API_ENDPOINT") + "notify/" success = False for recipient in recipients: notify_body = { - 'recipients': recipient, - 'content': { - 'subject': subject, - 'body': html_body - } + "recipients": recipient, + "content": {"subject": subject, "body": html_body}, } try: - notify_response = OAuthService.post(notify_url, token=token, - auth_header_type=AuthHeaderType.BEARER, - content_type=ContentType.JSON, data=notify_body) - current_app.logger.info(' PaymentModel: """Get the failed payment record for the invoice number.""" - payment: PaymentModel = db.session.query(PaymentModel) \ - .filter(PaymentModel.invoice_number == inv_number, - PaymentModel.payment_status_code == PaymentStatus.FAILED.value) \ - .order_by(PaymentModel.payment_date.desc()).first() + payment: PaymentModel = ( + db.session.query(PaymentModel) + .filter( + PaymentModel.invoice_number == inv_number, + PaymentModel.payment_status_code == PaymentStatus.FAILED.value, + ) + .order_by(PaymentModel.payment_date.desc()) + .first() + ) return payment @@ -178,10 +217,14 @@ def _get_payment_by_inv_number_and_status(inv_number: str, status: str) -> Payme # It's possible to look up null inv_number and return more than one. if inv_number is None: return None - payment: PaymentModel = db.session.query(PaymentModel) \ - .filter(PaymentModel.invoice_number == inv_number, - PaymentModel.payment_status_code == status) \ + payment: PaymentModel = ( + db.session.query(PaymentModel) + .filter( + PaymentModel.invoice_number == inv_number, + PaymentModel.payment_status_code == status, + ) .one_or_none() + ) return payment @@ -198,49 +241,61 @@ def reconcile_payments(ce): 4: If the transaction is On Account for Credit, apply the credit to the account. """ msg = ce.data - file_name: str = msg.get('fileName') - minio_location: str = msg.get('location') + file_name: str = msg.get("fileName") + minio_location: str = msg.get("location") - cas_settlement: CasSettlementModel = db.session.query(CasSettlementModel) \ - .filter(CasSettlementModel.file_name == file_name).one_or_none() + cas_settlement: CasSettlementModel = ( + db.session.query(CasSettlementModel).filter(CasSettlementModel.file_name == file_name).one_or_none() + ) if cas_settlement: - current_app.logger.info('File: %s has been processed or processing in progress. Skipping file. ' - 'Removing this row will allow processing to be restarted.', file_name) + current_app.logger.info( + "File: %s has been processed or processing in progress. Skipping file. " + "Removing this row will allow processing to be restarted.", + file_name, + ) return - current_app.logger.info('Creating cas_settlement record for file: %s', file_name) + current_app.logger.info("Creating cas_settlement record for file: %s", file_name) cas_settlement = _create_cas_settlement(file_name) file = get_object(minio_location, file_name) - content = file.data.decode('utf-8-sig') + content = file.data.decode("utf-8-sig") error_messages = [] has_errors, error_messages = _process_file_content(content, cas_settlement, msg, error_messages) - if has_errors and not current_app.config.get('DISABLE_CSV_ERROR_EMAIL'): + if has_errors and not current_app.config.get("DISABLE_CSV_ERROR_EMAIL"): email_service_params = EmailParams( - subject='Payment Reconciliation Failure', + subject="Payment Reconciliation Failure", file_name=file_name, minio_location=minio_location, error_messages=error_messages, ce=ce, - table_name=cas_settlement.__tablename__ + table_name=cas_settlement.__tablename__, ) send_error_email(email_service_params) -def _process_file_content(content: str, cas_settlement: CasSettlementModel, - msg: Dict[str, any], error_messages: List[Dict[str, any]]): +def _process_file_content( + content: str, + cas_settlement: CasSettlementModel, + msg: Dict[str, any], + error_messages: List[Dict[str, any]], +): """Process the content of the feedback file.""" has_errors = False # Iterate the rows and create key value pair for each row for row in csv.DictReader(content.splitlines()): # Convert lower case keys to avoid any key mismatch row = dict((k.lower(), v) for k, v in row.items()) - current_app.logger.debug('Processing %s', row) + current_app.logger.debug("Processing %s", row) # IF not PAD and application amount is zero, continue record_type = _get_row_value(row, Column.RECORD_TYPE) - pad_record_types: Tuple[str] = (RecordType.PAD.value, RecordType.PADR.value, RecordType.PAYR.value) + pad_record_types: Tuple[str] = ( + RecordType.PAD.value, + RecordType.PADR.value, + RecordType.PAYR.value, + ) if float(_get_row_value(row, Column.APP_AMOUNT)) == 0 and record_type not in pad_record_types: continue @@ -256,13 +311,17 @@ def _process_file_content(content: str, cas_settlement: CasSettlementModel, elif record_type in (RecordType.BOLP.value, RecordType.EFTP.value): # EFT, WIRE and Online Banking are one-to-one invoice. So handle them in same way. has_errors = _process_unconsolidated_invoices(row, error_messages) or has_errors - elif record_type in (RecordType.ONAC.value, RecordType.CMAP.value, RecordType.DRWP.value): + elif record_type in ( + RecordType.ONAC.value, + RecordType.CMAP.value, + RecordType.DRWP.value, + ): has_errors = _process_credit_on_invoices(row, error_messages) or has_errors elif record_type == RecordType.ADJS.value: - current_app.logger.info('Adjustment received for %s.', msg) + current_app.logger.info("Adjustment received for %s.", msg) else: # For any other transactions like DM log error and continue. - error_msg = f'Record Type is received as {record_type}, and cannot process {msg}.' + error_msg = f"Record Type is received as {record_type}, and cannot process {msg}." has_errors = True _csv_error_handling(row, error_msg, error_messages) # Continue processing @@ -273,26 +332,26 @@ def _process_file_content(content: str, cas_settlement: CasSettlementModel, # Create payment records for lines other than PAD try: _create_payment_records(content) - except Exception as e: # NOQA # pylint: disable=broad-except - error_msg = f'Error creating payment records: {str(e)}' + except Exception as e: # NOQA # pylint: disable=broad-except + error_msg = f"Error creating payment records: {str(e)}" has_errors = True - _csv_error_handling('N/A', error_msg, error_messages, e) + _csv_error_handling("N/A", error_msg, error_messages, e) return has_errors, error_messages try: _create_credit_records(content) - except Exception as e: # NOQA # pylint: disable=broad-except - error_msg = f'Error creating credit records: {str(e)}' + except Exception as e: # NOQA # pylint: disable=broad-except + error_msg = f"Error creating credit records: {str(e)}" has_errors = True - _csv_error_handling('N/A', error_msg, error_messages, e) + _csv_error_handling("N/A", error_msg, error_messages, e) return has_errors, error_messages try: _sync_credit_records_with_cfs() - except Exception as e: # NOQA # pylint: disable=broad-except - error_msg = f'Error syncing credit records: {str(e)}' + except Exception as e: # NOQA # pylint: disable=broad-except + error_msg = f"Error syncing credit records: {str(e)}" has_errors = True - _csv_error_handling('N/A', error_msg, error_messages, e) + _csv_error_handling("N/A", error_msg, error_messages, e) return has_errors, error_messages cas_settlement.processed_on = datetime.now() @@ -306,44 +365,48 @@ def _process_consolidated_invoices(row, error_messages: List[Dict[str, any]]) -> if (target_txn := _get_row_value(row, Column.TARGET_TXN)) == TargetTransaction.INV.value: inv_number = _get_row_value(row, Column.TARGET_TXN_NO) record_type = _get_row_value(row, Column.RECORD_TYPE) - current_app.logger.debug('Processing invoice : %s', inv_number) + current_app.logger.debug("Processing invoice : %s", inv_number) inv_references = _find_invoice_reference_by_number_and_status(inv_number, InvoiceReferenceStatus.ACTIVE.value) payment_account: PaymentAccountModel = _get_payment_account(row) if target_txn_status.lower() == Status.PAID.value.lower(): - current_app.logger.debug('Fully PAID payment.') + current_app.logger.debug("Fully PAID payment.") # if no inv reference is found, and if there are no COMPLETED inv ref, raise alert completed_inv_references = _find_invoice_reference_by_number_and_status( inv_number, InvoiceReferenceStatus.COMPLETED.value ) if not inv_references and not completed_inv_references: - error_msg = f'No invoice found for {inv_number} in the system, and cannot process {row}.' + error_msg = f"No invoice found for {inv_number} in the system, and cannot process {row}." has_errors = True _csv_error_handling(row, error_msg, error_messages) return has_errors _process_paid_invoices(inv_references, row) - elif target_txn_status.lower() == Status.NOT_PAID.value.lower() \ - or record_type in (RecordType.PADR.value, RecordType.PAYR.value): - current_app.logger.info('NOT PAID. NSF identified.') + elif target_txn_status.lower() == Status.NOT_PAID.value.lower() or record_type in ( + RecordType.PADR.value, + RecordType.PAYR.value, + ): + current_app.logger.info("NOT PAID. NSF identified.") # NSF Condition. Publish to account events for NSF. if _process_failed_payments(row): # Send mailer and account events to update status and send email notification _publish_account_events(QueueMessageTypes.NSF_LOCK_ACCOUNT.value, payment_account, row) else: - error_msg = f'Target Transaction Type is received as {target_txn} for PAD, and cannot process {row}.' + error_msg = f"Target Transaction Type is received as {target_txn} for PAD, and cannot process {row}." has_errors = True _csv_error_handling(row, error_msg, error_messages) return has_errors def _find_invoice_reference_by_number_and_status(inv_number: str, status: str): - inv_references: List[InvoiceReferenceModel] = db.session.query(InvoiceReferenceModel). \ - filter(InvoiceReferenceModel.status_code == status). \ - filter(InvoiceReferenceModel.invoice_number == inv_number). \ - all() + inv_references: List[InvoiceReferenceModel] = ( + db.session.query(InvoiceReferenceModel) + .filter(InvoiceReferenceModel.status_code == status) + .filter(InvoiceReferenceModel.invoice_number == inv_number) + .all() + ) return inv_references @@ -354,51 +417,60 @@ def _process_unconsolidated_invoices(row, error_messages: List[Dict[str, any]]) if (target_txn := _get_row_value(row, Column.TARGET_TXN)) == TargetTransaction.INV.value: inv_number = _get_row_value(row, Column.TARGET_TXN_NO) - inv_references: List[InvoiceReferenceModel] = db.session.query(InvoiceReferenceModel). \ - filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.ACTIVE.value). \ - filter(InvoiceReferenceModel.invoice_number == inv_number). \ - all() + inv_references: List[InvoiceReferenceModel] = ( + db.session.query(InvoiceReferenceModel) + .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.ACTIVE.value) + .filter(InvoiceReferenceModel.invoice_number == inv_number) + .all() + ) if len(inv_references) != 1: # There could be case where same invoice can appear as PAID in 2 lines, especially when there are credits. # Make sure there is one invoice_reference with completed status, else raise error. - completed_inv_references: List[InvoiceReferenceModel] = db.session.query(InvoiceReferenceModel). \ - filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value). \ - filter(InvoiceReferenceModel.invoice_number == inv_number). \ - all() - current_app.logger.info('Found %s completed invoice references for invoice number %s', - len(completed_inv_references), inv_number) + completed_inv_references: List[InvoiceReferenceModel] = ( + db.session.query(InvoiceReferenceModel) + .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value) + .filter(InvoiceReferenceModel.invoice_number == inv_number) + .all() + ) + current_app.logger.info( + "Found %s completed invoice references for invoice number %s", + len(completed_inv_references), + inv_number, + ) if len(completed_inv_references) != 1: - error_msg = (f'More than one or none invoice reference ' - f'received for invoice number {inv_number} for {record_type}') + error_msg = ( + f"More than one or none invoice reference " + f"received for invoice number {inv_number} for {record_type}" + ) has_errors = True _csv_error_handling(row, error_msg, error_messages) else: # Handle fully PAID and Partially Paid scenarios. if target_txn_status.lower() == Status.PAID.value.lower(): - current_app.logger.debug('Fully PAID payment.') + current_app.logger.debug("Fully PAID payment.") _process_paid_invoices(inv_references, row) elif target_txn_status.lower() == Status.PARTIAL.value.lower(): - current_app.logger.info('Partially PAID.') + current_app.logger.info("Partially PAID.") # As per validation above, get first and only inv ref _process_partial_paid_invoices(inv_references[0], row) else: - error_msg = (f'Target Transaction Type is received ' - f'as {target_txn} for {record_type}, and cannot process.') + error_msg = ( + f"Target Transaction Type is received " f"as {target_txn} for {record_type}, and cannot process." + ) has_errors = True _csv_error_handling(row, error_msg, error_messages) return has_errors -def _csv_error_handling(row, error_msg: str, error_messages: List[Dict[str, any]], - ex: Exception = None): +def _csv_error_handling(row, error_msg: str, error_messages: List[Dict[str, any]], ex: Exception = None): if ex: - formatted_traceback = ''.join(traceback.TracebackException.from_exception(ex).format()) - error_msg = f'{error_msg}\n{formatted_traceback}' + formatted_traceback = "".join(traceback.TracebackException.from_exception(ex).format()) + error_msg = f"{error_msg}\n{formatted_traceback}" current_app.logger.error(error_msg) - capture_message(error_msg, level='error') - error_messages.append({'error': error_msg, 'row': row}) + capture_message(error_msg, level="error") + error_messages.append({"error": error_msg, "row": row}) def _handle_credit_invoices_and_adjust_invoice_paid(row): @@ -406,10 +478,10 @@ def _handle_credit_invoices_and_adjust_invoice_paid(row): application_id = _get_row_value(row, Column.APP_ID) cfs_identifier = _get_row_value(row, Column.SOURCE_TXN_NO) if CfsCreditInvoices.find_by_application_id(application_id): - current_app.logger.warning(f'Credit invoices exists with application_id {application_id}.') + current_app.logger.warning(f"Credit invoices exists with application_id {application_id}.") return if not (credit := CreditModel.find_by_cfs_identifier(cfs_identifier=cfs_identifier, credit_memo=True)): - current_app.logger.warning(f'Credit with cfs_identifier {cfs_identifier} not found.') + current_app.logger.warning(f"Credit with cfs_identifier {cfs_identifier} not found.") return invoice_number = _get_row_value(row, Column.TARGET_TXN_NO) CfsCreditInvoices( @@ -418,21 +490,23 @@ def _handle_credit_invoices_and_adjust_invoice_paid(row): application_id=application_id, cfs_account=_get_row_value(row, Column.CUSTOMER_ACC), cfs_identifier=cfs_identifier, - created_on=datetime.strptime(_get_row_value(row, Column.APP_DATE), '%d-%b-%y'), + created_on=datetime.strptime(_get_row_value(row, Column.APP_DATE), "%d-%b-%y"), credit_id=credit.id, invoice_amount=Decimal(_get_row_value(row, Column.TARGET_TXN_ORIGINAL)), - invoice_number=invoice_number + invoice_number=invoice_number, ).save() amount = CfsCreditInvoices.credit_for_invoice_number(invoice_number) - invoices = db.session.query(InvoiceModel) \ - .join(InvoiceReferenceModel, InvoiceReferenceModel.invoice_id == InvoiceModel.id) \ - .filter(InvoiceReferenceModel.invoice_number == invoice_number) \ - .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value) \ - .filter(InvoiceReferenceModel.is_consolidated.is_(False)) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ - .order_by(InvoiceModel.id.asc()) \ + invoices = ( + db.session.query(InvoiceModel) + .join(InvoiceReferenceModel, InvoiceReferenceModel.invoice_id == InvoiceModel.id) + .filter(InvoiceReferenceModel.invoice_number == invoice_number) + .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value) + .filter(InvoiceReferenceModel.is_consolidated.is_(False)) + .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) + .order_by(InvoiceModel.id.asc()) .all() + ) for invoice in invoices: if amount <= 0: break @@ -442,7 +516,7 @@ def _handle_credit_invoices_and_adjust_invoice_paid(row): invoice.save() if amount >= 0: - current_app.logger.warning(f'Amount {amount} remaining after applying to invoices {invoice_number}.') + current_app.logger.warning(f"Amount {amount} remaining after applying to invoices {invoice_number}.") def _process_credit_on_invoices(row, error_messages: List[Dict[str, any]]) -> bool: @@ -451,21 +525,24 @@ def _process_credit_on_invoices(row, error_messages: List[Dict[str, any]]) -> bo target_txn_status = _get_row_value(row, Column.TARGET_TXN_STATUS) if _get_row_value(row, Column.TARGET_TXN) == TargetTransaction.INV.value: inv_number = _get_row_value(row, Column.TARGET_TXN_NO) - current_app.logger.debug('Processing invoice : %s', inv_number) + current_app.logger.debug("Processing invoice : %s", inv_number) - inv_references: List[InvoiceReferenceModel] = db.session.query(InvoiceReferenceModel). \ - filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.ACTIVE.value). \ - filter(InvoiceReferenceModel.invoice_number == inv_number). \ - all() + inv_references: List[InvoiceReferenceModel] = ( + db.session.query(InvoiceReferenceModel) + .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.ACTIVE.value) + .filter(InvoiceReferenceModel.invoice_number == inv_number) + .all() + ) if target_txn_status.lower() == Status.PAID.value.lower(): - current_app.logger.debug('Fully PAID payment.') + current_app.logger.debug("Fully PAID payment.") _process_paid_invoices(inv_references, row) elif target_txn_status.lower() == Status.PARTIAL.value.lower(): - current_app.logger.info('Partially PAID using credit memo. ' - 'Ignoring as the credit memo payment is already captured.') + current_app.logger.info( + "Partially PAID using credit memo. Ignoring as the credit memo payment is already captured." + ) else: - error_msg = f'Target Transaction status is received as {target_txn_status} for CMAP, and cannot process.' + error_msg = f"Target Transaction status is received as {target_txn_status} for CMAP, and cannot process." has_errors = True _csv_error_handling(row, error_msg, error_messages) return has_errors @@ -482,18 +559,21 @@ def _process_paid_invoices(inv_references, row): for inv_ref in inv_references: invoice = InvoiceModel.find_by_id(inv_ref.invoice_id) if invoice.payment_method_code == PaymentMethod.CC.value: - current_app.logger.info('Cannot mark CC invoices as PAID.') + current_app.logger.info("Cannot mark CC invoices as PAID.") return - receipt_date = datetime.strptime(_get_row_value(row, Column.APP_DATE), '%d-%b-%y') + receipt_date = datetime.strptime(_get_row_value(row, Column.APP_DATE), "%d-%b-%y") receipt_number = _get_row_value(row, Column.SOURCE_TXN_NO) for inv_ref in inv_references: inv_ref.status_code = InvoiceReferenceStatus.COMPLETED.value # Find invoice, update status inv = InvoiceModel.find_by_id(inv_ref.invoice_id) _validate_account(inv, row) - current_app.logger.debug('PAID Invoice. Invoice Reference ID : %s, invoice ID : %s', - inv_ref.id, inv_ref.invoice_id) + current_app.logger.debug( + "PAID Invoice. Invoice Reference ID : %s, invoice ID : %s", + inv_ref.id, + inv_ref.invoice_id, + ) inv.invoice_status_code = InvoiceStatus.PAID.value inv.payment_date = receipt_date @@ -507,7 +587,7 @@ def _process_paid_invoices(inv_references, row): db.session.add(receipt) # Publish to the queue if it's an Online Banking payment if inv.payment_method_code == PaymentMethod.ONLINE_BANKING.value: - current_app.logger.debug('Publishing payment event for OB. Invoice : %s', inv.id) + current_app.logger.debug("Publishing payment event for OB. Invoice : %s", inv.id) _publish_payment_event(inv) @@ -518,13 +598,16 @@ def _process_partial_paid_invoices(inv_ref: InvoiceReferenceModel, row): Update Transaction is COMPLETED. Update Invoice as PARTIAL. """ - receipt_date: datetime = datetime.strptime(_get_row_value(row, Column.APP_DATE), '%d-%b-%y') + receipt_date: datetime = datetime.strptime(_get_row_value(row, Column.APP_DATE), "%d-%b-%y") receipt_number: str = _get_row_value(row, Column.APP_ID) inv: InvoiceModel = InvoiceModel.find_by_id(inv_ref.invoice_id) _validate_account(inv, row) - current_app.logger.debug('Partial Invoice. Invoice Reference ID : %s, invoice ID : %s', - inv_ref.id, inv_ref.invoice_id) + current_app.logger.debug( + "Partial Invoice. Invoice Reference ID : %s, invoice ID : %s", + inv_ref.id, + inv_ref.invoice_id, + ) inv.invoice_status_code = InvoiceStatus.PARTIAL.value inv.paid = inv.total - Decimal(_get_row_value(row, Column.TARGET_TXN_OUTSTANDING)) # Create Receipt records @@ -549,41 +632,45 @@ def _process_failed_payments(row): payment_account: PaymentAccountModel = _get_payment_account(row) # If there is a FAILED payment record for this; it means it's a duplicate event. Ignore it. - payment = PaymentModel.find_payment_by_invoice_number_and_status( - inv_number, PaymentStatus.FAILED.value - ) + payment = PaymentModel.find_payment_by_invoice_number_and_status(inv_number, PaymentStatus.FAILED.value) if payment: - current_app.logger.info('Ignoring duplicate NSF message for invoice : %s ', inv_number) + current_app.logger.info("Ignoring duplicate NSF message for invoice : %s ", inv_number) return False # If there is an NSF row, it means it's a duplicate NSF event. Ignore it. if NonSufficientFundsService.exists_for_invoice_number(inv_number): - current_app.logger.info('Ignoring duplicate NSF event for account: %s ', payment_account.auth_account_id) + current_app.logger.info( + "Ignoring duplicate NSF event for account: %s ", + payment_account.auth_account_id, + ) return False # Set CFS Account Status. cfs_account = CfsAccountModel.find_effective_by_payment_method(payment_account.id, PaymentMethod.PAD.value) is_already_frozen = cfs_account.status == CfsAccountStatus.FREEZE.value - current_app.logger.info('setting payment account id : %s status as FREEZE', payment_account.id) + current_app.logger.info("setting payment account id : %s status as FREEZE", payment_account.id) cfs_account.status = CfsAccountStatus.FREEZE.value payment_account.has_nsf_invoices = datetime.now(tz=timezone.utc) # Call CFS to stop any further PAD transactions on this account. CFSService.update_site_receipt_method(cfs_account, receipt_method=RECEIPT_METHOD_PAD_STOP) if is_already_frozen: - current_app.logger.info('Ignoring NSF message for invoice : %s as the account is already FREEZE', inv_number) + current_app.logger.info( + "Ignoring NSF message for invoice : %s as the account is already FREEZE", + inv_number, + ) return False # Find the invoice_reference for this invoice and mark it as ACTIVE. - inv_references: List[InvoiceReferenceModel] = db.session.query(InvoiceReferenceModel). \ - filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value). \ - filter(InvoiceReferenceModel.invoice_number == inv_number). \ - all() + inv_references: List[InvoiceReferenceModel] = ( + db.session.query(InvoiceReferenceModel) + .filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value) + .filter(InvoiceReferenceModel.invoice_number == inv_number) + .all() + ) # Update status to ACTIVE, if it was marked COMPLETED for inv_reference in inv_references: inv_reference.status_code = InvoiceReferenceStatus.ACTIVE.value # Find receipt and delete it. - receipt: ReceiptModel = ReceiptModel.find_by_invoice_id_and_receipt_number( - invoice_id=inv_reference.invoice_id - ) + receipt: ReceiptModel = ReceiptModel.find_by_invoice_id_and_receipt_number(invoice_id=inv_reference.invoice_id) if receipt: db.session.delete(receipt) # Find invoice and update the status to SETTLEMENT_SCHED @@ -615,7 +702,7 @@ def _create_credit_records(csv_content: str): is_credit_memo=False, amount=float(_get_row_value(row, Column.TARGET_TXN_ORIGINAL)), remaining_amount=float(_get_row_value(row, Column.TARGET_TXN_ORIGINAL)), - account_id=pay_account.id + account_id=pay_account.id, ).save() for row in csv.DictReader(csv_content.splitlines()): @@ -637,8 +724,8 @@ def _check_cfs_accounts_for_pad_and_ob(credit): has_online_banking = True if has_pad and has_online_banking: raise Exception( # pylint: disable=broad-exception-raised - 'Multiple payment methods for the same account for CREDITS' - ' credits has no link to CFS account.') + "Multiple payment methods for the same account for CREDITS credits has no link to CFS account." + ) def _sync_credit_records_with_cfs(): @@ -648,34 +735,37 @@ def _sync_credit_records_with_cfs(): # 3. If it's credit memo, call credit memo endpoint and calculate balance. # 4. Roll up the credits to credit field in payment_account. active_credits: List[CreditModel] = db.session.query(CreditModel).filter(CreditModel.remaining_amount > 0).all() - current_app.logger.info('Found %s credit records', len(active_credits)) + current_app.logger.info("Found %s credit records", len(active_credits)) account_ids: List[int] = [] for credit in active_credits: _check_cfs_accounts_for_pad_and_ob(credit) - cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method(credit.account_id, - PaymentMethod.PAD.value) \ - or CfsAccountModel.find_effective_or_latest_by_payment_method(credit.account_id, - PaymentMethod.ONLINE_BANKING.value) + cfs_account = CfsAccountModel.find_effective_or_latest_by_payment_method( + credit.account_id, PaymentMethod.PAD.value + ) or CfsAccountModel.find_effective_or_latest_by_payment_method( + credit.account_id, PaymentMethod.ONLINE_BANKING.value + ) account_ids.append(credit.account_id) if credit.is_credit_memo: - credit_memo = CFSService.get_cms( - cfs_account=cfs_account, cms_number=credit.cfs_identifier) - credit.remaining_amount = abs(float(credit_memo.get('amount_due'))) + credit_memo = CFSService.get_cms(cfs_account=cfs_account, cms_number=credit.cfs_identifier) + credit.remaining_amount = abs(float(credit_memo.get("amount_due"))) else: - receipt = CFSService.get_receipt( - cfs_account=cfs_account, receipt_number=credit.cfs_identifier) - receipt_amount = float(receipt.get('receipt_amount')) + receipt = CFSService.get_receipt(cfs_account=cfs_account, receipt_number=credit.cfs_identifier) + receipt_amount = float(receipt.get("receipt_amount")) applied_amount: float = 0 - for invoice in receipt.get('invoices', []): - applied_amount += float(invoice.get('amount_applied')) + for invoice in receipt.get("invoices", []): + applied_amount += float(invoice.get("amount_applied")) credit.remaining_amount = receipt_amount - applied_amount credit.save() # Roll up the credits and add up to credit in payment_account. for account_id in set(account_ids): - account_credits: List[CreditModel] = db.session.query(CreditModel).filter( - CreditModel.remaining_amount > 0).filter(CreditModel.account_id == account_id).all() + account_credits: List[CreditModel] = ( + db.session.query(CreditModel) + .filter(CreditModel.remaining_amount > 0) + .filter(CreditModel.account_id == account_id) + .all() + ) credit_total: float = 0 for account_credit in account_credits: credit_total += account_credit.remaining_amount @@ -686,15 +776,23 @@ def _sync_credit_records_with_cfs(): def _get_payment_account(row) -> PaymentAccountModel: account_number: str = _get_row_value(row, Column.CUSTOMER_ACC) - payment_accounts: PaymentAccountModel = db.session.query(PaymentAccountModel) \ - .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ - .filter(CfsAccountModel.cfs_account == account_number) \ + payment_accounts: PaymentAccountModel = ( + db.session.query(PaymentAccountModel) + .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) + .filter(CfsAccountModel.cfs_account == account_number) .filter( - CfsAccountModel.status.in_( - [CfsAccountStatus.ACTIVE.value, CfsAccountStatus.FREEZE.value, CfsAccountStatus.INACTIVE.value] - )).all() + CfsAccountModel.status.in_( + [ + CfsAccountStatus.ACTIVE.value, + CfsAccountStatus.FREEZE.value, + CfsAccountStatus.INACTIVE.value, + ] + ) + ) + .all() + ) if not all(payment_account.id == payment_accounts[0].id for payment_account in payment_accounts): - raise Exception('Multiple unique payment accounts for cfs_account.') # pylint: disable=broad-exception-raised + raise Exception("Multiple unique payment accounts for cfs_account.") # pylint: disable=broad-exception-raised return payment_accounts[0] if payment_accounts else None @@ -703,32 +801,38 @@ def _validate_account(inv: InvoiceModel, row: Dict[str, str]): # This should never happen, just in case cfs_account: CfsAccountModel = CfsAccountModel.find_by_id(inv.cfs_account_id) if (account_number := _get_row_value(row, Column.CUSTOMER_ACC)) != cfs_account.cfs_account: - current_app.logger.error('Customer Account received as %s, but expected %s.', - account_number, cfs_account.cfs_account) - capture_message(f'Customer Account received as {account_number}, but expected {cfs_account.cfs_account}.', - level='error') + current_app.logger.error( + "Customer Account received as %s, but expected %s.", + account_number, + cfs_account.cfs_account, + ) + capture_message( + f"Customer Account received as {account_number}, but expected {cfs_account.cfs_account}.", + level="error", + ) - raise Exception('Invalid Account Number') # pylint: disable=broad-exception-raised + raise Exception("Invalid Account Number") # pylint: disable=broad-exception-raised def _publish_payment_event(inv: InvoiceModel): """Publish payment message to the queue.""" - payload = PaymentTransactionService.create_event_payload(invoice=inv, - status_code=PaymentStatus.COMPLETED.value) + payload = PaymentTransactionService.create_event_payload(invoice=inv, status_code=PaymentStatus.COMPLETED.value) try: gcp_queue_publisher.publish_to_queue( QueueMessage( source=QueueSources.PAY_QUEUE.value, message_type=QueueMessageTypes.PAYMENT.value, payload=payload, - topic=get_topic_for_corp_type(inv.corp_type_code) + topic=get_topic_for_corp_type(inv.corp_type_code), ) ) except Exception as e: # NOQA pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Payment Event - %s', payload) - capture_message(f'Notification to Queue failed for the Payment Event {payload}.', - level='error') + current_app.logger.warning("Notification to Queue failed for the Payment Event - %s", payload) + capture_message( + f"Notification to Queue failed for the Payment Event {payload}.", + level="error", + ) def _publish_mailer_events(message_type: str, pay_account: PaymentAccountModel, row: Dict[str, str]): @@ -741,15 +845,22 @@ def _publish_mailer_events(message_type: str, pay_account: PaymentAccountModel, source=QueueSources.PAY_QUEUE.value, message_type=message_type, payload=payload, - topic=current_app.config.get('ACCOUNT_MAILER_TOPIC') + topic=current_app.config.get("ACCOUNT_MAILER_TOPIC"), ) ) except Exception as e: # NOQA pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Account Mailer %s - %s', - pay_account.auth_account_id, payload) - capture_message('Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.'.format( - auth_account_id=pay_account.auth_account_id, msg=payload), level='error') + current_app.logger.warning( + "Notification to Queue failed for the Account Mailer %s - %s", + pay_account.auth_account_id, + payload, + ) + capture_message( + "Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.".format( + auth_account_id=pay_account.auth_account_id, msg=payload + ), + level="error", + ) def _publish_online_banking_mailer_events(rows: List[Dict[str, str]], paid_amount: float): @@ -758,9 +869,17 @@ def _publish_online_banking_mailer_events(rows: List[Dict[str, str]], paid_amoun pay_account = _get_payment_account(rows[0]) # All rows are for same account. # Check for credit, or fully paid or under paid payment credit_rows = list( - filter(lambda r: (_get_row_value(r, Column.TARGET_TXN) == TargetTransaction.RECEIPT.value), rows)) + filter( + lambda r: (_get_row_value(r, Column.TARGET_TXN) == TargetTransaction.RECEIPT.value), + rows, + ) + ) under_pay_rows = list( - filter(lambda r: (_get_row_value(r, Column.TARGET_TXN_STATUS).lower() == Status.PARTIAL.value.lower()), rows)) + filter( + lambda r: (_get_row_value(r, Column.TARGET_TXN_STATUS).lower() == Status.PARTIAL.value.lower()), + rows, + ) + ) credit_amount: float = 0 if credit_rows: @@ -773,10 +892,10 @@ def _publish_online_banking_mailer_events(rows: List[Dict[str, str]], paid_amoun message_type = QueueMessageTypes.ONLINE_BANKING_PAYMENT.value payload = { - 'accountId': pay_account.auth_account_id, - 'paymentMethod': PaymentMethod.ONLINE_BANKING.value, - 'amount': '{:.2f}'.format(paid_amount), # pylint: disable = consider-using-f-string - 'creditAmount': '{:.2f}'.format(credit_amount) # pylint: disable = consider-using-f-string + "accountId": pay_account.auth_account_id, + "paymentMethod": PaymentMethod.ONLINE_BANKING.value, + "amount": "{:.2f}".format(paid_amount), # pylint: disable = consider-using-f-string + "creditAmount": "{:.2f}".format(credit_amount), # pylint: disable = consider-using-f-string } try: @@ -785,16 +904,21 @@ def _publish_online_banking_mailer_events(rows: List[Dict[str, str]], paid_amoun source=QueueSources.PAY_QUEUE.value, message_type=message_type, payload=payload, - topic=current_app.config.get('ACCOUNT_MAILER_TOPIC') + topic=current_app.config.get("ACCOUNT_MAILER_TOPIC"), ) ) except Exception as e: # NOQA pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Account Mailer %s - %s', - pay_account.auth_account_id, payload) - capture_message('Notification to Queue failed for the Account Mailer ' - '{auth_account_id}, {msg}.'.format(auth_account_id=pay_account.auth_account_id, msg=payload), - level='error') + current_app.logger.warning( + "Notification to Queue failed for the Account Mailer %s - %s", + pay_account.auth_account_id, + payload, + ) + capture_message( + "Notification to Queue failed for the Account Mailer " + "{auth_account_id}, {msg}.".format(auth_account_id=pay_account.auth_account_id, msg=payload), + level="error", + ) def _publish_account_events(message_type: str, pay_account: PaymentAccountModel, row: Dict[str, str]): @@ -807,24 +931,31 @@ def _publish_account_events(message_type: str, pay_account: PaymentAccountModel, source=QueueSources.PAY_QUEUE.value, message_type=message_type, payload=payload, - topic=current_app.config.get('AUTH_EVENT_TOPIC') + topic=current_app.config.get("AUTH_EVENT_TOPIC"), ) ) except Exception as e: # NOQA pylint: disable=broad-except current_app.logger.error(e) - current_app.logger.warning('Notification to Queue failed for the Account %s - %s', pay_account.auth_account_id, - pay_account.name) - capture_message('Notification to Queue failed for the Account {auth_account_id}, {msg}.'.format( - auth_account_id=pay_account.auth_account_id, msg=payload), level='error') + current_app.logger.warning( + "Notification to Queue failed for the Account %s - %s", + pay_account.auth_account_id, + pay_account.name, + ) + capture_message( + "Notification to Queue failed for the Account {auth_account_id}, {msg}.".format( + auth_account_id=pay_account.auth_account_id, msg=payload + ), + level="error", + ) def _create_event_payload(pay_account, row): return { - 'accountId': pay_account.auth_account_id, - 'paymentMethod': _convert_payment_method(_get_row_value(row, Column.SOURCE_TXN)), - 'outstandingAmount': _get_row_value(row, Column.TARGET_TXN_OUTSTANDING), - 'originalAmount': _get_row_value(row, Column.TARGET_TXN_ORIGINAL), - 'amount': _get_row_value(row, Column.APP_AMOUNT) + "accountId": pay_account.auth_account_id, + "paymentMethod": _convert_payment_method(_get_row_value(row, Column.SOURCE_TXN)), + "outstandingAmount": _get_row_value(row, Column.TARGET_TXN_OUTSTANDING), + "originalAmount": _get_row_value(row, Column.TARGET_TXN_ORIGINAL), + "amount": _get_row_value(row, Column.APP_AMOUNT), } @@ -845,11 +976,16 @@ def _get_row_value(row: Dict[str, str], key: Column) -> str: return row.get(key.value.lower()) -def _create_nsf_invoice(cfs_account: CfsAccountModel, inv_number: str, - payment_account: PaymentAccountModel, reason_description: str) -> InvoiceModel: +def _create_nsf_invoice( + cfs_account: CfsAccountModel, + inv_number: str, + payment_account: PaymentAccountModel, + reason_description: str, +) -> InvoiceModel: """Create Invoice, line item and invoice referwnce records.""" - fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type(corp_type_code='BCR', - filing_type_code='NSF') + fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type( + corp_type_code="BCR", filing_type_code="NSF" + ) invoice = InvoiceModel( bcol_account=payment_account.bcol_account, payment_account_id=payment_account.id, @@ -859,19 +995,22 @@ def _create_nsf_invoice(cfs_account: CfsAccountModel, inv_number: str, service_fees=0, paid=0, payment_method_code=PaymentMethod.CC.value, - corp_type_code='BCR', + corp_type_code="BCR", created_on=datetime.now(), - created_by='SYSTEM' + created_by="SYSTEM", ) invoice = invoice.save() - NonSufficientFundsService.save_non_sufficient_funds(invoice_id=invoice.id, - invoice_number=inv_number, - cfs_account=cfs_account.cfs_account, - description=reason_description) + NonSufficientFundsService.save_non_sufficient_funds( + invoice_id=invoice.id, + invoice_number=inv_number, + cfs_account=cfs_account.cfs_account, + description=reason_description, + ) distribution: DistributionCodeModel = DistributionCodeModel.find_by_active_for_fee_schedule( - fee_schedule.fee_schedule_id) + fee_schedule.fee_schedule_id + ) line_item = PaymentLineItemModel( invoice_id=invoice.id, @@ -885,15 +1024,17 @@ def _create_nsf_invoice(cfs_account: CfsAccountModel, inv_number: str, future_effective_fees=0, line_item_status_code=LineItemStatus.ACTIVE.value, service_fees=0, - fee_distribution_id=distribution.distribution_code_id if distribution else 1) + fee_distribution_id=distribution.distribution_code_id if distribution else 1, + ) line_item.save() inv_ref: InvoiceReferenceModel = InvoiceReferenceModel( invoice_id=invoice.id, invoice_number=inv_number, reference_number=InvoiceReferenceModel.find_any_active_reference_by_invoice_number( - invoice_number=inv_number).reference_number, - status_code=InvoiceReferenceStatus.ACTIVE.value + invoice_number=inv_number + ).reference_number, + status_code=InvoiceReferenceStatus.ACTIVE.value, ) inv_ref.save() @@ -904,9 +1045,13 @@ def _get_settlement_type(payment_lines) -> str: """Exclude ONAC, ADJS, PAYR, ONAP and return the record type.""" settlement_type: str = None for row in payment_lines: - if _get_row_value(row, Column.RECORD_TYPE) in \ - (RecordType.BOLP.value, RecordType.EFTP.value, RecordType.PAD.value, RecordType.PADR.value, - RecordType.PAYR.value): + if _get_row_value(row, Column.RECORD_TYPE) in ( + RecordType.BOLP.value, + RecordType.EFTP.value, + RecordType.PAD.value, + RecordType.PADR.value, + RecordType.PAYR.value, + ): settlement_type = _get_row_value(row, Column.RECORD_TYPE) break return settlement_type diff --git a/pay-queue/src/pay_queue/version.py b/pay-queue/src/pay_queue/version.py index c29b19b02..08f693c1e 100644 --- a/pay-queue/src/pay_queue/version.py +++ b/pay-queue/src/pay_queue/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = '2.0.1' # pylint: disable=invalid-name +__version__ = "2.0.1" # pylint: disable=invalid-name diff --git a/pay-queue/tests/__init__.py b/pay-queue/tests/__init__.py index 999702cfd..99b79aef3 100644 --- a/pay-queue/tests/__init__.py +++ b/pay-queue/tests/__init__.py @@ -15,10 +15,9 @@ import datetime import os - EPOCH_DATETIME = datetime.datetime.utcfromtimestamp(0) FROZEN_DATETIME = datetime.datetime(2001, 8, 5, 7, 7, 58, 272362) -os.environ['DEPLOYMENT_ENV'] = 'testing' +os.environ["DEPLOYMENT_ENV"] = "testing" def add_years(d, years): diff --git a/pay-queue/tests/conftest.py b/pay-queue/tests/conftest.py index c257f1432..0d188c627 100644 --- a/pay-queue/tests/conftest.py +++ b/pay-queue/tests/conftest.py @@ -27,21 +27,21 @@ from pay_queue import create_app -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def app(): """Return a session-wide application configured in TEST mode.""" - _app = create_app('testing') + _app = create_app("testing") return _app -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def gcp_queue(app, mocker): """Mock GcpQueue to avoid initializing the external connections.""" - mocker.patch.object(GcpQueue, 'init_app') + mocker.patch.object(GcpQueue, "init_app") return GcpQueue(app) -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def db(app): # pylint: disable=redefined-outer-name, invalid-name """Return a session-wide initialised database.""" with app.app_context(): @@ -49,8 +49,8 @@ def db(app): # pylint: disable=redefined-outer-name, invalid-name drop_database(_db.engine.url) create_database(_db.engine.url) _db.session().execute(text('SET TIME ZONE "UTC";')) - pay_api_dir = os.path.abspath('.').replace('pay-queue', 'pay-api') - pay_api_dir = os.path.join(pay_api_dir, 'migrations') + pay_api_dir = os.path.abspath(".").replace("pay-queue", "pay-api") + pay_api_dir = os.path.join(pay_api_dir, "migrations") Migrate(app, _db, directory=pay_api_dir) upgrade() return _db @@ -62,13 +62,13 @@ def config(app): return app.config -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client(app): # pylint: disable=redefined-outer-name """Return a session-wide Flask test client.""" return app.test_client() -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def session(db, app): # pylint: disable=redefined-outer-name, invalid-name """Return a function-scoped session.""" with app.app_context(): @@ -82,7 +82,7 @@ def session(db, app): # pylint: disable=redefined-outer-name, invalid-name db.session.commit = nested.commit db.session.rollback = nested.rollback - @event.listens_for(sess, 'after_transaction_end') + @event.listens_for(sess, "after_transaction_end") def restart_savepoint(sess2, trans): # pylint: disable=unused-variable nonlocal nested if trans.nested: @@ -103,39 +103,42 @@ def restart_savepoint(sess2, trans): # pylint: disable=unused-variable finally: db.session.remove() transaction.rollback() - event.remove(sess, 'after_transaction_end', restart_savepoint) + event.remove(sess, "after_transaction_end", restart_savepoint) db.session = old_session -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def auto(docker_services, app): """Spin up docker containers.""" - if app.config['USE_DOCKER_MOCK']: - docker_services.start('minio') - docker_services.start('proxy') - docker_services.start('paybc') - docker_services.start('pubsub-emulator') + if app.config["USE_DOCKER_MOCK"]: + docker_services.start("minio") + docker_services.start("proxy") + docker_services.start("paybc") + docker_services.start("pubsub-emulator") @pytest.fixture() def mock_publish(monkeypatch): """Mock check_auth.""" - monkeypatch.setattr('pay_api.services.gcp_queue_publisher.publish_to_queue', lambda *args, **kwargs: None) + monkeypatch.setattr( + "pay_api.services.gcp_queue_publisher.publish_to_queue", + lambda *args, **kwargs: None, + ) @pytest.fixture(autouse=True) def mock_queue_auth(mocker): """Mock queue authorization.""" - mocker.patch('pay_queue.external.gcp_auth.verify_jwt', return_value='') + mocker.patch("pay_queue.external.gcp_auth.verify_jwt", return_value="") -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def initialize_pubsub(app): """Initialize pubsub emulator and respective publisher and subscribers.""" - os.environ['PUBSUB_EMULATOR_HOST'] = 'localhost:8085' - project = app.config.get('TEST_GCP_PROJECT_NAME') - topics = app.config.get('TEST_GCP_TOPICS') - push_config = pubsub.types.PushConfig(push_endpoint=app.config.get('TEST_PUSH_ENDPOINT')) + os.environ["PUBSUB_EMULATOR_HOST"] = "localhost:8085" + project = app.config.get("TEST_GCP_PROJECT_NAME") + topics = app.config.get("TEST_GCP_TOPICS") + push_config = pubsub.types.PushConfig(push_endpoint=app.config.get("TEST_PUSH_ENDPOINT")) publisher = pubsub.PublisherClient() subscriber = pubsub.SubscriberClient() with publisher, subscriber: @@ -146,16 +149,16 @@ def initialize_pubsub(app): except NotFound: pass publisher.create_topic(name=topic_path) - subscription_path = subscriber.subscription_path(project, f'{topic}_subscription') + subscription_path = subscriber.subscription_path(project, f"{topic}_subscription") try: subscriber.delete_subscription(subscription=subscription_path) except NotFound: pass subscriber.create_subscription( request={ - 'name': subscription_path, - 'topic': topic_path, - 'push_config': push_config, + "name": subscription_path, + "topic": topic_path, + "push_config": push_config, } ) @@ -163,6 +166,7 @@ def initialize_pubsub(app): @pytest.fixture(autouse=True) def mock_pub_sub_call(mocker): """Mock pub sub call.""" + class PublisherMock: """Publisher Mock.""" @@ -171,12 +175,12 @@ def __init__(self, *args, **kwargs): def publish(self, *args, **kwargs): """Publish mock.""" - raise CancelledError('This is a mock') + raise CancelledError("This is a mock") - mocker.patch('google.cloud.pubsub_v1.PublisherClient', PublisherMock) + mocker.patch("google.cloud.pubsub_v1.PublisherClient", PublisherMock) -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def set_eft_tdi17_location_id(app): """Set TDI17 Location ID for tests.""" - app.config['EFT_TDI17_LOCATION_ID'] = '85004' + app.config["EFT_TDI17_LOCATION_ID"] = "85004" diff --git a/pay-queue/tests/integration/__init__.py b/pay-queue/tests/integration/__init__.py index 4b0c4ea44..99d8fe9dc 100644 --- a/pay-queue/tests/integration/__init__.py +++ b/pay-queue/tests/integration/__init__.py @@ -19,21 +19,24 @@ from pay_api.utils.enums import InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus, PaymentSystem -def factory_payment_account(payment_system_code: str = 'PAYBC', payment_method_code: str = 'CC', account_number='4101', - bcol_user_id='test', - auth_account_id: str = '1234'): +def factory_payment_account( + payment_system_code: str = "PAYBC", + payment_method_code: str = "CC", + account_number="4101", + bcol_user_id="test", + auth_account_id: str = "1234", +): """Return Factory.""" # Create a payment account - account = PaymentAccount( - auth_account_id=auth_account_id, - bcol_user_id=bcol_user_id, - bcol_account='TEST' - ).save() + account = PaymentAccount(auth_account_id=auth_account_id, bcol_user_id=bcol_user_id, bcol_account="TEST").save() - CfsAccount(cfs_party='11111', - cfs_account=account_number, - cfs_site='29921', payment_account=account, - payment_method=payment_method_code).save() + CfsAccount( + cfs_party="11111", + cfs_account=account_number, + cfs_site="29921", + payment_account=account, + payment_method=payment_method_code, + ).save() if payment_system_code == PaymentSystem.BCOL.value: account.payment_method = PaymentMethod.DRAWDOWN.value @@ -44,46 +47,53 @@ def factory_payment_account(payment_system_code: str = 'PAYBC', payment_method_c def factory_payment( - payment_system_code: str = 'PAYBC', payment_method_code: str = 'CC', - payment_status_code: str = PaymentStatus.CREATED.value, - created_on: datetime = datetime.now(), - invoice_number: str = None + payment_system_code: str = "PAYBC", + payment_method_code: str = "CC", + payment_status_code: str = PaymentStatus.CREATED.value, + created_on: datetime = datetime.now(), + invoice_number: str = None, ): """Return Factory.""" return Payment( payment_system_code=payment_system_code, payment_method_code=payment_method_code, payment_status_code=payment_status_code, - invoice_number=invoice_number + invoice_number=invoice_number, ).save() -def factory_invoice(payment_account, status_code: str = InvoiceStatus.CREATED.value, - corp_type_code='CP', - business_identifier: str = 'CP0001234', - service_fees: float = 0.0, total=0, - payment_method_code: str = PaymentMethod.DIRECT_PAY.value, - created_on: datetime = datetime.now(), - disbursement_status_code=None): +def factory_invoice( + payment_account, + status_code: str = InvoiceStatus.CREATED.value, + corp_type_code="CP", + business_identifier: str = "CP0001234", + service_fees: float = 0.0, + total=0, + payment_method_code: str = PaymentMethod.DIRECT_PAY.value, + created_on: datetime = datetime.now(), + disbursement_status_code=None, +): """Return Factory.""" return Invoice( invoice_status_code=status_code, payment_account_id=payment_account.id, total=total, - created_by='test', + created_by="test", created_on=created_on, business_identifier=business_identifier, corp_type_code=corp_type_code, - folio_number='1234567890', + folio_number="1234567890", service_fees=service_fees, bcol_account=payment_account.bcol_account, payment_method_code=payment_method_code, - disbursement_status_code=disbursement_status_code + disbursement_status_code=disbursement_status_code, ).save() -def factory_invoice_reference(invoice_id: int, invoice_number: str = '10021'): +def factory_invoice_reference(invoice_id: int, invoice_number: str = "10021"): """Return Factory.""" - return InvoiceReference(invoice_id=invoice_id, - status_code=InvoiceReferenceStatus.ACTIVE.value, - invoice_number=invoice_number).save() + return InvoiceReference( + invoice_id=invoice_id, + status_code=InvoiceReferenceStatus.ACTIVE.value, + invoice_number=invoice_number, + ).save() diff --git a/pay-queue/tests/integration/factory.py b/pay-queue/tests/integration/factory.py index 2b9236cef..a779140cb 100644 --- a/pay-queue/tests/integration/factory.py +++ b/pay-queue/tests/integration/factory.py @@ -20,91 +20,130 @@ from datetime import datetime, timezone from pay_api.models import ( - CfsAccount, DistributionCode, EFTRefund, EFTShortnames, Invoice, InvoiceReference, Payment, PaymentAccount, - PaymentLineItem, PaymentTransaction, Receipt, Refund, RoutingSlip, Statement, StatementInvoices, StatementSettings) + CfsAccount, + DistributionCode, + EFTRefund, + EFTShortnames, + Invoice, + InvoiceReference, + Payment, + PaymentAccount, + PaymentLineItem, + PaymentTransaction, + Receipt, + Refund, + RoutingSlip, + Statement, + StatementInvoices, + StatementSettings, +) from pay_api.utils.enums import ( - CfsAccountStatus, DisbursementStatus, EFTShortnameRefundStatus, EFTShortnameType, InvoiceReferenceStatus, - InvoiceStatus, LineItemStatus, PaymentMethod, PaymentStatus, PaymentSystem, RoutingSlipStatus, TransactionStatus) - - -def factory_premium_payment_account(bcol_user_id='PB25020', bcol_account_id='1234567890', auth_account_id='1234'): + CfsAccountStatus, + DisbursementStatus, + EFTShortnameRefundStatus, + EFTShortnameType, + InvoiceReferenceStatus, + InvoiceStatus, + LineItemStatus, + PaymentMethod, + PaymentStatus, + PaymentSystem, + RoutingSlipStatus, + TransactionStatus, +) + + +def factory_premium_payment_account(bcol_user_id="PB25020", bcol_account_id="1234567890", auth_account_id="1234"): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - bcol_user_id=bcol_user_id, - bcol_account=bcol_account_id, - ).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + bcol_user_id=bcol_user_id, + bcol_account=bcol_account_id, + ).save() return account -def factory_statement_settings(pay_account_id: str, frequency='DAILY', from_date=datetime.now(), - to_date=None) -> StatementSettings: +def factory_statement_settings( + pay_account_id: str, frequency="DAILY", from_date=datetime.now(), to_date=None +) -> StatementSettings: """Return Factory.""" return StatementSettings( frequency=frequency, payment_account_id=pay_account_id, from_date=from_date, - to_date=to_date + to_date=to_date, ).save() def factory_statement( - frequency: str = 'WEEKLY', - payment_account_id: str = None, - from_date: datetime = datetime.now(tz=timezone.utc), - to_date: datetime = datetime.now(tz=timezone.utc), - statement_settings_id: str = None, - created_on: datetime = datetime.now(tz=timezone.utc), - payment_methods: str = PaymentMethod.EFT.value): - """Return Factory.""" - return Statement(frequency=frequency, - statement_settings_id=statement_settings_id, - payment_account_id=payment_account_id, - from_date=from_date, - to_date=to_date, - created_on=created_on, - payment_methods=payment_methods).save() - - -def factory_statement_invoices( - statement_id: str, - invoice_id: str): + frequency: str = "WEEKLY", + payment_account_id: str = None, + from_date: datetime = datetime.now(tz=timezone.utc), + to_date: datetime = datetime.now(tz=timezone.utc), + statement_settings_id: str = None, + created_on: datetime = datetime.now(tz=timezone.utc), + payment_methods: str = PaymentMethod.EFT.value, +): """Return Factory.""" - return StatementInvoices(statement_id=statement_id, - invoice_id=invoice_id).save() + return Statement( + frequency=frequency, + statement_settings_id=statement_settings_id, + payment_account_id=payment_account_id, + from_date=from_date, + to_date=to_date, + created_on=created_on, + payment_methods=payment_methods, + ).save() -def factory_invoice(payment_account: PaymentAccount, status_code: str = InvoiceStatus.CREATED.value, - corp_type_code='CP', - business_identifier: str = 'CP0001234', - service_fees: float = 0.0, total=0, - payment_method_code: str = PaymentMethod.DIRECT_PAY.value, - created_on: datetime = datetime.now(), - disbursement_status_code=None): +def factory_statement_invoices(statement_id: str, invoice_id: str): + """Return Factory.""" + return StatementInvoices(statement_id=statement_id, invoice_id=invoice_id).save() + + +def factory_invoice( + payment_account: PaymentAccount, + status_code: str = InvoiceStatus.CREATED.value, + corp_type_code="CP", + business_identifier: str = "CP0001234", + service_fees: float = 0.0, + total=0, + payment_method_code: str = PaymentMethod.DIRECT_PAY.value, + created_on: datetime = datetime.now(), + disbursement_status_code=None, +): """Return Factory.""" - cfs_account = CfsAccount.find_effective_by_payment_method(payment_account.id, - payment_method_code or payment_account.payment_method) + cfs_account = CfsAccount.find_effective_by_payment_method( + payment_account.id, payment_method_code or payment_account.payment_method + ) cfs_account_id = cfs_account.id if cfs_account else None return Invoice( invoice_status_code=status_code, payment_account_id=payment_account.id, total=total, paid=0, - created_by='test', + created_by="test", created_on=created_on, business_identifier=business_identifier, corp_type_code=corp_type_code, - folio_number='1234567890', + folio_number="1234567890", service_fees=service_fees, bcol_account=payment_account.bcol_account, cfs_account_id=cfs_account_id, payment_method_code=payment_method_code or payment_account.payment_method, - disbursement_status_code=disbursement_status_code + disbursement_status_code=disbursement_status_code, ).save() -def factory_payment_line_item(invoice_id: str, fee_schedule_id: int = 1, filing_fees: int = 10, total: int = 10, - service_fees: int = 0, status: str = LineItemStatus.ACTIVE.value, - fee_dist_id: int = None): +def factory_payment_line_item( + invoice_id: str, + fee_schedule_id: int = 1, + filing_fees: int = 10, + total: int = 10, + service_fees: int = 0, + status: str = LineItemStatus.ACTIVE.value, + fee_dist_id: int = None, +): """Return Factory.""" return PaymentLineItem( invoice_id=invoice_id, @@ -113,38 +152,51 @@ def factory_payment_line_item(invoice_id: str, fee_schedule_id: int = 1, filing_ total=total, service_fees=service_fees, line_item_status_code=status, - fee_distribution_id=fee_dist_id or DistributionCode.find_by_active_for_fee_schedule( - fee_schedule_id).distribution_code_id + fee_distribution_id=fee_dist_id + or DistributionCode.find_by_active_for_fee_schedule(fee_schedule_id).distribution_code_id, ).save() -def factory_invoice_reference(invoice_id: int, invoice_number: str = '10021', - status_code: str = InvoiceReferenceStatus.ACTIVE.value, - is_consolidated=False): +def factory_invoice_reference( + invoice_id: int, + invoice_number: str = "10021", + status_code: str = InvoiceReferenceStatus.ACTIVE.value, + is_consolidated=False, +): """Return Factory.""" - return InvoiceReference(invoice_id=invoice_id, - status_code=status_code, - invoice_number=invoice_number, - is_consolidated=is_consolidated).save() + return InvoiceReference( + invoice_id=invoice_id, + status_code=status_code, + invoice_number=invoice_number, + is_consolidated=is_consolidated, + ).save() -def factory_receipt(invoice_id: int, receipt_number: str = '10021'): +def factory_receipt(invoice_id: int, receipt_number: str = "10021"): """Return Factory.""" return Receipt(invoice_id=invoice_id, receipt_number=receipt_number).save() -def factory_payment(pay_account: PaymentAccount, - invoice_number: str = '10021', status=PaymentStatus.CREATED.value, - payment_method_code=PaymentMethod.ONLINE_BANKING.value, - invoice_amount: float = 100, paid_amount: float = 0, - receipt_number: str = ''): +def factory_payment( + pay_account: PaymentAccount, + invoice_number: str = "10021", + status=PaymentStatus.CREATED.value, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + invoice_amount: float = 100, + paid_amount: float = 0, + receipt_number: str = "", +): """Return Factory.""" - return Payment(payment_status_code=status, payment_system_code=PaymentSystem.PAYBC.value, - payment_method_code=payment_method_code, payment_account_id=pay_account.id, - invoice_amount=invoice_amount, - invoice_number=invoice_number, - paid_amount=paid_amount, - receipt_number=receipt_number).save() + return Payment( + payment_status_code=status, + payment_system_code=PaymentSystem.PAYBC.value, + payment_method_code=payment_method_code, + payment_account_id=pay_account.id, + invoice_amount=invoice_amount, + invoice_number=invoice_number, + paid_amount=paid_amount, + receipt_number=receipt_number, + ).save() def factory_payment_transaction(payment_id: int): @@ -152,26 +204,24 @@ def factory_payment_transaction(payment_id: int): return PaymentTransaction( payment_id=payment_id, status_code=TransactionStatus.CREATED.value, - transaction_start_time=datetime.now()).save() + transaction_start_time=datetime.now(), + ).save() def factory_create_eft_shortname(short_name: str, short_name_type: str = EFTShortnameType.EFT.value): """Return Factory.""" - short_name = EFTShortnames( - short_name=short_name, - type=short_name_type - ).save() + short_name = EFTShortnames(short_name=short_name, type=short_name_type).save() return short_name def factory_create_eft_refund( - cas_supplier_number: str = '1234', - comment: str = 'Test Comment', + cas_supplier_number: str = "1234", + comment: str = "Test Comment", refund_amount: float = 100.0, - refund_email: str = 'test@email.com', + refund_email: str = "test@email.com", short_name_id: int = 1, status: str = EFTShortnameRefundStatus.APPROVED.value, - disbursement_status_code: str = DisbursementStatus.ACKNOWLEDGED.value + disbursement_status_code: str = DisbursementStatus.ACKNOWLEDGED.value, ): """Return Factory.""" eft_refund = EFTRefund( @@ -182,102 +232,139 @@ def factory_create_eft_refund( refund_email=refund_email, short_name_id=short_name_id, status=status, - created_on=datetime.now(tz=timezone.utc) + created_on=datetime.now(tz=timezone.utc), ).save() return eft_refund -def factory_create_eft_account(auth_account_id='1234', status=CfsAccountStatus.ACTIVE.value, - cfs_account='1234'): +def factory_create_eft_account(auth_account_id="1234", status=CfsAccountStatus.ACTIVE.value, cfs_account="1234"): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.EFT.value, - name=f'Test EFT {auth_account_id}').save() - CfsAccount(status=status, account_id=account.id, cfs_account=cfs_account, payment_method=PaymentMethod.EFT.value) \ - .save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.EFT.value, + name=f"Test EFT {auth_account_id}", + ).save() + CfsAccount( + status=status, + account_id=account.id, + cfs_account=cfs_account, + payment_method=PaymentMethod.EFT.value, + ).save() return account -def factory_create_online_banking_account(auth_account_id='1234', status=CfsAccountStatus.PENDING.value, - cfs_account='1234'): +def factory_create_online_banking_account( + auth_account_id="1234", status=CfsAccountStatus.PENDING.value, cfs_account="1234" +): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.ONLINE_BANKING.value, - name=f'Test {auth_account_id}').save() - CfsAccount(status=status, account_id=account.id, cfs_account=cfs_account, - payment_method=PaymentMethod.ONLINE_BANKING.value).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.ONLINE_BANKING.value, + name=f"Test {auth_account_id}", + ).save() + CfsAccount( + status=status, + account_id=account.id, + cfs_account=cfs_account, + payment_method=PaymentMethod.ONLINE_BANKING.value, + ).save() return account -def factory_create_pad_account(auth_account_id='1234', bank_number='001', bank_branch='004', bank_account='1234567890', - status=CfsAccountStatus.PENDING.value, account_number='4101'): +def factory_create_pad_account( + auth_account_id="1234", + bank_number="001", + bank_branch="004", + bank_account="1234567890", + status=CfsAccountStatus.PENDING.value, + account_number="4101", +): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.PAD.value, - name=f'Test {auth_account_id}').save() - CfsAccount(status=status, account_id=account.id, bank_number=bank_number, - bank_branch_number=bank_branch, bank_account_number=bank_account, - cfs_party='11111', - cfs_account=account_number, - cfs_site='29921', - payment_method=PaymentMethod.PAD.value - ).save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.PAD.value, + name=f"Test {auth_account_id}", + ).save() + CfsAccount( + status=status, + account_id=account.id, + bank_number=bank_number, + bank_branch_number=bank_branch, + bank_account_number=bank_account, + cfs_party="11111", + cfs_account=account_number, + cfs_site="29921", + payment_method=PaymentMethod.PAD.value, + ).save() return account -def factory_create_ejv_account(auth_account_id='1234', - client: str = '112', - resp_centre: str = '11111', - service_line: str = '11111', - stob: str = '1111', - project_code: str = '1111111'): +def factory_create_ejv_account( + auth_account_id="1234", + client: str = "112", + resp_centre: str = "11111", + service_line: str = "11111", + stob: str = "1111", + project_code: str = "1111111", +): """Return Factory.""" - account = PaymentAccount(auth_account_id=auth_account_id, - payment_method=PaymentMethod.EJV.value, - name=f'Test {auth_account_id}').save() - DistributionCode(name=account.name, - client=client, - responsibility_centre=resp_centre, - service_line=service_line, - stob=stob, - project_code=project_code, - account_id=account.id, - start_date=datetime.now(tz=timezone.utc).date(), - created_by='test').save() + account = PaymentAccount( + auth_account_id=auth_account_id, + payment_method=PaymentMethod.EJV.value, + name=f"Test {auth_account_id}", + ).save() + DistributionCode( + name=account.name, + client=client, + responsibility_centre=resp_centre, + service_line=service_line, + stob=stob, + project_code=project_code, + account_id=account.id, + start_date=datetime.now(tz=timezone.utc).date(), + created_by="test", + ).save() return account -def factory_distribution(name: str, client: str = '111', reps_centre: str = '22222', service_line: str = '33333', - stob: str = '4444', project_code: str = '5555555', service_fee_dist_id: int = None, - disbursement_dist_id: int = None): +def factory_distribution( + name: str, + client: str = "111", + reps_centre: str = "22222", + service_line: str = "33333", + stob: str = "4444", + project_code: str = "5555555", + service_fee_dist_id: int = None, + disbursement_dist_id: int = None, +): """Return Factory.""" - return DistributionCode(name=name, - client=client, - responsibility_centre=reps_centre, - service_line=service_line, - stob=stob, - project_code=project_code, - service_fee_distribution_code_id=service_fee_dist_id, - disbursement_distribution_code_id=disbursement_dist_id, - start_date=datetime.now(tz=timezone.utc).date(), - created_by='test').save() + return DistributionCode( + name=name, + client=client, + responsibility_centre=reps_centre, + service_line=service_line, + stob=stob, + project_code=project_code, + service_fee_distribution_code_id=service_fee_dist_id, + disbursement_distribution_code_id=disbursement_dist_id, + start_date=datetime.now(tz=timezone.utc).date(), + created_by="test", + ).save() def factory_routing_slip_account( - number: str = '1234', - status: str = CfsAccountStatus.PENDING.value, - total: int = 0, - remaining_amount: int = 0, - routing_slip_date=datetime.now(), - payment_method=PaymentMethod.CASH.value, - auth_account_id='1234', - routing_slip_status=RoutingSlipStatus.ACTIVE.value, - refund_amount=0 + number: str = "1234", + status: str = CfsAccountStatus.PENDING.value, + total: int = 0, + remaining_amount: int = 0, + routing_slip_date=datetime.now(), + payment_method=PaymentMethod.CASH.value, + auth_account_id="1234", + routing_slip_status=RoutingSlipStatus.ACTIVE.value, + refund_amount=0, ) -> PaymentAccount: """Create routing slip and return payment account with it.""" - payment_account = PaymentAccount( - payment_method=payment_method, - name=f'Test {auth_account_id}') + payment_account = PaymentAccount(payment_method=payment_method, name=f"Test {auth_account_id}") payment_account.save() rs = RoutingSlip( @@ -286,36 +373,38 @@ def factory_routing_slip_account( status=routing_slip_status, total=total, remaining_amount=remaining_amount, - created_by='test', + created_by="test", routing_slip_date=routing_slip_date, - refund_amount=refund_amount + refund_amount=refund_amount, ).save() - Payment(payment_system_code=PaymentSystem.FAS.value, - payment_account_id=payment_account.id, - payment_method_code=PaymentMethod.CASH.value, - payment_status_code=PaymentStatus.COMPLETED.value, - receipt_number=number, - is_routing_slip=True, - paid_amount=rs.total, - created_by='TEST') - - CfsAccount(status=status, account_id=payment_account.id, payment_method=PaymentMethod.INTERNAL.value).save() + Payment( + payment_system_code=PaymentSystem.FAS.value, + payment_account_id=payment_account.id, + payment_method_code=PaymentMethod.CASH.value, + payment_status_code=PaymentStatus.COMPLETED.value, + receipt_number=number, + is_routing_slip=True, + paid_amount=rs.total, + created_by="TEST", + ) + + CfsAccount( + status=status, + account_id=payment_account.id, + payment_method=PaymentMethod.INTERNAL.value, + ).save() return payment_account -def factory_refund( - routing_slip_id: int = None, - details={}, - invoice_id: int = None -): +def factory_refund(routing_slip_id: int = None, details={}, invoice_id: int = None): """Return Factory.""" return Refund( invoice_id=invoice_id, routing_slip_id=routing_slip_id, requested_date=datetime.now(), - reason='TEST', - requested_by='TEST', - details=details + reason="TEST", + requested_by="TEST", + details=details, ).save() diff --git a/pay-queue/tests/integration/test_cgi_reconciliations.py b/pay-queue/tests/integration/test_cgi_reconciliations.py index 6c24f760f..90f3f6b39 100644 --- a/pay-queue/tests/integration/test_cgi_reconciliations.py +++ b/pay-queue/tests/integration/test_cgi_reconciliations.py @@ -35,17 +35,35 @@ from pay_api.models import RoutingSlip as RoutingSlipModel from pay_api.models import db from pay_api.utils.enums import ( - CfsAccountStatus, DisbursementStatus, EFTShortnameRefundStatus, EFTShortnameType, EjvFileType, EJVLinkType, - InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus, RoutingSlipStatus) + CfsAccountStatus, + DisbursementStatus, + EFTShortnameRefundStatus, + EFTShortnameType, + EjvFileType, + EJVLinkType, + InvoiceReferenceStatus, + InvoiceStatus, + PaymentMethod, + PaymentStatus, + RoutingSlipStatus, +) from sbc_common_components.utils.enums import QueueMessageTypes from sqlalchemy import text from tests.integration.utils import add_file_event_to_queue_and_process from .factory import ( - factory_create_eft_refund, factory_create_eft_shortname, factory_create_ejv_account, factory_create_pad_account, - factory_distribution, factory_invoice, factory_invoice_reference, factory_payment_line_item, factory_refund, - factory_routing_slip_account) + factory_create_eft_refund, + factory_create_eft_shortname, + factory_create_ejv_account, + factory_create_pad_account, + factory_distribution, + factory_invoice, + factory_invoice_reference, + factory_payment_line_item, + factory_refund, + factory_routing_slip_account, +) from .utils import upload_to_minio @@ -55,40 +73,54 @@ def test_successful_partner_ejv_reconciliations(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - partner_code = 'VS' + cfs_account_number = "1234" + partner_code = "VS" fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type( - corp_type_code=partner_code, filing_type_code='WILLSEARCH' + corp_type_code=partner_code, filing_type_code="WILLSEARCH" ) - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - corp_type_code='VS', - payment_method_code=PaymentMethod.ONLINE_BANKING.value, - status_code=InvoiceStatus.PAID.value) - eft_invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - corp_type_code='VS', - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.PAID.value) + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + corp_type_code="VS", + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + status_code=InvoiceStatus.PAID.value, + ) + eft_invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + corp_type_code="VS", + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.PAID.value, + ) invoice_id = invoice.id line_item = factory_payment_line_item( - invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0, - fee_schedule_id=fee_schedule.fee_schedule_id + invoice_id=invoice.id, + filing_fees=90.0, + service_fees=10.0, + total=90.0, + fee_schedule_id=fee_schedule.fee_schedule_id, ) dist_code = DistributionCodeModel.find_by_id(line_item.fee_distribution_id) # Check if the disbursement distribution is present for this. if not dist_code.disbursement_distribution_code_id: - disbursement_distribution_code = factory_distribution(name='Disbursement') + disbursement_distribution_code = factory_distribution(name="Disbursement") dist_code.disbursement_distribution_code_id = disbursement_distribution_code.distribution_code_id dist_code.save() - invoice_number = '1234567890' + invoice_number = "1234567890" factory_invoice_reference( - invoice_id=invoice.id, invoice_number=invoice_number, status_code=InvoiceReferenceStatus.COMPLETED.value + invoice_id=invoice.id, + invoice_number=invoice_number, + status_code=InvoiceReferenceStatus.COMPLETED.value, ) factory_invoice_reference( - invoice_id=eft_invoice.id, invoice_number='1234567899', status_code=InvoiceReferenceStatus.COMPLETED.value + invoice_id=eft_invoice.id, + invoice_number="1234567899", + status_code=InvoiceReferenceStatus.COMPLETED.value, ) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -99,38 +131,43 @@ def test_successful_partner_ejv_reconciliations(session, app, client): partner_code=eft_invoice.corp_type_code, status_code=DisbursementStatus.WAITING_FOR_RECEIPT.value, target_id=eft_invoice.id, - target_type=EJVLinkType.INVOICE.value + target_type=EJVLinkType.INVOICE.value, ).save() - eft_flowthrough = f'{eft_invoice.id}-{partner_disbursement.id}' + eft_flowthrough = f"{eft_invoice.id}-{partner_disbursement.id}" - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel(file_ref=file_ref, disbursement_status_code=DisbursementStatus.UPLOADED.value).save() ejv_file_id = ejv_file.id - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, - partner_code=partner_code, - payment_account_id=pay_account.id).save() + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + partner_code=partner_code, + payment_account_id=pay_account.id, + ).save() ejv_header_id = ejv_header.id EjvLinkModel( - link_id=invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() EjvLinkModel( - link_id=eft_invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=eft_invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) @@ -139,50 +176,52 @@ def test_successful_partner_ejv_reconciliations(session, app, client): # Now upload a feedback file and check the status. # Just create feedback file to mock the real feedback file. # Has legacy and added in PartnerDisbursements rows. - feedback_content = f'GABG...........00000000{ejv_file_id}...\n' \ - f'..BH...0000.................................................................................' \ - f'.....................................................................CGI\n' \ - f'..JH...FI0000000{ejv_header_id}.........................000000000090.00.....................' \ - f'............................................................................................' \ - f'............................................................................................' \ - f'.........0000...............................................................................' \ - f'.......................................................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000120230529................................................' \ - f'...........000000000090.00D.................................................................' \ - f'..................................#{invoice_id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000220230529................................................' \ - f'...........000000000090.00C.................................................................' \ - f'..................................#{invoice_id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000120230529................................................' \ - f'...........000000000090.00D.................................................................' \ - f'..................................#{eft_flowthrough} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000220230529................................................' \ - f'...........000000000090.00C.................................................................' \ - f'..................................#{eft_flowthrough} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..BT.......FI0000000{ejv_header_id}000000000000002000000000180.000000.......................' \ - f'............................................................................................' \ - f'...................................CGI' - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"GABG...........00000000{ejv_file_id}...\n" + f"..BH...0000................................................................................." + f".....................................................................CGI\n" + f"..JH...FI0000000{ejv_header_id}.........................000000000090.00....................." + f"............................................................................................" + f"............................................................................................" + f".........0000..............................................................................." + f".......................................................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000120230529................................................" + f"...........000000000090.00D................................................................." + f"..................................#{invoice_id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000220230529................................................" + f"...........000000000090.00C................................................................." + f"..................................#{invoice_id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000120230529................................................" + f"...........000000000090.00D................................................................." + f"..................................#{eft_flowthrough} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000220230529................................................" + f"...........000000000090.00C................................................................." + f"..................................#{eft_flowthrough} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..BT.......FI0000000{ejv_header_id}000000000000002000000000180.000000......................." + f"............................................................................................" + f"...................................CGI" + ) + + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -203,41 +242,55 @@ def test_failed_partner_ejv_reconciliations(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - partner_code = 'VS' + cfs_account_number = "1234" + partner_code = "VS" fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type( - corp_type_code=partner_code, filing_type_code='WILLSEARCH' + corp_type_code=partner_code, filing_type_code="WILLSEARCH" ) - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - corp_type_code='VS', - payment_method_code=PaymentMethod.ONLINE_BANKING.value, - status_code=InvoiceStatus.PAID.value) - eft_invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - corp_type_code='VS', - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.PAID.value) + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + corp_type_code="VS", + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + status_code=InvoiceStatus.PAID.value, + ) + eft_invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + corp_type_code="VS", + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.PAID.value, + ) invoice_id = invoice.id line_item = factory_payment_line_item( - invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0, - fee_schedule_id=fee_schedule.fee_schedule_id + invoice_id=invoice.id, + filing_fees=90.0, + service_fees=10.0, + total=90.0, + fee_schedule_id=fee_schedule.fee_schedule_id, ) dist_code = DistributionCodeModel.find_by_id(line_item.fee_distribution_id) # Check if the disbursement distribution is present for this. if not dist_code.disbursement_distribution_code_id: - disbursement_distribution_code = factory_distribution(name='Disbursement') + disbursement_distribution_code = factory_distribution(name="Disbursement") dist_code.disbursement_distribution_code_id = disbursement_distribution_code.distribution_code_id dist_code.save() disbursement_distribution_code_id = dist_code.disbursement_distribution_code_id - invoice_number = '1234567890' + invoice_number = "1234567890" factory_invoice_reference( - invoice_id=invoice.id, invoice_number=invoice_number, status_code=InvoiceReferenceStatus.COMPLETED.value + invoice_id=invoice.id, + invoice_number=invoice_number, + status_code=InvoiceReferenceStatus.COMPLETED.value, ) factory_invoice_reference( - invoice_id=eft_invoice.id, invoice_number='1234567899', status_code=InvoiceReferenceStatus.COMPLETED.value + invoice_id=eft_invoice.id, + invoice_number="1234567899", + status_code=InvoiceReferenceStatus.COMPLETED.value, ) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -248,40 +301,44 @@ def test_failed_partner_ejv_reconciliations(session, app, client): partner_code=eft_invoice.corp_type_code, status_code=DisbursementStatus.WAITING_FOR_RECEIPT.value, target_id=eft_invoice.id, - target_type=EJVLinkType.INVOICE.value + target_type=EJVLinkType.INVOICE.value, ).save() - eft_flowthrough = f'{eft_invoice.id}-{partner_disbursement.id}' + eft_flowthrough = f"{eft_invoice.id}-{partner_disbursement.id}" - file_ref = f'INBOX{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX{datetime.now()}" + ejv_file = EjvFileModel(file_ref=file_ref, disbursement_status_code=DisbursementStatus.UPLOADED.value).save() ejv_file_id = ejv_file.id - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, - partner_code=partner_code, - payment_account_id=pay_account.id).save() + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + partner_code=partner_code, + payment_account_id=pay_account.id, + ).save() ejv_header_id = ejv_header.id EjvLinkModel( link_id=invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() EjvLinkModel( - link_id=eft_invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=eft_invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() # Now upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) @@ -290,50 +347,52 @@ def test_failed_partner_ejv_reconciliations(session, app, client): # Now upload a feedback file and check the status. # Just create feedback file to mock the real feedback file. # Has legacy flow and PartnerDisbursements entries - feedback_content = f'GABG...........00000000{ejv_file_id}...\n' \ - f'..BH...1111TESTERRORMESSAGE................................................................' \ - f'......................................................................CGI\n' \ - f'..JH...FI0000000{ejv_header_id}.........................000000000090.00....................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'............1111TESTERRORMESSAGE...........................................................' \ - f'...........................................................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}00001.......................................................' \ - f'............000000000090.00D...............................................................' \ - f'....................................#{invoice_id} ' \ - f' 1111TESTERRORMESSAGE....' \ - f'...........................................................................................' \ - f'.......................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}00002.......................................................' \ - f'............000000000090.00C...............................................................' \ - f'....................................#{invoice_id} ' \ - f' 1111TESTERRORMESSAGE....' \ - f'...........................................................................................' \ - f'.......................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}00001.......................................................' \ - f'............000000000090.00D...............................................................' \ - f'....................................#{eft_flowthrough} ' \ - f' 1111TESTERRORMESSAGE.' \ - f'...........................................................................................' \ - f'..........................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}00002.......................................................' \ - f'............000000000090.00C...............................................................' \ - f'....................................#{eft_flowthrough} ' \ - f' 1111TESTERRORMESSAGE.' \ - f'...........................................................................................' \ - f'..........................................CGI\n' \ - f'..BT...........FI0000000{ejv_header_id}000000000000002000000000180.001111TESTERRORMESSAGE..' \ - f'...........................................................................................' \ - f'.........................................CGI\n' - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"GABG...........00000000{ejv_file_id}...\n" + f"..BH...1111TESTERRORMESSAGE................................................................" + f"......................................................................CGI\n" + f"..JH...FI0000000{ejv_header_id}.........................000000000090.00...................." + f"..........................................................................................." + f"..........................................................................................." + f"............1111TESTERRORMESSAGE..........................................................." + f"...........................................................................CGI\n" + f"..JD...FI0000000{ejv_header_id}00001......................................................." + f"............000000000090.00D..............................................................." + f"....................................#{invoice_id} " + f" 1111TESTERRORMESSAGE...." + f"..........................................................................................." + f".......................................CGI\n" + f"..JD...FI0000000{ejv_header_id}00002......................................................." + f"............000000000090.00C..............................................................." + f"....................................#{invoice_id} " + f" 1111TESTERRORMESSAGE...." + f"..........................................................................................." + f".......................................CGI\n" + f"..JD...FI0000000{ejv_header_id}00001......................................................." + f"............000000000090.00D..............................................................." + f"....................................#{eft_flowthrough} " + f" 1111TESTERRORMESSAGE." + f"..........................................................................................." + f"..........................................CGI\n" + f"..JD...FI0000000{ejv_header_id}00002......................................................." + f"............000000000090.00C..............................................................." + f"....................................#{eft_flowthrough} " + f" 1111TESTERRORMESSAGE." + f"..........................................................................................." + f"..........................................CGI\n" + f"..BT...........FI0000000{ejv_header_id}000000000000002000000000180.001111TESTERRORMESSAGE.." + f"..........................................................................................." + f".........................................CGI\n" + ) + + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -356,40 +415,54 @@ def test_successful_partner_reversal_ejv_reconciliations(session, app, client): # 3. Create CFS Invoice records # 4. Mark the invoice as REFUNDED # 5. Assert that the payment to partner account is reversed. - cfs_account_number = '1234' - partner_code = 'VS' + cfs_account_number = "1234" + partner_code = "VS" fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type( - corp_type_code=partner_code, filing_type_code='WILLSEARCH' + corp_type_code=partner_code, filing_type_code="WILLSEARCH" ) - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - corp_type_code='VS', - payment_method_code=PaymentMethod.ONLINE_BANKING.value, - status_code=InvoiceStatus.PAID.value) - eft_invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - corp_type_code='VS', - payment_method_code=PaymentMethod.EFT.value, - status_code=InvoiceStatus.PAID.value) + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + corp_type_code="VS", + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + status_code=InvoiceStatus.PAID.value, + ) + eft_invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + corp_type_code="VS", + payment_method_code=PaymentMethod.EFT.value, + status_code=InvoiceStatus.PAID.value, + ) invoice_id = invoice.id line_item = factory_payment_line_item( - invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0, - fee_schedule_id=fee_schedule.fee_schedule_id + invoice_id=invoice.id, + filing_fees=90.0, + service_fees=10.0, + total=90.0, + fee_schedule_id=fee_schedule.fee_schedule_id, ) dist_code = DistributionCodeModel.find_by_id(line_item.fee_distribution_id) # Check if the disbursement distribution is present for this. if not dist_code.disbursement_distribution_code_id: - disbursement_distribution_code = factory_distribution(name='Disbursement') + disbursement_distribution_code = factory_distribution(name="Disbursement") dist_code.disbursement_distribution_code_id = disbursement_distribution_code.distribution_code_id dist_code.save() - invoice_number = '1234567890' + invoice_number = "1234567890" factory_invoice_reference( - invoice_id=invoice.id, invoice_number=invoice_number, status_code=InvoiceReferenceStatus.COMPLETED.value + invoice_id=invoice.id, + invoice_number=invoice_number, + status_code=InvoiceReferenceStatus.COMPLETED.value, ) factory_invoice_reference( - invoice_id=eft_invoice.id, invoice_number='1234567899', status_code=InvoiceReferenceStatus.COMPLETED.value + invoice_id=eft_invoice.id, + invoice_number="1234567899", + status_code=InvoiceReferenceStatus.COMPLETED.value, ) invoice.invoice_status_code = InvoiceStatus.REFUND_REQUESTED.value invoice.disbursement_status_code = DisbursementStatus.COMPLETED.value @@ -405,39 +478,44 @@ def test_successful_partner_reversal_ejv_reconciliations(session, app, client): partner_code=eft_invoice.corp_type_code, status_code=DisbursementStatus.WAITING_FOR_RECEIPT.value, target_id=eft_invoice.id, - target_type=EJVLinkType.INVOICE.value + target_type=EJVLinkType.INVOICE.value, ).save() - eft_flowthrough = f'{eft_invoice.id}-{partner_disbursement.id}' + eft_flowthrough = f"{eft_invoice.id}-{partner_disbursement.id}" - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel(file_ref=file_ref, disbursement_status_code=DisbursementStatus.UPLOADED.value).save() ejv_file_id = ejv_file.id - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, - partner_code=partner_code, - payment_account_id=pay_account.id).save() + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + partner_code=partner_code, + payment_account_id=pay_account.id, + ).save() ejv_header_id = ejv_header.id EjvLinkModel( - link_id=invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() EjvLinkModel( - link_id=eft_invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=eft_invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() # Now upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) @@ -446,49 +524,51 @@ def test_successful_partner_reversal_ejv_reconciliations(session, app, client): # Now upload a feedback file and check the status. # Just create feedback file to mock the real feedback file. # Has legacy flow and PartnerDisbursements entries - feedback_content = f'GABG...........00000000{ejv_file_id}...\n' \ - f'..BH...0000.................................................................................' \ - f'.....................................................................CGI\n' \ - f'..JH...FI0000000{ejv_header_id}.........................000000000090.00.....................' \ - f'............................................................................................' \ - f'............................................................................................' \ - f'.........0000...............................................................................' \ - f'.......................................................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000120230529................................................' \ - f'...........000000000090.00C.................................................................' \ - f'..................................#{invoice_id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000220230529................................................' \ - f'...........000000000090.00D.................................................................' \ - f'..................................#{invoice_id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000120230529................................................' \ - f'...........000000000090.00C.................................................................' \ - f'...................................{eft_flowthrough} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header_id}0000220230529................................................' \ - f'...........000000000090.00D.................................................................' \ - f'...................................{eft_flowthrough} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..BT.......FI0000000{ejv_header_id}000000000000002000000000180.000000.......................' \ - f'............................................................................................' \ - f'...................................CGI' - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"GABG...........00000000{ejv_file_id}...\n" + f"..BH...0000................................................................................." + f".....................................................................CGI\n" + f"..JH...FI0000000{ejv_header_id}.........................000000000090.00....................." + f"............................................................................................" + f"............................................................................................" + f".........0000..............................................................................." + f".......................................................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000120230529................................................" + f"...........000000000090.00C................................................................." + f"..................................#{invoice_id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000220230529................................................" + f"...........000000000090.00D................................................................." + f"..................................#{invoice_id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000120230529................................................" + f"...........000000000090.00C................................................................." + f"...................................{eft_flowthrough} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header_id}0000220230529................................................" + f"...........000000000090.00D................................................................." + f"...................................{eft_flowthrough} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..BT.......FI0000000{ejv_header_id}000000000000002000000000180.000000......................." + f"............................................................................................" + f"...................................CGI" + ) + + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -508,47 +588,55 @@ def test_successful_payment_ejv_reconciliations(session, app, client): # 1. Create EJV payment accounts # 2. Create invoice and related records # 3. Create a feedback file and assert status - corp_type = 'BEN' - filing_type = 'BCINC' + corp_type = "BEN" + filing_type = "BCINC" # Find fee schedule which have service fees. fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type(corp_type, filing_type) # Create a service fee distribution code - service_fee_dist_code = factory_distribution(name='service fee', client='112', reps_centre='99999', - service_line='99999', - stob='9999', project_code='9999998') + service_fee_dist_code = factory_distribution( + name="service fee", + client="112", + reps_centre="99999", + service_line="99999", + stob="9999", + project_code="9999998", + ) service_fee_dist_code.save() - dist_code = DistributionCodeModel.find_by_active_for_fee_schedule( - fee_schedule.fee_schedule_id) + dist_code = DistributionCodeModel.find_by_active_for_fee_schedule(fee_schedule.fee_schedule_id) # Update fee dist code to match the requirement. - dist_code.client = '112' - dist_code.responsibility_centre = '22222' - dist_code.service_line = '33333' - dist_code.stob = '4444' - dist_code.project_code = '5555559' + dist_code.client = "112" + dist_code.responsibility_centre = "22222" + dist_code.service_line = "33333" + dist_code.stob = "4444" + dist_code.project_code = "5555559" dist_code.service_fee_distribution_code_id = service_fee_dist_code.distribution_code_id dist_code.save() # GA - jv_account_1 = factory_create_ejv_account(auth_account_id='1') - jv_account_2 = factory_create_ejv_account(auth_account_id='2') + jv_account_1 = factory_create_ejv_account(auth_account_id="1") + jv_account_2 = factory_create_ejv_account(auth_account_id="2") # GI - jv_account_3 = factory_create_ejv_account(auth_account_id='3', client='111') - jv_account_4 = factory_create_ejv_account(auth_account_id='4', client='111') + jv_account_3 = factory_create_ejv_account(auth_account_id="3", client="111") + jv_account_4 = factory_create_ejv_account(auth_account_id="4", client="111") # Now create JV records. # Create EJV File model - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - disbursement_status_code=DisbursementStatus.UPLOADED.value, - file_type=EjvFileType.PAYMENT.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + file_type=EjvFileType.PAYMENT.value, + ).save() ejv_file_id = ejv_file.id - feedback_content = f'GABG...........00000000{ejv_file_id}...\n' \ - f'..BH...0000.................................................................................' \ - f'.....................................................................CGI\n' + feedback_content = ( + f"GABG...........00000000{ejv_file_id}...\n" + f"..BH...0000................................................................................." + f".....................................................................CGI\n" + ) jv_accounts = [jv_account_1, jv_account_2, jv_account_3, jv_account_4] inv_ids = [] @@ -556,80 +644,97 @@ def test_successful_payment_ejv_reconciliations(session, app, client): inv_total_amount = 101.5 for jv_acc in jv_accounts: jv_account_ids.append(jv_acc.id) - inv = factory_invoice(payment_account=jv_acc, corp_type_code=corp_type, total=inv_total_amount, - status_code=InvoiceStatus.APPROVED.value, payment_method_code=None) + inv = factory_invoice( + payment_account=jv_acc, + corp_type_code=corp_type, + total=inv_total_amount, + status_code=InvoiceStatus.APPROVED.value, + payment_method_code=None, + ) factory_invoice_reference(inv.id, status_code=InvoiceReferenceStatus.ACTIVE.value) - line = factory_payment_line_item(invoice_id=inv.id, - fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=100, - total=100, - service_fees=1.5, - fee_dist_id=dist_code.distribution_code_id) + line = factory_payment_line_item( + invoice_id=inv.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=100, + total=100, + service_fees=1.5, + fee_dist_id=dist_code.distribution_code_id, + ) inv_ids.append(inv.id) - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, payment_account_id=jv_acc.id).save() - - EjvLinkModel(link_id=inv.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value - ).save() - inv_total = f'{inv.total:.2f}'.zfill(15) - pay_line_amount = f'{line.total:.2f}'.zfill(15) - service_fee_amount = f'{line.service_fees:.2f}'.zfill(15) + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + payment_account_id=jv_acc.id, + ).save() + + EjvLinkModel( + link_id=inv.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() + inv_total = f"{inv.total:.2f}".zfill(15) + pay_line_amount = f"{line.total:.2f}".zfill(15) + service_fee_amount = f"{line.service_fees:.2f}".zfill(15) # one JD has a shortened width (outside of spec). - jh_and_jd = f'..JH...FI0000000{ejv_header.id}.........................{inv_total}.....................' \ - f'............................................................................................' \ - f'............................................................................................' \ - f'.........0000...............................................................................' \ - f'.......................................................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000120230529................................................' \ - f'...........{pay_line_amount}D.................................................................' \ - f'...................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000220230529................................................' \ - f'...........{pay_line_amount}C.................................................................' \ - f'...................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000320230529...................................................' \ - f'........{service_fee_amount}D.................................................................' \ - f'...................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000420230529................................................' \ - f'...........{service_fee_amount}C..............................................................' \ - f'......................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' + jh_and_jd = ( + f"..JH...FI0000000{ejv_header.id}.........................{inv_total}....................." + f"............................................................................................" + f"............................................................................................" + f".........0000..............................................................................." + f".......................................................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000120230529................................................" + f"...........{pay_line_amount}D................................................................." + f"...................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000220230529................................................" + f"...........{pay_line_amount}C................................................................." + f"...................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000320230529..................................................." + f"........{service_fee_amount}D................................................................." + f"...................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000420230529................................................" + f"...........{service_fee_amount}C.............................................................." + f"......................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + ) feedback_content = feedback_content + jh_and_jd - feedback_content = feedback_content + f'..BT.......FI0000000{ejv_header.id}000000000000002{inv_total}0000.......' \ - f'.........................................................................' \ - f'......................................................................CGI' - ack_file_name = f'ACK.{file_ref}' + feedback_content = ( + feedback_content + f"..BT.......FI0000000{ejv_header.id}000000000000002{inv_total}0000......." + f"........................................................................." + f"......................................................................CGI" + ) + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() # Now upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) ejv_file = EjvFileModel.find_by_id(ejv_file_id) - feedback_file_name = f'FEEDBACK.{file_ref}' + feedback_file_name = f"FEEDBACK.{file_ref}" - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -653,9 +758,12 @@ def test_successful_payment_ejv_reconciliations(session, app, client): # Assert payment records for jv_account_id in jv_account_ids: account = PaymentAccountModel.find_by_id(jv_account_id) - payment = PaymentModel.search_account_payments(auth_account_id=account.auth_account_id, - payment_status=PaymentStatus.COMPLETED.value, - page=1, limit=100)[0] + payment = PaymentModel.search_account_payments( + auth_account_id=account.auth_account_id, + payment_status=PaymentStatus.COMPLETED.value, + page=1, + limit=100, + )[0] assert len(payment) == 1 assert payment[0][0].paid_amount == inv_total_amount @@ -665,51 +773,59 @@ def test_successful_payment_reversal_ejv_reconciliations(session, app, client): # 1. Create EJV payment accounts # 2. Create invoice and related records # 3. Create a feedback file and assert status - corp_type = 'CP' - filing_type = 'OTFDR' + corp_type = "CP" + filing_type = "OTFDR" InvoiceModel.query.delete() # Reset the sequence, because the unit test is only dealing with 1 character for the invoice id. # This becomes more apparent when running unit tests in parallel. - db.session.execute(text('ALTER SEQUENCE invoices_id_seq RESTART WITH 1')) + db.session.execute(text("ALTER SEQUENCE invoices_id_seq RESTART WITH 1")) db.session.commit() # Find fee schedule which have service fees. fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type(corp_type, filing_type) # Create a service fee distribution code - service_fee_dist_code = factory_distribution(name='service fee', client='112', reps_centre='99999', - service_line='99999', - stob='9999', project_code='9999999') + service_fee_dist_code = factory_distribution( + name="service fee", + client="112", + reps_centre="99999", + service_line="99999", + stob="9999", + project_code="9999999", + ) service_fee_dist_code.save() - dist_code = DistributionCodeModel.find_by_active_for_fee_schedule( - fee_schedule.fee_schedule_id) + dist_code = DistributionCodeModel.find_by_active_for_fee_schedule(fee_schedule.fee_schedule_id) # Update fee dist code to match the requirement. - dist_code.client = '112' - dist_code.responsibility_centre = '22222' - dist_code.service_line = '33333' - dist_code.stob = '4444' - dist_code.project_code = '5555557' + dist_code.client = "112" + dist_code.responsibility_centre = "22222" + dist_code.service_line = "33333" + dist_code.stob = "4444" + dist_code.project_code = "5555557" dist_code.service_fee_distribution_code_id = service_fee_dist_code.distribution_code_id dist_code.save() # GA - jv_account_1 = factory_create_ejv_account(auth_account_id='1') + jv_account_1 = factory_create_ejv_account(auth_account_id="1") # GI - jv_account_3 = factory_create_ejv_account(auth_account_id='3', client='111') + jv_account_3 = factory_create_ejv_account(auth_account_id="3", client="111") # Now create JV records. # Create EJV File model - file_ref = f'INBOX.{datetime.now(tz=timezone.utc)}' - ejv_file = EjvFileModel(file_ref=file_ref, - disbursement_status_code=DisbursementStatus.UPLOADED.value, - file_type=EjvFileType.PAYMENT.value).save() + file_ref = f"INBOX.{datetime.now(tz=timezone.utc)}" + ejv_file = EjvFileModel( + file_ref=file_ref, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + file_type=EjvFileType.PAYMENT.value, + ).save() ejv_file_id = ejv_file.id - feedback_content = f'GABG...........00000000{ejv_file_id}...\n' \ - f'..BH...0000.................................................................................' \ - f'.....................................................................CGI\n' + feedback_content = ( + f"GABG...........00000000{ejv_file_id}...\n" + f"..BH...0000................................................................................." + f".....................................................................CGI\n" + ) jv_accounts = [jv_account_1, jv_account_3] inv_ids = [] @@ -717,80 +833,95 @@ def test_successful_payment_reversal_ejv_reconciliations(session, app, client): inv_total_amount = 101.5 for jv_acc in jv_accounts: jv_account_ids.append(jv_acc.id) - inv = factory_invoice(payment_account=jv_acc, corp_type_code=corp_type, total=inv_total_amount, - status_code=InvoiceStatus.REFUND_REQUESTED.value, payment_method_code=None - ) + inv = factory_invoice( + payment_account=jv_acc, + corp_type_code=corp_type, + total=inv_total_amount, + status_code=InvoiceStatus.REFUND_REQUESTED.value, + payment_method_code=None, + ) factory_invoice_reference(inv.id, status_code=InvoiceReferenceStatus.ACTIVE.value) - line = factory_payment_line_item(invoice_id=inv.id, - fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=100, - total=100, - service_fees=1.5, - fee_dist_id=dist_code.distribution_code_id) + line = factory_payment_line_item( + invoice_id=inv.id, + fee_schedule_id=fee_schedule.fee_schedule_id, + filing_fees=100, + total=100, + service_fees=1.5, + fee_dist_id=dist_code.distribution_code_id, + ) inv_ids.append(inv.id) - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, payment_account_id=jv_acc.id).save() + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + payment_account_id=jv_acc.id, + ).save() EjvLinkModel( - link_id=inv.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=inv.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() - inv_total = f'{inv.total:.2f}'.zfill(15) - pay_line_amount = f'{line.total:.2f}'.zfill(15) - service_fee_amount = f'{line.service_fees:.2f}'.zfill(15) - jh_and_jd = f'..JH...FI0000000{ejv_header.id}.........................{inv_total}.....................' \ - f'............................................................................................' \ - f'............................................................................................' \ - f'.........0000...............................................................................' \ - f'.......................................................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000120230529................................................' \ - f'...........{pay_line_amount}C.................................................................' \ - f'...................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000220230529................................................' \ - f'...........{pay_line_amount}D.................................................................' \ - f'...................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000320230529...................................................' \ - f'........{service_fee_amount}C.................................................................' \ - f'...................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' \ - f'..JD...FI0000000{ejv_header.id}0000420230529................................................' \ - f'...........{service_fee_amount}D..............................................................' \ - f'......................................{inv.id} ' \ - f' 0000........................' \ - f'............................................................................................' \ - f'..................................CGI\n' + inv_total = f"{inv.total:.2f}".zfill(15) + pay_line_amount = f"{line.total:.2f}".zfill(15) + service_fee_amount = f"{line.service_fees:.2f}".zfill(15) + jh_and_jd = ( + f"..JH...FI0000000{ejv_header.id}.........................{inv_total}....................." + f"............................................................................................" + f"............................................................................................" + f".........0000..............................................................................." + f".......................................................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000120230529................................................" + f"...........{pay_line_amount}C................................................................." + f"...................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000220230529................................................" + f"...........{pay_line_amount}D................................................................." + f"...................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000320230529..................................................." + f"........{service_fee_amount}C................................................................." + f"...................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + f"..JD...FI0000000{ejv_header.id}0000420230529................................................" + f"...........{service_fee_amount}D.............................................................." + f"......................................{inv.id} " + f" 0000........................" + f"............................................................................................" + f"..................................CGI\n" + ) feedback_content = feedback_content + jh_and_jd - feedback_content = feedback_content + f'..BT.......FI0000000{ejv_header.id}000000000000002{inv_total}0000.......' \ - f'.........................................................................' \ - f'......................................................................CGI' - ack_file_name = f'ACK.{file_ref}' + feedback_content = ( + feedback_content + f"..BT.......FI0000000{ejv_header.id}000000000000002{inv_total}0000......." + f"........................................................................." + f"......................................................................CGI" + ) + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) ejv_file = EjvFileModel.find_by_id(ejv_file_id) - feedback_file_name = f'FEEDBACK.{file_ref}' + feedback_file_name = f"FEEDBACK.{file_ref}" - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -812,9 +943,12 @@ def test_successful_payment_reversal_ejv_reconciliations(session, app, client): # Assert payment records for jv_account_id in jv_account_ids: account = PaymentAccountModel.find_by_id(jv_account_id) - payment = PaymentModel.search_account_payments(auth_account_id=account.auth_account_id, - payment_status=PaymentStatus.COMPLETED.value, - page=1, limit=100)[0] + payment = PaymentModel.search_account_payments( + auth_account_id=account.auth_account_id, + payment_status=PaymentStatus.COMPLETED.value, + page=1, + limit=100, + )[0] assert len(payment) == 1 assert payment[0][0].paid_amount == inv_total_amount @@ -825,7 +959,7 @@ def test_successful_refund_reconciliations(session, app, client): # 2. Mark the routing slip for refund. # 3. Create a AP reconciliation file. # 4. Assert the status. - rs_numbers = ('TEST00001', 'TEST00002') + rs_numbers = ("TEST00001", "TEST00002") for rs_number in rs_numbers: factory_routing_slip_account( number=rs_number, @@ -833,39 +967,44 @@ def test_successful_refund_reconciliations(session, app, client): total=100, remaining_amount=50, routing_slip_status=RoutingSlipStatus.REFUND_AUTHORIZED.value, - refund_amount=50) + refund_amount=50, + ) routing_slip = RoutingSlipModel.find_by_number(rs_number) - factory_refund(routing_slip_id=routing_slip.id, - details={ - 'name': 'TEST', - 'mailingAddress': { - 'city': 'Victoria', - 'region': 'BC', - 'street': '655 Douglas St', - 'country': 'CA', - 'postalCode': 'V8V 0B6', - 'streetAdditional': '' - } - }) + factory_refund( + routing_slip_id=routing_slip.id, + details={ + "name": "TEST", + "mailingAddress": { + "city": "Victoria", + "region": "BC", + "street": "655 Douglas St", + "country": "CA", + "postalCode": "V8V 0B6", + "streetAdditional": "", + }, + }, + ) routing_slip.status = RoutingSlipStatus.REFUND_UPLOADED.value # Now create AP records. # Create EJV File model - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - file_type=EjvFileType.REFUND.value, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + file_type=EjvFileType.REFUND.value, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() ejv_file_id = ejv_file.id # Upload an acknowledgement file - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() # Now upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) @@ -873,71 +1012,72 @@ def test_successful_refund_reconciliations(session, app, client): # Now upload a feedback file and check the status. # Just create feedback file to mock the real feedback file. - feedback_content = f'APBG...........00000000{ejv_file_id}....\n' \ - f'APBH...0000.................................................................................' \ - f'.....................................................................CGI\n' \ - f'APIH...000000000...{rs_numbers[0]} ................' \ - f'...........................................................................................' \ - f'........................................................................................REF' \ - f'UND_FAS....................................................................................' \ - f'........................................................0000...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{rs_numbers[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0000..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{rs_numbers[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0000..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{rs_numbers[0]} ................' \ - f'............................0000...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APIH...000000000...{rs_numbers[1]} ................' \ - f'...........................................................................................' \ - f'........................................................................................REF' \ - f'UND_FAS....................................................................................' \ - f'........................................................0000...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{rs_numbers[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0000..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{rs_numbers[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0000..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{rs_numbers[1]} ................' \ - f'............................0000...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APBT...........00000000{ejv_file_id}..............................0000.....................' \ - f'...........................................................................................' \ - f'......................................CGI\n' \ - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"APBG...........00000000{ejv_file_id}....\n" + f"APBH...0000................................................................................." + f".....................................................................CGI\n" + f"APIH...000000000...{rs_numbers[0]} ................" + f"..........................................................................................." + f"........................................................................................REF" + f"UND_FAS...................................................................................." + f"........................................................0000..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{rs_numbers[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0000.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{rs_numbers[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0000.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{rs_numbers[0]} ................" + f"............................0000..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APIH...000000000...{rs_numbers[1]} ................" + f"..........................................................................................." + f"........................................................................................REF" + f"UND_FAS...................................................................................." + f"........................................................0000..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{rs_numbers[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0000.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{rs_numbers[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0000.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{rs_numbers[1]} ................" + f"............................0000..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APBT...........00000000{ejv_file_id}..............................0000....................." + f"..........................................................................................." + f"......................................CGI\n" + ) + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -956,7 +1096,7 @@ def test_failed_refund_reconciliations(session, app, client): # 2. Mark the routing slip for refund. # 3. Create a AP reconciliation file. # 4. Assert the status. - rs_numbers = ('TEST00001', 'TEST00002') + rs_numbers = ("TEST00001", "TEST00002") for rs_number in rs_numbers: factory_routing_slip_account( number=rs_number, @@ -964,39 +1104,44 @@ def test_failed_refund_reconciliations(session, app, client): total=100, remaining_amount=50, routing_slip_status=RoutingSlipStatus.REFUND_AUTHORIZED.value, - refund_amount=50) + refund_amount=50, + ) routing_slip = RoutingSlipModel.find_by_number(rs_number) - factory_refund(routing_slip_id=routing_slip.id, - details={ - 'name': 'TEST', - 'mailingAddress': { - 'city': 'Victoria', - 'region': 'BC', - 'street': '655 Douglas St', - 'country': 'CA', - 'postalCode': 'V8V 0B6', - 'streetAdditional': '' - } - }) + factory_refund( + routing_slip_id=routing_slip.id, + details={ + "name": "TEST", + "mailingAddress": { + "city": "Victoria", + "region": "BC", + "street": "655 Douglas St", + "country": "CA", + "postalCode": "V8V 0B6", + "streetAdditional": "", + }, + }, + ) routing_slip.status = RoutingSlipStatus.REFUND_UPLOADED.value # Now create AP records. # Create EJV File model - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - file_type=EjvFileType.REFUND.value, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + file_type=EjvFileType.REFUND.value, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() ejv_file_id = ejv_file.id # Upload an acknowledgement file - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() # Now upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) @@ -1005,71 +1150,72 @@ def test_failed_refund_reconciliations(session, app, client): # Now upload a feedback file and check the status. # Just create feedback file to mock the real feedback file. # Set first routing slip to be success and second to ve failed - feedback_content = f'APBG...........00000000{ejv_file_id}....\n' \ - f'APBH...0000.................................................................................' \ - f'.....................................................................CGI\n' \ - f'APIH...000000000...{rs_numbers[0]} ................' \ - f'...........................................................................................' \ - f'........................................................................................REF' \ - f'UND_FAS....................................................................................' \ - f'........................................................0000...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{rs_numbers[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0000..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{rs_numbers[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0000..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{rs_numbers[0]} ................' \ - f'............................0000...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APIH...000000000...{rs_numbers[1]} ................' \ - f'...........................................................................................' \ - f'........................................................................................REF' \ - f'UND_FAS....................................................................................' \ - f'........................................................0001...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{rs_numbers[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0001..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{rs_numbers[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0001..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{rs_numbers[1]} ................' \ - f'............................0001...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APBT...........00000000{ejv_file_id}..............................0000.....................' \ - f'...........................................................................................' \ - f'......................................CGI\n' \ - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"APBG...........00000000{ejv_file_id}....\n" + f"APBH...0000................................................................................." + f".....................................................................CGI\n" + f"APIH...000000000...{rs_numbers[0]} ................" + f"..........................................................................................." + f"........................................................................................REF" + f"UND_FAS...................................................................................." + f"........................................................0000..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{rs_numbers[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0000.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{rs_numbers[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0000.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{rs_numbers[0]} ................" + f"............................0000..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APIH...000000000...{rs_numbers[1]} ................" + f"..........................................................................................." + f"........................................................................................REF" + f"UND_FAS...................................................................................." + f"........................................................0001..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{rs_numbers[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0001.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{rs_numbers[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0001.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{rs_numbers[1]} ................" + f"............................0001..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APBT...........00000000{ejv_file_id}..............................0000....................." + f"..........................................................................................." + f"......................................CGI\n" + ) + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -1089,7 +1235,7 @@ def test_successful_eft_refund_reconciliations(session, app, client): # 1. Create EFT refund. # 2. Create a AP reconciliation file. # 3. Assert the status. - eft_short_name_names = ('TEST00001', 'TEST00002') + eft_short_name_names = ("TEST00001", "TEST00002") eft_refund_ids = [] for eft_short_name_name in eft_short_name_names: factory_create_eft_shortname(short_name=eft_short_name_name) @@ -1097,88 +1243,93 @@ def test_successful_eft_refund_reconciliations(session, app, client): eft_refund = factory_create_eft_refund( disbursement_status_code=DisbursementStatus.ACKNOWLEDGED.value, refund_amount=100, - refund_email='test@test.com', + refund_email="test@test.com", short_name_id=eft_short_name.id, status=EFTShortnameRefundStatus.APPROVED.value, - comment=eft_short_name.short_name) + comment=eft_short_name.short_name, + ) eft_refund_ids.append(str(eft_refund.id).zfill(9)) # Now create AP records. # Create EJV File model - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, file_type=EjvFileType.EFT_REFUND.value, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + file_type=EjvFileType.EFT_REFUND.value, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() ejv_file_id = ejv_file.id # Upload an acknowledgement file - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+") as jv_file: + jv_file.write("") jv_file.close() # Upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) ejv_file = EjvFileModel.find_by_id(ejv_file_id) # Create and upload a feedback file and check the status. - feedback_content = f'APBG...........00000000{ejv_file_id}....\n' \ - f'APBH...0000................................................................................' \ - f'......................................................................CGI\n' \ - f'APIH...000000000...{eft_refund_ids[0]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.REFUND_EFT................................................................................' \ - f'............................................................0000...........................' \ - f'...........................................................................................' \ - f'................................CGI\n' \ - f'APIL...............{eft_refund_ids[0]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................................................................' \ - f'0000.......................................................................................' \ - f'...............................................................CGI\n' \ - f'APIC...............{eft_short_name_names[0]} ......' \ - f'......................................0000.................................................' \ - f'...........................................................................................' \ - f'..........CGI\n' \ - f'APIH...000000000...{eft_refund_ids[1]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.REFUND_EFT................................................................................' \ - f'............................................................0000...........................' \ - f'...........................................................................................' \ - f'................................CGI\n' \ - f'APIL...............{eft_refund_ids[1]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................................................................' \ - f'0000.......................................................................................' \ - f'...............................................................CGI\n' \ - f'APIC...............{eft_short_name_names[1]} ......' \ - f'......................................0000.................................................' \ - f'...........................................................................................' \ - f'..........CGI\n' \ - f'APBT...........00000000{ejv_file_id}..............................0000.....................' \ - f'...........................................................................................' \ - f'......................................CGI\n' \ - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"APBG...........00000000{ejv_file_id}....\n" + f"APBH...0000................................................................................" + f"......................................................................CGI\n" + f"APIH...000000000...{eft_refund_ids[0]} ............" + f"..........................................................................................." + f"..........................................................................................." + f".REFUND_EFT................................................................................" + f"............................................................0000..........................." + f"..........................................................................................." + f"................................CGI\n" + f"APIL...............{eft_refund_ids[0]} ............" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................................................." + f"0000......................................................................................." + f"...............................................................CGI\n" + f"APIC...............{eft_short_name_names[0]} ......" + f"......................................0000................................................." + f"..........................................................................................." + f"..........CGI\n" + f"APIH...000000000...{eft_refund_ids[1]} ............" + f"..........................................................................................." + f"..........................................................................................." + f".REFUND_EFT................................................................................" + f"............................................................0000..........................." + f"..........................................................................................." + f"................................CGI\n" + f"APIL...............{eft_refund_ids[1]} ............" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................................................." + f"0000......................................................................................." + f"...............................................................CGI\n" + f"APIC...............{eft_short_name_names[1]} ......" + f"......................................0000................................................." + f"..........................................................................................." + f"..........CGI\n" + f"APBT...........00000000{ejv_file_id}..............................0000....................." + f"..........................................................................................." + f"......................................CGI\n" + ) + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -1198,7 +1349,7 @@ def test_failed_eft_refund_reconciliations(session, app, client): # 1. Create EFT refund. # 2. Create a AP reconciliation file. # 3. Assert the status. - eft_short_name_names = ('TEST00001', 'TEST00002') + eft_short_name_names = ("TEST00001", "TEST00002") eft_refund_ids = [] for eft_short_name_name in eft_short_name_names: factory_create_eft_shortname(short_name=eft_short_name_name) @@ -1206,88 +1357,93 @@ def test_failed_eft_refund_reconciliations(session, app, client): eft_refund = factory_create_eft_refund( disbursement_status_code=DisbursementStatus.ACKNOWLEDGED.value, refund_amount=100, - refund_email='test@test.com', + refund_email="test@test.com", short_name_id=eft_short_name.id, status=EFTShortnameRefundStatus.APPROVED.value, - comment=eft_short_name.short_name) + comment=eft_short_name.short_name, + ) eft_refund_ids.append(str(eft_refund.id).zfill(9)) # Now create AP records. # Create EJV File model - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, file_type=EjvFileType.EFT_REFUND.value, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + file_type=EjvFileType.EFT_REFUND.value, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() ejv_file_id = ejv_file.id # Upload an acknowledgement file - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+") as jv_file: + jv_file.write("") jv_file.close() # Now upload the ACK file to minio and publish message. - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) ejv_file = EjvFileModel.find_by_id(ejv_file_id) # Create and upload a feedback file and check the status. - feedback_content = f'APBG...........00000000{ejv_file_id}....\n' \ - f'APBH...0000................................................................................' \ - f'......................................................................CGI\n' \ - f'APIH...000000000...{eft_refund_ids[0]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.REFUND_EFT................................................................................' \ - f'............................................................0000...........................' \ - f'...........................................................................................' \ - f'................................CGI\n' \ - f'APIL...............{eft_refund_ids[0]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................................................................' \ - f'0000.......................................................................................' \ - f'...............................................................CGI\n' \ - f'APIC...............{eft_short_name_names[0]} ......' \ - f'......................................0000.................................................' \ - f'...........................................................................................' \ - f'..........CGI\n' \ - f'APIH...000000000...{eft_refund_ids[1]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.REFUND_EFT................................................................................' \ - f'............................................................0001...........................' \ - f'...........................................................................................' \ - f'................................CGI\n' \ - f'APIL...............{eft_refund_ids[1]} ............' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................................................................' \ - f'0001.......................................................................................' \ - f'...............................................................CGI\n' \ - f'APIC...............{eft_short_name_names[1]} ......' \ - f'......................................0001.................................................' \ - f'...........................................................................................' \ - f'..........CGI\n' \ - f'APBT...........00000000{ejv_file_id}..............................0000.....................' \ - f'...........................................................................................' \ - f'......................................CGI\n' \ - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"APBG...........00000000{ejv_file_id}....\n" + f"APBH...0000................................................................................" + f"......................................................................CGI\n" + f"APIH...000000000...{eft_refund_ids[0]} ............" + f"..........................................................................................." + f"..........................................................................................." + f".REFUND_EFT................................................................................" + f"............................................................0000..........................." + f"..........................................................................................." + f"................................CGI\n" + f"APIL...............{eft_refund_ids[0]} ............" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................................................." + f"0000......................................................................................." + f"...............................................................CGI\n" + f"APIC...............{eft_short_name_names[0]} ......" + f"......................................0000................................................." + f"..........................................................................................." + f"..........CGI\n" + f"APIH...000000000...{eft_refund_ids[1]} ............" + f"..........................................................................................." + f"..........................................................................................." + f".REFUND_EFT................................................................................" + f"............................................................0001..........................." + f"..........................................................................................." + f"................................CGI\n" + f"APIL...............{eft_refund_ids[1]} ............" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................................................." + f"0001......................................................................................." + f"...............................................................CGI\n" + f"APIC...............{eft_short_name_names[1]} ......" + f"......................................0001................................................." + f"..........................................................................................." + f"..........CGI\n" + f"APBT...........00000000{ejv_file_id}..............................0000....................." + f"..........................................................................................." + f"......................................CGI\n" + ) + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() # Now upload the ACK file to minio and publish message. - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -1311,16 +1467,16 @@ def test_successful_ap_disbursement(session, app, client): # 2. Create a AP reconciliation file. # 3. Assert the status. invoice_ids = [] - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) invoice = factory_invoice( payment_account=account, status_code=InvoiceStatus.PAID.value, total=10, - corp_type_code='BCA' + corp_type_code="BCA", ) invoice_ids.append(invoice.id) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('BCA', 'OLAARTOQ') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("BCA", "OLAARTOQ") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -1329,7 +1485,7 @@ def test_successful_ap_disbursement(session, app, client): status_code=InvoiceStatus.REFUNDED.value, total=10, disbursement_status_code=DisbursementStatus.COMPLETED.value, - corp_type_code='BCA' + corp_type_code="BCA", ) invoice_ids.append(refund_invoice.id) @@ -1338,103 +1494,113 @@ def test_successful_ap_disbursement(session, app, client): factory_refund(invoice_id=refund_invoice.id) - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - file_type=EjvFileType.NON_GOV_DISBURSEMENT.value, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + file_type=EjvFileType.NON_GOV_DISBURSEMENT.value, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() ejv_file_id = ejv_file.id - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, payment_account_id=account.id).save() + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + payment_account_id=account.id, + ).save() EjvLinkModel( - link_id=invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() EjvLinkModel( - link_id=refund_invoice.id, link_type=EJVLinkType.INVOICE.value, ejv_header_id=ejv_header.id, - disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=refund_invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) ejv_file = EjvFileModel.find_by_id(ejv_file_id) invoice_str = [str(invoice_id).zfill(9) for invoice_id in invoice_ids] - feedback_content = f'APBG...........{str(ejv_file_id).zfill(9)}....\n' \ - f'APBH...0000.................................................................................' \ - f'.....................................................................CGI\n' \ - f'APIH...000000000...{invoice_str[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'........................................................0000...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{invoice_str[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0000..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{invoice_str[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0000..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{invoice_str[0]} ................' \ - f'............................0000...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APIH...000000000...{invoice_str[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'........................................................0000...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{invoice_str[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0000..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{invoice_str[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0000..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{invoice_str[1]} ................' \ - f'............................0000...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APBT...........00000000{ejv_file_id}..............................0000.....................' \ - f'...........................................................................................' \ - f'......................................CGI\n' \ - - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"APBG...........{str(ejv_file_id).zfill(9)}....\n" + f"APBH...0000................................................................................." + f".....................................................................CGI\n" + f"APIH...000000000...{invoice_str[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................0000..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{invoice_str[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0000.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{invoice_str[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0000.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{invoice_str[0]} ................" + f"............................0000..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APIH...000000000...{invoice_str[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................0000..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{invoice_str[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0000.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{invoice_str[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0000.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{invoice_str[1]} ................" + f"............................0000..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APBT...........00000000{ejv_file_id}..............................0000....................." + f"..........................................................................................." + f"......................................CGI\n" + ) + + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -1459,15 +1625,15 @@ def test_failure_ap_disbursement(session, app, client): # 2. Create a AP reconciliation file. # 3. Assert the status. invoice_ids = [] - account = factory_create_pad_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value) + account = factory_create_pad_account(auth_account_id="1", status=CfsAccountStatus.ACTIVE.value) invoice = factory_invoice( payment_account=account, status_code=InvoiceStatus.PAID.value, total=10, - corp_type_code='BCA' + corp_type_code="BCA", ) invoice_ids.append(invoice.id) - fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('BCA', 'OLAARTOQ') + fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type("BCA", "OLAARTOQ") line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) line.save() @@ -1476,7 +1642,7 @@ def test_failure_ap_disbursement(session, app, client): status_code=InvoiceStatus.REFUNDED.value, total=10, disbursement_status_code=DisbursementStatus.COMPLETED.value, - corp_type_code='BCA' + corp_type_code="BCA", ) invoice_ids.append(refund_invoice.id) line = factory_payment_line_item(refund_invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id) @@ -1484,32 +1650,41 @@ def test_failure_ap_disbursement(session, app, client): factory_refund(invoice_id=refund_invoice.id) - file_ref = f'INBOX.{datetime.now()}' - ejv_file = EjvFileModel(file_ref=file_ref, - file_type=EjvFileType.NON_GOV_DISBURSEMENT.value, - disbursement_status_code=DisbursementStatus.UPLOADED.value).save() + file_ref = f"INBOX.{datetime.now()}" + ejv_file = EjvFileModel( + file_ref=file_ref, + file_type=EjvFileType.NON_GOV_DISBURSEMENT.value, + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ).save() ejv_file_id = ejv_file.id - ejv_header = EjvHeaderModel(disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file.id, payment_account_id=account.id).save() + ejv_header = EjvHeaderModel( + disbursement_status_code=DisbursementStatus.UPLOADED.value, + ejv_file_id=ejv_file.id, + payment_account_id=account.id, + ).save() EjvLinkModel( - link_id=invoice.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header.id, disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() EjvLinkModel( - link_id=refund_invoice.id, link_type=EJVLinkType.INVOICE.value, ejv_header_id=ejv_header.id, - disbursement_status_code=DisbursementStatus.UPLOADED.value + link_id=refund_invoice.id, + link_type=EJVLinkType.INVOICE.value, + ejv_header_id=ejv_header.id, + disbursement_status_code=DisbursementStatus.UPLOADED.value, ).save() - ack_file_name = f'ACK.{file_ref}' + ack_file_name = f"ACK.{file_ref}" - with open(ack_file_name, 'a+', encoding='utf-8') as jv_file: - jv_file.write('') + with open(ack_file_name, "a+", encoding="utf-8") as jv_file: + jv_file.write("") jv_file.close() - upload_to_minio(str.encode(''), ack_file_name) + upload_to_minio(str.encode(""), ack_file_name) add_file_event_to_queue_and_process(client, ack_file_name, QueueMessageTypes.CGI_ACK_MESSAGE_TYPE.value) @@ -1520,70 +1695,71 @@ def test_failure_ap_disbursement(session, app, client): # Now upload a feedback file and check the status. # Just create feedback file to mock the real feedback file. # Set first invoice to be success and second to be failed - feedback_content = f'APBG...........{str(ejv_file_id).zfill(9)}....\n' \ - f'APBH...0000.................................................................................' \ - f'.....................................................................CGI\n' \ - f'APIH...000000000...{invoice_str[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'........................................................0000...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{invoice_str[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0000..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{invoice_str[0]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0000..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{invoice_str[0]} ................' \ - f'............................0000...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APIH...000000000...{invoice_str[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'........................................................0001...............................' \ - f'...........................................................................................' \ - f'............................CGI\n' \ - f'APNA...............{invoice_str[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.........................................0001..............................................' \ - f'...........................................................................................' \ - f'.............CGI\n' \ - f'APIL...............{invoice_str[1]} ................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'...........................................................................................' \ - f'.....................................................................................0001..' \ - f'...........................................................................................' \ - f'.........................................................CGI\n' \ - f'APIC...............{invoice_str[1]} ................' \ - f'............................0001...........................................................' \ - f'........................................................................................' \ - f'...CGI\n' \ - f'APBT...........00000000{ejv_file_id}..............................0000.....................' \ - f'...........................................................................................' \ - f'......................................CGI\n' \ - - feedback_file_name = f'FEEDBACK.{file_ref}' - - with open(feedback_file_name, 'a+', encoding='utf-8') as jv_file: + feedback_content = ( + f"APBG...........{str(ejv_file_id).zfill(9)}....\n" + f"APBH...0000................................................................................." + f".....................................................................CGI\n" + f"APIH...000000000...{invoice_str[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................0000..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{invoice_str[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0000.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{invoice_str[0]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0000.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{invoice_str[0]} ................" + f"............................0000..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APIH...000000000...{invoice_str[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"........................................................0001..............................." + f"..........................................................................................." + f"............................CGI\n" + f"APNA...............{invoice_str[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f".........................................0001.............................................." + f"..........................................................................................." + f".............CGI\n" + f"APIL...............{invoice_str[1]} ................" + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f"..........................................................................................." + f".....................................................................................0001.." + f"..........................................................................................." + f".........................................................CGI\n" + f"APIC...............{invoice_str[1]} ................" + f"............................0001..........................................................." + f"........................................................................................" + f"...CGI\n" + f"APBT...........00000000{ejv_file_id}..............................0000....................." + f"..........................................................................................." + f"......................................CGI\n" + ) + feedback_file_name = f"FEEDBACK.{file_ref}" + + with open(feedback_file_name, "a+", encoding="utf-8") as jv_file: jv_file.write(feedback_content) jv_file.close() - with open(feedback_file_name, 'rb') as f: + with open(feedback_file_name, "rb") as f: upload_to_minio(f.read(), feedback_file_name) add_file_event_to_queue_and_process(client, feedback_file_name, QueueMessageTypes.CGI_FEEDBACK_MESSAGE_TYPE.value) @@ -1593,14 +1769,10 @@ def test_failure_ap_disbursement(session, app, client): invoice_1 = InvoiceModel.find_by_id(invoice_ids[0]) assert invoice_1.disbursement_status_code == DisbursementStatus.COMPLETED.value assert invoice_1.disbursement_date is not None - invoice_link = db.session.query(EjvLinkModel)\ - .filter(EjvLinkModel.link_id == invoice_ids[0])\ - .one_or_none() + invoice_link = db.session.query(EjvLinkModel).filter(EjvLinkModel.link_id == invoice_ids[0]).one_or_none() assert invoice_link.disbursement_status_code == DisbursementStatus.COMPLETED.value invoice_2 = InvoiceModel.find_by_id(invoice_ids[1]) assert invoice_2.disbursement_status_code == DisbursementStatus.ERRORED.value - invoice_link = db.session.query(EjvLinkModel)\ - .filter(EjvLinkModel.link_id == invoice_ids[1])\ - .one_or_none() + invoice_link = db.session.query(EjvLinkModel).filter(EjvLinkModel.link_id == invoice_ids[1]).one_or_none() assert invoice_link.disbursement_status_code == DisbursementStatus.ERRORED.value diff --git a/pay-queue/tests/integration/test_eft_reconciliation.py b/pay-queue/tests/integration/test_eft_reconciliation.py index a76ae0b8f..c6f18e0f3 100644 --- a/pay-queue/tests/integration/test_eft_reconciliation.py +++ b/pay-queue/tests/integration/test_eft_reconciliation.py @@ -31,34 +31,52 @@ from pay_api.models import PaymentAccount as PaymentAccountModel from pay_api.services import EFTShortNamesService from pay_api.utils.enums import ( - EFTCreditInvoiceStatus, EFTFileLineType, EFTHistoricalTypes, EFTProcessStatus, EFTShortnameStatus, EFTShortnameType, - InvoiceStatus, PaymentMethod, StatementFrequency) + EFTCreditInvoiceStatus, + EFTFileLineType, + EFTHistoricalTypes, + EFTProcessStatus, + EFTShortnameStatus, + EFTShortnameType, + InvoiceStatus, + PaymentMethod, + StatementFrequency, +) from sbc_common_components.utils.enums import QueueMessageTypes from pay_queue.services.eft import EFTRecord from pay_queue.services.eft.eft_enums import EFTConstants from tests.integration.factory import ( - factory_create_eft_account, factory_invoice, factory_statement, factory_statement_invoices, - factory_statement_settings) + factory_create_eft_account, + factory_invoice, + factory_statement, + factory_statement_invoices, + factory_statement_settings, +) from tests.integration.utils import add_file_event_to_queue_and_process, create_and_upload_eft_file from tests.utilities.factory_utils import factory_eft_header, factory_eft_record, factory_eft_trailer def test_eft_tdi17_fail_header(session, app, client, mocker): """Test EFT Reconciliations properly fails for a bad EFT header.""" - mock_send_email = mocker.patch('pay_queue.services.eft.eft_reconciliation.send_error_email') + mock_send_email = mocker.patch("pay_queue.services.eft.eft_reconciliation.send_error_email") # Generate file with invalid header - file_name: str = 'test_eft_tdi17.txt' - header = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, file_creation_date='20230814', - file_creation_time='FAIL', deposit_start_date='20230810', deposit_end_date='20230810') + file_name: str = "test_eft_tdi17.txt" + header = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="FAIL", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) create_and_upload_eft_file(file_name, [header]) add_file_event_to_queue_and_process(client, file_name, QueueMessageTypes.EFT_FILE_UPLOADED.value) # Assert EFT File record was created - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.id is not None @@ -71,9 +89,12 @@ def test_eft_tdi17_fail_header(session, app, client, mocker): assert eft_file_model.number_of_details is None assert eft_file_model.total_deposit_cents is None - eft_header_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is not None assert eft_header_transaction.id is not None @@ -82,47 +103,63 @@ def test_eft_tdi17_fail_header(session, app, client, mocker): assert eft_header_transaction.status_code == EFTProcessStatus.FAILED.value assert eft_header_transaction.line_number == 0 assert len(eft_header_transaction.error_messages) == 1 - assert eft_header_transaction.error_messages[0] == 'Invalid header creation date time.' + assert eft_header_transaction.error_messages[0] == "Invalid header creation date time." - eft_trailer_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is None - eft_transactions: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert not bool(eft_transactions) mock_send_email.assert_called_once() call_args = mock_send_email.call_args[0] - expected_error = 'Failed to process file test_eft_tdi17.txt with an invalid header or trailer.' - actual_error = call_args[0].error_messages[0]['error'] + expected_error = "Failed to process file test_eft_tdi17.txt with an invalid header or trailer." + actual_error = call_args[0].error_messages[0]["error"] assert expected_error == actual_error def test_eft_tdi17_fail_trailer(session, app, client, mocker): """Test EFT Reconciliations properly fails for a bad EFT trailer.""" - mock_send_email = mocker.patch( - 'pay_queue.services.eft.eft_reconciliation.send_error_email') + mock_send_email = mocker.patch("pay_queue.services.eft.eft_reconciliation.send_error_email") # Generate file with invalid trailer - file_name: str = 'test_eft_tdi17.txt' - header = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, file_creation_date='20230814', - file_creation_time='1601', deposit_start_date='20230810', deposit_end_date='20230810') - trailer = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, number_of_details='A', - total_deposit_amount='3733750') + file_name: str = "test_eft_tdi17.txt" + header = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) + trailer = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="A", + total_deposit_amount="3733750", + ) create_and_upload_eft_file(file_name, [header, trailer]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) # Assert EFT File record was created - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.id is not None @@ -135,9 +172,12 @@ def test_eft_tdi17_fail_trailer(session, app, client, mocker): assert eft_file_model.number_of_details is None assert eft_file_model.total_deposit_cents == 3733750 - eft_trailer_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is not None assert eft_trailer_transaction.id is not None @@ -146,55 +186,83 @@ def test_eft_tdi17_fail_trailer(session, app, client, mocker): assert eft_trailer_transaction.status_code == EFTProcessStatus.FAILED.value assert eft_trailer_transaction.line_number == 1 assert len(eft_trailer_transaction.error_messages) == 1 - assert eft_trailer_transaction.error_messages[0] == 'Invalid trailer number of details value.' + assert eft_trailer_transaction.error_messages[0] == "Invalid trailer number of details value." - eft_header_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is None - eft_transactions: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert not bool(eft_transactions) mock_send_email.assert_called_once() call_args = mock_send_email.call_args[0] - expected_error = 'Failed to process file test_eft_tdi17.txt with an invalid header or trailer.' - actual_error = call_args[0].error_messages[0]['error'] + expected_error = "Failed to process file test_eft_tdi17.txt with an invalid header or trailer." + actual_error = call_args[0].error_messages[0]["error"] assert expected_error == actual_error def test_eft_tdi17_fail_transactions(session, app, client, mocker): """Test EFT Reconciliations properly fails for a bad EFT trailer.""" - mock_send_email = mocker.patch( - 'pay_queue.services.eft.eft_reconciliation.send_error_email') + mock_send_email = mocker.patch("pay_queue.services.eft.eft_reconciliation.send_error_email") # Generate file with invalid trailer - file_name: str = 'test_eft_tdi17.txt' - header = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, file_creation_date='20230814', - file_creation_time='1601', deposit_start_date='20230810', deposit_end_date='20230810') - trailer = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, number_of_details='1', - total_deposit_amount='3733750') - - transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='001', - transaction_description='ABC123', deposit_amount='13500', - currency='', exchange_adj_amount='0', deposit_amount_cad='FAIL', - destination_bank_number='0003', batch_number='002400986', jv_type='I', - jv_number='002425669', transaction_date='') + file_name: str = "test_eft_tdi17.txt" + header = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) + trailer = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="1", + total_deposit_amount="3733750", + ) + + transaction_1 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="ABC123", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="FAIL", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) create_and_upload_eft_file(file_name, [header, transaction_1, trailer]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) # Assert EFT File record was created - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.id is not None @@ -207,44 +275,56 @@ def test_eft_tdi17_fail_transactions(session, app, client, mocker): assert eft_file_model.number_of_details == 1 assert eft_file_model.total_deposit_cents == 3733750 - eft_trailer_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is None - eft_header_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is None - eft_transactions: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert eft_transactions is not None assert len(eft_transactions) == 1 - assert eft_transactions[0].error_messages[0] == 'Invalid transaction deposit amount CAD.' + assert eft_transactions[0].error_messages[0] == "Invalid transaction deposit amount CAD." mock_send_email.assert_called_once() call_args = mock_send_email.call_args[0] - assert 'Invalid transaction deposit amount CAD.' == call_args[0].error_messages[0]['error'] + assert "Invalid transaction deposit amount CAD." == call_args[0].error_messages[0]["error"] def test_eft_tdi17_basic_process(session, app, client): """Test EFT Reconciliations worker is able to create basic EFT processing records.""" # Generate happy path file - file_name: str = 'test_eft_tdi17.txt' + file_name: str = "test_eft_tdi17.txt" generate_basic_tdi17_file(file_name) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) # Assert EFT File record was created - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.id is not None @@ -260,22 +340,31 @@ def test_eft_tdi17_basic_process(session, app, client): assert eft_file_model.status_code == EFTProcessStatus.COMPLETED.value # Stored as part of the EFT File record - expecting none when no errors - eft_header_transaction = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is None # Stored as part of the EFT File record - expecting none when no errors - eft_trailer_transaction = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is None - eft_transactions: List[EFTTransactionModel] = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions: List[EFTTransactionModel] = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert eft_transactions is not None assert len(eft_transactions) == 2 @@ -291,9 +380,9 @@ def test_eft_tdi17_basic_process(session, app, client): short_name_link_2 = EFTShortnameLinksModel.find_by_short_name_id(eft_shortnames[1].id) assert not short_name_link_1 - assert eft_shortnames[0].short_name == 'ABC123' + assert eft_shortnames[0].short_name == "ABC123" assert not short_name_link_2 - assert eft_shortnames[1].short_name == 'DEF456' + assert eft_shortnames[1].short_name == "DEF456" eft_credits: List[EFTCreditModel] = db.session.query(EFTCreditModel).order_by(EFTCreditModel.id).all() assert eft_credits is not None @@ -318,8 +407,11 @@ def test_eft_tdi17_basic_process(session, app, client): assert_funds_received_history(eft_credits[1], history[1]) -def assert_funds_received_history(eft_credit: EFTCreditModel, eft_history: EFTHistoryModel, - assert_balance: bool = True): +def assert_funds_received_history( + eft_credit: EFTCreditModel, + eft_history: EFTHistoryModel, + assert_balance: bool = True, +): """Assert credit and history records match.""" assert eft_history.short_name_id == eft_credit.short_name_id assert eft_history.amount == eft_credit.amount @@ -339,16 +431,19 @@ def test_eft_tdi17_process(session, app, client): assert eft_shortname is not None assert invoice is not None # Generate happy path file - file_name: str = 'test_eft_tdi17.txt' + file_name: str = "test_eft_tdi17.txt" generate_tdi17_file(file_name) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) # Assert EFT File record was created - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.id is not None @@ -362,22 +457,31 @@ def test_eft_tdi17_process(session, app, client): assert eft_file_model.total_deposit_cents == 3733750 # Stored as part of the EFT File record - expecting none when no errors - eft_header_transaction = db.session.query(EFTTransactionModel)\ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is None # Stored as part of the EFT File record - expecting none when no errors - eft_trailer_transaction = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is None - eft_transactions = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert eft_transactions is not None assert len(eft_transactions) == 3 @@ -393,9 +497,9 @@ def test_eft_tdi17_process(session, app, client): assert len(eft_shortnames) == 2 assert short_name_link_1 assert short_name_link_1.auth_account_id == payment_account.auth_account_id - assert eft_shortnames[0].short_name == 'TESTSHORTNAME' + assert eft_shortnames[0].short_name == "TESTSHORTNAME" assert not short_name_link_2 - assert eft_shortnames[1].short_name == 'ABC123' + assert eft_shortnames[1].short_name == "ABC123" eft_credits: List[EFTCreditModel] = db.session.query(EFTCreditModel).order_by(EFTCreditModel.id).all() history: List[EFTHistoryModel] = db.session.query(EFTHistoryModel).order_by(EFTHistoryModel.id).all() @@ -410,89 +514,145 @@ def test_eft_tdi17_rerun(session, app, client): payment_account, eft_shortname, invoice = create_test_data() # Generate file with invalid trailer - file_name: str = 'test_eft_tdi17.txt' - header = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, file_creation_date='20230814', - file_creation_time='1601', deposit_start_date='20230810', deposit_end_date='20230810') - trailer = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, number_of_details='1', - total_deposit_amount='3733750') - - transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='001', - transaction_description='MISC PAYMENT TESTSHORTNAME', deposit_amount='13500', - currency='', exchange_adj_amount='0', deposit_amount_cad='FAIL', - destination_bank_number='0003', batch_number='002400986', jv_type='I', - jv_number='002425669', transaction_date='') + file_name: str = "test_eft_tdi17.txt" + header = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) + trailer = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="1", + total_deposit_amount="3733750", + ) + + transaction_1 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="MISC PAYMENT TESTSHORTNAME", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="FAIL", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) create_and_upload_eft_file(file_name, [header, transaction_1, trailer]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) # Assert EFT File record was created - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.status_code == EFTProcessStatus.FAILED.value - eft_trailer_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is None - eft_header_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is None - eft_transactions: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert eft_transactions is not None assert len(eft_transactions) == 1 - assert eft_transactions[0].error_messages[0] == 'Invalid transaction deposit amount CAD.' + assert eft_transactions[0].error_messages[0] == "Invalid transaction deposit amount CAD." # Correct transaction error and re-process - transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='001', - transaction_description='MISC PAYMENT TESTSHORTNAME', deposit_amount='13500', - currency='', exchange_adj_amount='0', deposit_amount_cad='13500', - destination_bank_number='0003', batch_number='002400986', jv_type='I', - jv_number='002425669', transaction_date='') + transaction_1 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="MISC PAYMENT TESTSHORTNAME", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="13500", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) create_and_upload_eft_file(file_name, [header, transaction_1, trailer]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) # Check file is completed after correction - eft_file_model: EFTFileModel = db.session.query(EFTFileModel).filter( - EFTFileModel.file_ref == file_name).one_or_none() + eft_file_model: EFTFileModel = ( + db.session.query(EFTFileModel).filter(EFTFileModel.file_ref == file_name).one_or_none() + ) assert eft_file_model is not None assert eft_file_model.status_code == EFTProcessStatus.COMPLETED.value - eft_trailer_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value).one_or_none() + eft_trailer_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRAILER.value) + .one_or_none() + ) assert eft_trailer_transaction is None - eft_header_transaction: EFTTransactionModel = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value).one_or_none() + eft_header_transaction: EFTTransactionModel = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.HEADER.value) + .one_or_none() + ) assert eft_header_transaction is None - eft_transactions: List[EFTTransactionModel] = db.session.query(EFTTransactionModel) \ - .filter(EFTTransactionModel.file_id == eft_file_model.id) \ - .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value).all() + eft_transactions: List[EFTTransactionModel] = ( + db.session.query(EFTTransactionModel) + .filter(EFTTransactionModel.file_id == eft_file_model.id) + .filter(EFTTransactionModel.line_type == EFTFileLineType.TRANSACTION.value) + .all() + ) assert eft_transactions is not None assert len(eft_transactions) == 1 @@ -504,131 +664,241 @@ def test_eft_tdi17_rerun(session, app, client): def create_test_data(): """Create test seed data.""" payment_account = factory_create_eft_account() - eft_short_name = (EFTShortnameModel(short_name='TESTSHORTNAME', - type=EFTShortnameType.EFT.value).save()) + eft_short_name = EFTShortnameModel(short_name="TESTSHORTNAME", type=EFTShortnameType.EFT.value).save() EFTShortnameLinksModel( eft_short_name_id=eft_short_name.id, auth_account_id=payment_account.auth_account_id, status_code=EFTShortnameStatus.LINKED.value, - updated_by='IDIR/JSMITH', - updated_by_name='IDIR/JSMITH', - updated_on=datetime.now() + updated_by="IDIR/JSMITH", + updated_by_name="IDIR/JSMITH", + updated_on=datetime.now(), ).save() - invoice = factory_invoice(payment_account=payment_account, - status_code=InvoiceStatus.APPROVED.value, - total=150.50, - service_fees=1.50, - payment_method_code=PaymentMethod.EFT.value) + invoice = factory_invoice( + payment_account=payment_account, + status_code=InvoiceStatus.APPROVED.value, + total=150.50, + service_fees=1.50, + payment_method_code=PaymentMethod.EFT.value, + ) return payment_account, eft_short_name, invoice def generate_basic_tdi17_file(file_name: str): """Generate a complete TDI17 EFT file.""" - header = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, file_creation_date='20230814', - file_creation_time='1601', deposit_start_date='20230810', deposit_end_date='20230810') - - trailer = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, number_of_details='5', - total_deposit_amount='3733750') - - transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='001', - transaction_description=f'{EFTRecord.EFT_DESCRIPTION_PATTERN} ABC123', - deposit_amount='13500', currency='', exchange_adj_amount='0', - deposit_amount_cad='13500', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - transaction_2 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='', - location_id='85004', transaction_sequence='002', - transaction_description=f'{EFTRecord.WIRE_DESCRIPTION_PATTERN} DEF456', - deposit_amount='525000', currency='', exchange_adj_amount='0', - deposit_amount_cad='525000', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - transaction_3 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='003', - transaction_description='SHOULDIGNORE', - deposit_amount='525000', currency='', exchange_adj_amount='0', - deposit_amount_cad='525000', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - transaction_4 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='004', - transaction_description=f'{EFTRecord.PAD_DESCRIPTION_PATTERN} SHOULDIGNORE', - deposit_amount='525000', currency='', exchange_adj_amount='0', - deposit_amount_cad='525000', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - create_and_upload_eft_file(file_name, [header, - transaction_1, transaction_2, transaction_3, transaction_4, - trailer]) + header = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) + + trailer = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="5", + total_deposit_amount="3733750", + ) + + transaction_1 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description=f"{EFTRecord.EFT_DESCRIPTION_PATTERN} ABC123", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="13500", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + transaction_2 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="", + location_id="85004", + transaction_sequence="002", + transaction_description=f"{EFTRecord.WIRE_DESCRIPTION_PATTERN} DEF456", + deposit_amount="525000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="525000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + transaction_3 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="003", + transaction_description="SHOULDIGNORE", + deposit_amount="525000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="525000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + transaction_4 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="004", + transaction_description=f"{EFTRecord.PAD_DESCRIPTION_PATTERN} SHOULDIGNORE", + deposit_amount="525000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="525000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + create_and_upload_eft_file( + file_name, + [header, transaction_1, transaction_2, transaction_3, transaction_4, trailer], + ) def generate_tdi17_file(file_name: str): """Generate a complete TDI17 EFT file.""" - header = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, file_creation_date='20230814', - file_creation_time='1601', deposit_start_date='20230810', deposit_end_date='20230810') - - trailer = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, number_of_details='5', - total_deposit_amount='3733750') - - transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='001', - transaction_description=f'{EFTRecord.EFT_DESCRIPTION_PATTERN} TESTSHORTNAME', - deposit_amount='10000', currency='', exchange_adj_amount='0', - deposit_amount_cad='10000', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - transaction_2 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='', - location_id='85004', transaction_sequence='002', - transaction_description=f'{EFTRecord.EFT_DESCRIPTION_PATTERN} TESTSHORTNAME', - deposit_amount='5050', currency='', exchange_adj_amount='0', - deposit_amount_cad='5050', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - transaction_3 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='003', - transaction_description=f'{EFTRecord.WIRE_DESCRIPTION_PATTERN} ABC123', - deposit_amount='35150', currency='', exchange_adj_amount='0', - deposit_amount_cad='35150', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - transaction_4 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', - location_id='85004', transaction_sequence='004', - transaction_description=f'{EFTRecord.PAD_DESCRIPTION_PATTERN} SHOULDIGNORE', - deposit_amount='525000', currency='', exchange_adj_amount='0', - deposit_amount_cad='525000', destination_bank_number='0003', - batch_number='002400986', jv_type='I', jv_number='002425669', - transaction_date='') - - create_and_upload_eft_file(file_name, [header, - transaction_1, transaction_2, transaction_3, transaction_4, - trailer]) + header = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) + + trailer = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="5", + total_deposit_amount="3733750", + ) + + transaction_1 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description=f"{EFTRecord.EFT_DESCRIPTION_PATTERN} TESTSHORTNAME", + deposit_amount="10000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="10000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + transaction_2 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="", + location_id="85004", + transaction_sequence="002", + transaction_description=f"{EFTRecord.EFT_DESCRIPTION_PATTERN} TESTSHORTNAME", + deposit_amount="5050", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="5050", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + transaction_3 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="003", + transaction_description=f"{EFTRecord.WIRE_DESCRIPTION_PATTERN} ABC123", + deposit_amount="35150", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="35150", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + transaction_4 = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="004", + transaction_description=f"{EFTRecord.PAD_DESCRIPTION_PATTERN} SHOULDIGNORE", + deposit_amount="525000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="525000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) + + create_and_upload_eft_file( + file_name, + [header, transaction_1, transaction_2, transaction_3, transaction_4, trailer], + ) def create_statement_from_invoices(account: PaymentAccountModel, invoices: List[InvoiceModel]): """Generate a statement from a list of invoices.""" - statement_settings = factory_statement_settings(pay_account_id=account.id, - frequency=StatementFrequency.MONTHLY.value) - statement = factory_statement(payment_account_id=account.id, - frequency=StatementFrequency.MONTHLY.value, - statement_settings_id=statement_settings.id) + statement_settings = factory_statement_settings( + pay_account_id=account.id, frequency=StatementFrequency.MONTHLY.value + ) + statement = factory_statement( + payment_account_id=account.id, + frequency=StatementFrequency.MONTHLY.value, + statement_settings_id=statement_settings.id, + ) for invoice in invoices: factory_statement_invoices(statement_id=statement.id, invoice_id=invoice.id) return statement @@ -638,33 +908,37 @@ def test_apply_pending_payments(session, app, client): """Test automatically applying a pending eft credit invoice link when there is a credit.""" payment_account, eft_short_name, invoice = create_test_data() create_statement_from_invoices(payment_account, [invoice]) - file_name: str = 'test_eft_tdi17.txt' + file_name: str = "test_eft_tdi17.txt" generate_tdi17_file(file_name) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) short_name_id = eft_short_name.id eft_credit_balance = EFTCreditModel.get_eft_credit_balance(short_name_id) assert eft_credit_balance == 0 short_name_links = EFTShortNamesService.get_shortname_links(short_name_id) - assert short_name_links['items'] - assert len(short_name_links['items']) == 1 + assert short_name_links["items"] + assert len(short_name_links["items"]) == 1 - short_name_link = short_name_links['items'][0] - assert short_name_link.get('has_pending_payment') is True - assert short_name_link.get('amount_owing') == 150.50 + short_name_link = short_name_links["items"][0] + assert short_name_link.get("has_pending_payment") is True + assert short_name_link.get("amount_owing") == 150.50 def test_skip_on_existing_pending_payments(session, app, client): """Test auto payment skipping payment when there exists a pending payment.""" payment_account, eft_short_name, invoice = create_test_data() - file_name: str = 'test_eft_tdi17.txt' + file_name: str = "test_eft_tdi17.txt" generate_tdi17_file(file_name) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) create_statement_from_invoices(payment_account, [invoice]) eft_credits = EFTCreditModel.get_eft_credits(eft_short_name.id) @@ -675,7 +949,8 @@ def test_skip_on_existing_pending_payments(session, app, client): status_code=EFTCreditInvoiceStatus.PENDING.value, invoice_id=invoice.id, amount=invoice.total, - link_group_id=1) + link_group_id=1, + ) short_name_id = eft_short_name.id eft_credit_balance = EFTCreditModel.get_eft_credit_balance(short_name_id) @@ -688,11 +963,13 @@ def test_skip_on_insufficient_balance(session, app, client): payment_account, eft_short_name, invoice = create_test_data() invoice.total = 99999 invoice.save() - file_name: str = 'test_eft_tdi17.txt' + file_name: str = "test_eft_tdi17.txt" generate_tdi17_file(file_name) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.EFT_FILE_UPLOADED.value, + ) create_statement_from_invoices(payment_account, [invoice]) @@ -701,9 +978,9 @@ def test_skip_on_insufficient_balance(session, app, client): assert eft_credit_balance == 150.50 short_name_links = EFTShortNamesService.get_shortname_links(short_name_id) - assert short_name_links['items'] - assert len(short_name_links['items']) == 1 + assert short_name_links["items"] + assert len(short_name_links["items"]) == 1 - short_name_link = short_name_links['items'][0] - assert short_name_link.get('has_pending_payment') is False - assert short_name_link.get('amount_owing') == 99999 + short_name_link = short_name_links["items"][0] + assert short_name_link.get("has_pending_payment") is False + assert short_name_link.get("amount_owing") == 99999 diff --git a/pay-queue/tests/integration/test_payment_reconciliations.py b/pay-queue/tests/integration/test_payment_reconciliations.py index 5fb7bdafa..4f475b779 100644 --- a/pay-queue/tests/integration/test_payment_reconciliations.py +++ b/pay-queue/tests/integration/test_payment_reconciliations.py @@ -33,8 +33,14 @@ from pay_queue.enums import RecordType, SourceTransaction, Status, TargetTransaction from .factory import ( - factory_create_online_banking_account, factory_create_pad_account, factory_invoice, factory_invoice_reference, - factory_payment, factory_payment_line_item, factory_receipt) + factory_create_online_banking_account, + factory_create_pad_account, + factory_invoice, + factory_invoice_reference, + factory_payment, + factory_payment_line_item, + factory_receipt, +) from .utils import add_file_event_to_queue_and_process, create_and_upload_settlement_file @@ -44,14 +50,18 @@ def test_online_banking_reconciliations(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -59,18 +69,30 @@ def test_online_banking_reconciliations(session, app, client): total = invoice.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - date = datetime.now().strftime('%d-%b-%y') - receipt_number = '1234567890' - row = [RecordType.BOLP.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, - total, cfs_account_number, - TargetTransaction.INV.value, invoice_number, - total, 0, Status.PAID.value] + date = datetime.now().strftime("%d-%b-%y") + receipt_number = "1234567890" + row = [ + RecordType.BOLP.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + total, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice = InvoiceModel.find_by_id(invoice_id) @@ -90,14 +112,18 @@ def test_online_banking_reconciliations_over_payment(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -105,20 +131,45 @@ def test_online_banking_reconciliations_over_payment(session, app, client): total = invoice.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - date = datetime.now().strftime('%d-%b-%y') - receipt_number = '1234567890' + date = datetime.now().strftime("%d-%b-%y") + receipt_number = "1234567890" over_payment_amount = 100 - inv_row = [RecordType.BOLP.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, total, - cfs_account_number, TargetTransaction.INV.value, invoice_number, total, 0, Status.PAID.value] - credit_row = [RecordType.ONAC.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, - over_payment_amount, cfs_account_number, TargetTransaction.INV.value, invoice_number, - over_payment_amount, 0, Status.ON_ACC.value] + inv_row = [ + RecordType.BOLP.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + total, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] + credit_row = [ + RecordType.ONAC.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + over_payment_amount, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + over_payment_amount, + 0, + Status.ON_ACC.value, + ] create_and_upload_settlement_file(file_name, [inv_row, credit_row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice = InvoiceModel.find_by_id(invoice_id) @@ -138,14 +189,18 @@ def test_online_banking_reconciliations_with_credit(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -153,20 +208,45 @@ def test_online_banking_reconciliations_with_credit(session, app, client): total = invoice.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - date = datetime.now().strftime('%d-%b-%y') - receipt_number = '1234567890' + date = datetime.now().strftime("%d-%b-%y") + receipt_number = "1234567890" credit_amount = 10 - inv_row = [RecordType.BOLP.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, - total - credit_amount, cfs_account_number, TargetTransaction.INV.value, invoice_number, total, 0, - Status.PAID.value] - credit_row = [RecordType.ONAC.value, SourceTransaction.EFT_WIRE.value, '555566677', 100001, date, credit_amount, - cfs_account_number, TargetTransaction.INV.value, invoice_number, total, 0, Status.PAID.value] + inv_row = [ + RecordType.BOLP.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + total - credit_amount, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] + credit_row = [ + RecordType.ONAC.value, + SourceTransaction.EFT_WIRE.value, + "555566677", + 100001, + date, + credit_amount, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] create_and_upload_settlement_file(file_name, [inv_row, credit_row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice = InvoiceModel.find_by_id(invoice_id) @@ -186,14 +266,18 @@ def test_online_banking_reconciliations_overflows_credit(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -201,25 +285,61 @@ def test_online_banking_reconciliations_overflows_credit(session, app, client): total = invoice.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - date = datetime.now().strftime('%d-%b-%y') - receipt_number = '1234567890' + date = datetime.now().strftime("%d-%b-%y") + receipt_number = "1234567890" credit_amount = 10 onac_amount = 100 - credit_row = [RecordType.ONAC.value, SourceTransaction.EFT_WIRE.value, '555566677', 100001, date, credit_amount, - cfs_account_number, TargetTransaction.INV.value, invoice_number, total, 0, Status.PAID.value] - inv_row = [RecordType.BOLP.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, - total - credit_amount, cfs_account_number, TargetTransaction.INV.value, invoice_number, total, 0, - Status.PAID.value] - onac_row = [RecordType.ONAC.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, - onac_amount, cfs_account_number, TargetTransaction.RECEIPT.value, receipt_number, total, 0, - Status.ON_ACC.value] + credit_row = [ + RecordType.ONAC.value, + SourceTransaction.EFT_WIRE.value, + "555566677", + 100001, + date, + credit_amount, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] + inv_row = [ + RecordType.BOLP.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + total - credit_amount, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] + onac_row = [ + RecordType.ONAC.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + onac_amount, + cfs_account_number, + TargetTransaction.RECEIPT.value, + receipt_number, + total, + 0, + Status.ON_ACC.value, + ] create_and_upload_settlement_file(file_name, [inv_row, credit_row, onac_row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice = InvoiceModel.find_by_id(invoice_id) @@ -239,14 +359,18 @@ def test_online_banking_under_payment(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() @@ -254,20 +378,32 @@ def test_online_banking_under_payment(session, app, client): total = invoice.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" paid_amount = 10 # Settlement row - date = datetime.now().strftime('%d-%b-%y') - receipt_number = '1234567890' - - row = [RecordType.BOLP.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, paid_amount, - cfs_account_number, - TargetTransaction.INV.value, invoice_number, - total, total - paid_amount, Status.PARTIAL.value] + date = datetime.now().strftime("%d-%b-%y") + receipt_number = "1234567890" + + row = [ + RecordType.BOLP.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + paid_amount, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + total - paid_amount, + Status.PARTIAL.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice = InvoiceModel.find_by_id(invoice_id) @@ -288,20 +424,25 @@ def test_pad_reconciliations(session, app, client): # 2. Create invoices and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice1 = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value) - factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - - invoice2 = factory_invoice(payment_account=pay_account, total=200, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value) - factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, - service_fees=10.0, total=190.0) - - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice1 = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + ) + factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, service_fees=10.0, total=90.0) + + invoice2 = factory_invoice( + payment_account=pay_account, + total=200, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + ) + factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, service_fees=10.0, total=190.0) + + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice1.id, invoice_number=invoice_number) factory_invoice_reference(invoice_id=invoice2.id, invoice_number=invoice_number) @@ -315,18 +456,30 @@ def test_pad_reconciliations(session, app, client): total = invoice1.total + invoice2.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - receipt_number = '1234567890' - date = datetime.now().strftime('%d-%b-%y') - row = [RecordType.PAD.value, SourceTransaction.PAD.value, receipt_number, 100001, date, total, - cfs_account_number, - 'INV', invoice_number, - total, 0, Status.PAID.value] + receipt_number = "1234567890" + date = datetime.now().strftime("%d-%b-%y") + row = [ + RecordType.PAD.value, + SourceTransaction.PAD.value, + receipt_number, + 100001, + date, + total, + cfs_account_number, + "INV", + invoice_number, + total, + 0, + Status.PAID.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice1 = InvoiceModel.find_by_id(invoice1_id) @@ -355,20 +508,25 @@ def test_pad_reconciliations_with_credit_memo(session, app, client): # 3. Create CFS Invoice records # 4. Mimic some credits on the account # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice1 = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value) - factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - - invoice2 = factory_invoice(payment_account=pay_account, total=200, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value) - factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, - service_fees=10.0, total=190.0) - - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice1 = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + ) + factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, service_fees=10.0, total=90.0) + + invoice2 = factory_invoice( + payment_account=pay_account, + total=200, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + ) + factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, service_fees=10.0, total=190.0) + + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice1.id, invoice_number=invoice_number) factory_invoice_reference(invoice_id=invoice2.id, invoice_number=invoice_number) @@ -382,21 +540,47 @@ def test_pad_reconciliations_with_credit_memo(session, app, client): total = invoice1.total + invoice2.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - receipt_number = '1234567890' - credit_memo_number = 'CM123' - date = datetime.now().strftime('%d-%b-%y') + receipt_number = "1234567890" + credit_memo_number = "CM123" + date = datetime.now().strftime("%d-%b-%y") credit_amount = 25 - credit_row = [RecordType.CMAP.value, SourceTransaction.CREDIT_MEMO.value, credit_memo_number, 100002, date, - credit_amount, cfs_account_number, 'INV', invoice_number, total, 0, Status.PAID.value] - pad_row = [RecordType.PAD.value, SourceTransaction.PAD.value, receipt_number, 100001, date, total - credit_amount, - cfs_account_number, 'INV', invoice_number, total, 0, Status.PAID.value] + credit_row = [ + RecordType.CMAP.value, + SourceTransaction.CREDIT_MEMO.value, + credit_memo_number, + 100002, + date, + credit_amount, + cfs_account_number, + "INV", + invoice_number, + total, + 0, + Status.PAID.value, + ] + pad_row = [ + RecordType.PAD.value, + SourceTransaction.PAD.value, + receipt_number, + 100001, + date, + total - credit_amount, + cfs_account_number, + "INV", + invoice_number, + total, + 0, + Status.PAID.value, + ] create_and_upload_settlement_file(file_name, [credit_row, pad_row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice1 = InvoiceModel.find_by_id(invoice1_id) @@ -424,20 +608,25 @@ def test_pad_nsf_reconciliations(session, app, client): # 2. Create invoices and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice1 = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value) - factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - - invoice2 = factory_invoice(payment_account=pay_account, total=200, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value) - factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, - service_fees=10.0, total=190.0) - - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice1 = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + ) + factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, service_fees=10.0, total=90.0) + + invoice2 = factory_invoice( + payment_account=pay_account, + total=200, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + ) + factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, service_fees=10.0, total=190.0) + + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice1.id, invoice_number=invoice_number) factory_invoice_reference(invoice_id=invoice2.id, invoice_number=invoice_number) @@ -453,17 +642,30 @@ def test_pad_nsf_reconciliations(session, app, client): total = invoice1.total + invoice2.total # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - receipt_number = '1234567890' - date = datetime.now().strftime('%d-%b-%y') - row = [RecordType.PAD.value, SourceTransaction.PAD.value, receipt_number, 100001, date, 0, cfs_account_number, - 'INV', invoice_number, - total, total, Status.NOT_PAID.value] + receipt_number = "1234567890" + date = datetime.now().strftime("%d-%b-%y") + row = [ + RecordType.PAD.value, + SourceTransaction.PAD.value, + receipt_number, + 100001, + date, + 0, + cfs_account_number, + "INV", + invoice_number, + total, + total, + Status.NOT_PAID.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in SETTLEMENT_SCHEDULED status and Payment should be FAILED updated_invoice1 = InvoiceModel.find_by_id(invoice1_id) @@ -478,8 +680,9 @@ def test_pad_nsf_reconciliations(session, app, client): assert payment.payment_method_code == PaymentMethod.PAD.value assert payment.invoice_number == invoice_number - cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_payment_method(pay_account_id, - PaymentMethod.PAD.value) + cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_payment_method( + pay_account_id, PaymentMethod.PAD.value + ) assert cfs_account.status == CfsAccountStatus.FREEZE.value assert pay_account.has_nsf_invoices @@ -490,28 +693,39 @@ def test_pad_reversal_reconciliations(session, app, client): # 2. Create invoices and related records for a completed payment # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, - account_number=cfs_account_number) - invoice1 = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value, - status_code=InvoiceStatus.PAID.value) - factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - - invoice2 = factory_invoice(payment_account=pay_account, total=200, service_fees=10.0, - payment_method_code=PaymentMethod.PAD.value, - status_code=InvoiceStatus.PAID.value) - factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, - service_fees=10.0, total=190.0) - - invoice_number = '1234567890' - receipt_number = '9999999999' - - factory_invoice_reference(invoice_id=invoice1.id, invoice_number=invoice_number, - status_code=InvoiceReferenceStatus.COMPLETED.value) - factory_invoice_reference(invoice_id=invoice2.id, invoice_number=invoice_number, - status_code=InvoiceReferenceStatus.COMPLETED.value) + cfs_account_number = "1234" + pay_account = factory_create_pad_account(status=CfsAccountStatus.ACTIVE.value, account_number=cfs_account_number) + invoice1 = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + status_code=InvoiceStatus.PAID.value, + ) + factory_payment_line_item(invoice_id=invoice1.id, filing_fees=90.0, service_fees=10.0, total=90.0) + + invoice2 = factory_invoice( + payment_account=pay_account, + total=200, + service_fees=10.0, + payment_method_code=PaymentMethod.PAD.value, + status_code=InvoiceStatus.PAID.value, + ) + factory_payment_line_item(invoice_id=invoice2.id, filing_fees=190.0, service_fees=10.0, total=190.0) + + invoice_number = "1234567890" + receipt_number = "9999999999" + + factory_invoice_reference( + invoice_id=invoice1.id, + invoice_number=invoice_number, + status_code=InvoiceReferenceStatus.COMPLETED.value, + ) + factory_invoice_reference( + invoice_id=invoice2.id, + invoice_number=invoice_number, + status_code=InvoiceReferenceStatus.COMPLETED.value, + ) receipt_id1 = factory_receipt(invoice_id=invoice1.id, receipt_number=receipt_number).save().id receipt_id2 = factory_receipt(invoice_id=invoice2.id, receipt_number=receipt_number).save().id @@ -522,23 +736,41 @@ def test_pad_reversal_reconciliations(session, app, client): total = invoice1.total + invoice2.total - payment = factory_payment(pay_account=pay_account, paid_amount=total, invoice_amount=total, - invoice_number=invoice_number, - receipt_number=receipt_number, status=PaymentStatus.COMPLETED.value) + payment = factory_payment( + pay_account=pay_account, + paid_amount=total, + invoice_amount=total, + invoice_number=invoice_number, + receipt_number=receipt_number, + status=PaymentStatus.COMPLETED.value, + ) pay_id = payment.id # Now publish message saying payment has been reversed. # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - date = datetime.now().strftime('%d-%b-%y') - row = [RecordType.PADR.value, SourceTransaction.PAD.value, receipt_number, 100001, date, 0, cfs_account_number, - 'INV', invoice_number, - total, total, Status.NOT_PAID.value] + date = datetime.now().strftime("%d-%b-%y") + row = [ + RecordType.PADR.value, + SourceTransaction.PAD.value, + receipt_number, + 100001, + date, + 0, + cfs_account_number, + "INV", + invoice_number, + total, + total, + Status.NOT_PAID.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in SETTLEMENT_SCHEDULED status and Payment should be FAILED updated_invoice1 = InvoiceModel.find_by_id(invoice1_id) @@ -569,43 +801,63 @@ async def test_eft_wire_reconciliations(session, app, client): # 2. Create invoice and related records # 3. Create CFS Invoice records # 4. Create a CFS settlement file, and verify the records - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() invoice_id = invoice.id total = invoice.total - receipt = 'RCPT0012345' + receipt = "RCPT0012345" paid_amount = 100 - PaymentModel(payment_method_code=PaymentMethod.EFT.value, - payment_status_code=PaymentStatus.CREATED.value, - payment_system_code='PAYBC', - payment_account_id=pay_account.id, - payment_date=datetime.now(), - paid_amount=paid_amount, - receipt_number=receipt).save() + PaymentModel( + payment_method_code=PaymentMethod.EFT.value, + payment_status_code=PaymentStatus.CREATED.value, + payment_system_code="PAYBC", + payment_account_id=pay_account.id, + payment_date=datetime.now(), + paid_amount=paid_amount, + receipt_number=receipt, + ).save() # Create a settlement file and publish. - file_name: str = 'cas_settlement_file.csv' + file_name: str = "cas_settlement_file.csv" # Settlement row - date = datetime.now().strftime('%d-%b-%y') - - row = [RecordType.EFTP.value, SourceTransaction.EFT_WIRE.value, receipt, 100001, date, total, - cfs_account_number, TargetTransaction.INV.value, invoice_number, total, 0, Status.PAID.value] + date = datetime.now().strftime("%d-%b-%y") + + row = [ + RecordType.EFTP.value, + SourceTransaction.EFT_WIRE.value, + receipt, + 100001, + date, + total, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # The invoice should be in PAID status and Payment should be completed updated_invoice = InvoiceModel.find_by_id(invoice_id) @@ -625,73 +877,113 @@ def test_credits(session, app, client, monkeypatch): # 4. Publish credit in settlement file. # 5. Mock CFS Response for the receipt and credit memo. # 6. Confirm the credit matches the records. - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) pay_account_id = pay_account.id - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) - receipt_number = 'RCPT0012345' + receipt_number = "RCPT0012345" onac_amount = 100 cm_identifier = 1000 cm_amount = 100 cm_used_amount = 50 - PaymentModel(payment_method_code=PaymentMethod.EFT.value, - payment_status_code=PaymentStatus.CREATED.value, - payment_system_code='PAYBC', - payment_account_id=pay_account.id, - payment_date=datetime.now(), - paid_amount=onac_amount, - receipt_number=receipt_number).save() - - credit = CreditModel(cfs_identifier=cm_identifier, - is_credit_memo=True, - amount=cm_amount, - remaining_amount=cm_amount, - account_id=pay_account_id).save() + PaymentModel( + payment_method_code=PaymentMethod.EFT.value, + payment_status_code=PaymentStatus.CREATED.value, + payment_system_code="PAYBC", + payment_account_id=pay_account.id, + payment_date=datetime.now(), + paid_amount=onac_amount, + receipt_number=receipt_number, + ).save() + + credit = CreditModel( + cfs_identifier=cm_identifier, + is_credit_memo=True, + amount=cm_amount, + remaining_amount=cm_amount, + account_id=pay_account_id, + ).save() credit_id = credit.id - def mock_receipt(cfs_account: CfsAccountModel, - receipt_number: str): # pylint: disable=unused-argument; mocks of library methods - return { - 'receipt_amount': onac_amount - } + def mock_receipt( + cfs_account: CfsAccountModel, receipt_number: str + ): # pylint: disable=unused-argument; mocks of library methods + return {"receipt_amount": onac_amount} - def mock_cms(cfs_account: CfsAccountModel, - cms_number: str): # pylint: disable=unused-argument; mocks of library methods - return { - 'amount_due': cm_amount - cm_used_amount - } + def mock_cms( + cfs_account: CfsAccountModel, cms_number: str + ): # pylint: disable=unused-argument; mocks of library methods + return {"amount_due": cm_amount - cm_used_amount} - monkeypatch.setattr('pay_api.services.cfs_service.CFSService.get_receipt', mock_receipt) - monkeypatch.setattr('pay_api.services.cfs_service.CFSService.get_cms', mock_cms) + monkeypatch.setattr("pay_api.services.cfs_service.CFSService.get_receipt", mock_receipt) + monkeypatch.setattr("pay_api.services.cfs_service.CFSService.get_cms", mock_cms) - file_name = 'cas_settlement_file.csv' + file_name = "cas_settlement_file.csv" date = datetime.now(tz=timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0).replace(tzinfo=None) - date_str = date.strftime('%d-%b-%y') - - row = [RecordType.ONAC.value, SourceTransaction.EFT_WIRE.value, receipt_number, 100001, date_str, onac_amount, - cfs_account_number, TargetTransaction.RECEIPT.value, receipt_number, onac_amount, 0, Status.ON_ACC.value] + date_str = date.strftime("%d-%b-%y") + + row = [ + RecordType.ONAC.value, + SourceTransaction.EFT_WIRE.value, + receipt_number, + 100001, + date_str, + onac_amount, + cfs_account_number, + TargetTransaction.RECEIPT.value, + receipt_number, + onac_amount, + 0, + Status.ON_ACC.value, + ] credit_invoices_row = [ - RecordType.CMAP.value, SourceTransaction.CREDIT_MEMO.value, cm_identifier, 100003, date_str, 2.5, - cfs_account_number, TargetTransaction.INV.value, invoice_number, 100, 0, Status.PAID.value + RecordType.CMAP.value, + SourceTransaction.CREDIT_MEMO.value, + cm_identifier, + 100003, + date_str, + 2.5, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + 100, + 0, + Status.PAID.value, ] credit_invoices_row2 = [ - RecordType.CMAP.value, SourceTransaction.CREDIT_MEMO.value, cm_identifier, 100004, date_str, 5.5, - cfs_account_number, TargetTransaction.INV.value, invoice_number, 100, 0, Status.PAID.value + RecordType.CMAP.value, + SourceTransaction.CREDIT_MEMO.value, + cm_identifier, + 100004, + date_str, + 5.5, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + 100, + 0, + Status.PAID.value, ] create_and_upload_settlement_file(file_name, [row, credit_invoices_row, credit_invoices_row2]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) # Look up credit file and make sure the credits are recorded. pay_account = PaymentAccountModel.find_by_id(pay_account_id) @@ -720,38 +1012,56 @@ def mock_cms(cfs_account: CfsAccountModel, def test_unconsolidated_invoices_errors(session, app, client, mocker): """Test error scenarios for unconsolidated invoices in the reconciliation worker.""" - cfs_account_number = '1234' - pay_account = factory_create_online_banking_account(status=CfsAccountStatus.ACTIVE.value, - cfs_account=cfs_account_number) - - invoice = factory_invoice(payment_account=pay_account, total=100, service_fees=10.0, - payment_method_code=PaymentMethod.ONLINE_BANKING.value) - factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, - service_fees=10.0, total=90.0) - invoice_number = '1234567890' + cfs_account_number = "1234" + pay_account = factory_create_online_banking_account( + status=CfsAccountStatus.ACTIVE.value, cfs_account=cfs_account_number + ) + + invoice = factory_invoice( + payment_account=pay_account, + total=100, + service_fees=10.0, + payment_method_code=PaymentMethod.ONLINE_BANKING.value, + ) + factory_payment_line_item(invoice_id=invoice.id, filing_fees=90.0, service_fees=10.0, total=90.0) + invoice_number = "1234567890" factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice = invoice.save() invoice_id = invoice.id total = invoice.total - error_messages = [{'error': 'Test error message', 'row': 'row 2'}] - mocker.patch('pay_queue.services.payment_reconciliations._process_file_content', - return_value=(True, error_messages)) - mock_send_error_email = mocker.patch('pay_queue.services.payment_reconciliations.send_error_email') + error_messages = [{"error": "Test error message", "row": "row 2"}] + mocker.patch( + "pay_queue.services.payment_reconciliations._process_file_content", + return_value=(True, error_messages), + ) + mock_send_error_email = mocker.patch("pay_queue.services.payment_reconciliations.send_error_email") - file_name: str = 'BCR_PAYMENT_APPL_20240619.csv' + file_name: str = "BCR_PAYMENT_APPL_20240619.csv" date = datetime.now(tz=timezone.utc).isoformat() - receipt_number = '1234567890' - row = [RecordType.BOLP.value, SourceTransaction.ONLINE_BANKING.value, receipt_number, 100001, date, - total, cfs_account_number, - TargetTransaction.INV.value, invoice_number, - total, 0, Status.PAID.value] + receipt_number = "1234567890" + row = [ + RecordType.BOLP.value, + SourceTransaction.ONLINE_BANKING.value, + receipt_number, + 100001, + date, + total, + cfs_account_number, + TargetTransaction.INV.value, + invoice_number, + total, + 0, + Status.PAID.value, + ] create_and_upload_settlement_file(file_name, [row]) - add_file_event_to_queue_and_process(client, - file_name=file_name, - message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value) + add_file_event_to_queue_and_process( + client, + file_name=file_name, + message_type=QueueMessageTypes.CAS_MESSAGE_TYPE.value, + ) updated_invoice = InvoiceModel.find_by_id(invoice_id) assert updated_invoice.invoice_status_code == InvoiceStatus.SETTLEMENT_SCHEDULED.value @@ -759,8 +1069,8 @@ def test_unconsolidated_invoices_errors(session, app, client, mocker): mock_send_error_email.assert_called_once() call_args = mock_send_error_email.call_args email_params = call_args[0][0] - assert email_params.subject == 'Payment Reconciliation Failure' + assert email_params.subject == "Payment Reconciliation Failure" assert email_params.file_name == file_name - assert email_params.minio_location == 'payment-sftp' + assert email_params.minio_location == "payment-sftp" assert email_params.error_messages == error_messages assert email_params.table_name == CasSettlementModel.__tablename__ diff --git a/pay-queue/tests/integration/test_worker_queue.py b/pay-queue/tests/integration/test_worker_queue.py index 79d1002e2..b511ebc2f 100644 --- a/pay-queue/tests/integration/test_worker_queue.py +++ b/pay-queue/tests/integration/test_worker_queue.py @@ -24,23 +24,24 @@ def test_update_payment(session, app, client): """Assert that the update internal payment records works.""" # vars - old_identifier = 'T000000000' - new_identifier = 'BC12345678' + old_identifier = "T000000000" + new_identifier = "BC12345678" # Create an Internal Payment payment_account = factory_payment_account(payment_system_code=PaymentSystem.BCOL.value).save() - invoice: Invoice = factory_invoice(payment_account=payment_account, - business_identifier=old_identifier, - payment_method_code=PaymentMethod.INTERNAL.value).save() + invoice: Invoice = factory_invoice( + payment_account=payment_account, + business_identifier=old_identifier, + payment_method_code=PaymentMethod.INTERNAL.value, + ).save() inv_ref = factory_invoice_reference(invoice_id=invoice.id) factory_payment(invoice_number=inv_ref.invoice_number) invoice_id = invoice.id - helper_add_identifier_event_to_queue(client, old_identifier=old_identifier, - new_identifier=new_identifier) + helper_add_identifier_event_to_queue(client, old_identifier=old_identifier, new_identifier=new_identifier) # Get the internal account and invoice and assert that the identifier is new identifier invoice = Invoice.find_by_id(invoice_id) diff --git a/pay-queue/tests/integration/utils.py b/pay-queue/tests/integration/utils.py index 07b9e7c7e..fa4e5cd97 100644 --- a/pay-queue/tests/integration/utils.py +++ b/pay-queue/tests/integration/utils.py @@ -34,69 +34,90 @@ def build_request_for_queue_push(message_type, payload): """Build request for queue message.""" - queue_message_bytes = to_queue_message(SimpleCloudEvent( - id=str(uuid.uuid4()), - source='pay-queue', - subject=None, - time=datetime.now(tz=timezone.utc).isoformat(), - type=message_type, - data=payload - )) + queue_message_bytes = to_queue_message( + SimpleCloudEvent( + id=str(uuid.uuid4()), + source="pay-queue", + subject=None, + time=datetime.now(tz=timezone.utc).isoformat(), + type=message_type, + data=payload, + ) + ) return { - 'message': { - 'data': base64.b64encode(queue_message_bytes).decode('utf-8') - }, - 'subscription': 'foobar' + "message": {"data": base64.b64encode(queue_message_bytes).decode("utf-8")}, + "subscription": "foobar", } def post_to_queue(client, request_payload): """Post request to worker using an http request on our wrapped flask instance.""" - response = client.post('/', data=json.dumps(request_payload), - headers={'Content-Type': 'application/json'}) + response = client.post( + "/", + data=json.dumps(request_payload), + headers={"Content-Type": "application/json"}, + ) assert response.status_code == 200 def create_and_upload_settlement_file(file_name: str, rows: List[List]): """Create settlement file, upload to minio and send event.""" - headers = ['Record type', 'Source Transaction Type', 'Source Transaction Number', - 'Application Id', 'Application Date', 'Application amount', 'Customer Account', - 'Target transaction type', - 'Target transaction Number', 'Target Transaction Original amount', - 'Target Transaction Outstanding Amount', - 'Target transaction status', 'Reversal Reason code', 'Reversal reason description'] - with open(file_name, mode='w', encoding='utf-8') as cas_file: + headers = [ + "Record type", + "Source Transaction Type", + "Source Transaction Number", + "Application Id", + "Application Date", + "Application amount", + "Customer Account", + "Target transaction type", + "Target transaction Number", + "Target Transaction Original amount", + "Target Transaction Outstanding Amount", + "Target transaction status", + "Reversal Reason code", + "Reversal reason description", + ] + with open(file_name, mode="w", encoding="utf-8") as cas_file: cas_writer = csv.writer(cas_file, quoting=csv.QUOTE_ALL) cas_writer.writerow(headers) for row in rows: cas_writer.writerow(row) - with open(file_name, 'rb') as f: + with open(file_name, "rb") as f: upload_to_minio(f.read(), file_name) def create_and_upload_eft_file(file_name: str, rows: List[List]): """Create eft file, upload to minio and send event.""" - with open(file_name, mode='w', encoding='utf-8') as eft_file: + with open(file_name, mode="w", encoding="utf-8") as eft_file: for row in rows: print(row, file=eft_file) - with open(file_name, 'rb') as f: + with open(file_name, "rb") as f: upload_to_minio(f.read(), file_name) def upload_to_minio(value_as_bytes, file_name: str): """Return a pre-signed URL for new doc upload.""" - minio_endpoint = current_app.config['MINIO_ENDPOINT'] - minio_key = current_app.config['MINIO_ACCESS_KEY'] - minio_secret = current_app.config['MINIO_ACCESS_SECRET'] - minio_client = Minio(minio_endpoint, access_key=minio_key, secret_key=minio_secret, - secure=current_app.config['MINIO_SECURE']) + minio_endpoint = current_app.config["MINIO_ENDPOINT"] + minio_key = current_app.config["MINIO_ACCESS_KEY"] + minio_secret = current_app.config["MINIO_ACCESS_SECRET"] + minio_client = Minio( + minio_endpoint, + access_key=minio_key, + secret_key=minio_secret, + secure=current_app.config["MINIO_SECURE"], + ) value_as_stream = io.BytesIO(value_as_bytes) - minio_client.put_object(current_app.config['MINIO_BUCKET_NAME'], file_name, value_as_stream, - os.stat(file_name).st_size) + minio_client.put_object( + current_app.config["MINIO_BUCKET_NAME"], + file_name, + value_as_stream, + os.stat(file_name).st_size, + ) def forward_incoming_message_to_test_instance(session, app, client): @@ -107,16 +128,16 @@ def forward_incoming_message_to_test_instance(session, app, client): with socket() as server_socket: server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server_socket.settimeout(2) - server_socket.bind(('0.0.0.0', current_app.config.get('TEST_PUSH_ENDPOINT_PORT'))) + server_socket.bind(("0.0.0.0", current_app.config.get("TEST_PUSH_ENDPOINT_PORT"))) server_socket.listen(10) tries = 100 while tries > 0: client_socket, _ = server_socket.accept() if socket_data := client_socket.recv(4096): - body = socket_data.decode('utf8').split('\r\n')[-1] + body = socket_data.decode("utf8").split("\r\n")[-1] payload = json.loads(body) post_to_queue(client, payload) - client_socket.send('HTTP/1.1 200 OK\n\n'.encode('utf8')) + client_socket.send("HTTP/1.1 200 OK\n\n".encode("utf8")) break sleep(0.01) tries -= 1 @@ -126,8 +147,8 @@ def forward_incoming_message_to_test_instance(session, app, client): def add_file_event_to_queue_and_process(client, file_name: str, message_type: str, use_pubsub_emulator=False): """Add event to the Queue.""" queue_payload = { - 'fileName': file_name, - 'location': current_app.config['MINIO_BUCKET_NAME'] + "fileName": file_name, + "location": current_app.config["MINIO_BUCKET_NAME"], } if use_pubsub_emulator: gcp_queue_publisher.publish_to_queue( @@ -135,7 +156,7 @@ def add_file_event_to_queue_and_process(client, file_name: str, message_type: st source=QueueSources.FTP_POLLER.value, message_type=message_type, payload=queue_payload, - topic=f'projects/{current_app.config["TEST_GCP_PROJECT_NAME"]}/topics/ftp-poller-dev' + topic=f'projects/{current_app.config["TEST_GCP_PROJECT_NAME"]}/topics/ftp-poller-dev', ) ) forward_incoming_message_to_test_instance(client) @@ -144,17 +165,18 @@ def add_file_event_to_queue_and_process(client, file_name: str, message_type: st post_to_queue(client, payload) -def helper_add_identifier_event_to_queue(client, old_identifier: str = 'T1234567890', - new_identifier: str = 'BC1234567890'): +def helper_add_identifier_event_to_queue( + client, old_identifier: str = "T1234567890", new_identifier: str = "BC1234567890" +): """Add event to the Queue.""" message_type = QueueMessageTypes.INCORPORATION.value queue_payload = { - 'filing': { - 'header': {'filingId': '12345678'}, - 'business': {'identifier': 'BC1234567'} + "filing": { + "header": {"filingId": "12345678"}, + "business": {"identifier": "BC1234567"}, }, - 'identifier': new_identifier, - 'tempidentifier': old_identifier, + "identifier": new_identifier, + "tempidentifier": old_identifier, } request_payload = build_request_for_queue_push(message_type, queue_payload) post_to_queue(client, request_payload) diff --git a/pay-queue/tests/unit/test_eft_file_parser.py b/pay-queue/tests/unit/test_eft_file_parser.py index 33f0933c9..b033aa7c0 100644 --- a/pay-queue/tests/unit/test_eft_file_parser.py +++ b/pay-queue/tests/unit/test_eft_file_parser.py @@ -28,11 +28,13 @@ def test_eft_parse_header(): """Test EFT header parser.""" - content = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, - file_creation_date='20230814', - file_creation_time='1601', - deposit_start_date='20230810', - deposit_end_date='20230810') + content = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) header: EFTHeader = EFTHeader(content, 0) @@ -41,7 +43,7 @@ def test_eft_parse_header(): deposit_date_end = datetime(2023, 8, 10) assert header.index == 0 - assert header.record_type == '1' + assert header.record_type == "1" assert header.creation_datetime == creation_datetime assert header.starting_deposit_date == deposit_date_start assert header.ending_deposit_date == deposit_date_end @@ -49,7 +51,7 @@ def test_eft_parse_header(): def test_eft_parse_header_invalid_length(): """Test EFT header parser invalid length.""" - content = ' ' + content = " " header: EFTHeader = EFTHeader(content, 0) assert header.errors @@ -61,11 +63,13 @@ def test_eft_parse_header_invalid_length(): def test_eft_parse_header_invalid_record_type(): """Test EFT header parser invalid record type.""" - content = factory_eft_header(record_type='X', - file_creation_date='20230814', - file_creation_time='1601', - deposit_start_date='20230810', - deposit_end_date='20230810') + content = factory_eft_header( + record_type="X", + file_creation_date="20230814", + file_creation_time="1601", + deposit_start_date="20230810", + deposit_end_date="20230810", + ) header: EFTHeader = EFTHeader(content, 0) @@ -78,11 +82,13 @@ def test_eft_parse_header_invalid_record_type(): def test_eft_parse_header_invalid_dates(): """Test EFT header parser invalid dates.""" - content = factory_eft_header(record_type=EFTConstants.HEADER_RECORD_TYPE.value, - file_creation_date='2023081_', - file_creation_time='160 ', - deposit_start_date='20230850', - deposit_end_date='202308AB') + content = factory_eft_header( + record_type=EFTConstants.HEADER_RECORD_TYPE.value, + file_creation_date="2023081_", + file_creation_time="160 ", + deposit_start_date="20230850", + deposit_end_date="202308AB", + ) header: EFTHeader = EFTHeader(content, 0) @@ -101,20 +107,22 @@ def test_eft_parse_header_invalid_dates(): def test_eft_parse_trailer(): """Test EFT trailer parser.""" - content = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, - number_of_details='5', - total_deposit_amount='3733750') + content = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="5", + total_deposit_amount="3733750", + ) trailer: EFTTrailer = EFTTrailer(content, 1) assert trailer.index == 1 - assert trailer.record_type == '7' + assert trailer.record_type == "7" assert trailer.number_of_details == 5 assert trailer.total_deposit_amount == 3733750 def test_eft_parse_trailer_invalid_length(): """Test EFT trailer parser invalid number types.""" - content = ' ' + content = " " trailer: EFTTrailer = EFTTrailer(content, 1) assert trailer.errors @@ -126,9 +134,7 @@ def test_eft_parse_trailer_invalid_length(): def test_eft_parse_trailer_invalid_record_type(): """Test EFT trailer parser invalid record_type.""" - content = factory_eft_trailer(record_type='X', - number_of_details='5', - total_deposit_amount='3733750') + content = factory_eft_trailer(record_type="X", number_of_details="5", total_deposit_amount="3733750") trailer: EFTTrailer = EFTTrailer(content, 1) assert trailer.errors @@ -140,9 +146,11 @@ def test_eft_parse_trailer_invalid_record_type(): def test_eft_parse_trailer_invalid_numbers(): """Test EFT trailer parser invalid number values.""" - content = factory_eft_trailer(record_type=EFTConstants.TRAILER_RECORD_TYPE.value, - number_of_details='B', - total_deposit_amount='3733A50') + content = factory_eft_trailer( + record_type=EFTConstants.TRAILER_RECORD_TYPE.value, + number_of_details="B", + total_deposit_amount="3733A50", + ) trailer: EFTTrailer = EFTTrailer(content, 1) assert trailer.errors @@ -157,209 +165,214 @@ def test_eft_parse_trailer_invalid_numbers(): def test_eft_parse_record(): """Test EFT record parser.""" - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='001', - transaction_description='DEPOSIT 26', - deposit_amount='13500', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='13500', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="DEPOSIT 26", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="13500", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 1) deposit_datetime = datetime(2023, 8, 10, 0, 0) transaction_date = None assert record.index == 1 - assert record.record_type == '2' - assert record.ministry_code == 'AT' - assert record.program_code == '0146' + assert record.record_type == "2" + assert record.ministry_code == "AT" + assert record.program_code == "0146" assert record.deposit_datetime == deposit_datetime - assert record.location_id == '85004' - assert record.transaction_sequence == '001' - assert record.transaction_description == 'DEPOSIT 26' + assert record.location_id == "85004" + assert record.transaction_sequence == "001" + assert record.transaction_description == "DEPOSIT 26" assert record.deposit_amount == 13500 assert record.currency == EFTConstants.CURRENCY_CAD.value assert record.exchange_adj_amount == 0 assert record.deposit_amount_cad == 13500 - assert record.dest_bank_number == '0003' - assert record.batch_number == '002400986' - assert record.jv_type == 'I' - assert record.jv_number == '002425669' + assert record.dest_bank_number == "0003" + assert record.batch_number == "002400986" + assert record.jv_type == "I" + assert record.jv_number == "002425669" assert record.transaction_date == transaction_date assert record.short_name_type is None - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='002', - transaction_description='FUNDS TRANSFER CR TT INTERBLOCK C', - deposit_amount='525000', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='525000', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="002", + transaction_description="FUNDS TRANSFER CR TT INTERBLOCK C", + deposit_amount="525000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="525000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 2) assert record.index == 2 - assert record.record_type == '2' - assert record.ministry_code == 'AT' - assert record.program_code == '0146' + assert record.record_type == "2" + assert record.ministry_code == "AT" + assert record.program_code == "0146" assert record.deposit_datetime == deposit_datetime - assert record.location_id == '85004' - assert record.transaction_sequence == '002' - assert record.transaction_description == 'INTERBLOCK C' + assert record.location_id == "85004" + assert record.transaction_sequence == "002" + assert record.transaction_description == "INTERBLOCK C" assert record.deposit_amount == 525000 assert record.currency == EFTConstants.CURRENCY_CAD.value assert record.exchange_adj_amount == 0 assert record.deposit_amount_cad == 525000 - assert record.dest_bank_number == '0003' - assert record.batch_number == '002400986' - assert record.jv_type == 'I' - assert record.jv_number == '002425669' + assert record.dest_bank_number == "0003" + assert record.batch_number == "002400986" + assert record.jv_type == "I" + assert record.jv_number == "002425669" assert record.transaction_date == transaction_date assert record.short_name_type == EFTShortnameType.WIRE.value - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='003', - transaction_description='MISC PAYMENT ABC1234567', - deposit_amount='951250', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='951250', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="003", + transaction_description="MISC PAYMENT ABC1234567", + deposit_amount="951250", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="951250", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 3) assert record.index == 3 - assert record.record_type == '2' - assert record.ministry_code == 'AT' - assert record.program_code == '0146' + assert record.record_type == "2" + assert record.ministry_code == "AT" + assert record.program_code == "0146" assert record.deposit_datetime == deposit_datetime - assert record.location_id == '85004' - assert record.transaction_sequence == '003' - assert record.transaction_description == 'ABC1234567' + assert record.location_id == "85004" + assert record.transaction_sequence == "003" + assert record.transaction_description == "ABC1234567" assert record.deposit_amount == 951250 assert record.currency == EFTConstants.CURRENCY_CAD.value assert record.exchange_adj_amount == 0 assert record.deposit_amount_cad == 951250 - assert record.dest_bank_number == '0003' - assert record.batch_number == '002400986' - assert record.jv_type == 'I' - assert record.jv_number == '002425669' + assert record.dest_bank_number == "0003" + assert record.batch_number == "002400986" + assert record.jv_type == "I" + assert record.jv_number == "002425669" assert record.transaction_date == transaction_date assert record.short_name_type == EFTShortnameType.EFT.value - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='004', - transaction_description='MISC PAYMENT BCONLINE INTERBLOCK C', - deposit_amount='2125000', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='2125000', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="004", + transaction_description="MISC PAYMENT BCONLINE INTERBLOCK C", + deposit_amount="2125000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="2125000", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 4) assert record.index == 4 - assert record.record_type == '2' - assert record.ministry_code == 'AT' - assert record.program_code == '0146' + assert record.record_type == "2" + assert record.ministry_code == "AT" + assert record.program_code == "0146" assert record.deposit_datetime == deposit_datetime - assert record.location_id == '85004' - assert record.transaction_sequence == '004' - assert record.transaction_description == 'MISC PAYMENT BCONLINE INTERBLOCK C' + assert record.location_id == "85004" + assert record.transaction_sequence == "004" + assert record.transaction_description == "MISC PAYMENT BCONLINE INTERBLOCK C" assert record.deposit_amount == 2125000 assert record.currency == EFTConstants.CURRENCY_CAD.value assert record.exchange_adj_amount == 0 assert record.deposit_amount_cad == 2125000 - assert record.dest_bank_number == '0003' - assert record.batch_number == '002400986' - assert record.jv_type == 'I' - assert record.jv_number == '002425669' + assert record.dest_bank_number == "0003" + assert record.batch_number == "002400986" + assert record.jv_type == "I" + assert record.jv_number == "002425669" assert record.transaction_date == transaction_date assert record.short_name_type is None - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='1600', - location_id='85020', - transaction_sequence='001', - transaction_description='', - deposit_amount='119000', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='119000', - destination_bank_number='0010', - batch_number='002400989', - jv_type='I', - jv_number='002425836', - transaction_date='20230810' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="1600", + location_id="85020", + transaction_sequence="001", + transaction_description="", + deposit_amount="119000", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="119000", + destination_bank_number="0010", + batch_number="002400989", + jv_type="I", + jv_number="002425836", + transaction_date="20230810", + ) record: EFTRecord = EFTRecord(content, 5) deposit_datetime = datetime(2023, 8, 10, 16, 0) transaction_date = datetime(2023, 8, 10) assert record.index == 5 - assert record.record_type == '2' - assert record.ministry_code == 'AT' - assert record.program_code == '0146' + assert record.record_type == "2" + assert record.ministry_code == "AT" + assert record.program_code == "0146" assert record.deposit_datetime == deposit_datetime - assert record.location_id == '85020' - assert record.transaction_sequence == '001' - assert record.transaction_description == '' + assert record.location_id == "85020" + assert record.transaction_sequence == "001" + assert record.transaction_description == "" assert record.deposit_amount == 119000 assert record.currency == EFTConstants.CURRENCY_CAD.value assert record.exchange_adj_amount == 0 assert record.deposit_amount_cad == 119000 - assert record.dest_bank_number == '0010' - assert record.batch_number == '002400989' - assert record.jv_type == 'I' - assert record.jv_number == '002425836' + assert record.dest_bank_number == "0010" + assert record.batch_number == "002400989" + assert record.jv_type == "I" + assert record.jv_number == "002425836" assert record.transaction_date == transaction_date def test_eft_parse_record_invalid_length(): """Test EFT record parser invalid length.""" - content = ' ' + content = " " record: EFTRecord = EFTRecord(content, 0) assert record.errors @@ -371,24 +384,25 @@ def test_eft_parse_record_invalid_length(): def test_eft_parse_record_invalid_record_type(): """Test EFT record parser invalid record_type.""" - content = factory_eft_record(record_type='X', - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='001', - transaction_description='DEPOSIT 26', - deposit_amount='13500', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='13500', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type="X", + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="DEPOSIT 26", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="13500", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 0) assert record.errors @@ -400,24 +414,25 @@ def test_eft_parse_record_invalid_record_type(): def test_eft_parse_record_invalid_dates(): """Test EFT record parser for invalid dates.""" - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='2023081 ', - deposit_time='A000', - location_id='85004', - transaction_sequence='001', - transaction_description='DEPOSIT 26', - deposit_amount='13500', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='13500', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='20233001' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="2023081 ", + deposit_time="A000", + location_id="85004", + transaction_sequence="001", + transaction_description="DEPOSIT 26", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="13500", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="20233001", + ) record: EFTRecord = EFTRecord(content, 1) assert record.errors @@ -430,44 +445,45 @@ def test_eft_parse_record_invalid_dates(): assert record.errors[1].index == 1 assert record.index == 1 - assert record.record_type == '2' - assert record.ministry_code == 'AT' - assert record.program_code == '0146' + assert record.record_type == "2" + assert record.ministry_code == "AT" + assert record.program_code == "0146" assert record.deposit_datetime is None - assert record.location_id == '85004' - assert record.transaction_sequence == '001' - assert record.transaction_description == 'DEPOSIT 26' + assert record.location_id == "85004" + assert record.transaction_sequence == "001" + assert record.transaction_description == "DEPOSIT 26" assert record.deposit_amount == 13500 assert record.currency == EFTConstants.CURRENCY_CAD.value assert record.exchange_adj_amount == 0 assert record.deposit_amount_cad == 13500 - assert record.dest_bank_number == '0003' - assert record.batch_number == '002400986' - assert record.jv_type == 'I' - assert record.jv_number == '002425669' + assert record.dest_bank_number == "0003" + assert record.batch_number == "002400986" + assert record.jv_type == "I" + assert record.jv_number == "002425669" assert record.transaction_date is None def test_eft_parse_record_invalid_numbers(): """Test EFT record parser for invalid numbers.""" - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='001', - transaction_description='1234', - deposit_amount='1350A', - currency='', - exchange_adj_amount='ABC', - deposit_amount_cad='1350A', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="1234", + deposit_amount="1350A", + currency="", + exchange_adj_amount="ABC", + deposit_amount_cad="1350A", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 0) # We are expecting the transaction description as this is where we get the BCROS Account number @@ -486,24 +502,25 @@ def test_eft_parse_record_invalid_numbers(): def test_eft_parse_record_transaction_description_required(): """Test EFT record parser transaction description required.""" - content = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, - ministry_code='AT', - program_code='0146', - deposit_date='20230810', - deposit_time='0000', - location_id='85004', - transaction_sequence='001', - transaction_description='', - deposit_amount='13500', - currency='', - exchange_adj_amount='0', - deposit_amount_cad='13500', - destination_bank_number='0003', - batch_number='002400986', - jv_type='I', - jv_number='002425669', - transaction_date='' - ) + content = factory_eft_record( + record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, + ministry_code="AT", + program_code="0146", + deposit_date="20230810", + deposit_time="0000", + location_id="85004", + transaction_sequence="001", + transaction_description="", + deposit_amount="13500", + currency="", + exchange_adj_amount="0", + deposit_amount_cad="13500", + destination_bank_number="0003", + batch_number="002400986", + jv_type="I", + jv_number="002425669", + transaction_date="", + ) record: EFTRecord = EFTRecord(content, 0) # We are expecting the transaction description as this is where we get the BCROS Account number @@ -516,7 +533,7 @@ def test_eft_parse_record_transaction_description_required(): def test_eft_parse_file(): """Test EFT parsing a file.""" - with open('tests/unit/test_data/tdi17_sample.txt', 'r') as f: + with open("tests/unit/test_data/tdi17_sample.txt", "r") as f: contents = f.read() lines = contents.splitlines() header_index = 0 @@ -534,107 +551,107 @@ def test_eft_parse_file(): assert len(eft_records) == 5 assert eft_header.index == 0 - assert eft_header.record_type == '1' + assert eft_header.record_type == "1" assert eft_header.creation_datetime == datetime(2023, 8, 14, 16, 1) assert eft_header.starting_deposit_date == datetime(2023, 8, 10) assert eft_header.ending_deposit_date == datetime(2023, 8, 10) assert eft_trailer.index == 6 - assert eft_trailer.record_type == '7' + assert eft_trailer.record_type == "7" assert eft_trailer.number_of_details == 5 assert eft_trailer.total_deposit_amount == 3733750 assert eft_records[0].index == 1 - assert eft_records[0].record_type == '2' - assert eft_records[0].ministry_code == 'AT' - assert eft_records[0].program_code == '0146' + assert eft_records[0].record_type == "2" + assert eft_records[0].ministry_code == "AT" + assert eft_records[0].program_code == "0146" assert eft_records[0].deposit_datetime == datetime(2023, 8, 10, 0, 0) - assert eft_records[0].location_id == '85004' - assert eft_records[0].transaction_sequence == '001' - assert eft_records[0].transaction_description == 'DEPOSIT 26' + assert eft_records[0].location_id == "85004" + assert eft_records[0].transaction_sequence == "001" + assert eft_records[0].transaction_description == "DEPOSIT 26" assert eft_records[0].deposit_amount == 13500 assert eft_records[0].currency == EFTConstants.CURRENCY_CAD.value assert eft_records[0].exchange_adj_amount == 0 assert eft_records[0].deposit_amount_cad == 13500 - assert eft_records[0].dest_bank_number == '0003' - assert eft_records[0].batch_number == '002400986' - assert eft_records[0].jv_type == 'I' - assert eft_records[0].jv_number == '002425669' + assert eft_records[0].dest_bank_number == "0003" + assert eft_records[0].batch_number == "002400986" + assert eft_records[0].jv_type == "I" + assert eft_records[0].jv_number == "002425669" assert eft_records[0].transaction_date is None assert eft_records[0].short_name_type is None assert eft_records[1].index == 2 - assert eft_records[1].record_type == '2' - assert eft_records[1].ministry_code == 'AT' - assert eft_records[1].program_code == '0146' + assert eft_records[1].record_type == "2" + assert eft_records[1].ministry_code == "AT" + assert eft_records[1].program_code == "0146" assert eft_records[1].deposit_datetime == datetime(2023, 8, 10, 0, 0) - assert eft_records[1].location_id == '85004' - assert eft_records[1].transaction_sequence == '002' - assert eft_records[1].transaction_description == 'HSIMPSON' + assert eft_records[1].location_id == "85004" + assert eft_records[1].transaction_sequence == "002" + assert eft_records[1].transaction_description == "HSIMPSON" assert eft_records[1].deposit_amount == 525000 assert eft_records[1].currency == EFTConstants.CURRENCY_CAD.value assert eft_records[1].exchange_adj_amount == 0 assert eft_records[1].deposit_amount_cad == 525000 - assert eft_records[1].dest_bank_number == '0003' - assert eft_records[1].batch_number == '002400986' - assert eft_records[1].jv_type == 'I' - assert eft_records[1].jv_number == '002425669' + assert eft_records[1].dest_bank_number == "0003" + assert eft_records[1].batch_number == "002400986" + assert eft_records[1].jv_type == "I" + assert eft_records[1].jv_number == "002425669" assert eft_records[1].transaction_date is None assert eft_records[1].short_name_type == EFTShortnameType.WIRE.value assert eft_records[2].index == 3 - assert eft_records[2].record_type == '2' - assert eft_records[2].ministry_code == 'AT' - assert eft_records[2].program_code == '0146' + assert eft_records[2].record_type == "2" + assert eft_records[2].ministry_code == "AT" + assert eft_records[2].program_code == "0146" assert eft_records[2].deposit_datetime == datetime(2023, 8, 10, 0, 0) - assert eft_records[2].location_id == '85004' - assert eft_records[2].transaction_sequence == '003' - assert eft_records[2].transaction_description == 'ABC1234567' + assert eft_records[2].location_id == "85004" + assert eft_records[2].transaction_sequence == "003" + assert eft_records[2].transaction_description == "ABC1234567" assert eft_records[2].deposit_amount == 951250 assert eft_records[2].currency == EFTConstants.CURRENCY_CAD.value assert eft_records[2].exchange_adj_amount == 0 assert eft_records[2].deposit_amount_cad == 951250 - assert eft_records[2].dest_bank_number == '0003' - assert eft_records[2].batch_number == '002400986' - assert eft_records[2].jv_type == 'I' - assert eft_records[2].jv_number == '002425669' + assert eft_records[2].dest_bank_number == "0003" + assert eft_records[2].batch_number == "002400986" + assert eft_records[2].jv_type == "I" + assert eft_records[2].jv_number == "002425669" assert eft_records[2].transaction_date is None assert eft_records[2].short_name_type == EFTShortnameType.EFT.value assert eft_records[3].index == 4 - assert eft_records[3].record_type == '2' - assert eft_records[3].ministry_code == 'AT' - assert eft_records[3].program_code == '0146' + assert eft_records[3].record_type == "2" + assert eft_records[3].ministry_code == "AT" + assert eft_records[3].program_code == "0146" assert eft_records[3].deposit_datetime == datetime(2023, 8, 10, 0, 0) - assert eft_records[3].location_id == '85004' - assert eft_records[3].transaction_sequence == '004' - assert eft_records[3].transaction_description == 'INTERBLOCK C' + assert eft_records[3].location_id == "85004" + assert eft_records[3].transaction_sequence == "004" + assert eft_records[3].transaction_description == "INTERBLOCK C" assert eft_records[3].deposit_amount == 2125000 assert eft_records[3].currency == EFTConstants.CURRENCY_CAD.value assert eft_records[3].exchange_adj_amount == 0 assert eft_records[3].deposit_amount_cad == 2125000 - assert eft_records[3].dest_bank_number == '0003' - assert eft_records[3].batch_number == '002400986' - assert eft_records[3].jv_type == 'I' - assert eft_records[3].jv_number == '002425669' + assert eft_records[3].dest_bank_number == "0003" + assert eft_records[3].batch_number == "002400986" + assert eft_records[3].jv_type == "I" + assert eft_records[3].jv_number == "002425669" assert eft_records[3].transaction_date is None assert eft_records[3].short_name_type == EFTShortnameType.WIRE.value assert eft_records[4].index == 5 - assert eft_records[4].record_type == '2' - assert eft_records[4].ministry_code == 'AT' - assert eft_records[4].program_code == '0146' + assert eft_records[4].record_type == "2" + assert eft_records[4].ministry_code == "AT" + assert eft_records[4].program_code == "0146" assert eft_records[4].deposit_datetime == datetime(2023, 8, 10, 16, 0) - assert eft_records[4].location_id == '85020' - assert eft_records[4].transaction_sequence == '001' - assert eft_records[4].transaction_description == '' + assert eft_records[4].location_id == "85020" + assert eft_records[4].transaction_sequence == "001" + assert eft_records[4].transaction_description == "" assert eft_records[4].deposit_amount == 119000 assert eft_records[4].currency == EFTConstants.CURRENCY_CAD.value assert eft_records[4].exchange_adj_amount == 0 assert eft_records[4].deposit_amount_cad == 119000 - assert eft_records[4].dest_bank_number == '0010' - assert eft_records[4].batch_number == '002400989' - assert eft_records[4].jv_type == 'I' - assert eft_records[4].jv_number == '002425836' + assert eft_records[4].dest_bank_number == "0010" + assert eft_records[4].batch_number == "002400989" + assert eft_records[4].jv_type == "I" + assert eft_records[4].jv_number == "002425836" assert eft_records[4].transaction_date is None assert eft_records[4].short_name_type is None diff --git a/pay-queue/tests/utilities/factory_utils.py b/pay-queue/tests/utilities/factory_utils.py index f493b3b88..faf9abdc8 100644 --- a/pay-queue/tests/utilities/factory_utils.py +++ b/pay-queue/tests/utilities/factory_utils.py @@ -18,11 +18,18 @@ from pay_queue.services.eft.eft_enums import EFTConstants -def factory_eft_header(record_type: str, file_creation_date: str, file_creation_time: str, - deposit_start_date: str, deposit_end_date) -> str: +def factory_eft_header( + record_type: str, + file_creation_date: str, + file_creation_time: str, + deposit_start_date: str, + deposit_end_date, +) -> str: """Produce eft header TDI17 formatted string.""" - result = f'{record_type}CREATION DATE: {file_creation_date}CREATION TIME: {file_creation_time}' \ - f'DEPOSIT DATE(S) FROM: {deposit_start_date} TO DATE : {deposit_end_date}' + result = ( + f"{record_type}CREATION DATE: {file_creation_date}CREATION TIME: {file_creation_time}" + f"DEPOSIT DATE(S) FROM: {deposit_start_date} TO DATE : {deposit_end_date}" + ) result = eft_pad_line_length(result) # Pad end of line length return result @@ -31,28 +38,44 @@ def factory_eft_header(record_type: str, file_creation_date: str, file_creation_ def factory_eft_trailer(record_type: str, number_of_details: str, total_deposit_amount: str) -> str: """Produce eft trailer TDI17 formatted string.""" total_deposit_amount = transform_money_string(total_deposit_amount) - result = f'{record_type}{left_pad_zero(number_of_details, 6)}{left_pad_zero(total_deposit_amount, 14)}' + result = f"{record_type}{left_pad_zero(number_of_details, 6)}{left_pad_zero(total_deposit_amount, 14)}" result = eft_pad_line_length(result) return result -def factory_eft_record(record_type: str, ministry_code: str, program_code: str, - deposit_date: str, deposit_time: str, location_id: str, transaction_sequence: str, - transaction_description: str, deposit_amount: str, currency: str, - exchange_adj_amount: str, deposit_amount_cad: str, destination_bank_number: str, - batch_number: str, jv_type: str, jv_number: str, transaction_date: str) -> str: +def factory_eft_record( + record_type: str, + ministry_code: str, + program_code: str, + deposit_date: str, + deposit_time: str, + location_id: str, + transaction_sequence: str, + transaction_description: str, + deposit_amount: str, + currency: str, + exchange_adj_amount: str, + deposit_amount_cad: str, + destination_bank_number: str, + batch_number: str, + jv_type: str, + jv_number: str, + transaction_date: str, +) -> str: """Produce eft transaction record TDI17 formatted string.""" deposit_amount = transform_money_string(deposit_amount) exchange_adj_amount = transform_money_string(exchange_adj_amount) deposit_amount_cad = transform_money_string(deposit_amount_cad) - result = f'{record_type}{ministry_code}{program_code}{deposit_date}{location_id}' \ - f'{right_pad_space(deposit_time, 4)}' \ - f'{transaction_sequence}{right_pad_space(transaction_description, 40)}' \ - f'{left_pad_zero(deposit_amount, 13)}{right_pad_space(currency, 2)}' \ - f'{left_pad_zero(exchange_adj_amount, 13)}{left_pad_zero(deposit_amount_cad, 13)}' \ - f'{destination_bank_number}{batch_number}{jv_type}{jv_number}{transaction_date}' + result = ( + f"{record_type}{ministry_code}{program_code}{deposit_date}{location_id}" + f"{right_pad_space(deposit_time, 4)}" + f"{transaction_sequence}{right_pad_space(transaction_description, 40)}" + f"{left_pad_zero(deposit_amount, 13)}{right_pad_space(currency, 2)}" + f"{left_pad_zero(exchange_adj_amount, 13)}{left_pad_zero(deposit_amount_cad, 13)}" + f"{destination_bank_number}{batch_number}{jv_type}{jv_number}{transaction_date}" + ) result = eft_pad_line_length(result) return result @@ -62,20 +85,20 @@ def transform_money_string(money_str: str) -> str: """Produce a properly formatted string for TDI17 money values.""" money_str = money_str.strip() - if not money_str.endswith('-'): # Ends with minus sign if it is a negative value - money_str = money_str + ' ' # Add a blank for positive value + if not money_str.endswith("-"): # Ends with minus sign if it is a negative value + money_str = money_str + " " # Add a blank for positive value return money_str def left_pad_zero(value: str, width: int) -> str: """Produce left padded zero string.""" - return '{:0>{}}'.format(value, width) + return "{:0>{}}".format(value, width) def right_pad_space(value: str, width: int) -> str: """Produce end padded white spaced string.""" - return '{:<{}}'.format(value, width) # Pad end of line length + return "{:<{}}".format(value, width) # Pad end of line length def eft_pad_line_length(value: str) -> str: From a49aa23d65faa96006eacc2864e15261aa54fede Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:31:12 -0700 Subject: [PATCH 07/10] point to main --- jobs/payment-jobs/poetry.lock | 8 +-- jobs/payment-jobs/pyproject.toml | 2 +- pay-admin/poetry.lock | 2 +- pay-admin/setup.cfg | 98 ------------------------- pay-queue/poetry.lock | 6 +- pay-queue/pyproject.toml | 2 +- pay-queue/setup.cfg | 119 ------------------------------- 7 files changed, 10 insertions(+), 227 deletions(-) delete mode 100755 pay-admin/setup.cfg delete mode 100644 pay-queue/setup.cfg diff --git a/jobs/payment-jobs/poetry.lock b/jobs/payment-jobs/poetry.lock index 544dc55fd..2056809df 100644 --- a/jobs/payment-jobs/poetry.lock +++ b/jobs/payment-jobs/poetry.lock @@ -2103,9 +2103,9 @@ werkzeug = "3.0.3" [package.source] type = "git" -url = "https://github.com/ochiu/sbc-pay.git" -reference = "ld-override-email-fix" -resolved_reference = "f9ab33c8d6a1336bc40daa2eddf2d7961a46bcee" +url = "https://github.com/bcgov/sbc-pay.git" +reference = "main" +resolved_reference = "726b002cf90082cfbb795c3e7329da9ed8ef1a03" subdirectory = "pay-api" [[package]] @@ -3256,4 +3256,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "c2501e19a1d6786aafc63cf2f5c713817708b2aeea42dde1638af23532b31428" +content-hash = "bb90125c5851f39bc01b06c79c8c5573c0a8b165fe43bb52be8a5f2d6522d345" diff --git a/jobs/payment-jobs/pyproject.toml b/jobs/payment-jobs/pyproject.toml index 44d53e4af..c5010e80d 100644 --- a/jobs/payment-jobs/pyproject.toml +++ b/jobs/payment-jobs/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" -pay-api = {git = "https://github.com/ochiu/sbc-pay.git", branch = "ld-override-email-fix", subdirectory = "pay-api"} +pay-api = {git = "https://github.com/bcgov/sbc-pay.git", branch = "main", subdirectory = "pay-api"} flask = "^3.0.2" flask-sqlalchemy = "^3.1.1" sqlalchemy = "^2.0.28" diff --git a/pay-admin/poetry.lock b/pay-admin/poetry.lock index aa2499a87..27126d3a2 100644 --- a/pay-admin/poetry.lock +++ b/pay-admin/poetry.lock @@ -2009,7 +2009,7 @@ werkzeug = "3.0.3" type = "git" url = "https://github.com/bcgov/sbc-pay.git" reference = "main" -resolved_reference = "d6b54079b123c7f9ca7bb66e32f044f5fed576e7" +resolved_reference = "726b002cf90082cfbb795c3e7329da9ed8ef1a03" subdirectory = "pay-api" [[package]] diff --git a/pay-admin/setup.cfg b/pay-admin/setup.cfg deleted file mode 100755 index 5d4238d87..000000000 --- a/pay-admin/setup.cfg +++ /dev/null @@ -1,98 +0,0 @@ -[metadata] -name = admin -url = https://github.com/bcgov/sbc-pay/ -author = Pay Team -author_email = -classifiers = - Development Status :: Beta - Intended Audience :: Developers / QA - Topic :: Payment - License :: OSI Approved :: Apache Software License - Natural Language :: English - Programming Language :: Python :: 3.7 -license = Apache Software License Version 2.0 -description = A short description of the project -long_description = file: README.md -keywords = - -[options] -zip_safe = True -python_requires = >=3.12 -include_package_data = True -packages = find: - -[options.package_data] -admin = - -[wheel] -universal = 1 - -[bdist_wheel] -universal = 1 - -[aliases] -test = pytest - -[flake8] -ignore = I001, I003, I004, E126, W504 -exclude = .git,*migrations* -max-line-length = 120 -docstring-min-length=10 -per-file-ignores = - */__init__.py:F401 - -[pycodestyle] -max_line_length = 120 -ignore = E501 -docstring-min-length=10 -notes=FIXME,XXX -match_dir = admin -ignored-modules=flask_sqlalchemy - sqlalchemy -per-file-ignores = - */__init__.py:F401 -good-names= - b, - d, - i, - e, - f, - u, - rv, - logger, - id, - p, - kc, - -[pylint] -ignore=migrations,test -notes=FIXME,XXX,TODO -ignored-modules=flask_sqlalchemy,sqlalchemy,SQLAlchemy,alembic,scoped_session -ignored-classes=scoped_session -min-similarity-lines=15 -disable=C0301,W0511 -good-names= - b, - d, - i, - e, - f, - u, - rv, - logger, - id, - p, - kc, - -[isort] -line_length = 120 -indent = 4 -multi_line_output = 4 -lines_after_imports = 2 - - -[tool:pytest] -addopts = --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml -testpaths = tests/unit -filterwarnings = - ignore::UserWarning diff --git a/pay-queue/poetry.lock b/pay-queue/poetry.lock index 9e5c1beee..790079672 100644 --- a/pay-queue/poetry.lock +++ b/pay-queue/poetry.lock @@ -2062,8 +2062,8 @@ werkzeug = "3.0.3" [package.source] type = "git" url = "https://github.com/bcgov/sbc-pay.git" -reference = "28731edaf70d6626d9bc122bf6cae3c1590788c5" -resolved_reference = "28731edaf70d6626d9bc122bf6cae3c1590788c5" +reference = "main" +resolved_reference = "726b002cf90082cfbb795c3e7329da9ed8ef1a03" subdirectory = "pay-api" [[package]] @@ -3186,4 +3186,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "6f6bbae22c7f261d99596df24c3e9dd621d97b8dd00956357dfdc3ec420cba59" +content-hash = "6c1a79b267ceaf69c00825240f6e5f58f3b93c0bb9fb61d9ae5735829f90518f" diff --git a/pay-queue/pyproject.toml b/pay-queue/pyproject.toml index 74830de62..71fc8719c 100644 --- a/pay-queue/pyproject.toml +++ b/pay-queue/pyproject.toml @@ -19,7 +19,7 @@ itsdangerous = "^2.1.2" protobuf = "4.25.3" launchdarkly-server-sdk = "^8.2.1" cachecontrol = "^0.14.0" -pay-api = {git = "https://github.com/bcgov/sbc-pay.git", subdirectory = "pay-api", rev = "28731edaf70d6626d9bc122bf6cae3c1590788c5"} +pay-api = {git = "https://github.com/bcgov/sbc-pay.git", subdirectory = "pay-api", branch = "main"} pg8000 = "^1.30.5" diff --git a/pay-queue/setup.cfg b/pay-queue/setup.cfg deleted file mode 100644 index 8cf744f13..000000000 --- a/pay-queue/setup.cfg +++ /dev/null @@ -1,119 +0,0 @@ -[metadata] -name = pay_queue -url = https://github.com/bcgov/sbc-pay/ -author = Pay Team -author_email = -classifiers = - Development Status :: Beta - Intended Audience :: Developers / QA - Topic :: Payments - License :: OSI Approved :: Apache Software License - Natural Language :: English - Programming Language :: Python :: 3.8 -license = Apache Software License Version 2.0 -description = A short description of the project -long_description = file: README.md -keywords = - -[options] -zip_safe = True -python_requires = >=3.12 -include_package_data = True -packages = find: - -[options.package_data] -pay_queue = - -[wheel] -universal = 1 - -[bdist_wheel] -universal = 1 - -[aliases] -test = pytest - -[flake8] -ignore = B902 -exclude = .git,*migrations* -max-line-length = 120 -docstring-min-length=10 -per-file-ignores = - */__init__.py:F401 - -[pycodestyle] -max_line_length = 120 -ignore = E501 -docstring-min-length=10 -notes=FIXME,XXX -match_dir = src/pay_queue -ignored-modules=flask_sqlalchemy - sqlalchemy -per-file-ignores = - */__init__.py:F401 -good-names= - b, - d, - i, - e, - f, - k, - u, - v, - ar, - cb, #common shorthand for callback - nc, - rv, - sc, - event_loop, - logger, - loop, - -[pylint] -ignore=migrations,test -notes=FIXME,XXX,TODO -ignored-modules=flask_sqlalchemy,sqlalchemy,SQLAlchemy,alembic,scoped_session -ignored-classes=scoped_session -disable=C0301,W0511,R0801,R0902 -good-names= - b, - d, - i, - e, - f, - k, - u, - v, - ar, - cb, #common shorthand for callback - nc, - rv, - sc, - event_loop, - logger, - loop, - -[isort] -line_length = 120 -indent = 4 -multi_line_output = 4 -lines_after_imports = 2 - -[tool:pytest] -addopts = --cov=src --cov-report html:htmlcov --cov-report xml:coverage.xml -testpaths = tests -filterwarnings = - ignore::UserWarning - -[report:run] -exclude_lines = - pragma: no cover - from - import - def __repr__ - if self.debug: - if settings.DEBUG - raise AssertionError - raise NotImplementedError - if 0: - if __name__ == .__main__.: From db538a0aa6d7b5efa36d6476030c6102df516f58 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:33:01 -0700 Subject: [PATCH 08/10] update ftp-poller --- jobs/ftp-poller/poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/ftp-poller/poetry.lock b/jobs/ftp-poller/poetry.lock index 1a1af6bc5..46984c068 100644 --- a/jobs/ftp-poller/poetry.lock +++ b/jobs/ftp-poller/poetry.lock @@ -2057,7 +2057,7 @@ werkzeug = "3.0.3" type = "git" url = "https://github.com/bcgov/sbc-pay.git" reference = "main" -resolved_reference = "d6b54079b123c7f9ca7bb66e32f044f5fed576e7" +resolved_reference = "726b002cf90082cfbb795c3e7329da9ed8ef1a03" subdirectory = "pay-api" [[package]] From f043e7234e8070ce34b93f25e3fc6c19bc40c4e2 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:37:45 -0700 Subject: [PATCH 09/10] missing disbursement_date in conflict merge --- pay-api/src/pay_api/models/eft_refund.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pay-api/src/pay_api/models/eft_refund.py b/pay-api/src/pay_api/models/eft_refund.py index 1b6f152d5..d3fa6d7c5 100644 --- a/pay-api/src/pay_api/models/eft_refund.py +++ b/pay-api/src/pay_api/models/eft_refund.py @@ -43,6 +43,7 @@ class EFTRefund(Audit): "created_by", "created_name", "created_on", + "disbursement_date", "disbursement_status_code", "decline_reason", "id", From a7cf64112c0aa16b1c979c608e54e0bbf7f8eb38 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Tue, 8 Oct 2024 14:39:01 -0700 Subject: [PATCH 10/10] point to local repo --- pay-queue/poetry.lock | 8 ++++---- pay-queue/pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pay-queue/poetry.lock b/pay-queue/poetry.lock index 790079672..5d5f8e30c 100644 --- a/pay-queue/poetry.lock +++ b/pay-queue/poetry.lock @@ -2061,9 +2061,9 @@ werkzeug = "3.0.3" [package.source] type = "git" -url = "https://github.com/bcgov/sbc-pay.git" -reference = "main" -resolved_reference = "726b002cf90082cfbb795c3e7329da9ed8ef1a03" +url = "https://github.com/seeker25/sbc-pay.git" +reference = "black_part2" +resolved_reference = "f043e7234e8070ce34b93f25e3fc6c19bc40c4e2" subdirectory = "pay-api" [[package]] @@ -3186,4 +3186,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "6c1a79b267ceaf69c00825240f6e5f58f3b93c0bb9fb61d9ae5735829f90518f" +content-hash = "9ef37533c38d42fd867220ad785adab4b9badd424e5361dd64fd8e188d4e2442" diff --git a/pay-queue/pyproject.toml b/pay-queue/pyproject.toml index 71fc8719c..10d17ebf3 100644 --- a/pay-queue/pyproject.toml +++ b/pay-queue/pyproject.toml @@ -19,7 +19,7 @@ itsdangerous = "^2.1.2" protobuf = "4.25.3" launchdarkly-server-sdk = "^8.2.1" cachecontrol = "^0.14.0" -pay-api = {git = "https://github.com/bcgov/sbc-pay.git", subdirectory = "pay-api", branch = "main"} +pay-api = {git = "https://github.com/seeker25/sbc-pay.git", subdirectory = "pay-api", branch = "black_part2"} pg8000 = "^1.30.5"