From 2d6ff1adc206c5700bac0a8f6d88cbf381f473af Mon Sep 17 00:00:00 2001 From: Chinenye Nmoh Date: Sat, 5 Jul 2025 20:49:57 +0100 Subject: [PATCH 1/4] updated loan and repayment endpoint --- Dockerfile | 9 +++-- app/api/routes/routes.py | 3 +- app/api/services/loan_service.py | 12 +++++- app/api/services/repayment_service.py | 10 ++++- app/models/loan.py | 18 ++++++++- app/models/repayment.py | 19 ++++++++- app/swagger/schemas/LoansResponse.json | 11 ++++++ app/swagger/schemas/RepaymentsResponse.json | 44 +++++++++++++++++++++ 8 files changed, 117 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7765b85..da5f4cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,10 @@ ENV FLASK_APP=app.py ENV FLASK_RUN_HOST=0.0.0.0 #COPY scripts/enterypointone.sh scripts/enterypointone.sh +#RUN chmod +x scripts/entry.sh +#ENTRYPOINT ["scripts/entry.sh"] -RUN chmod +x scripts/entry.sh - -ENTRYPOINT ["scripts/entry.sh"] \ No newline at end of file +# Run the application +CMD [ "sh", "-c", \ + "echo 'Starting Gunicorn server...' && \ + exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app"] diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index b0d4827..2c1078e 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -123,7 +123,8 @@ def get_loans(): 'page': request.args.get('page', 1), 'limit': request.args.get('limit', 20) } - # logger.info(f"Get loans request received with filters: {filters}") + #logger.info(f"Get loans request received with filters: {filters}") + response = LoanService.process_request(filters) return response diff --git a/app/api/services/loan_service.py b/app/api/services/loan_service.py index 1628e9d..5ad082b 100644 --- a/app/api/services/loan_service.py +++ b/app/api/services/loan_service.py @@ -81,7 +81,7 @@ class LoanService: limit=limit ) - logger.info(f"Result from loans model cme back") + logger.info(f"Result from loans model cme back ") # Convert loans to dictionary format loans_data = [] @@ -98,6 +98,8 @@ class LoanService: 'current_loan_amount': loan.current_loan_amount, 'status': loan.status, 'tenor': loan.tenor, + 'balance': loan.balance, + 'reference': loan.reference, 'product_id': loan.product_id, 'default_penalty_fee': loan.default_penalty_fee, 'continuous_fee': loan.continuous_fee, @@ -106,7 +108,13 @@ class LoanService: 'installment_amount': loan.installment_amount, 'due_date': loan.due_date.isoformat() if loan.due_date else None, 'created_at': loan.created_at.isoformat() if loan.created_at else None, - 'updated_at': loan.updated_at.isoformat() if loan.updated_at else None + 'updated_at': loan.updated_at.isoformat() if loan.updated_at else None, + 'disburseResult': loan.disburse_result, + 'disburseDescription': loan.disburse_description, + 'verifyResult': loan.verify_result, + 'verifyDescription': loan.verify_description, + 'disburseDate': loan.disburse_date.isoformat() if loan.disburse_date else None, + 'disburseVerify': loan.disburse_verify.isoformat() if loan.disburse_verify else None, }) # Calculate total pages diff --git a/app/api/services/repayment_service.py b/app/api/services/repayment_service.py index 48b2dcc..5c24204 100644 --- a/app/api/services/repayment_service.py +++ b/app/api/services/repayment_service.py @@ -69,7 +69,15 @@ class RepaymentService: 'product_id': repayment.product_id, 'transaction_id': repayment.transaction_id, 'created_at': repayment.created_at.isoformat() if repayment.created_at else None, - 'updated_at': repayment.updated_at.isoformat() if repayment.updated_at else None + 'updated_at': repayment.updated_at.isoformat() if repayment.updated_at else None, + 'repay_date': repayment.repay_date.isoformat() if repayment.repay_date else None, + 'initiated_by': repayment.initiated_by, + 'salary_amount': repayment.salary_amount, + 'verify_date': repayment.verify_date.isoformat() if repayment.verify_date else None, + 'repay_result': repayment.repay_result, + 'repay_description': repayment.repay_description, + 'verify_result': repayment.verify_result, + 'verify_description': repayment.verify_description }) # Calculate total pages diff --git a/app/models/loan.py b/app/models/loan.py index 4f5b0ec..0b1f35c 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -36,6 +36,14 @@ class Loan(db.Model): 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)) eligible_amount = db.Column(db.Float, nullable=True, default=0.0) + disburse_date = db.Column(db.DateTime, nullable=True) + disburse_verify = db.Column(db.DateTime, nullable=True) + disburse_result = db.Column(db.String(10), nullable=True) + disburse_description = db.Column(db.String(100), nullable=True) + verify_result = db.Column(db.String(10), nullable=True) + verify_description = db.Column(db.String(100), nullable=True) + reference = db.Column(db.String(50), nullable=True) + balance = db.Column(db.Float, nullable=True, default=0.0) customer = relationship( "Customer", @@ -147,7 +155,15 @@ class Loan(db.Model): 'installment_amount': self.installment_amount, 'due_date': self.due_date.isoformat() if self.due_date else None, 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + 'disburseResult': self.disburse_result, + 'disburseDescription': self.disburse_description, + 'verifyResult': self.verify_result, + 'verifyDescription': self.verify_description, + 'disburseDate': self.disburse_date.isoformat() if self.disburse_date else None, + 'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None, + 'reference': self.reference, + 'balance': self.balance, } def __repr__(self): diff --git a/app/models/repayment.py b/app/models/repayment.py index 6635dc2..8bc92a0 100644 --- a/app/models/repayment.py +++ b/app/models/repayment.py @@ -12,6 +12,14 @@ class Repayment(db.Model): 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)) transaction_id = db.Column(db.String(50)) + repay_date = db.Column(db.DateTime, nullable=True) + initiated_by = db.Column(db.String(50), nullable=True) + salary_amount = db.Column(db.Float, nullable=True, default=0.0) + verify_date = db.Column(db.DateTime, nullable=True) + repay_result = db.Column(db.String(10), nullable=True) + repay_description = db.Column(db.String(100), nullable=True) + verify_result = db.Column(db.String(10), nullable=True) + verify_description = db.Column(db.String(100), nullable=True) @classmethod def get_all_repayments(cls, loan_id=None, customer_id=None, product_id=None, @@ -71,7 +79,16 @@ class Repayment(db.Model): 'product_id': self.product_id, 'transaction_id': self.transaction_id, 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + 'repay_date': self.repay_date.isoformat() if self.repay_date else None, + 'initiated_by': self.initiated_by, + 'salary_amount': self.salary_amount, + 'verify_date': self.verify_date.isoformat() if self.verify_date else None, + 'repay_result': self.repay_result, + 'repay_description': self.repay_description, + 'verify_result': self.verify_result, + 'verify_description': self.verify_description + } def __repr__(self): diff --git a/app/swagger/schemas/LoansResponse.json b/app/swagger/schemas/LoansResponse.json index b2a6e6a..bf0304f 100644 --- a/app/swagger/schemas/LoansResponse.json +++ b/app/swagger/schemas/LoansResponse.json @@ -76,6 +76,17 @@ "example": 10500.0, "nullable": true }, + "balance": { + "type": "number", + "format": "float", + "example": 5000.0, + "nullable": true + }, + "reference": { + "type": "string", + "example": "REF12345", + "nullable": true + }, "installment_amount": { "type": "number", "format": "float", diff --git a/app/swagger/schemas/RepaymentsResponse.json b/app/swagger/schemas/RepaymentsResponse.json index f451483..a32cf06 100644 --- a/app/swagger/schemas/RepaymentsResponse.json +++ b/app/swagger/schemas/RepaymentsResponse.json @@ -23,6 +23,50 @@ "example": "TRX123456", "nullable": true }, + "initiated_by": { + "type": "string", + "example": "system", + "nullable": true + }, + "salary_amount": { + "type": "number", + "format": "float", + "example": 1000.0, + "nullable": true + }, + "repay_date": { + "type": "string", + "format": "date-time", + "example": "2025-04-10T16:45:47.879552Z", + "nullable": true + }, + "verify_date": { + "type": "string", + "format": "date-time", + "example": "2025-04-10T16:45:47.879552Z", + "nullable": true + }, + "repay_result": { + "type": "string", + "example": "success", + "nullable": true + }, + "repay_description": { + "type": "string", + "example": "Repayment processed successfully", + "nullable": true + }, + "verify_result": { + "type": "string", + "example": "verified", + "nullable": true + }, + "verify_description": { + "type": "string", + "example": "Verification completed successfully", + "nullable": true + }, + "created_at": { "type": "string", "format": "date-time", From 3f59ed7da3e23b7a05aa6c067bf7cf502efa96c0 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:27:16 +0100 Subject: [PATCH 2/4] [add]: Oracle database configuration --- .example.env | 17 ++++++++++++----- app/config.py | 7 ++++++- requirements.txt | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.example.env b/.example.env index ea2d3c2..294d6b6 100644 --- a/.example.env +++ b/.example.env @@ -19,11 +19,18 @@ APP_PORT=4700 #DATABASE_PORT=5432 #DATABASE_NAME=firstadvancedev -DATABASE_USER=firstadvance -DATABASE_PASSWORD=FirstAdvance! -DATABASE_HOST=dev-data.simbrellang.net -DATABASE_PORT=10532 -DATABASE_NAME=firstadvancedev +DATABASE_USER=system +DATABASE_PASSWORD=FIRSTADV_PASS +DATABASE_HOST=10.10.33.65 +DATABASE_PORT=1521 +DATABASE_SID=FREE + + +# DATABASE_USER=firstadvance +# DATABASE_PASSWORD=FirstAdvance! +# DATABASE_HOST=dev-data.simbrellang.net +# DATABASE_PORT=10532 +# DATABASE_NAME=firstadvancedev #Events if Needed diff --git a/app/config.py b/app/config.py index d1f3065..2e6964f 100644 --- a/app/config.py +++ b/app/config.py @@ -19,8 +19,13 @@ class Config: DATABASE_HOST = os.environ.get("DATABASE_HOST") DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532) DATABASE_NAME = os.environ.get("DATABASE_NAME") + DATABASE_SID = os.environ.get("DATABASE_SID", "FREE") + DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))" + + + # SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" + SQLALCHEMY_DATABASE_URI = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}") - SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" SQLALCHEMY_TRACK_MODIFICATIONS = False SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337") diff --git a/requirements.txt b/requirements.txt index 9d5c945..68abcbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ flask-sqlalchemy flask-migrate psycopg2-binary alembic +oracledb # Schema for validations Flask-Marshmallow==0.15.0 From 04861d2c5222583aece6134a30386645c42ab149 Mon Sep 17 00:00:00 2001 From: Chinenye Nmoh Date: Mon, 7 Jul 2025 20:25:03 +0100 Subject: [PATCH 3/4] added repayment data --- app/api/routes/routes.py | 17 +++ app/api/services/__init__.py | 1 + app/api/services/repayment_data_service.py | 82 ++++++++++++++ app/models/repayment_data.py | 80 +++++++++++++ app/swagger/digifi_swagger.json | 14 +++ app/swagger/paths/RepaymentData.json | 104 +++++++++++++++++ .../schemas/RepaymentDataResponse.json | 106 ++++++++++++++++++ requirements.txt | 2 + 8 files changed, 406 insertions(+) create mode 100644 app/api/services/repayment_data_service.py create mode 100644 app/models/repayment_data.py create mode 100644 app/swagger/paths/RepaymentData.json create mode 100644 app/swagger/schemas/RepaymentDataResponse.json diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index 2c1078e..0f599c0 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -11,6 +11,7 @@ from app.api.services.auth_service import AuthService from app.api.services.dashboard_service import DashboardService from app.api.services.offer_service import OfferService from app.api.services.charge_service import ChargeService +from app.api.services.repayment_data_service import RepaymentDataService from functools import wraps from app.utils.logger import logger from app.api.middlewares import enforce_json, require_auth @@ -180,6 +181,22 @@ def get_all_repayments(): # logger.info(f"Get repayments request received with filters: {filters}") response = RepaymentService.get_all_repayments(filters) return response +@api.route('/repayment-data', methods=['GET']) +# @token_required +def get_all_repayments_data(): + # Extract query parameters for filtering + filters = { + 'customer_id': request.args.get('customer_id'), + 'account_id': request.args.get('account_id'), + 'added_date': request.args.get('added_date'), + 'transaction_id': request.args.get('transaction_id'), + 'fbn_transaction_id': request.args.get('fbn_transaction_id'), + 'page': request.args.get('page', 1), + 'limit': request.args.get('limit', 20) + } + # logger.info(f"Get repayments request received with filters: {filters}") + response = RepaymentDataService.get_all_repayments_data(filters) + return response @api.route('/loan-charges', methods=['GET']) # @token_required diff --git a/app/api/services/__init__.py b/app/api/services/__init__.py index 8ff98b2..8cb2682 100644 --- a/app/api/services/__init__.py +++ b/app/api/services/__init__.py @@ -9,3 +9,4 @@ from app.api.services.repayment_service import RepaymentService from app.api.services.loan_charge_service import LoanChargeService from app.api.services.loan_repayment_schedule_service import LoanRepaymentScheduleService from app.api.services.transaction_offers_service import TransactionOfferService +from app.api.services.repayment_data_service import RepaymentDataService diff --git a/app/api/services/repayment_data_service.py b/app/api/services/repayment_data_service.py new file mode 100644 index 0000000..3cf0d65 --- /dev/null +++ b/app/api/services/repayment_data_service.py @@ -0,0 +1,82 @@ +import logging +from datetime import datetime +from app.models.repayment_data import RepaymentsData +from dateutil.parser import parse as parse_date + +logger = logging.getLogger(__name__) + +class RepaymentDataService: + """ + Service class for handling repayment-data operations. + """ + + @staticmethod + def get_all_repayments_data(filters=None): + try: + if filters is None: + filters = {} + + customer_id = filters.get('customer_id') + account_id = filters.get('account_id') + added_date = filters.get('added_date') + transaction_id = filters.get('transaction_id') + fbn_transaction_id = filters.get('fbn_transaction_id') + + page = int(filters.get('page', 1)) + limit = int(filters.get('limit', 20)) + + if page < 1: + page = 1 + if limit < 1 or limit > 100: + limit = 20 + + if added_date and isinstance(added_date, str): + try: + added_date = parse_date(added_date) + except Exception as parse_err: + logger.error(f"Invalid date format for 'added_date': {added_date}") + return {"message": "Invalid date format for 'added_date'"}, 400 + + repayments_data, total_count = RepaymentsData.get_all_repayment_data( + customer_id=customer_id, + account_id=account_id, + added_date=added_date, + transaction_id=transaction_id, + fbn_transaction_id=fbn_transaction_id, + page=page, + limit=limit + ) + + repayments_list = [] + for repayment in repayments_data: + repayments_list.append({ + 'customer_id': repayment.customer_id, + 'account_id': repayment.account_id, + 'transaction_id': repayment.transaction_id, + 'fbn_transaction_id': repayment.fbn_transaction_id, + 'added_date': repayment.added_date.isoformat() if repayment.added_date else None, + 'response_code': repayment.response_code, + 'response_descr': repayment.response_descr, + 'repayment_amount': repayment.repayment_amount, + 'amount_collected': repayment.amount_collected, + 'balance': repayment.balance + }) + + total_pages = (total_count + limit - 1) // limit + + return { + 'repayment_data': repayments_list, + 'count': len(repayments_list), + 'pagination': { + 'total_count': total_count, + 'total_pages': total_pages, + 'current_page': page, + 'limit': limit, + 'has_next': page < total_pages, + 'has_prev': page > 1 + } + } + + except Exception as e: + logger.error(f"An error occurred: {str(e)}", exc_info=True) + return {"message": "Internal Server Error"}, 500 diff --git a/app/models/repayment_data.py b/app/models/repayment_data.py new file mode 100644 index 0000000..eaa018a --- /dev/null +++ b/app/models/repayment_data.py @@ -0,0 +1,80 @@ +from datetime import datetime, timezone +from app.extensions import db +from app.utils.logger import logger + +class RepaymentsData(db.Model): + __tablename__ = 'repayments_data' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + transaction_id = db.Column(db.String(50), nullable=False) + added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False) + response_code = db.Column(db.String(10), nullable=True) + response_descr = db.Column(db.String(255), nullable=True) + fbn_transaction_id = db.Column(db.String(255),nullable=True) + account_id = db.Column(db.String(50), nullable=True) + customer_id = db.Column(db.String(50), nullable=True) + repayment_amount = db.Column(db.Float, nullable=True) + amount_collected = db.Column(db.Float, nullable=True) + balance = db.Column(db.Float, nullable=True, default=0.0) + + def to_dict(self): + return { + "id": self.id, + "transaction_id": self.transaction_id, + "added_date": self.added_date.isoformat() if self.added_date else None, + "response_code": self.response_code, + "response_descr": self.response_descr, + "customerId": self.customer_id, + "accountId": self.account_id, + "fbnTransactionId": self.fbn_transaction_id, + "repaymentAmount": self.repayment_amount, + "amountCollected": self.amount_collected, + "balance": self.balance + } + + + def __repr__(self): + return f"" + + @classmethod + def get_all_repayment_data(cls, customer_id=None, account_id=None, transaction_id=None, fbn_transaction_id=None, + added_date=None, page=1, limit=20): + """ + Get all repayment data with optional filtering + + Args: + customer_id (str, optional): Filter by customer ID + account_id (str, optional): Filter by account ID + added_date (datetime, optional): Filter by added date + transaction_id (str, optional): Filter by transaction ID + fbn_transaction_id (str, optional): Filter by FBN transaction ID + page (int, optional): Page number for pagination + limit (int, optional): Number of items per page + + Returns: + tuple: (list of Repayment objects, total count) + """ + query = cls.query + if customer_id: + query = query.filter(cls.customer_id == customer_id) + if account_id: + query = query.filter(cls.account_id == account_id) + if transaction_id: + query = query.filter(cls.transaction_id == transaction_id) + if fbn_transaction_id: + query = query.filter(cls.fbn_transaction_id == fbn_transaction_id) + + if added_date: + query = query.filter(cls.added_date >= added_date) + + # Get total count before pagination + total_count = query.count() + + #order + query = query.order_by(cls.added_date.desc()) + + # Apply pagination + offset = (page - 1) * limit + query = query.limit(limit).offset(offset) + + return query.all(), total_count \ No newline at end of file diff --git a/app/swagger/digifi_swagger.json b/app/swagger/digifi_swagger.json index 7df9c43..82befee 100644 --- a/app/swagger/digifi_swagger.json +++ b/app/swagger/digifi_swagger.json @@ -80,6 +80,14 @@ "url": "https://www.simbrellang.net" } }, + { + "name": "Repayment Data", + "description": "Get all repayment data with optional filtering.", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + }, { "name": "Offers", "description": "Get all offers with optional filtering.", @@ -119,6 +127,9 @@ "/loan-charges": { "$ref": "../swagger/paths/LoanCharges.json" }, + "/repayment-data": { + "$ref": "../swagger/paths/RepaymentData.json" + }, "/repayment-schedules": { "$ref": "../swagger/paths/RepaymentSchedules.json" }, @@ -158,6 +169,9 @@ "RepaymentsResponse": { "$ref": "../swagger/schemas/RepaymentsResponse.json" }, + "RepaymentDataResponse": { + "$ref": "../swagger/schemas/RepaymentDataResponse.json" + }, "LoanChargesResponse": { "$ref": "../swagger/schemas/LoanChargesResponse.json" }, diff --git a/app/swagger/paths/RepaymentData.json b/app/swagger/paths/RepaymentData.json new file mode 100644 index 0000000..07f4151 --- /dev/null +++ b/app/swagger/paths/RepaymentData.json @@ -0,0 +1,104 @@ +{ + "get": { + "tags": ["RepaymentData"], + "summary": "Get all repayment data with optional filtering", + "description": "Retrieve repayment data records with optional filtering by transaction ID, customer ID, account ID, FBN transaction ID, and added date. Supports pagination.", + "operationId": "getRepaymentData", + "parameters": [ + { + "name": "transaction_id", + "in": "query", + "description": "Filter by transaction ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "TRX123456789" + }, + { + "name": "customer_id", + "in": "query", + "description": "Filter by customer ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "CID0000025585" + }, + { + "name": "account_id", + "in": "query", + "description": "Filter by account ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "ACCT000000123" + }, + { + "name": "fbn_transaction_id", + "in": "query", + "description": "Filter by FBN transaction ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "FBNTRX7890" + }, + { + "name": "added_date", + "in": "query", + "description": "Filter by added date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2024-01-01T00:00:00Z" + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "minimum": 1 + }, + "example": 1 + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page (max 100)", + "required": false, + "schema": { + "type": "integer", + "default": 20, + "minimum": 1, + "maximum": 100 + }, + "example": 20 + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/RepaymentDataResponse.json" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "500": { + "description": "Internal server error" + } + } + } +} diff --git a/app/swagger/schemas/RepaymentDataResponse.json b/app/swagger/schemas/RepaymentDataResponse.json new file mode 100644 index 0000000..35cbfd2 --- /dev/null +++ b/app/swagger/schemas/RepaymentDataResponse.json @@ -0,0 +1,106 @@ +{ + "type": "object", + "properties": { + "repayment_data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "transaction_id": { + "type": "string", + "example": "TRX123456", + "nullable": false + }, + "added_date": { + "type": "string", + "format": "date-time", + "example": "2025-04-10T16:45:47.879Z" + }, + "response_code": { + "type": "string", + "example": "00", + "nullable": true + }, + "response_descr": { + "type": "string", + "example": "Repayment successful", + "nullable": true + }, + "fbn_transaction_id": { + "type": "string", + "example": "FBN123456", + "nullable": true + }, + "customer_id": { + "type": "string", + "example": "CID0000025585", + "nullable": true + }, + "account_id": { + "type": "string", + "example": "ACCT000000123", + "nullable": true + }, + "repayment_amount": { + "type": "number", + "format": "float", + "example": 15000.0, + "nullable": true + }, + "amount_collected": { + "type": "number", + "format": "float", + "example": 14500.0, + "nullable": true + }, + "balance": { + "type": "number", + "format": "float", + "example": 500.0, + "nullable": true + } + } + } + }, + "count": { + "type": "integer", + "example": 1 + }, + "pagination": { + "type": "object", + "properties": { + "total_count": { + "type": "integer", + "example": 100 + }, + "total_pages": { + "type": "integer", + "example": 5 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "limit": { + "type": "integer", + "example": 20 + }, + "has_next": { + "type": "boolean", + "example": true + }, + "has_prev": { + "type": "boolean", + "example": false + } + } + } + }, + "xml": { + "name": "RepaymentDataResponse" + } +} diff --git a/requirements.txt b/requirements.txt index 9d5c945..7b051ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,8 @@ alembic Flask-Marshmallow==0.15.0 marshmallow==3.19.0 +python-dateutil==2.9.0 + # CORS Flask-Cors==3.0.10 From f4bc5543960a16cb2e9bc3be0de85d51743519b6 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:21:53 +0100 Subject: [PATCH 4/4] [add]: health endpoint --- app/api/routes/routes.py | 46 ++++++++++++++++++++++++++++++++- app/swagger/digifi_swagger.json | 37 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index 0f599c0..7f10dce 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -23,6 +23,9 @@ from flask_jwt_extended import ( get_jwt_identity, create_refresh_token, ) +from sqlalchemy import text +from app.extensions import db +from app.config import settings api = Blueprint('api', __name__) @@ -266,4 +269,45 @@ def get_all_offers(): # } # # logger.info(f"Get charges request received with filters: {filters}") # response = ChargeService.get_all_charges(filters) -# return jsonify(response) \ No newline at end of file +# return jsonify(response) + + +# Health Check Endpoint +@api.route("/health", methods=["GET"]) +def health_check(): + SQLALCHEMY_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI + response = {} + db_status = "Connection Successful" + errors = [] + status = "ok" + + + # Extract the database URI + try: + db_uri = db.engine.url.render_as_string(hide_password=False) + db_uri = db_uri + except Exception as e: + db_uri = "Unavailable" + errors.append(f"Database URI Error: {str(e)}") + + + + # Check database connection + try: + logger.info(f"Database Health == : {SQLALCHEMY_DATABASE_URI}") + db.session.execute(text("SELECT 1")) + except Exception as e: + db_status = "Connection Failed" + errors.append(f"Database Error: {str(e)}") + status = "failed" + + response = { + "status": status, + "db_status": db_status, + "db_uri": db_uri, + "errors": errors or None + } + + + return jsonify(response), 200 if status == "ok" else 500 + diff --git a/app/swagger/digifi_swagger.json b/app/swagger/digifi_swagger.json index 82befee..e478ee5 100644 --- a/app/swagger/digifi_swagger.json +++ b/app/swagger/digifi_swagger.json @@ -103,6 +103,10 @@ "description": "Find out more", "url": "https://www.simbrellang.net" } + }, + { + "name": "Health", + "description": "System health check including DB status." } ], "paths": { @@ -138,6 +142,39 @@ }, "/transaction-offers": { "$ref": "../swagger/paths/TransactionOffers.json" + }, + "/health": { + "get": { + "tags": ["Health"], + "summary": "Health Check", + "description": "Returns service health information including DB connection status.", + "responses": { + "200": { + "description": "Health check successful", + "content": { + "application/json": { + "example": { + "status": "ok", + "db_status": "Connection Successful", + "error": [] + } + } + } + }, + "500": { + "description": "Health check failed", + "content": { + "application/json": { + "example": { + "status": "ok", + "db_status": "Connection Failed", + "error":["could not connect to server: Connection refused"] + } + } + } + } + } + } } }, "components": {