From 9b36592fcec57ca2b642e787a7e789332b625344 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 14:16:36 +0100 Subject: [PATCH 1/9] move kafka to threadings and setup database connection --- app/__init__.py | 4 ++++ app/config.py | 10 ++++++++++ app/extensions.py | 3 +++ openapi.yml | 18 ++++++++++++++++++ requirements.txt | 5 ++++- wsgi.py | 18 +++++++++--------- 6 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 app/extensions.py diff --git a/app/__init__.py b/app/__init__.py index a4a03c8..52cca2e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,6 +3,7 @@ from flask_cors import CORS from app.config import Config from app.routes import auth_bp, autocall_bp from app.errors import method_not_allowed, unsupported_media_type +from app.extensions import db def create_app(): @@ -23,4 +24,7 @@ def create_app(): app.register_error_handler(405, method_not_allowed) app.register_error_handler(415, unsupported_media_type) + # Database + db.init_app(app) + return app diff --git a/app/config.py b/app/config.py index 9a20816..917a7fb 100644 --- a/app/config.py +++ b/app/config.py @@ -28,5 +28,15 @@ class Config: "BANK_CALL_BASIC_AUTH_PASSWORD", "password" ) + DATABASE_USER = os.getenv("DATABASE_USER") + DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD") + DATABASE_HOST = os.getenv("DATABASE_HOST") + DATABASE_NAME = os.getenv("DATABASE_NAME") + DATABASE_PORT = os.getenv("DATABASE_PORT", 10532) + + SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" + SQLALCHEMY_TRACK_MODIFICATIONS = False + + settings = Config() diff --git a/app/extensions.py b/app/extensions.py new file mode 100644 index 0000000..2e1eeb6 --- /dev/null +++ b/app/extensions.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/openapi.yml b/openapi.yml index 8db5711..c4bc462 100644 --- a/openapi.yml +++ b/openapi.yml @@ -95,6 +95,24 @@ paths: unicode: type: boolean example: true + responses: + 200: + description: A successful response + /autocall/refresh-verify-disbursement: + get: + summary: Refresh the disbursement to verify + responses: + 200: + description: A successful response + /autocall/refresh-disbursement: + get: + summary: Refresh the disbursement + responses: + 200: + description: A successful response + /autocall/payment-callback: + get: + summary: The Payment callback responses: 200: description: A successful response \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a5968da..a9e0716 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,7 @@ marshmallow==3.19.0 Flask-Cors==3.0.10 gunicorn requests -confluent-kafka==1.9.2 \ No newline at end of file +confluent-kafka==1.9.2 +flask-sqlalchemy +psycopg2-binary +alembic \ No newline at end of file diff --git a/wsgi.py b/wsgi.py index 55e8617..b59093d 100644 --- a/wsgi.py +++ b/wsgi.py @@ -1,17 +1,13 @@ +import threading from app import create_app from app.integrations import KafkaIntegration from app.config import settings from app.utils.logger import logger app = create_app() +kafka = KafkaIntegration() -if __name__ != "__main__": - - #Expose WSGI app instance for Gunicorn - wsgi_app = app - - kafka = KafkaIntegration() - +def start_kafka_consumer(): logger.info("Starting Kafka consumer...") while True: try: @@ -28,8 +24,12 @@ if __name__ != "__main__": except Exception as e: logger.error(f"Error while receiving message: {e}") - raise +if __name__ != "__main__": + # Expose WSGI app instance for Gunicorn - # wsgi_app = app + wsgi_app = app + + # Start kafka in a thread + # threading.Thread(target=start_kafka_consumer, daemon=True).start() From b21a4c00298a4e52e79a9a55b9fd44bf47de3ffb Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 14:44:37 +0100 Subject: [PATCH 2/9] improve responses --- app/__init__.py | 4 +++- app/errors/__init__.py | 1 - app/response/__init__.py | 2 ++ app/{errors => response}/handlers.py | 12 ++++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) delete mode 100644 app/errors/__init__.py create mode 100644 app/response/__init__.py rename app/{errors => response}/handlers.py (70%) diff --git a/app/__init__.py b/app/__init__.py index 52cca2e..a42cecb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,7 +2,7 @@ from flask import Flask from flask_cors import CORS from app.config import Config from app.routes import auth_bp, autocall_bp -from app.errors import method_not_allowed, unsupported_media_type +from app.response import (method_not_allowed, unsupported_media_type, not_found, bad_request) from app.extensions import db @@ -23,6 +23,8 @@ def create_app(): # Error Handlers app.register_error_handler(405, method_not_allowed) app.register_error_handler(415, unsupported_media_type) + app.register_error_handler(404, not_found) + app.register_error_handler(400, bad_request) # Database db.init_app(app) diff --git a/app/errors/__init__.py b/app/errors/__init__.py deleted file mode 100644 index 15c380c..0000000 --- a/app/errors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .handlers import method_not_allowed, unsupported_media_type \ No newline at end of file diff --git a/app/response/__init__.py b/app/response/__init__.py new file mode 100644 index 0000000..7c0277e --- /dev/null +++ b/app/response/__init__.py @@ -0,0 +1,2 @@ +from .handlers import (method_not_allowed, unsupported_media_type, +not_found, bad_request, success, created, updated) \ No newline at end of file diff --git a/app/errors/handlers.py b/app/response/handlers.py similarity index 70% rename from app/errors/handlers.py rename to app/response/handlers.py index 30257b3..2ce566a 100644 --- a/app/errors/handlers.py +++ b/app/response/handlers.py @@ -16,3 +16,15 @@ def bad_request(error): def unsupported_media_type(error): return ResponseHelper.error(message="Unsupported Media Type", status_code=415) + + +def success(data): + return ResponseHelper.success(data=data) + + +def created(data): + return ResponseHelper.created(data=data) + + +def updated(data): + return ResponseHelper.updated(data=data) From 32e3ca5bb0fdf6001cda7b49bb9cd59322922152 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 15:01:58 +0100 Subject: [PATCH 3/9] progress on connecting db --- app/models/__init__.py | 1 + app/models/transactions.py | 25 +++++++++++++++++++++++++ wsgi.py | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 app/models/__init__.py create mode 100644 app/models/transactions.py diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..f386329 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +from .transactions import Transaction \ No newline at end of file diff --git a/app/models/transactions.py b/app/models/transactions.py new file mode 100644 index 0000000..9d500d2 --- /dev/null +++ b/app/models/transactions.py @@ -0,0 +1,25 @@ +from app.extensions import db +from datetime import datetime, timezone + +class Transaction(db.Model): + __tablename__ = "transactions" + + id = db.Column( + db.Integer, + primary_key=True, + autoincrement=True, + ) + transaction_id = db.Column(db.String(50), nullable=False) + account_id = db.Column(db.String(50), nullable=True) + customer_id = db.Column(db.String(50), nullable=True) + type = db.Column(db.String(50), nullable=False) + channel = db.Column(db.String(50), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) + + def __repr__(self): + return f'' + + @classmethod + def get_transaction_by_id(cls, transaction_id): + return cls.query.get(transaction_id) \ No newline at end of file diff --git a/wsgi.py b/wsgi.py index b59093d..f49c773 100644 --- a/wsgi.py +++ b/wsgi.py @@ -32,4 +32,4 @@ if __name__ != "__main__": wsgi_app = app # Start kafka in a thread - # threading.Thread(target=start_kafka_consumer, daemon=True).start() + threading.Thread(target=start_kafka_consumer, daemon=True).start() From 4685ddc1c82f435bfa49bddfff7edcd389857867 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 16:39:05 +0100 Subject: [PATCH 4/9] bug fix on application context with threads --- app/__init__.py | 2 +- app/integrations/simbrella.py | 39 +++++++++++++---------------------- app/models/__init__.py | 4 +++- app/utils/auth.py | 10 ++------- wsgi.py | 30 ++++++++++++++------------- 5 files changed, 36 insertions(+), 49 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index a42cecb..e5e7e35 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -13,13 +13,13 @@ def create_app(): # Load configuration app.config.from_object(Config) + # Setup CORS CORS(app) # Register blueprints app.register_blueprint(auth_bp) app.register_blueprint(autocall_bp, url_prefix="/autocall") - # Error Handlers app.register_error_handler(405, method_not_allowed) app.register_error_handler(415, unsupported_media_type) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index b279368..392142f 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -2,7 +2,9 @@ import requests from app.config import settings from app.utils.auth import get_headers from app.utils.logger import logger -from flask import jsonify +from flask import jsonify, current_app +from app.models.transactions import Transaction + class SimbrellaClient: @@ -11,9 +13,14 @@ class SimbrellaClient: @staticmethod def disbursement(data): api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/Disbursement" - logger.info(f"BANK_CALL_BASE_URL = {SimbrellaClient.BANK_CALL_BASE_URL}") logger.info(f"Calling Disbursement endpoint with data: {data}") + # Check if the transaction exists + logger.info(f"Checking if transaction exists") + with current_app.app_context(): + transaction = Transaction.get_transaction_by_id(transaction_id=data['transactionId']) + logger.info(f"Response from database: {transaction}") + disbursement_data ={ "requestId": data['requestId'], "transactionId": data['transactionId'], @@ -45,7 +52,6 @@ class SimbrellaClient: @staticmethod def collect_loan(data): api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/CollectLoan" - logger.info(f"BANK_CALL_BASE_URL = {SimbrellaClient.BANK_CALL_BASE_URL}") logger.info(f"Calling CollectLoan endpoint with data: {data}") collect_loan_data = { @@ -76,14 +82,7 @@ class SimbrellaClient: @staticmethod def verify_transaction(): - # api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/TransactionVerify" - # logger.info(f"BANK_CALL_BASE_URL = {SimbrellaClient.BANK_CALL_BASE_URL}") - # logger.info(f"Calling TransactionVerify endpoint with data: {data}") - try: - # logger.info(f"Here is your TransactionVerify Request data ***** : {data}") - # response = requests.post(api_url, json=data, headers=get_headers()) - # logger.info(f"TransactionVerify response: {response.json()}") return { "status": "00", @@ -96,16 +95,9 @@ class SimbrellaClient: @staticmethod def refresh_disbursement(data): - # api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/Disbursement" - # logger.info(f"BANK_CALL_BASE_URL = {SimbrellaClient.BANK_CALL_BASE_URL}") - # logger.info(f"Calling Disbursement endpoint with data: {data}") try: logger.info(f"Here is your Disbursement Request data ***** : {data}") - # response = requests.post(api_url, json=data, headers=get_headers()) - # logger.info(f"Disbursement response: {response.json()}") - - # return response.json() return data @@ -115,19 +107,16 @@ class SimbrellaClient: @staticmethod def payment_callback(data): - # api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/Payment" - # logger.info(f"BANK_CALL_BASE_URL = {SimbrellaClient.BANK_CALL_BASE_URL}") - # logger.info(f"Calling Payment Callback endpoint with data: {data}") try: logger.info(f"Here is your Payment Callback Request data ***** : {data}") - # response = requests.post(api_url, json=data, headers=get_headers()) - # logger.info(f"Payment Callback response: {response.json()}") - - # return response.json() return data except Exception as e: logger.info(f"Failed to call Payment Callback endpoint: {e}") - raise \ No newline at end of file + raise + + @staticmethod + def check_transaction(txn_id): + return run_in_app_context(Transaction.get_transaction_by_id(txn_id)) \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index f386329..0f0d015 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1 +1,3 @@ -from .transactions import Transaction \ No newline at end of file +from .transactions import Transaction + +__all__ = ['Transaction'] \ No newline at end of file diff --git a/app/utils/auth.py b/app/utils/auth.py index 5daf068..0523774 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -2,14 +2,8 @@ from app.config import settings def get_headers(): - # return { - # "Content-Type": "application/json", - # "x-api_key": settings.BANK_CALL_API_KEY, - # "App-Id": settings.BANK_CALL_APP_ID, - # } return { "Content-Type": "application/json", - "x-api-key": "test-api-key-12345", - "App-Id": "app1", + "x-api-key": settings.BANK_CALL_API_KEY, + "App-Id": settings.BANK_CALL_APP_ID, } - diff --git a/wsgi.py b/wsgi.py index f49c773..a5c275c 100644 --- a/wsgi.py +++ b/wsgi.py @@ -7,23 +7,25 @@ from app.utils.logger import logger app = create_app() kafka = KafkaIntegration() -def start_kafka_consumer(): - logger.info("Starting Kafka consumer...") - while True: - try: +def start_kafka_consumer(app): + with app.app_context(): + logger.info("Starting Kafka consumer...") + while True: + try: - message = kafka.receive_disbursement_messages( - topic=settings.KAFKA_PAYMENT_TOPIC, timeout=settings.KAFKA_TIMEOUT - ) + message = kafka.receive_disbursement_messages( + topic=settings.KAFKA_PAYMENT_TOPIC, timeout=settings.KAFKA_TIMEOUT + ) - if message: - logger.info(f"Processed message: {message}") - else: - logger.info("No message received within timeout") + if message: + logger.info(f"Processed message: {message}") + else: + logger.info("No message received within timeout") - except Exception as e: - logger.error(f"Error while receiving message: {e}") + except Exception as e: + logger.error(f"Error while receiving message: {e}") + if __name__ != "__main__": @@ -32,4 +34,4 @@ if __name__ != "__main__": wsgi_app = app # Start kafka in a thread - threading.Thread(target=start_kafka_consumer, daemon=True).start() + threading.Thread(target=start_kafka_consumer, args=(app,), daemon=True).start() \ No newline at end of file From 80b55a06e5d98ec5a30a3ee94abfdd655ebb1c5a Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 16:41:01 +0100 Subject: [PATCH 5/9] now works with database to call transactionId --- app/integrations/simbrella.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index 392142f..a658808 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -17,8 +17,7 @@ class SimbrellaClient: # Check if the transaction exists logger.info(f"Checking if transaction exists") - with current_app.app_context(): - transaction = Transaction.get_transaction_by_id(transaction_id=data['transactionId']) + transaction = Transaction.get_transaction_by_id(transaction_id=data['transactionId']) logger.info(f"Response from database: {transaction}") disbursement_data ={ From 040822f003609606da9336f3361017e32374001b Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 16:51:31 +0100 Subject: [PATCH 6/9] setup services to talk to models --- app/integrations/simbrella.py | 4 ++-- app/services/__init__.py | 1 + app/services/transactions.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 app/services/__init__.py create mode 100644 app/services/transactions.py diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index a658808..40aaac4 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -3,7 +3,7 @@ from app.config import settings from app.utils.auth import get_headers from app.utils.logger import logger from flask import jsonify, current_app -from app.models.transactions import Transaction +from app.services.transactions import TransactionService class SimbrellaClient: @@ -17,7 +17,7 @@ class SimbrellaClient: # Check if the transaction exists logger.info(f"Checking if transaction exists") - transaction = Transaction.get_transaction_by_id(transaction_id=data['transactionId']) + transaction = TransactionService.get_transaction_by_id(transaction_id=data['transactionId']) logger.info(f"Response from database: {transaction}") disbursement_data ={ diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..6cb6078 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1 @@ +from .transactions import TransactionService \ No newline at end of file diff --git a/app/services/transactions.py b/app/services/transactions.py new file mode 100644 index 0000000..f46a680 --- /dev/null +++ b/app/services/transactions.py @@ -0,0 +1,10 @@ +from app.models import Transaction + +class TransactionService: + + @staticmethod + def get_transaction_by_id(transaction_id): + """ + Get the transaction by ID + """ + return Transaction.get_transaction_by_id(transaction_id) \ No newline at end of file From 67fa05c3c06335166153bc08fa95d232e11a2083 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 16:59:30 +0100 Subject: [PATCH 7/9] bug fix on query --- app/models/transactions.py | 4 ++-- app/services/transactions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/transactions.py b/app/models/transactions.py index 9d500d2..17cb957 100644 --- a/app/models/transactions.py +++ b/app/models/transactions.py @@ -21,5 +21,5 @@ class Transaction(db.Model): return f'' @classmethod - def get_transaction_by_id(cls, transaction_id): - return cls.query.get(transaction_id) \ No newline at end of file + def get_transaction_by_transaction_id(cls, transaction_id): + return cls.query.filter_by(transaction_id=transaction_id).first() \ No newline at end of file diff --git a/app/services/transactions.py b/app/services/transactions.py index f46a680..3ce6177 100644 --- a/app/services/transactions.py +++ b/app/services/transactions.py @@ -7,4 +7,4 @@ class TransactionService: """ Get the transaction by ID """ - return Transaction.get_transaction_by_id(transaction_id) \ No newline at end of file + return Transaction.get_transaction_by_transaction_id(transaction_id) \ No newline at end of file From 77a442c5d2e5014556a7221718600b9a1e67d14e Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 17:00:35 +0100 Subject: [PATCH 8/9] done with task --- app/integrations/simbrella.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index 40aaac4..a08e925 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -20,6 +20,9 @@ class SimbrellaClient: transaction = TransactionService.get_transaction_by_id(transaction_id=data['transactionId']) logger.info(f"Response from database: {transaction}") + if not transaction: + return 0 + disbursement_data ={ "requestId": data['requestId'], "transactionId": data['transactionId'], From 1eb53cd44b0d65c9f6ff9131bb9d7ad5403984c4 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Tue, 15 Apr 2025 17:01:24 +0100 Subject: [PATCH 9/9] chore --- app/integrations/simbrella.py | 2 +- app/services/transactions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index a08e925..72217f2 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -17,7 +17,7 @@ class SimbrellaClient: # Check if the transaction exists logger.info(f"Checking if transaction exists") - transaction = TransactionService.get_transaction_by_id(transaction_id=data['transactionId']) + transaction = TransactionService.get_transaction_by_transaction_id(transaction_id=data['transactionId']) logger.info(f"Response from database: {transaction}") if not transaction: diff --git a/app/services/transactions.py b/app/services/transactions.py index 3ce6177..bc09457 100644 --- a/app/services/transactions.py +++ b/app/services/transactions.py @@ -3,7 +3,7 @@ from app.models import Transaction class TransactionService: @staticmethod - def get_transaction_by_id(transaction_id): + def get_transaction_by_transaction_id(transaction_id): """ Get the transaction by ID """