first commit

This commit is contained in:
Azeez Muibi
2025-04-10 19:32:50 +01:00
commit 075f953dbc
89 changed files with 3641 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
VALID_APP_ID=**********
VALID_API_KEY=*************
BASIC_AUTH_USERNAME=******
BASIC_AUTH_PASSWORD=******
SWAGGER_URL="/documentation"
API_URL="/swagger.json"
JWT_SECRET_KEY=******
JWT_ACCESS_TOKEN_EXPIRES=******
JWT_REFRESH_TOKEN_EXPIRES=******
DATABASE_USER=*****
DATABASE_PASSWORD=*****
DATABASE_HOST=******
DATABASE_PORT=******
DATABASE_NAME=*****
# Flask Configuration
FLASK_APP=wsgi.py
FLASK_ENV=development
APP_PORT=4500
SIMBRELLA_BASE_URL=***************
+8
View File
@@ -0,0 +1,8 @@
__pycache__/
.env
app.log
.DS_Store
migrations/__pycache__/
migrations/*.pycg
./vscode
.vscode/settings.json
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (digifi-FirstCore)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (digifi-FirstCore)" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/digifi-FirstCore.iml" filepath="$PROJECT_DIR$/.idea/digifi-FirstCore.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+22
View File
@@ -0,0 +1,22 @@
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Expose port 5000 for the Flask app
EXPOSE 5000
# Set environment variables
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN chmod +x scripts/entrypoint.sh
ENTRYPOINT ["scripts/entrypoint.sh"]
+101
View File
@@ -0,0 +1,101 @@
# Setup
This guide provides instructions on how to set up and run the application.
## Prerequisites
Ensure you have the following installed on your system:
- **Docker**: Install Docker from [docker.com](https://www.docker.com/get-started)
- **Docker Compose**: Docker Compose is included with Docker Desktop (for macOS/Windows) or can be installed separately on Linux.
## Steps to Set Up the Application
### 1. Clone the Repository
First, clone the repository to your local machine:
```bash
git clone https://github.com/username/repository.git
cd repository
```
### 2. Configure Environment Variables
Instead of creating a new `.env` file, rename the provided `.env.example` file and update the necessary variables:
```bash
cp .env.example .env
```
Then, open the `.env` file and **update the following variables with your actual configuration:**
- Database credentials:
```ini
DATABASE_USER=your_database_username
DATABASE_PASSWORD=your_database_password
DATABASE_NAME=your_database_name
DATABASE_PORT=5432
```
- App ID and API Key:
```ini
APP_ID=your_app_id
API_KEY=your_api_key
```
This ensures that the application is properly configured with your environment variables.
> **Optional:** Change file permissions for the entrypoint script
```bash
chmod +x scripts/entrypoint.sh
```
---
### 3. Run the Application with Docker Compose
Once you have the repository cloned, you can easily set up and run the application using Docker Compose. Simply execute the following command:
```bash
docker-compose up --build
```
This command will build the Docker image and start the Flask application in a container. By default, the application will be accessible at `http://localhost:4500`.
### 4. Health Check
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:4500/health
```
If the application is running properly, you should receive a response similar to this:
```json
{
"status": "ok"
}
```
### 5. Documentation
You can check the Swagger Doc by accessing the `/documentation` endpoint. Run the following command:
```bash
curl http://localhost:4500/documentation
```
### 6. Stop the Application
To stop the application, use:
```bash
docker-compose down
```
This will stop and remove the containers created by Docker Compose.
+6
View File
@@ -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)
+51
View File
@@ -0,0 +1,51 @@
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.errors import register_error_handlers
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from app.extensions import db, migrate
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"""
app = Flask(__name__)
# Load configuration
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
app.register_blueprint(api)
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
# Error Handlers
register_error_handlers(app)
from . import models
# Database and Migrations
db.init_app(app)
migrate.init_app(app, db)
return app
+2
View File
@@ -0,0 +1,2 @@
from .transaction_type import TransactionType
from .loan_status import LoanStatus
+6
View File
@@ -0,0 +1,6 @@
from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
REPAID = "repaid"
+10
View File
@@ -0,0 +1,10 @@
from enum import Enum
class TransactionType(str, Enum):
ELIGIBILITY_CHECK = "eligibility_check"
CUSTOMER_CONSENT = "customer_consent"
LOAN_STATUS = "loan_status"
NOTIFICATION_CALLBACK = "notification_callback"
PROVIDE_LOAN = "provide_loan"
REPAYMENT = "repayment"
SELECT_OFFER = "select_offer"
+251
View File
@@ -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)
+2
View File
@@ -0,0 +1,2 @@
from .simbrella import SimbrellaIntegration
from .kafka import KafkaIntegration
+82
View File
@@ -0,0 +1,82 @@
from confluent_kafka import Producer
import json
import logging
from app.config import settings
logger = logging.getLogger(__name__)
class KafkaIntegration:
_producer = None
_config = {
"bootstrap.servers": settings.KAFKA_BROKER,
"client.id": "loan-service-producer",
"acks": "all",
"retries": 3,
"debug": "broker,topic,msg",
}
@staticmethod
def _get_producer():
"""Kafka producer"""
if not KafkaIntegration._producer:
KafkaIntegration._producer = Producer(KafkaIntegration._config)
logger.info(
f"Connected to Kafka broker at {KafkaIntegration._config['bootstrap.servers']}"
)
return KafkaIntegration._producer
@staticmethod
def delivery_report(err, msg):
"""Called once for each message produced"""
if err is not None:
logger.error(f'Message delivery failed: {err}')
raise RuntimeError(f"Message delivery failed: {err}")
else:
logger.debug(f'Message delivered to {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}')
@staticmethod
def send_loan_request(loan_data, request_id, topic):
"""
Send loan request to topic
Args:
loan_data: Loan request payload as dict
request_id: Unique request identifier (used as Kafka key)
"""
try:
# Proceed to send loan request to Kafka
producer = KafkaIntegration._get_producer()
# Sending loan request message to Kafka
producer.produce(
topic=topic,
key=str(request_id),
value=json.dumps(loan_data).encode("utf-8"),
callback=KafkaIntegration.delivery_report,
)
producer.poll(0)
logger.info(f"Loan request {request_id} queued for processing")
except Exception as e:
logger.error(
f"Failed to send loan request to Kafka: {str(e)}", exc_info=True
)
raise Exception(f"Failed to send loan request to Kafka: {str(e)}")
@staticmethod
def flush():
"""Shutdown"""
producer = KafkaIntegration._get_producer()
producer.flush()
+55
View File
@@ -0,0 +1,55 @@
import requests
import json
from requests.auth import HTTPBasicAuth
from app.utils.logger import logger
from app.config import settings
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
"""
Calls the RACCheck endpoit
"""
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
payload = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": transaction_id,
"RAC_Array": [
{
"salaryAccount": True,
"bvn": "12345678901",
"crc": False,
"crms": True,
"accountStatus": "active",
"lien": False,
"noBouncedCheck": True,
"existingLoan": False,
"whitelist": True,
"noPastDueSalaryLoan": True,
"noPastDueOtherLoans": False
}
]
}
logger.error(f"This is PayLoad: {str(payload)}",exc_info=True)
headers = {
'Content-Type': 'application/json',
'x-api-key': f'{settings.VALID_API_KEY}',
'App-Id': f'{settings.VALID_APP_ID}'
}
try:
response = requests.post(url, json=payload, timeout=10, headers=headers)
logger.error(f"This is Response: {str(response)}", exc_info=True)
# Raise an error for non-200 responses
if response.status_code != 200:
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as err:
logger.error(f"RACCheck API call failed: {str(err)}", exc_info=True)
return {"error": "RACCheck API error"}
+4
View File
@@ -0,0 +1,4 @@
from .verify_api_key import require_api_key
from .app_id_checker import require_app_id
from .cors import enforce_json
from .basic_auth import require_auth
+27
View File
@@ -0,0 +1,27 @@
from functools import wraps
from flask import request, jsonify
from app.utils.logger import logger
from app.config import Config
VALID_APP_ID = Config.VALID_APP_ID
def require_app_id(f):
"""Decorator to enforce App-ID validation."""
@wraps(f)
def decorated_function(*args, **kwargs):
app_id = request.headers.get("App-ID")
if not app_id:
logger.error("Unauthorized access: Missing App-ID.")
return jsonify({"message": "Invalid request"}), 400
if app_id != VALID_APP_ID:
logger.error(f"Unauthorized access: Invalid App-ID {app_id}.")
return jsonify({"message": "Invalid request"}), 400
return f(*args, **kwargs)
return decorated_function
+30
View File
@@ -0,0 +1,30 @@
from functools import wraps
from flask import request, jsonify
import base64
from app.config import Config
USERNAME = Config.BASIC_AUTH_USERNAME
PASSWORD = Config.BASIC_AUTH_PASSWORD
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get('Authorization')
if not auth or not check_auth(auth):
return jsonify({"message": "Invalid request"}), 401
return f(*args, **kwargs)
return decorated
def check_auth(auth_header):
if not auth_header:
return False
try:
auth_type, credentials = auth_header.split()
if auth_type.lower() != "basic":
return False
decoded_credentials = base64.b64decode(credentials).decode("utf-8")
user, pwd = decoded_credentials.split(":", 1)
return user == USERNAME and pwd == PASSWORD
except Exception:
return False
+7
View File
@@ -0,0 +1,7 @@
from flask import request, jsonify
def enforce_json():
"""Middleware to enforce JSON Content-Type for incoming requests"""
if request.method in ["POST", "PUT", "PATCH"] and request.content_type != "application/json":
return jsonify({"message": "Invalid request"}), 400
+25
View File
@@ -0,0 +1,25 @@
from functools import wraps
from flask import request, jsonify
from app.utils.logger import logger
from app.config import Config
# Load valid API key from environment variables (fallback for testing)
VALID_API_KEY = Config.VALID_API_KEY
def require_api_key(f):
"""Decorator to enforce API key authentication."""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get("X-API-KEY")
if not api_key:
logger.error("Unauthorized access: Missing API key.")
return jsonify({"message": "Invalid request"}), 400
if api_key != VALID_API_KEY:
logger.error("Unauthorized access: Invalid API key.")
return jsonify({"message": "Invalid request"}), 400
return f(*args, **kwargs)
return decorated_function
+1
View File
@@ -0,0 +1 @@
from .routes import api
+138
View File
@@ -0,0 +1,138 @@
from flask import Blueprint, request, jsonify, send_from_directory
from app.api.services import (
EligibilityCheckService,
SelectOfferService,
ProvideLoanService,
LoanStatusService,
RepaymentService,
CustomerConsentService,
NotificationCallbackService,
AuthorizationService,
)
from app.utils.logger import logger
from app.api.middlewares import enforce_json, require_auth
import os
from flask_jwt_extended import (
JWTManager,
jwt_required,
create_access_token,
get_jwt_identity,
create_refresh_token,
)
api = Blueprint("api", __name__)
@api.before_request
def cors_middleware():
"""Middleware applied globally to all API routes in this blueprint"""
return enforce_json()
# Swagger JSON file
@api.route("/swagger.json", methods=["GET"])
def swagger_json():
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, "digifi_swagger.json")
@api.route("/swagger/<path:filename>")
def serve_paths(filename):
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, filename)
# EligibilityCheck Endpoint
@api.route("/EligibilityCheck", methods=["POST"])
@jwt_required()
def eligibility_check():
data = request.get_json()
# logger.info(f"EligibilityCheck request received: {data}")
response = EligibilityCheckService.process_request(data)
return response
# SelectOffer Endpoint
@api.route("/SelectOffer", methods=["POST"])
@jwt_required()
def select_offer():
data = request.get_json()
# logger.info(f"SelectOffer request received: {data}")
response = SelectOfferService.process_request(data)
return response
# ProvideLoan Endpoint
@api.route("/ProvideLoan", methods=["POST"])
@jwt_required()
def provide_loan():
data = request.get_json()
# logger.info(f"ProvideLoan request received: {data}")
response = ProvideLoanService.process_request(data)
return response
# LoanStatus Endpoint
@api.route("/LoanStatus", methods=["POST"])
@jwt_required()
def loan_status():
data = request.get_json()
# logger.info(f"LoanStatus request received: {data}")
response = LoanStatusService.process_request(data)
return response
# Repayment Endpoint
@api.route("/Repayment", methods=["POST"])
@jwt_required()
def repayment():
data = request.get_json()
# logger.info(f"Repayment request received: {data}")
response = RepaymentService.process_request(data)
return response
# CustomerConsent Endpoint
@api.route("/CustomerConsent", methods=["POST"])
@jwt_required()
def customer_consent():
data = request.get_json()
# logger.info(f"CustomerConsent request received: {data}")
response = CustomerConsentService.process_request(data)
return response
# NotificationCallback Endpoint
@api.route("/NotificationCallback", methods=["POST"])
@jwt_required()
def notification_callback():
data = request.get_json()
# logger.info(f"NotificationCallback request received: {data}")
response = NotificationCallbackService.process_request(data)
return response
# Health Check Endpoint
@api.route("/health", methods=["GET"])
def health_check():
return {"status": "ok"}, 200
# Authorize endpoint
@api.route("/Authorize", methods=["POST"])
def authorize():
data = request.get_json()
# logger.info(f"Authorize request received: {data}")
response = AuthorizationService.process_request(data)
return response
# Authorize refresh endpoint
@api.route("/AuthorizeRefresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
data = request.get_json()
# logger.info(f"Authorize refresh request received: {data}")
response = AuthorizationService.process_refresh_request()
return response
View File
+6
View File
@@ -0,0 +1,6 @@
from marshmallow import Schema, fields
class AuthorizeRequestSchema(Schema):
username = fields.Str(required=True)
password = fields.Str(required=True)
+11
View File
@@ -0,0 +1,11 @@
from marshmallow import Schema, fields
# Customer Consent Schema
class CustomerConsentSchema(Schema):
type = fields.Str(required=True)
transactionId = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
requestTime = fields.DateTime(required=True, format="%Y-%m-%d %H:%M:%S.%f")
consentType = fields.Str(required=True)
channel = fields.Str(required=True)
+10
View File
@@ -0,0 +1,10 @@
from marshmallow import Schema, fields
class EligibilityCheckSchema(Schema):
transactionId = fields.Str(required=True)
countryCode = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=True)
accountId = fields.Str(required=True)
channel = fields.Str(required=True)
+8
View File
@@ -0,0 +1,8 @@
from marshmallow import Schema, fields
# Loan Information Schema
class LoanStatusSchema(Schema):
transactionId = fields.Str(required=True)
customerId = fields.Str(required=True)
msisdn = fields.Str(required=False)
channel = fields.Str(required=True)
+14
View File
@@ -0,0 +1,14 @@
from marshmallow import Schema, fields
# Notification Callback Schema
class NotificationCallbackSchema(Schema):
fbnTransactionId = fields.Str(required=True)
transactionId = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
debtId = fields.Str(required=True)
transactionType = fields.Str(required=True)
amountProvided = fields.Float(required=True)
amountCollected = fields.Float(required=True)
responseCode = fields.Str(required=True)
responseDescription = fields.Str(required=True)
+16
View File
@@ -0,0 +1,16 @@
from marshmallow import Schema, fields
# Provide Loan Schema
class ProvideLoanSchema(Schema):
type = fields.Str(required=False)
requestId = fields.Str(required=True)
transactionId = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=False)
# productId = fields.Str(required=True)
# lienAmount = fields.Float(required=True)
requestedAmount = fields.Float(required=True)
collectionType = fields.Int(required=True)
offerId = fields.Int(required=True)
channel = fields.Str(required=True)
+11
View File
@@ -0,0 +1,11 @@
from marshmallow import Schema, fields
# Repayment Schema
class RepaymentSchema(Schema):
type = fields.Str(required=False)
msisdn = fields.Str(required=False) #optional
debtId = fields.Str(required=True)
productId = fields.Str(required=True)
transactionId = fields.Str(required=True)
customerId = fields.Str(required=True)
channel = fields.Str(required=True)
+13
View File
@@ -0,0 +1,13 @@
from marshmallow import Schema, fields
# Select Offer Schema
class SelectOfferSchema(Schema):
requestId = fields.Str(required=True)
transactionId = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=True)
requestedAmount = fields.Float(required=True)
productId = fields.Str(required=True)
channel = fields.Str(required=True)
+8
View File
@@ -0,0 +1,8 @@
from app.api.services.eligibility_check import EligibilityCheckService
from app.api.services.select_offer import SelectOfferService
from app.api.services.provide_loan import ProvideLoanService
from app.api.services.loan_status import LoanStatusService
from app.api.services.repayment import RepaymentService
from app.api.services.customer_consent import CustomerConsentService
from app.api.services.notification_callback import NotificationCallbackService
from app.api.services.authorization import AuthorizationService
+102
View File
@@ -0,0 +1,102 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.services.base_service import BaseService
from app.utils.logger import logger
from app.api.schemas.authorization import AuthorizeRequestSchema
from app.api.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(message="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(message="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=response_data, message="Authorization processed successfully"
)
except ValidationError as e:
logger.error(f"Validation error: {e}")
return ResponseHelper.bad_request(message=f"Validation error: {e}")
except Exception as e:
logger.error(f"Error processing Authorization request: {e}")
return ResponseHelper.internal_server_error(
message=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=response_data, message="RefreshToken processed successfully"
)
except Exception as e:
logger.error(f"Error processing RefreshToken request: {e}")
return ResponseHelper.internal_server_error(
message=f"Error processing RefreshToken request: {e}"
)
+61
View File
@@ -0,0 +1,61 @@
from app.models import Customer, Account, Transaction
from app.api.enums import TransactionType
from flask import jsonify
from marshmallow import ValidationError
import logging
from app.api.integrations import KafkaIntegration
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 get_or_create_customer(cls, validated_data):
"""
Check if a customer exists; if not, create one.
"""
customer = Customer.query.filter_by(id=validated_data.get("customerId")).first()
if not customer:
customer = Customer.create_customer(
id=validated_data.get("customerId"),
msisdn=validated_data.get("msisdn"),
country_code=validated_data.get("countryCode"),
account_id=validated_data.get("accountId"),
)
return customer
@classmethod
def validate_account_ownership(cls, account_id, customer_id):
"""
Check if the provided account belongs to the customer.
"""
is_valid = Account.is_valid_account(account_id, customer_id)
return is_valid
@classmethod
def log_transaction(cls, validated_data):
"""
Create a new transaction.
"""
return Transaction.create_transaction(
transaction_id =validated_data.get("transactionId"),
account_id=validated_data.get("accountId"),
type=cls.TRANSACTION_TYPE,
channel=validated_data.get("channel"),
)
@classmethod
def async_send_to_kafka(cls, loan_data, request_id, topic):
KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id, topic = topic)
KafkaIntegration.flush()
+71
View File
@@ -0,0 +1,71 @@
from flask import request, jsonify
from app.api.services.base_service import BaseService
from marshmallow import ValidationError
from app.utils.logger import logger
from app.api.schemas.customer_consent import CustomerConsentSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
class CustomerConsentService(BaseService):
TRANSACTION_TYPE = TransactionType.CUSTOMER_CONSENT
@staticmethod
def process_request(data):
"""
Process the CustomerConsent request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = CustomerConsentService.validate_data(data, CustomerConsentSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
if(CustomerConsentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = CustomerConsentService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Request is received"
}
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+108
View File
@@ -0,0 +1,108 @@
from flask import session, jsonify
from app.utils.logger import logger
from app.api.services.base_service import BaseService
from app.api.schemas.eligibility_check import EligibilityCheckSchema
from marshmallow import ValidationError
from app.api.enums import TransactionType
from app.api.integrations import SimbrellaIntegration
class EligibilityCheckService(BaseService):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@staticmethod
def process_request(data):
"""
Process the EligibilityCheck request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
transactionId = validated_data.get('transactionId')
msisdn = validated_data.get('msisdn')
customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data)
if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Call RACCheck
response = SimbrellaIntegration.rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.id,
)
logger.error(f"This is Response Returned ****** : {str(response)}")
# this chck for error is not valid
logger.error(f"Check for ERROR is not valid ****** FIX THIS !!!!!")
#if "error" in response or response.get("status") != 200:
# return jsonify({"message": "RACCheck failed"}), 400
offers = [
{
"offerId": "SAL90",
"productId": "2030",
"minAmount": 5000,
"maxAmount": 100000,
"tenor": 30
},
{
"offerId": "SAL30",
"productId": "2090",
"minAmount": 3000,
"maxAmount": 500000,
"tenor": 90
}
]
# Simulate processing
response_data = {
"customerId": customer_id,
"transactionId": transactionId,
"countryCode": "NG",
"msisdn": msisdn,
"eligibleOffers": offers,
"resultDescription": "Successful",
"resultCode": "00",
"accountId": account_id
}
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+88
View File
@@ -0,0 +1,88 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
class LoanStatusService(BaseService):
TRANSACTION_TYPE = TransactionType.LOAN_STATUS
@staticmethod
def process_request(data):
"""
Process the Loan Information request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
customer_id = validated_data.get('customerId')
customer = LoanStatusService.get_or_create_customer(validated_data)
account = customer.accounts[0]
if (LoanStatusService.validate_account_ownership(account_id = account.id, customer_id = customer_id)):
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
loans = [
{
"debtId": "123456789",
"loanDate": "2019-10-18 14:26:21.063",
"dueDate": "2019-11-20 14:26:21.063",
"currentLoanAmount": 8500,
"initialLoanAmount": 10000,
"defaultPenaltyFee": 0,
"continuousFee": 0,
"productId": "101"
}
]
# Simulated processing logic
response_data = {
"customerId": "CN621868",
"transactionId": "Tr201712RK9232P115",
"loans": loans,
"totalDebtAmount": 8500,
"resultCode": "00",
"resultDescription": "Successful"
}
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+62
View File
@@ -0,0 +1,62 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.notification_callback import NotificationCallbackSchema
class NotificationCallbackService(BaseService):
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
@staticmethod
def process_request(data):
"""
Process the NotificationCallback request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
logger.info("Processing NotificationCallback request")
# Validate input data using the NotificationCallback schema
schema = NotificationCallbackSchema()
validated_data = schema.load(data) # Raises ValidationError if invalid
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success(
# data=response_data,
# message="Notification callback processed successfully"
# )
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+109
View File
@@ -0,0 +1,109 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.integrations.kafka import KafkaIntegration
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.provide_loan import ProvideLoanSchema
from threading import Thread
from app.models.loan import Loan
from app.api.enums import LoanStatus
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@staticmethod
def process_request(data):
"""
Process the ProvideLoan request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId')
transaction_id = validated_data.get('transactionId')
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Save the loan details
loan = Loan.create_loan(
customer_id=customer_id,
account_id=account_id,
offer_id=validated_data.get('offerId'),
principal_amount=validated_data.get('requestedAmount'),
status=LoanStatus.ACTIVE
)
if not loan:
logger.error(f"Failed to save loan details")
return jsonify({
"message": "Failed to save loan details."
}), 400
# Log Transaction
transaction = ProvideLoanService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"customerId": customer_id,
"accountId": account_id,
"msisdn": "3451342",
"resultCode": "00",
"resultDescription": "Successful"
}
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# Call Kafka in a background thread
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start()
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+106
View File
@@ -0,0 +1,106 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.models import Repayment
from app.models.loan import Loan
from app.utils.logger import logger
from app.api.schemas.repayment import RepaymentSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from threading import Thread
class RepaymentService(BaseService):
TRANSACTION_TYPE = TransactionType.REPAYMENT
@staticmethod
def process_request(data):
"""
Process the Repayment request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
customer_id = validated_data.get('customerId')
customer = RepaymentService.get_or_create_customer(validated_data)
account = customer.accounts[0]
validated_data['accountId'] = account.id
request_id = validated_data.get('requestId')
loan_id = validated_data.get('debtId')
if (RepaymentService.validate_account_ownership(account_id = account.id, customer_id = customer_id)):
# Save the repayment details
repayment = Repayment.create_repayment(
customer_id = customer_id,
loan_id = loan_id,
product_id = validated_data.get('productId')
)
if not repayment:
logger.error(f"Failed to save repayment details")
return jsonify({
"message": "Failed to save repayment details."
}), 400
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic
response_data = {
"customerId": "CN621868",
"productId": "101",
"debtId": "273194670",
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success(
# data=response_data,
# message="Repayment processed successfully"
# )
# Call Kafka in a background thread
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start()
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+93
View File
@@ -0,0 +1,93 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.select_offer import SelectOfferSchema
class SelectOfferService(BaseService):
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
@staticmethod
def process_request(data):
"""
Process the SelectOffer request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = SelectOfferService.validate_data(data, SelectOfferSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
if (SelectOfferService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = SelectOfferService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
offers = [
{
"offerId": "14451",
"productId": "2030",
"amount": 10000.0,
"upfrontPayment": 1000.0,
"interestRate": 3.0,
"managementRate": 1.0,
"managementFee": 1.0,
"insuranceRate": 1.0,
"insuranceFee": 100.0,
"VATRate": 7.5,
"VATAmount": 100.0,
"recommendedRepaymentDates": ["2022-11-30"],
"installmentAmount": 11000.0,
"totalRepaymentAmount": 11000.0
}
]
# Business logic - selecting an offer
response_data = {
"outstandingDebtAmount": 0,
"requestId": "202111170001371256908",
"transactionId": transaction.id,
"customerId": customer_id,
"accountId": account_id,
"loan": offers,
"resultCode": "00",
"resultDescription": "Successful"
}
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
+38
View File
@@ -0,0 +1,38 @@
import os
from datetime import timedelta
class Config:
"""Base configuration for Flask app"""
SWAGGER_URL = os.getenv("SWAGGER_URL", "/documentation")
API_URL = os.getenv("API_URL", "/swagger.json")
DEBUG = True
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345")
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
DATABASE_USER = os.environ.get("DATABASE_USER")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME")
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")
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)
)
KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_PAYMENT_TOPIC = 'PROCESS_PAYMENT'
settings = Config()
+1
View File
@@ -0,0 +1 @@
from .handlers import register_error_handlers
+24
View File
@@ -0,0 +1,24 @@
from werkzeug.exceptions import HTTPException
from flask import jsonify
from app.api.helpers.response_helper import ResponseHelper
def register_error_handlers(app):
@app.errorhandler(HTTPException)
def handle_http_exception(e):
return jsonify({'error': e.description}), e.code
@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({"message": "Method Not Allowed"}), 405
@app.errorhandler(404)
def not_found(error):
return jsonify({"message": "Resource not found"}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({"message": "Bad Request"}), 400
@app.errorhandler(415)
def unsupported_media_type(error):
return jsonify({"message": "Unsupported Media Type"}), 415
+5
View File
@@ -0,0 +1,5 @@
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
+7
View File
@@ -0,0 +1,7 @@
from .customer import Customer
from .account import Account
from .loan import Loan
from .transaction import Transaction
from .repayment import Repayment
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment']
+51
View File
@@ -0,0 +1,51 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
from app.extensions import db
from sqlalchemy.exc import IntegrityError
class Account(db.Model):
__tablename__ = 'accounts'
id = db.Column(db.String(50), primary_key=True)
customer_id = db.Column(db.String(50), nullable=False)
account_type = db.Column(db.String(50))
status = db.Column(db.String(20), default='active')
lien_amount = db.Column(db.Float, default=0.0)
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))
customer = relationship(
"Customer",
primaryjoin="Customer.id == Account.customer_id",
foreign_keys=[customer_id],
back_populates="accounts",
)
@classmethod
def create_account(cls, id, customer_id, account_type, status='active'):
account = cls(
id=id,
customer_id=customer_id,
account_type=account_type
)
try:
db.session.add(account)
db.session.commit()
except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}")
return account
@classmethod
def is_valid_account(cls, account_id, customer_id):
account = cls.query.filter_by(id=account_id, customer_id=customer_id).first()
if not account:
return False
if account.lien_amount > 0:
return False
return True
def __repr__(self):
return f'<Account {self.id}>'
+54
View File
@@ -0,0 +1,54 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
from app.extensions import db
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
class Customer(db.Model):
__tablename__ = 'customers'
id = db.Column(db.String(50), primary_key=True)
msisdn = db.Column(db.String(20), unique=True, nullable=False)
country_code = db.Column(db.String(3), 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))
accounts = relationship(
"Account",
primaryjoin="Customer.id == Account.customer_id",
foreign_keys="Account.customer_id",
back_populates="customer",
)
@classmethod
def is_valid_customer(cls, customer_id):
customer = cls.query.filter_by(id=customer_id).first()
if not customer:
return False
return True
@classmethod
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
if cls.query.filter_by(id=id).first():
raise ValueError("Customer already exists")
# Create the customer
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
try:
db.session.add(customer)
# Create an associated account
account = Account.create_account(
id=account_id,
customer_id=id,
account_type=account_type
)
db.session.commit()
except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}")
return customer
def __repr__(self):
return f'<Customer {self.id}>'
+99
View File
@@ -0,0 +1,99 @@
from datetime import datetime, timezone
from app.extensions import db
from app.models.customer import Customer
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
class Loan(db.Model):
__tablename__ = 'loans'
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
)
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=False)
offer_id = db.Column(db.String(20), nullable=False)
principal_amount = db.Column(db.Float, nullable=False)
status = db.Column(db.String(20), default='pending')
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))
@classmethod
def create_loan(cls, customer_id, account_id, offer_id, principal_amount, status='pending'):
# Check if customer exists
is_valid = Customer.is_valid_customer(customer_id)
if not is_valid:
raise ValueError("Customer does not exist")
# # Check for active loans
# has_active_loans = cls.has_active_loans(customer_id)
# if has_active_loans:
# raise ValueError("Customer has active loans")
# Create and save the loan
loan = cls(
customer_id=customer_id,
account_id=account_id,
offer_id=offer_id,
principal_amount=principal_amount,
status=status
)
try:
db.session.add(loan)
db.session.commit()
except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}")
return loan
@classmethod
def has_active_loans(cls, customer_id):
active_loans = cls.query.filter_by(
customer_id=customer_id,
status='active'
).count()
if active_loans > 0:
return False
return True
@classmethod
def get_customer_loan(cls, loan_id, customer_id):
"""
Get customer's active loans.
"""
loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first()
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
return loan
@classmethod
def update_status(cls, loan_id, status):
"""
Update the status of the loan with the given loan_id.
"""
# Retrieve loan
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
if loan.status == status:
return
# Update loan status and the updated_at timestamp
loan.status = status
db.session.commit()
def __repr__(self):
return f'<Loan {self.id}>'
+16
View File
@@ -0,0 +1,16 @@
from datetime import datetime, timezone
from app.extensions import db
class Offer(db.Model):
__tablename__ = 'offers'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.String, nullable=False)
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
tenor = db.Column(db.Integer, 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'<LoanOffer {self.id}>'
+55
View File
@@ -0,0 +1,55 @@
from datetime import datetime, timezone
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from app.models.customer import Customer
from app.models.loan import Loan
from sqlalchemy.exc import IntegrityError
class Repayment(db.Model):
__tablename__ = 'repayments'
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
)
loan_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
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))
@classmethod
def create_repayment(cls, customer_id, loan_id, product_id):
# Check customer exists
if not Customer.is_valid_customer(customer_id):
raise ValueError("Invalid customer")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Check that the loan is active
if loan.status != LoanStatus.ACTIVE:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
loan_id=loan.id,
product_id=product_id,
)
try:
db.session.add(repayment)
db.session.commit()
except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}")
return repayment
def __repr__(self):
return f'<Repayment {self.id}>'
+53
View File
@@ -0,0 +1,53 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_
class Transaction(db.Model):
__tablename__ = 'transactions'
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
)
#id = db.Column(db.Int, primary_key=True)
transaction_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=False)
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'<Transaction {self.id}>'
@classmethod
def create_transaction(cls, transaction_id, account_id, type, channel):
# if cls.query.filter_by(transaction_id=transaction_id).first():
# raise ValueError("Duplicate Transaction")
if cls.query.filter( and_( cls.transaction_id ==transaction_id, cls.type==type) ).first():
raise ValueError("Duplicate Transaction")
transaction = cls(
transaction_id=transaction_id,
account_id=account_id,
type=type,
channel=channel
)
try:
db.session.add(transaction)
db.session.commit()
except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}")
return transaction
@classmethod
def get_transaction_by_id(cls, transaction_id):
return cls.query.get(transaction_id)
+180
View File
@@ -0,0 +1,180 @@
{
"openapi": "3.0.3",
"info": {
"title": "Swagger Bank Channel to Simbrella FirstAdvance - OpenAPI 3.0",
"description": "This is a Simbrella FirstAdvance Backend Server 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:4500"
},
{
"url": "http://api.dev.simbrellang.net:4500"
},
{
"url": "https://api.dev.simbrellang.net"
}
],
"tags": [
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "AuthorizeRefresh",
"description": "This feature will be used for refreshing authorized customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "EligibilityCheck",
"description": "Eligibility Check Request",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "SelectOffer",
"description": "This method is used the send the offer the customer selected to Simbrella.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "ProvideLoan",
"description": "Provide Loan Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "LoanStatus",
"description": "Loan Information Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Repayment",
"description": "Repayment Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
}
],
"paths": {
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
"/AuthorizeRefresh": {
"$ref": "swagger/paths/AuthorizeRefresh.json"
},
"/EligibilityCheck": {
"$ref": "swagger/paths/EligibilityCheck.json"
},
"/SelectOffer": {
"$ref": "swagger/paths/SelectOffer.json"
},
"/ProvideLoan": {
"$ref": "swagger/paths/ProvideLoan.json"
},
"/LoanStatus": {
"$ref": "swagger/paths/LoanStatus.json"
},
"/Repayment": {
"$ref": "swagger/paths/Repayment.json"
}
},
"components": {
"schemas": {
"EligibilityCheckRequest": {
"$ref": "swagger/schemas/EligibilityCheckRequest.json"
},
"EligibilityCheckResponse": {
"$ref": "swagger/schemas/EligibilityCheckResponse.json"
},
"SelectOfferRequest": {
"$ref": "swagger/schemas/SelectOfferRequest.json"
},
"SelectOfferResponse": {
"$ref": "swagger/schemas/SelectOfferResponse.json"
},
"LoanStatusRequest": {
"$ref": "swagger/schemas/LoanStatusRequest.json"
},
"LoanStatusResponse": {
"$ref": "swagger/schemas/LoanStatusResponse.json"
},
"RepaymentRequest": {
"$ref": "swagger/schemas/RepaymentRequest.json"
},
"RepaymentResponse": {
"$ref": "swagger/schemas/RepaymentResponse.json"
},
"CustomerConsentRequest": {
"$ref": "swagger/schemas/CustomerConsentRequest.json"
},
"CustomerConsentResponse": {
"$ref": "swagger/schemas/CustomerConsentResponse.json"
},
"NotificationCallbackRequest": {
"$ref": "swagger/schemas/NotificationCallbackRequest.json"
},
"NotificationCallbackResponse": {
"$ref": "swagger/schemas/NotificationCallbackResponse.json"
},
"ApiResponse": {
"$ref": "swagger/schemas/ApiResponse.json"
},
"AuthorizeResponse": {
"$ref": "swagger/schemas/AuthorizeResponse.json"
},
"AuthorizeRequest": {
"$ref": "swagger/schemas/AuthorizeRequest.json"
},
"AuthorizeRefreshResponse": {
"$ref": "swagger/schemas/AuthorizeRefreshResponse.json"
},
"AuthorizeRefreshRequest": {
"$ref": "swagger/schemas/AuthorizeRefreshRequest.json"
}
},
"securitySchemes": {
"basicAuth": {
"type": "http",
"scheme": "basic"
},
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"security": [
{
"basicAuth": [],
"bearerAuth": []
}
]
}
+54
View File
@@ -0,0 +1,54 @@
{
"post": {
"tags": ["Authorize"],
"summary": "Customer Authorize Request",
"description": "Customer Authorize Request",
"operationId": "Authorize",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/AuthorizeRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/AuthorizeRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/AuthorizeRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/AuthorizeResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/AuthorizeResponse.json"
}
}
}
},
"400": {
"description": "Invalid request parameters"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
{
"post": {
"tags": ["Authorize Refresh"],
"summary": "Customer Authorize Refresh Request",
"description": "Customer Authorize Refresh Request",
"operationId": "AuthorizeRefresh",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/AuthorizeRefreshRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/AuthorizeRefreshRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/AuthorizeRefreshRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/AuthorizeRefreshResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/AuthorizeRefreshResponse.json"
}
}
}
},
"400": {
"description": "Invalid request parameters"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
{
"post": {
"tags": [
"CustomerConsent"
],
"summary": "Customer Consent Request",
"description": "Customer Consent Request",
"operationId": "CustomerConsent",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/CustomerConsentResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/CustomerConsentResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"post": {
"tags": [
"EligibilityCheck"
],
"summary": "Start the process - initiate steps to eligibility RAC Checks ",
"description": "Initiate Eligibility Check Request",
"operationId": "startEligibilityCheck",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/EligibilityCheckRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/EligibilityCheckRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/EligibilityCheckRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/EligibilityCheckResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/EligibilityCheckResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"post": {
"tags": [
"LoanStatus"
],
"summary": "Loan Information Request ",
"description": "Loan Information Request",
"operationId": "LoanStatus",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/LoanStatusRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/LoanStatusRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/LoanStatusRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/LoanStatusResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/LoanStatusResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
@@ -0,0 +1,57 @@
{
"post": {
"tags": [
"NotificationCallback"
],
"summary": "Loan Information Request ",
"description": "Loan Information Request",
"operationId": "NotificationCallback",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/NotificationCallbackResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/NotificationCallbackResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"post": {
"tags": [
"ProvideLoan"
],
"summary": "Provide Loan Request ",
"description": "Provide Loan Request",
"operationId": "ProvideLoan",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/ProvideLoanRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/ProvideLoanRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/ProvideLoanRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/ProvideLoanResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/ProvideLoanResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
{
"post": {
"tags": [
"Repayment"
],
"summary": "Repayment Request",
"description": "Repayment Request",
"operationId": "Repayment",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/RepaymentRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/RepaymentRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/RepaymentRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Repayment Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/RepaymentResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/RepaymentResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"post": {
"tags": [
"SelectOffer"
],
"summary": "This method is used the send the offer the customer selected to Simbrella ",
"description": "This method is used the send the offer the customer selected to Simbrella",
"operationId": "SelectOffer",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/SelectOfferRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/SelectOfferRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/SelectOfferRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/SelectOfferResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/SelectOfferResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": {
"type": "string"
},
"message": {
"type": "string"
}
},
"xml": {
"name": "##default"
}
}
@@ -0,0 +1,7 @@
{
"type": "object",
"properties": {},
"xml": {
"name": "AuthorizeRefreshRequest"
}
}
@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"access_token": {
"type": "string",
"example": "access_token"
}
},
"xml": {
"name": "AuthorizeRefreshResponse"
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"username": {
"type": "string",
"example": "user"
},
"password": {
"type": "string",
"example": "password"
}
},
"xml": {
"name": "AuthorizeRequest"
}
}
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"access_token": {
"type": "string",
"example": "access_token"
},
"refresh_token": {
"type": "string",
"example": "refresh_token"
}
},
"xml": {
"name": "AuthorizeResponse"
}
}
@@ -0,0 +1,37 @@
{
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "CustomerConsentRequest"
},
"transactionId": {
"type": "string",
"example": "20171209232177"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"requestTime": {
"type": "string",
"format": "date-time",
"example": "2019-10-18 14:26:21.063"
},
"consentType": {
"type": "string",
"example": "Revoke"
},
"channel": {
"type": "string",
"example": "USSD"
}
},
"xml": {
"name": "CustomerConsentRequest"
}
}
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Request is received"
}
},
"xml": {
"name": "CustomerConsentResponse"
}
}
@@ -0,0 +1,32 @@
{
"type": "object",
"properties": {
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"countryCode": {
"type": "string",
"example": "NGR"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "8093451342"
},
"channel": {
"type": "string",
"example": "100"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
}
},
"xml": {
"name": "EligibilityCheckRequest"
}
}
@@ -0,0 +1,82 @@
{
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
},
"transactionId": {
"type": "string",
"example": "TX12345"
},
"countryCode": {
"type": "string",
"example": "NG"
},
"msisdn": {
"type": "string",
"example": "3451342"
},
"eligibleOffers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"offerId": {
"type": "string",
"example": "Offer1"
},
"productId": {
"type": "string",
"example": "Product1"
},
"minAamount": {
"type": "number",
"format": "decimal",
"example": 100.00
},
"maxAamount": {
"type": "number",
"format": "decimal",
"example": 1000.00
},
"tenor": {
"type": "integer",
"example": 12
}
}
},
"example": [
{
"offerId": "Offer1",
"productId": "Product1",
"minAamount": 100.00,
"maxAamount": 1000.00,
"tenor": 12
},
{
"offerId": "Offer2",
"productId": "Product2",
"minAamount": 200.00,
"maxAamount": 2000.00,
"tenor": 24
}
]
},
"resultDescription": {
"type": "string",
"example": "Successful"
},
"resultCode": {
"type": "string",
"example": "00"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
}
},
"xml": {
"name": "EligibilityCheckResponse"
}
}
@@ -0,0 +1,24 @@
{
"type": "object",
"properties": {
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "3451342"
},
"channel": {
"type": "string",
"example": "USSD"
}
},
"xml": {
"name": "LoanStatusRequest"
}
}
@@ -0,0 +1,70 @@
{
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
},
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"loans": {
"type": "array",
"items": {
"type": "object",
"properties": {
"debtId": {
"type": "string",
"example": "123456789"
},
"loanDate": {
"type": "string",
"format": "date-time",
"example": "2019-10-18 14:26:21.063"
},
"dueDate": {
"type": "string",
"format": "date-time",
"example": "2019-11-20 14:26:21.063"
},
"currentLoanAmount": {
"type": "integer",
"example": 8500
},
"initialLoanAmount": {
"type": "integer",
"example": 10000
},
"defaultPenaltyFee": {
"type": "integer",
"example": 0
},
"continuousFee": {
"type": "integer",
"example": 0
},
"productId": {
"type": "string",
"example": "101"
}
}
}
},
"totalDebtAmount": {
"type": "integer",
"example": 8500
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "LoanStatusResponse"
}
}
@@ -0,0 +1,50 @@
{
"type": "object",
"properties": {
"fbnTransactionId": {
"type": "string",
"example": "123456789"
},
"transactionId": {
"type": "string",
"example": "123456789"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"debtId": {
"type": "string",
"example": "987654321"
},
"transactionType": {
"type": "string",
"example": "Disbursement"
},
"amountProvided": {
"type": "number",
"format": "float",
"example": 1000.00
},
"amountCollected": {
"type": "number",
"format": "float",
"example": 0.00
},
"responseCode": {
"type": "string",
"example": "00"
},
"responseDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "NotificationCallbackRequest"
}
}
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "NotificationCallbackResponse"
}
}
@@ -0,0 +1,45 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"msisdn": {
"type": "string",
"example": "3451342"
},
"requestedAmount": {
"type": "number",
"format": "decimal",
"example": 900
},
"collectionType": {
"type": "integer",
"example": 1
},
"offerId": {
"type": "integer",
"example": 1127
},
"channel": {
"type": "string",
"example": "100"
}
},
"xml": {
"name": "ProvideLoanRequest"
}
}
@@ -0,0 +1,36 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"msisdn": {
"type": "string",
"example": "3451342"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "ProvideLoanResponse"
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"type": "object",
"properties": {
"msisdn": {
"type": "string",
"example": "3451342"
},
"debtId": {
"type": "string",
"example": "10"
},
"productId": {
"type": "string",
"example": "101"
},
"transactionId": {
"type": "string",
"example": "20171209232115"
},
"customerId": {
"type": "string",
"example": "CID0000025585"
},
"channel": {
"type": "string",
"example": "USSD"
}
},
"xml": {
"name": "RepaymentRequest"
}
}
@@ -0,0 +1,28 @@
{
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
},
"productId": {
"type": "string",
"example": "101"
},
"debtId": {
"type": "string",
"example": "273194670"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "RepaymentResponse"
}
}
@@ -0,0 +1,41 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "1231231321232"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "123456789"
},
"requestedAmount": {
"type": "number",
"format": "double",
"example": 10000.55
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"productId": {
"type": "string",
"example": "101"
},
"channel": {
"type": "string",
"example": ""
}
},
"xml": {
"name": "SelectOffersRequest"
}
}
@@ -0,0 +1,112 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "1231231321232"
},
"customerId": {
"type": "string",
"example": "1256907"
},
"accountId": {
"type": "string",
"example": "5948306019"
},
"loan": {
"type": "array",
"items": {
"type": "object",
"properties": {
"offerId": {
"type": "string",
"example": "14451"
},
"productId": {
"type": "string",
"example": "2030"
},
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"upfrontPayment": {
"type": "number",
"format": "float",
"example": 1000.0
},
"interestRate": {
"type": "number",
"format": "float",
"example": 3.0
},
"ManagementRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"ManagementFee": {
"type": "number",
"format": "float",
"example": 1.0
},
"InsuranceRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"InsuranceFee": {
"type": "number",
"format": "float",
"example": 100.0
},
"VATRate": {
"type": "number",
"format": "float",
"example": 7.5
},
"VATamount": {
"type": "number",
"format": "float",
"example": 100.0
},
"recommendedRepaymentDates": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"2022-11-30"
]
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 11000.0
},
"totalRepaymentAmount": {
"type": "number",
"format": "float",
"example": 11000.0
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "SelectOffersResponse"
}
}
+13
View File
@@ -0,0 +1,13 @@
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
# logging.StreamHandler(),
logging.FileHandler("app.log", mode='a') # Log to file
]
)
logger = logging.getLogger("DetectionService")
+14
View File
@@ -0,0 +1,14 @@
services:
digifi-bank-to-product-core:
build: .
env_file:
- .env
ports:
- "${APP_PORT:-4500}:5000"
environment:
- FLASK_APP=${FLASK_APP}
- FLASK_ENV=${FLASK_ENV}
- DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
volumes:
- .:/app
restart: always
+36
View File
@@ -0,0 +1,36 @@
# Flask and Extensions
Flask==2.3.3
# Database
flask-sqlalchemy
flask-migrate
psycopg2-binary
alembic
# Schema for validations
Flask-Marshmallow==0.15.0
marshmallow==3.19.0
# CORS
Flask-Cors==3.0.10
# Deployment
gunicorn
# Swagger
flask-swagger-ui
# Env
python-dotenv
# Requests
requests
# JWT
flask-jwt-extended
# Kafka
confluent-kafka==1.9.2
+7
View File
@@ -0,0 +1,7 @@
from app import create_app
app = create_app()
if __name__ != "__main__":
# Expose WSGI app instance for Gunicorn
wsgi_app = app