diff --git a/.DS_Store b/.DS_Store index e81c242..42c677d 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 119c583..2f5571e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ This command will build the Docker image and start the Flask application in a co You can check if the Flask application is running by accessing the `/health` endpoint. To perform a health check, run the following command: ```bash -curl http://localhost:6337/health +curl http://localhost:6337/api/health +curl http://localhost:6337/eco/health ``` If the application is running properly, you should receive a response similar to this: @@ -71,7 +72,9 @@ If the application is running properly, you should receive a response similar to You can check the Swagger Doc by accessing the `/documentation` endpoint. Run the following command: ```bash -curl http://localhost:6337/documentation +curl http://localhost:6337/api/documentation +curl http://localhost:6337/eco/documentation + ``` diff --git a/app/__init__.py b/app/__init__.py index 7d6c4f2..ae31d4e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,10 +1,18 @@ +from re import S from flask import Flask import os from flask_swagger_ui import get_swaggerui_blueprint from flask_cors import CORS from app.config import Config from app.api.routes import api +from app.eco.routes import eco from app.errors import register_error_handlers +from flask_jwt_extended import ( + JWTManager, + jwt_required, + create_access_token, + get_jwt_identity, +) def create_app(): """ Factory function to create a Flask app instance """ @@ -14,16 +22,31 @@ def create_app(): app.config.from_object(Config) CORS(app) + JWTManager(app) + CORS(app, supports_credentials=True) # Swagger Doc SWAGGER_URL = app.config.get("SWAGGER_URL") API_URL = app.config.get("API_URL") # Register blueprints with /api prefix for the main API routes - app.register_blueprint(api, url_prefix='/api') + app.register_blueprint(api, url_prefix="/api") + app.register_blueprint(eco, url_prefix="/eco") + + # Swagger UI Blueprint + api_docs = "/api" + SWAGGER_URL + api_url = "/api" + API_URL + swagger_ui_api = get_swaggerui_blueprint(api_docs, api_url) + swagger_ui_api.name = 'swagger_ui_api' # Rename blueprint + app.register_blueprint(swagger_ui_api, url_prefix=api_docs) + + # Second UI (ECO) + eco_docs = "/eco" + SWAGGER_URL + eco_api = "/eco" + API_URL + swagger_ui_eco = get_swaggerui_blueprint(eco_docs, eco_api) + swagger_ui_eco.name = 'swagger_ui_eco' # Rename blueprint + app.register_blueprint(swagger_ui_eco, url_prefix=eco_docs) - swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL) - app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL) # Error Handlers diff --git a/app/config.py b/app/config.py index 3587b8f..fa1822a 100644 --- a/app/config.py +++ b/app/config.py @@ -1,11 +1,12 @@ import os from dotenv import load_dotenv +from datetime import timedelta class Config: """Base configuration for Flask app""" load_dotenv() SWAGGER_URL = os.getenv("SWAGGER_URL") - API_URL = '/api/swagger.json' + API_URL = os.getenv("API_URL") DEBUG = True VALID_APP_ID = os.getenv("VALID_APP_ID", "app1") @@ -15,6 +16,18 @@ class Config: # SQLALCHEMY_TRACK_MODIFICATIONS = False # SECRET_KEY = os.environ.get("SECRET_KEY", "your_secret_key") + JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key") + JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1)) + JWT_REFRESH_TOKEN_EXPIRES = os.getenv( + "JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30) + ) + + BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user") + BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password") + DEBUG = True + def configure(): - load_dotenv() \ No newline at end of file + load_dotenv() + +settings = Config() \ No newline at end of file diff --git a/app/eco/enums/__init__.py b/app/eco/enums/__init__.py new file mode 100644 index 0000000..2e3a1d8 --- /dev/null +++ b/app/eco/enums/__init__.py @@ -0,0 +1 @@ +from .transaction_type import TransactionType \ No newline at end of file diff --git a/app/eco/enums/installment_status.py b/app/eco/enums/installment_status.py new file mode 100644 index 0000000..209c1f1 --- /dev/null +++ b/app/eco/enums/installment_status.py @@ -0,0 +1,7 @@ +from enum import Enum + +class InstallmentStatus(str, Enum): + PENDING = 'PENDING' + PAID = 'PAID' + OVERDUE = 'OVERDUE' + PARTIALLY_PAID = 'PARTIALLY_PAID' \ No newline at end of file diff --git a/app/eco/enums/loan_status.py b/app/eco/enums/loan_status.py new file mode 100644 index 0000000..c0cf7bd --- /dev/null +++ b/app/eco/enums/loan_status.py @@ -0,0 +1,8 @@ +from enum import Enum + +class LoanStatus(str, Enum): + PENDING = 'PENDING' + ACTIVE = 'ACTIVE' + PAID = 'PAID' + DEFAULTED = 'DEFAULTED' + CLOSED = 'CLOSED' \ No newline at end of file diff --git a/app/eco/enums/transaction_type.py b/app/eco/enums/transaction_type.py new file mode 100644 index 0000000..9787788 --- /dev/null +++ b/app/eco/enums/transaction_type.py @@ -0,0 +1,7 @@ +from enum import Enum + +class TransactionType(str, Enum): + DEBT_CLOSURE_NOTIFICATION = "debt_closure_notification" + DISBURSEMENT = "disbursement" + SEND_SMS = "send_sms" + COLLECT_LOAN = "collect_loan" diff --git a/app/eco/helpers/response_helper.py b/app/eco/helpers/response_helper.py new file mode 100644 index 0000000..d2158f0 --- /dev/null +++ b/app/eco/helpers/response_helper.py @@ -0,0 +1,112 @@ +from flask import jsonify +from typing import Optional, Union, Dict, List, Any + + +class ResponseHelper: + """ + A helper class for building standardized JSON responses using resultCode and resultDescription. + """ + + @staticmethod + def build_response( + result_code: str, + result_description: str, + data: Optional[Union[Dict, List, str]] = None + ) -> Dict[str, Any]: + response = { + "resultCode": result_code, + "resultDescription": result_description + } + + if isinstance(data, dict): + response.update(data) + + return jsonify(response) + + @staticmethod + def success( + result_description: str = "Successful", + result_code: str = "00", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def error( + result_description: str = "An error occurred", + result_code: str = "01", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def created( + result_description: str = "Resource created successfully", + result_code: str = "00", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def updated( + result_description: str = "Resource updated successfully", + result_code: str = "00", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def internal_server_error( + result_description: str = "Internal Server Error", + result_code: str = "500", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def unauthorized( + result_description: str = "Unauthorized", + result_code: str = "401", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def forbidden( + result_description: str = "Forbidden", + result_code: str = "403", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def not_found( + result_description: str = "Resource not found", + result_code: str = "404", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def unprocessable_entity( + result_description: str = "Unprocessable entity", + result_code: str = "422", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def method_not_allowed( + result_description: str = "Method Not Allowed", + result_code: str = "405", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) + + @staticmethod + def bad_request( + result_description: str = "Bad Request", + result_code: str = "400", + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + return ResponseHelper.build_response(result_code, result_description, data) \ No newline at end of file diff --git a/app/eco/routes/__init__.py b/app/eco/routes/__init__.py new file mode 100644 index 0000000..4b3e6c4 --- /dev/null +++ b/app/eco/routes/__init__.py @@ -0,0 +1 @@ +from .routes import eco diff --git a/app/eco/routes/routes.py b/app/eco/routes/routes.py new file mode 100644 index 0000000..26d3734 --- /dev/null +++ b/app/eco/routes/routes.py @@ -0,0 +1,87 @@ +from flask import Blueprint, request, jsonify, send_from_directory +from app.eco.services import ( + AuthorizationService, + DisbursementService, + CollectLoanService, + DebtClosureNotificationService, + SendSMSService +) +from app.utils.logger import logger +from flask_jwt_extended import jwt_required +import os + +eco = Blueprint("eco", __name__) + +# Swagger JSON file +@eco.route("/swagger.json", methods=["GET"]) +def swagger_json(): + swagger_dir = os.path.join("swagger") + return send_from_directory(swagger_dir, "eco_digifi_swagger.json") + +@eco.route("/swagger/") +def serve_paths(filename): + swagger_dir = os.path.join("swagger") + return send_from_directory(swagger_dir, filename) + + + +# Disbursement (Simbrella -> EcoBank) +@eco.route("/Disbursement", methods=["POST"]) +@jwt_required() +def disbursement(): + data = request.get_json() + response = DisbursementService.process_request(data) + return response + + +# CollectLoan (Simbrella -> EcoBank) +@eco.route("/CollectLoan", methods=["POST"]) +@jwt_required() +def collect_loan(): + data = request.get_json() + response = CollectLoanService.process_request(data) + return response + + +# Debt Closure Notification (Simbrella -> EcoBank) +@eco.route("/DebtClosureNotification", methods=["POST"]) +@jwt_required() +def debt_closure(): + data = request.get_json() + response = DebtClosureNotificationService.process_request(data) + return response + + +# Send SMS (Simbrella -> EcoBank) +@eco.route("/SendSMS", methods=["POST"]) +@jwt_required() +def send_sms(): + data = request.get_json() + response = SendSMSService.process_request(data) + return response + + + + + +# Health Check +@eco.route("/health", methods=["GET"]) +def health_check(): + return jsonify({"status": "ok"}), 200 + + + +# Authorize endpoint +@eco.route("/Authorize", methods=["POST"]) +def authorize(): + data = request.get_json() + response = AuthorizationService.process_request(data) + return response + + +# Authorize refresh endpoint +@eco.route("/AuthorizeRefresh", methods=["POST"]) +@jwt_required(refresh=True) +def refresh(): + response = AuthorizationService.process_refresh_request() + return response \ No newline at end of file diff --git a/app/eco/schemas/__init__.py b/app/eco/schemas/__init__.py new file mode 100644 index 0000000..591a1da --- /dev/null +++ b/app/eco/schemas/__init__.py @@ -0,0 +1,5 @@ +from .authorization import AuthorizeRequestSchema +from .disbursement import DisbursementSchema +from .debt_closure_notification import DebtClosureNotificationSchema +from .send_sms import SendSmsSchema +from .collect_loan import CollectLoanSchema \ No newline at end of file diff --git a/app/eco/schemas/authorization.py b/app/eco/schemas/authorization.py new file mode 100644 index 0000000..785dd2b --- /dev/null +++ b/app/eco/schemas/authorization.py @@ -0,0 +1,6 @@ +from marshmallow import Schema, fields + + +class AuthorizeRequestSchema(Schema): + username = fields.Str(required=True) + password = fields.Str(required=True) diff --git a/app/eco/schemas/collect_loan.py b/app/eco/schemas/collect_loan.py new file mode 100644 index 0000000..94f51ab --- /dev/null +++ b/app/eco/schemas/collect_loan.py @@ -0,0 +1,13 @@ +from marshmallow import Schema, fields, EXCLUDE + +class CollectLoanSchema(Schema): + requestId = fields.Str(required=True) + affiliateCode = fields.Str(required=True) + debtId = fields.Int(required=True) + principal = fields.Decimal(required=True) + interest = fields.Decimal(required=True) + penalty = fields.Decimal(required=True) + collectAmount = fields.Decimal(required=True) + + class Meta: + unknown = EXCLUDE \ No newline at end of file diff --git a/app/eco/schemas/debt_closure_notification.py b/app/eco/schemas/debt_closure_notification.py new file mode 100644 index 0000000..b3564b3 --- /dev/null +++ b/app/eco/schemas/debt_closure_notification.py @@ -0,0 +1,11 @@ +from marshmallow import Schema, fields, EXCLUDE + +class DebtClosureNotificationSchema(Schema): + requestId = fields.Str(required=True) + affiliateCode = fields.Str(required=True) + customerId = fields.Str(required=True) + accountId = fields.Str(required=True) + debtId = fields.Int(required=True) + + class Meta: + unknown = EXCLUDE diff --git a/app/eco/schemas/disbursement.py b/app/eco/schemas/disbursement.py new file mode 100644 index 0000000..93d8574 --- /dev/null +++ b/app/eco/schemas/disbursement.py @@ -0,0 +1,15 @@ +from marshmallow import Schema, fields, EXCLUDE + +class DisbursementSchema(Schema): + requestId = fields.Str(required=True) + affiliateCode = fields.Str(required=True) + debtId = fields.Int(required=True) + productId = fields.Str(required=True) + customerId = fields.Str(required=True) + accountId = fields.Str(required=True) + provideAmount = fields.Decimal(required=True) + collectAmount = fields.Decimal(required=True) + interestRate = fields.Decimal(required=True) + + class Meta: + unknown = EXCLUDE \ No newline at end of file diff --git a/app/eco/schemas/send_sms.py b/app/eco/schemas/send_sms.py new file mode 100644 index 0000000..4c1302d --- /dev/null +++ b/app/eco/schemas/send_sms.py @@ -0,0 +1,10 @@ +from marshmallow import Schema, fields, EXCLUDE + +class SendSmsSchema(Schema): + requestId = fields.Str(required=True) + phoneNums = fields.List(fields.Str(), required=True) + affiliateCode = fields.Str(required=True) + message = fields.Str(required=True) + + class Meta: + unknown = EXCLUDE \ No newline at end of file diff --git a/app/eco/services/__init__.py b/app/eco/services/__init__.py new file mode 100644 index 0000000..5c31e3b --- /dev/null +++ b/app/eco/services/__init__.py @@ -0,0 +1,5 @@ +from .authorization import AuthorizationService +from .disbursement import DisbursementService +from .debt_closure_notification import DebtClosureNotificationService +from .send_sms import SendSMSService +from .collect_loan import CollectLoanService diff --git a/app/eco/services/authorization.py b/app/eco/services/authorization.py new file mode 100644 index 0000000..7a7c04d --- /dev/null +++ b/app/eco/services/authorization.py @@ -0,0 +1,102 @@ +from flask import request, jsonify +from marshmallow import ValidationError +from app.eco.services.base_service import BaseService +from app.utils.logger import logger +from app.eco.schemas.authorization import AuthorizeRequestSchema +from app.eco.helpers.response_helper import ResponseHelper +from flask_jwt_extended import ( + JWTManager, + jwt_required, + create_access_token, + create_refresh_token, + get_jwt_identity, +) +from app.config import Config + +USERNAME = Config.BASIC_AUTH_USERNAME +PASSWORD = Config.BASIC_AUTH_PASSWORD + + +class AuthorizationService(BaseService): + + @staticmethod + def process_request(data): + """ + Process the Authorization request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + logger.info("Processing Authorization request") + + if not data: + return ResponseHelper.bad_request(result_description="Missing JSON in request") + + # Validate input data using the Authorization schema + schema = AuthorizeRequestSchema() + validated_data = schema.load(data) # Raises ValidationError if invalid + + if ( + validated_data["username"] != USERNAME + or validated_data["password"] != PASSWORD + ): + return ResponseHelper.unauthorized(result_description="Invalid credentials") + + access_token = create_access_token(identity=validated_data["username"]) + refresh_token = create_refresh_token(identity=validated_data["username"]) + + # Simulated processing logic + response_data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + return ResponseHelper.success( + data={"data": response_data}, result_description="Authorization processed successfully" + ) + + except ValidationError as e: + logger.error(f"Validation error: {e}") + return ResponseHelper.bad_request(result_description=f"Validation error: {e}") + + except Exception as e: + logger.error(f"Error processing Authorization request: {e}") + return ResponseHelper.internal_server_error( + result_description=f"Error processing Authorization request: {e}" + ) + + @staticmethod + def process_refresh_request(): + """ + Process the RefreshToken request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + logger.info("Processing RefreshToken request") + + identity = get_jwt_identity() + access_token = create_access_token(identity=identity) + + # Simulated processing logic + response_data = { + "access_token": access_token, + } + + return ResponseHelper.success( + data={"data": response_data}, result_description="RefreshToken processed successfully" + ) + + except Exception as e: + logger.error(f"Error processing RefreshToken request: {e}") + return ResponseHelper.internal_server_error( + result_description=f"Error processing RefreshToken request: {e}" + ) diff --git a/app/eco/services/base_service.py b/app/eco/services/base_service.py new file mode 100644 index 0000000..ca39ecb --- /dev/null +++ b/app/eco/services/base_service.py @@ -0,0 +1,32 @@ +from app.eco.enums import TransactionType +from flask import jsonify +from marshmallow import ValidationError +import logging + +logger = logging.getLogger(__name__) + +class BaseService: + TRANSACTION_TYPE = None + + @classmethod + def validate_data(cls, data, schema): + """ + Validate input data based on the provided schema. + """ + logger.info(f"Processing {cls.TRANSACTION_TYPE} request") + return schema.load(data) + + # @classmethod + # def log_session(cls, validated_data): + # """ + # Create a new session. + # """ + # channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel") + + # return Session.create_session( + # session_id = validated_data.get("transactionId"), + # customer_id = validated_data.get('customerId', None), + # account_id = validated_data.get("accountId", None), + # type = cls.TRANSACTION_TYPE, + # channel = channel, + # ) diff --git a/app/eco/services/collect_loan.py b/app/eco/services/collect_loan.py new file mode 100644 index 0000000..008a0a4 --- /dev/null +++ b/app/eco/services/collect_loan.py @@ -0,0 +1,33 @@ +from app.eco.enums.transaction_type import TransactionType +from app.eco.services.base_service import BaseService +from app.eco.schemas.collect_loan import CollectLoanSchema +from marshmallow import ValidationError +from app.eco.helpers.response_helper import ResponseHelper + +class CollectLoanService(BaseService): + TRANSACTION_TYPE = TransactionType.COLLECT_LOAN + + @staticmethod + def process_request(data): + """ + Process the loan collection request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + validated_data = CollectLoanService.validate_data(data, CollectLoanSchema()) + + response_data = { + "transactionId": "01135062", + "amountCollected": 900.0, + } + + return ResponseHelper.success(data=response_data) + except ValidationError as err: + return ResponseHelper.unprocessable_entity(result_description="Validation exception") + except Exception as e: + return ResponseHelper.internal_server_error() diff --git a/app/eco/services/debt_closure_notification.py b/app/eco/services/debt_closure_notification.py new file mode 100644 index 0000000..3aa3204 --- /dev/null +++ b/app/eco/services/debt_closure_notification.py @@ -0,0 +1,27 @@ +from app.eco.enums.transaction_type import TransactionType +from app.eco.services.base_service import BaseService +from app.eco.schemas.debt_closure_notification import DebtClosureNotificationSchema +from marshmallow import ValidationError +from app.eco.helpers.response_helper import ResponseHelper + +class DebtClosureNotificationService(BaseService): + TRANSACTION_TYPE = TransactionType.DEBT_CLOSURE_NOTIFICATION + + @staticmethod + def process_request(data): + """ + Process the debt closure notification request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + validated_data = DebtClosureNotificationService.validate_data(data, DebtClosureNotificationSchema()) + return ResponseHelper.success() + except ValidationError as err: + return ResponseHelper.unprocessable_entity(result_description="Validation exception") + except Exception as e: + return ResponseHelper.internal_server_error() diff --git a/app/eco/services/disbursement.py b/app/eco/services/disbursement.py new file mode 100644 index 0000000..efd038e --- /dev/null +++ b/app/eco/services/disbursement.py @@ -0,0 +1,32 @@ +from app.eco.enums.transaction_type import TransactionType +from app.eco.services.base_service import BaseService +from app.eco.schemas.disbursement import DisbursementSchema +from marshmallow import ValidationError +from app.eco.helpers.response_helper import ResponseHelper + +class DisbursementService(BaseService): + TRANSACTION_TYPE = TransactionType.DISBURSEMENT + + @staticmethod + def process_request(data): + """ + Process the disbursement request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + validated_data = DisbursementService.validate_data(data, DisbursementSchema()) + response_data = { + "transactionId": "SIM01135042", + } + + return ResponseHelper.success(data=response_data) + + except ValidationError as err: + return ResponseHelper.unprocessable_entity(result_description="Validation exception") + except Exception as e: + return ResponseHelper.internal_server_error() diff --git a/app/eco/services/send_sms.py b/app/eco/services/send_sms.py new file mode 100644 index 0000000..245fb33 --- /dev/null +++ b/app/eco/services/send_sms.py @@ -0,0 +1,31 @@ +from app.eco.enums.transaction_type import TransactionType +from app.eco.services.base_service import BaseService +from app.eco.schemas.send_sms import SendSmsSchema +from marshmallow import ValidationError +from app.eco.helpers.response_helper import ResponseHelper + +class SendSMSService(BaseService): + TRANSACTION_TYPE = TransactionType.SEND_SMS + + @staticmethod + def process_request(data): + """ + Process the SMS sending request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + validated_data = SendSMSService.validate_data(data, SendSmsSchema()) + + response_data = { + "undelivered": [] + } + return ResponseHelper.success(data=response_data) + except ValidationError as err: + return ResponseHelper.unprocessable_entity(result_description="Validation exception") + except Exception as e: + return ResponseHelper.internal_server_error() diff --git a/app/swagger/eco_digifi_swagger.json b/app/swagger/eco_digifi_swagger.json new file mode 100644 index 0000000..1d7a4e0 --- /dev/null +++ b/app/swagger/eco_digifi_swagger.json @@ -0,0 +1,128 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "bank Emulator Swagger Simbrella EcoBank - OpenAPI 3.0", + "description": "This is a Simbrella EcoBank bank Backend Server Emulator with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "support@chiefsoft.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.11" + }, + "servers": [ + { + "url": "http://localhost:6337" + }, + { + "url": "http://localhost:5000" + }, + { + "url": "http://10.10.11.17:6337" + }, + { + "url": "https://bank-emulator.dev.simbrellang.net" + } + ], + "tags": [ + { + "name": "Authentication", + "description": "EcoBank Authentication Token Request", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + }, + { + "name": "Disbursement", + "description": "Loan Disbursement Request to EcoBank", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + }, + { + "name": "CollectLoan", + "description": "Collect Loan Repayment from EcoBank Customer", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + }, + { + "name": "DebtClosureNotification", + "description": "Notify EcoBank of Loan Closure", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + }, + { + "name": "SendSMS", + "description": "Send SMS to EcoBank Customers", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + } + ], + "paths": { + "/eco/Authorize": { + "$ref": "swagger/paths/eco/Authentication.json" + }, + "/eco/Disbursement": { + "$ref": "swagger/paths/eco/Disbursement.json" + }, + "/eco/CollectLoan": { + "$ref": "swagger/paths/eco/CollectLoan.json" + }, + "/eco/DebtClosureNotification": { + "$ref": "swagger/paths/eco/DebtClosureNotification.json" + }, + "/eco/SendSMS": { + "$ref": "swagger/paths/eco/SendSMS.json" + } + }, + "components": { + "schemas": { + "AuthenticationRequest": { + "$ref": "./schemas/eco/AuthenticationRequest.json" + }, + "AuthenticationResponse": { + "$ref": "./schemas/eco/AuthenticationResponse.json" + }, + "DebtClosureNotificationRequest": { + "$ref": "./schemas/eco/DebtClosureNotificationRequest.json" + }, + "DebtClosureNotificationResponse": { + "$ref": "./schemas/eco/DebtClosureNotificationResponse.json" + }, + "SendSMSRequest": { + "$ref": "./schemas/eco/SendSMSRequest.json" + }, + "SendSMSResponse": { + "$ref": "./schemas/eco/SendSMSResponse.json" + } + }, + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [] + } + ] +} \ No newline at end of file diff --git a/app/swagger/paths/eco/Authentication.json b/app/swagger/paths/eco/Authentication.json new file mode 100644 index 0000000..89cb05f --- /dev/null +++ b/app/swagger/paths/eco/Authentication.json @@ -0,0 +1,32 @@ +{ + "post": { + "tags": ["Authentication"], + "summary": "EcoBank Authentication", + "description": "Get bearer token from EcoBank", + "operationId": "authenticate", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/AuthenticationRequest.json" + } + } + } + }, + "responses": { + "200": { + "description": "Authentication Successful", + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/AuthenticationResponse.json" + } + } + } + }, + "400": { "description": "Invalid request" }, + "500": { "description": "Internal server error" } + } + } +} diff --git a/app/swagger/paths/eco/CollectLoan.json b/app/swagger/paths/eco/CollectLoan.json new file mode 100644 index 0000000..733f54c --- /dev/null +++ b/app/swagger/paths/eco/CollectLoan.json @@ -0,0 +1,33 @@ +{ + "post": { + "tags": ["CollectLoan"], + "summary": "Collect Loan Repayment", + "description": "Collect repayment amount from EcoBank customer", + "operationId": "collectLoan", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/CollectLoanRequest.json" + } + } + } + }, + "responses": { + "200": { + "description": "Loan Collection Successful", + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/CollectLoanResponse.json" + } + } + } + }, + "400": { "description": "Invalid request" }, + "422": { "description": "Validation exception" }, + "500": { "description": "Internal server error" } + } + } +} diff --git a/app/swagger/paths/eco/DebtClosureNotification.json b/app/swagger/paths/eco/DebtClosureNotification.json new file mode 100644 index 0000000..b051cc3 --- /dev/null +++ b/app/swagger/paths/eco/DebtClosureNotification.json @@ -0,0 +1,33 @@ +{ + "post": { + "tags": ["DebtClosureNotification"], + "summary": "Debt Closure Notification", + "description": "Notify EcoBank that debt has been fully repaid", + "operationId": "notifyDebtClosure", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/DebtClosureNotificationRequest.json" + } + } + } + }, + "responses": { + "200": { + "description": "Debt Closure Acknowledged", + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/DebtClosureNotificationResponse.json" + } + } + } + }, + "400": { "description": "Invalid request" }, + "422": { "description": "Validation exception" }, + "500": { "description": "Internal server error" } + } + } +} diff --git a/app/swagger/paths/eco/Disbursement.json b/app/swagger/paths/eco/Disbursement.json new file mode 100644 index 0000000..efaf8d8 --- /dev/null +++ b/app/swagger/paths/eco/Disbursement.json @@ -0,0 +1,33 @@ +{ + "post": { + "tags": ["Disbursement"], + "summary": "Loan Disbursement", + "description": "Disburse loan to EcoBank customer account", + "operationId": "disburseLoan", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/DisbursementRequest.json" + } + } + } + }, + "responses": { + "200": { + "description": "Disbursement Successful", + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/DisbursementResponse.json" + } + } + } + }, + "400": { "description": "Invalid request" }, + "422": { "description": "Validation exception" }, + "500": { "description": "Internal server error" } + } + } +} diff --git a/app/swagger/paths/eco/SendSMS.json b/app/swagger/paths/eco/SendSMS.json new file mode 100644 index 0000000..b1234bb --- /dev/null +++ b/app/swagger/paths/eco/SendSMS.json @@ -0,0 +1,33 @@ +{ + "post": { + "tags": ["SendSMS"], + "summary": "Send SMS Notification", + "description": "Send a message to one or more EcoBank customers", + "operationId": "sendSms", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/SendSMSRequest.json" + } + } + } + }, + "responses": { + "200": { + "description": "SMS Sent Successfully", + "content": { + "application/json": { + "schema": { + "$ref": "../../schemas/eco/SendSMSResponse.json" + } + } + } + }, + "400": { "description": "Invalid request" }, + "422": { "description": "Validation exception" }, + "500": { "description": "Internal server error" } + } + } +} diff --git a/app/swagger/schemas/eco/AuthenticationRequest.json b/app/swagger/schemas/eco/AuthenticationRequest.json new file mode 100644 index 0000000..20a0ee6 --- /dev/null +++ b/app/swagger/schemas/eco/AuthenticationRequest.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required": ["username", "password"], + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } +} diff --git a/app/swagger/schemas/eco/AuthenticationResponse.json b/app/swagger/schemas/eco/AuthenticationResponse.json new file mode 100644 index 0000000..6441ec0 --- /dev/null +++ b/app/swagger/schemas/eco/AuthenticationResponse.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "resultCode": { + "type": "string" + }, + "resultDescription": { + "type": "string" + }, + "token": { + "type": "string" + } + } +} diff --git a/app/swagger/schemas/eco/CollectLoanRequest.json b/app/swagger/schemas/eco/CollectLoanRequest.json new file mode 100644 index 0000000..b56a42c --- /dev/null +++ b/app/swagger/schemas/eco/CollectLoanRequest.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "required": ["requestId", "affiliateCode", "debtId", "principal", "interest", "penalty", "collectAmount"], + "properties": { + "requestId": { "type": "string" }, + "affiliateCode": { "type": "string" }, + "debtId": { "type": "integer" }, + "principal": { "type": "number" }, + "interest": { "type": "number" }, + "penalty": { "type": "number" }, + "collectAmount": { "type": "number" } + } +} diff --git a/app/swagger/schemas/eco/CollectLoanResponse.json b/app/swagger/schemas/eco/CollectLoanResponse.json new file mode 100644 index 0000000..6a1d458 --- /dev/null +++ b/app/swagger/schemas/eco/CollectLoanResponse.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "transactionId": { "type": "string" }, + "amountCollected": { "type": "number" }, + "resultCode": { "type": "integer" }, + "resultDescription": { "type": "string" } + } +} diff --git a/app/swagger/schemas/eco/DebtClosureNotificationRequest.json b/app/swagger/schemas/eco/DebtClosureNotificationRequest.json new file mode 100644 index 0000000..3df3242 --- /dev/null +++ b/app/swagger/schemas/eco/DebtClosureNotificationRequest.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": ["requestId", "affiliateCode", "customerId", "accountId", "debtId"], + "properties": { + "requestId": { "type": "string" }, + "affiliateCode": { "type": "string" }, + "customerId": { "type": "string" }, + "accountId": { "type": "string" }, + "debtId": { "type": "integer" } + } +} diff --git a/app/swagger/schemas/eco/DebtClosureNotificationResponse.json b/app/swagger/schemas/eco/DebtClosureNotificationResponse.json new file mode 100644 index 0000000..9aa7844 --- /dev/null +++ b/app/swagger/schemas/eco/DebtClosureNotificationResponse.json @@ -0,0 +1,7 @@ +{ + "type": "object", + "properties": { + "resultCode": { "type": "integer" }, + "resultDescription": { "type": "string" } + } +} diff --git a/app/swagger/schemas/eco/DisbursementRequest.json b/app/swagger/schemas/eco/DisbursementRequest.json new file mode 100644 index 0000000..9ce9ef4 --- /dev/null +++ b/app/swagger/schemas/eco/DisbursementRequest.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": ["requestId", "affiliateCode", "debtId", "productId", "customerId", "accountId", "provideAmount", "collectAmount", "interestRate"], + "properties": { + "requestId": { "type": "string" }, + "affiliateCode": { "type": "string" }, + "debtId": { "type": "integer" }, + "productId": { "type": "string" }, + "customerId": { "type": "string" }, + "accountId": { "type": "string" }, + "provideAmount": { "type": "number" }, + "collectAmount": { "type": "number" }, + "interestRate": { "type": "number" } + } +} diff --git a/app/swagger/schemas/eco/DisbursementResponse.json b/app/swagger/schemas/eco/DisbursementResponse.json new file mode 100644 index 0000000..fb32424 --- /dev/null +++ b/app/swagger/schemas/eco/DisbursementResponse.json @@ -0,0 +1,8 @@ +{ + "type": "object", + "properties": { + "transactionId": { "type": "string" }, + "resultCode": { "type": "integer" }, + "resultDescription": { "type": "string" } + } +} diff --git a/app/swagger/schemas/eco/SendSMSRequest.json b/app/swagger/schemas/eco/SendSMSRequest.json new file mode 100644 index 0000000..1d55501 --- /dev/null +++ b/app/swagger/schemas/eco/SendSMSRequest.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "required": ["requestId", "phoneNums", "affiliateCode", "message"], + "properties": { + "requestId": { "type": "string" }, + "phoneNums": { + "type": "array", + "items": { "type": "string" } + }, + "affiliateCode": { "type": "string" }, + "message": { "type": "string" } + } +} diff --git a/app/swagger/schemas/eco/SendSMSResponse.json b/app/swagger/schemas/eco/SendSMSResponse.json new file mode 100644 index 0000000..a373eb6 --- /dev/null +++ b/app/swagger/schemas/eco/SendSMSResponse.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "undelivered": { + "type": "array", + "items": { "type": "string" } + }, + "resultCode": { "type": "integer" }, + "resultDescription": { "type": "string" } + } +} diff --git a/requirements.txt b/requirements.txt index bb8f998..7f6f8f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,8 @@ flask-swagger-ui python-dotenv +flask-jwt-extended +flask-apispec # Logging (Python Standard Library, for reference) \ No newline at end of file