commit 8e2d3712180476507213e469763956616bc64ef3 Author: lennyaiko Date: Thu Mar 27 11:29:36 2025 +0100 initial commit diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70700a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +./vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f1682a3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "editor.lineNumbers": "off", + "editor.padding.top": 3, + "editor.padding.bottom": 3, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.fontSize": 14, + "editor.lineHeight": 4.5, + "editor.suggestFontSize": 15, + // "editor.suggestLineHeight": 4, + "breadcrumbs.enabled": false, + "workbench.tips.enabled": false, + "workbench.statusBar.visible": false, + // "workbench.editor.showTabs": "single", + "git.enableSmartCommit": true, + "workbench.editor.editorActionsLocation": "hidden", + // "workbench.activityBar.location": "hidden", + "workbench.editor.enablePreviewFromQuickOpen": false, + "editor.lightbulb.enabled": "off", + "editor.selectionHighlight": false, + "editor.overviewRulerBorder": false, + "editor.renderLineHighlight": "none", + "editor.occurrencesHighlight": "off" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3148809 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Use Python base image +FROM python:3.10 + +# Set working directory +WORKDIR /app + +# Copy files to container +COPY . /app + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Expose port 5000 +EXPOSE 5000 + +# Set environment variables +ENV FLASK_APP=app.py +ENV FLASK_RUN_HOST=0.0.0.0 + +# Run the application +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "wsgi:wsgi_app"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..634fcf8 Binary files /dev/null and b/__pycache__/app.cpython-310.pyc differ diff --git a/__pycache__/wsgi.cpython-310.pyc b/__pycache__/wsgi.cpython-310.pyc new file mode 100644 index 0000000..991ee41 Binary files /dev/null and b/__pycache__/wsgi.cpython-310.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..031e1d7 --- /dev/null +++ b/app.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..a98c30b --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,27 @@ +from flask import Flask +from flask_cors import CORS +from app.config import Config +from app.routes import auth_bp, eligibility_bp +from app.errors import method_not_allowed, unsupported_media_type + + +def create_app(): + """Factory function to create a Flask app instance""" + app = Flask(__name__) + + # Load configuration + app.config.from_object(Config) + + CORS(app) + + # Register blueprints + app.register_blueprint(auth_bp) + # app.register_blueprint(loan_bp) + app.register_blueprint(eligibility_bp, url_prefix="/eligibility") + # app.register_blueprint(repayment_bp) + + # Error Handlers + app.register_error_handler(405, method_not_allowed) + app.register_error_handler(415, unsupported_media_type) + + return app diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..30e884e Binary files /dev/null and b/app/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/__pycache__/config.cpython-310.pyc b/app/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..b2fd6e9 Binary files /dev/null and b/app/__pycache__/config.cpython-310.pyc differ diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..598d366 --- /dev/null +++ b/app/config.py @@ -0,0 +1,9 @@ +import os + +class Config: + """Base configuration for Flask app""" + + SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey") + API_BASE_URL = "https://coreapi.dev.simbrellang.net/v1/api/salary" + JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your_jwt_secret") + DEBUG = True \ No newline at end of file diff --git a/app/errors/__init__.py b/app/errors/__init__.py new file mode 100644 index 0000000..15c380c --- /dev/null +++ b/app/errors/__init__.py @@ -0,0 +1 @@ +from .handlers import method_not_allowed, unsupported_media_type \ No newline at end of file diff --git a/app/errors/__pycache__/__init__.cpython-310.pyc b/app/errors/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..4ca4fc6 Binary files /dev/null and b/app/errors/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/errors/__pycache__/handlers.cpython-310.pyc b/app/errors/__pycache__/handlers.cpython-310.pyc new file mode 100644 index 0000000..aed3a0e Binary files /dev/null and b/app/errors/__pycache__/handlers.cpython-310.pyc differ diff --git a/app/errors/handlers.py b/app/errors/handlers.py new file mode 100644 index 0000000..30257b3 --- /dev/null +++ b/app/errors/handlers.py @@ -0,0 +1,18 @@ +from flask import jsonify +from app.helpers.response_helper import ResponseHelper + + +def method_not_allowed(error): + return ResponseHelper.method_not_allowed(message="Method Not Allowed") + + +def not_found(error): + return ResponseHelper.not_found(message="URL Not Found") + + +def bad_request(error): + return ResponseHelper.bad_request(message="Bad Request") + + +def unsupported_media_type(error): + return ResponseHelper.error(message="Unsupported Media Type", status_code=415) diff --git a/app/helpers/__pycache__/response_helper.cpython-310.pyc b/app/helpers/__pycache__/response_helper.cpython-310.pyc new file mode 100644 index 0000000..2f0f071 Binary files /dev/null and b/app/helpers/__pycache__/response_helper.cpython-310.pyc differ diff --git a/app/helpers/response_helper.py b/app/helpers/response_helper.py new file mode 100644 index 0000000..adcc178 --- /dev/null +++ b/app/helpers/response_helper.py @@ -0,0 +1,251 @@ +from flask import jsonify +from typing import List, Dict, Union, Optional, Any + + +class ResponseHelper: + """ + A helper class for building standardized JSON responses in Flask. + """ + + @staticmethod + def build_response( + status: bool, + message: str, + data: Optional[Union[Dict, List, str]] = None, + status_code: int = 200, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Build a standardized JSON response. + + Args: + status (bool): Indicates whether the request was successful. + message (str): A message describing the result of the request. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + status_code (int): The HTTP status code for the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + response = { + "status": status, + "statusCode": status_code, + "message": message, + "data": data if data is not None else {}, + "error": error if error is not None else {}, + } + return jsonify(response), status_code + + @staticmethod + def success( + data: Optional[Union[Dict, List, str]] = None, + message: str = "Successful", + status_code: int = 200, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a success response. + + Args: + data (Optional[Union[Dict, List, str]]): The data to return in the response. + message (str): A message describing the result of the request. + status_code (int): The HTTP status code for the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(True, message, data, status_code, error) + + @staticmethod + def error( + message: str = "An error occurred", + status_code: int = 400, + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return an error response. + + Args: + message (str): A message describing the error. + status_code (int): The HTTP status code for the response. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, status_code, error) + + @staticmethod + def created( + data: Optional[Union[Dict, List, str]] = None, + message: str = "Resource created successfully", + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for a created resource. + + Args: + data (Optional[Union[Dict, List, str]]): The data to return in the response. + message (str): A message describing the result of the request. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(True, message, data, 201, error) + + @staticmethod + def updated( + data: Optional[Union[Dict, List, str]] = None, + message: str = "Resource updated successfully", + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for an updated resource. + + Args: + data (Optional[Union[Dict, List, str]]): The data to return in the response. + message (str): A message describing the result of the request. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(True, message, data, 200, error) + + @staticmethod + def internal_server_error( + message: str = "Internal Server Error", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for an internal server error. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 500, error) + + @staticmethod + def unauthorized( + message: str = "Unauthorized", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for an unauthorized request. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 401, error) + + @staticmethod + def forbidden( + message: str = "Forbidden", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for a forbidden request. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 403, error) + + @staticmethod + def not_found( + message: str = "Resource not found", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for a not found resource. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 404, error) + + @staticmethod + def unprocessable_entity( + message: str = "Unprocessable entity", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for an unprocessable entity. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 422, error) + + @staticmethod + def method_not_allowed( + message: str = "Method Not Allowed", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for a method not allowed error. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 405, error) + + @staticmethod + def bad_request( + message: str = "Bad Request", + data: Optional[Union[Dict, List, str]] = None, + error: Optional[Union[Dict, str]] = None, + ) -> Dict[str, Any]: + """ + Return a response for a bad request error. + + Args: + message (str): A message describing the error. + data (Optional[Union[Dict, List, str]]): The data to return in the response. + error (Optional[Union[Dict, str]]): Any error details to include in the response. + + Returns: + Dict[str, Any]: A dictionary representing the JSON response. + """ + return ResponseHelper.build_response(False, message, data, 400, error) \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..d7d4421 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,4 @@ +from .authentication import auth_bp +from .eligibility import eligibility_bp +# from .loan import loan_bp +# from .repayment import repayment_bp \ No newline at end of file diff --git a/app/routes/__pycache__/__init__.cpython-310.pyc b/app/routes/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..be0688c Binary files /dev/null and b/app/routes/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/routes/__pycache__/authentication.cpython-310.pyc b/app/routes/__pycache__/authentication.cpython-310.pyc new file mode 100644 index 0000000..a0bed10 Binary files /dev/null and b/app/routes/__pycache__/authentication.cpython-310.pyc differ diff --git a/app/routes/__pycache__/eligibility.cpython-310.pyc b/app/routes/__pycache__/eligibility.cpython-310.pyc new file mode 100644 index 0000000..e8711c0 Binary files /dev/null and b/app/routes/__pycache__/eligibility.cpython-310.pyc differ diff --git a/app/routes/authentication.py b/app/routes/authentication.py new file mode 100644 index 0000000..d8e3524 --- /dev/null +++ b/app/routes/authentication.py @@ -0,0 +1,19 @@ +from flask import Blueprint, request, jsonify +import requests +from app.utils.auth import get_headers + +auth_bp = Blueprint("auth", __name__) + +@auth_bp.route("/health", methods=["GET"]) +def health(): + return jsonify({"status": "Up"}) + +@auth_bp.route("/login", methods=["POST"]) +def login(): + data = request.json + api_url = "https://coreapi.dev.simbrellang.net/api/auth/login" + + response = requests.post(api_url, json=data) + if response.status_code == 200: + return jsonify(response.json()), 200 + return jsonify({"error": "Invalid credentials"}), response.status_code \ No newline at end of file diff --git a/app/routes/eligibility.py b/app/routes/eligibility.py new file mode 100644 index 0000000..d8c7cea --- /dev/null +++ b/app/routes/eligibility.py @@ -0,0 +1,42 @@ +from flask import Blueprint, request, jsonify, current_app +import requests +from app.utils.auth import get_headers + +eligibility_bp = Blueprint("eligibility", __name__) + + +@eligibility_bp.route("/check", methods=["POST"]) +def eligibility_check(): + data = request.json + api_url = f"{current_app.config['API_BASE_URL']}/EligibilityCheck" + + print(api_url) + + # response = requests.post(api_url, json=data, headers=get_headers()) + # return jsonify(response.json()), response.status_code + response = { + "customerId": "CN621868", + "transactionId": "Tr201712RK9232P115", + "countryCode": "NGR", + "msisdn": "2348012345678", + "eligibleOffers": [ + { + "offerId": 101, + "minAmount": 5000, + "maxAmount": 20000, + "productId": 2030, + "tenor": 30, + }, + { + "offerId": 102, + "minAmount": 20000, + "maxAmount": 50000, + "productId": 2090, + "tenor": 90, + }, + ], + "resultCode": "00", + "resultDescription": "Successful", + } + + return jsonify(response), 200 diff --git a/app/routes/loan.py b/app/routes/loan.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/repayment.py b/app/routes/repayment.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/__pycache__/__init__.cpython-310.pyc b/app/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..eea9cde Binary files /dev/null and b/app/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/utils/__pycache__/auth.cpython-310.pyc b/app/utils/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000..bb7109e Binary files /dev/null and b/app/utils/__pycache__/auth.cpython-310.pyc differ diff --git a/app/utils/auth.py b/app/utils/auth.py new file mode 100644 index 0000000..0ef72b2 --- /dev/null +++ b/app/utils/auth.py @@ -0,0 +1,8 @@ +import requests +from flask import current_app + +def get_headers(): + return { + "Authorization": f"Bearer {current_app.config['JWT_SECRET_KEY']}", + "Content-Type": "application/json" + } diff --git a/app/utils/logger.py b/app/utils/logger.py new file mode 100644 index 0000000..ecf9380 --- /dev/null +++ b/app/utils/logger.py @@ -0,0 +1,13 @@ +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + # logging.StreamHandler(), # Log to console + logging.FileHandler("app.log", mode='a') # Log to file + ] +) + +logger = logging.getLogger("DetectionService") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..29fbe9f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.8" + +services: + flask: + build: . + ports: + - "5000:5000" + environment: + - FLASK_APP=app.py + - FLASK_RUN_HOST=0.0.0.0 + volumes: + - .:/app + restart: always + networks: + - digital + + swagger: + image: swaggerapi/swagger-ui + ports: + - "9000:8080" + volumes: + - ./openapi.yml:/usr/local/openapi.yml + environment: + - SWAGGER_JSON=/usr/local/openapi.yml + restart: always + networks: + - digital +networks: + digital: + driver: bridge diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..25f7fbf --- /dev/null +++ b/openapi.yml @@ -0,0 +1,60 @@ +openapi: 3.0.3 +info: + title: Sample Flask API + description: A simple Flask API with Swagger documentation running in Docker + version: 1.0.0 + contact: + name: API Support + email: support@example.com + license: + name: MIT + url: https://opensource.org/licenses/MIT + +servers: + - url: http://localhost:5000 + description: Local development server + +paths: + /health: + get: + summary: Returns a health message + responses: + 200: + description: A successful response + /eligibility/check: + post: + summary: Performs eligibility check on a user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + transactionId: + type: string + description: The transaction ID + example: Tr201712RK9232P115 + customerId: + type: string + description: The customer ID + example: CN621868 + countryCode: + type: string + description: The country code + example: NGR + accountId: + type: string + description: The account ID + example: ACN8263457 + msisdn: + type: string + description: The MSISDN + example: 8012345678 + channel: + type: string + description: The channel + example: 100 + responses: + 200: + description: A successful response diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ae70549 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# Flask and Extensions +Flask==2.3.3 +Flask-Marshmallow==0.15.0 +marshmallow==3.19.0 +Flask-Cors==3.0.10 +gunicorn +requests \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..49f70e1 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,7 @@ +from app import create_app + +app = create_app() + +if __name__ != "__main__": + # Expose WSGI app instance for Gunicorn + wsgi_app = app