first commit
This commit is contained in:
@@ -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=***************
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__/
|
||||||
|
.env
|
||||||
|
app.log
|
||||||
|
.DS_Store
|
||||||
|
migrations/__pycache__/
|
||||||
|
migrations/*.pycg
|
||||||
|
./vscode
|
||||||
|
.vscode/settings.json
|
||||||
Generated
+8
@@ -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
|
||||||
Generated
+10
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+6
@@ -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>
|
||||||
Generated
+8
@@ -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
@@ -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
@@ -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"]
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from .transaction_type import TransactionType
|
||||||
|
from .loan_status import LoanStatus
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class LoanStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
ACTIVE = "active"
|
||||||
|
REPAID = "repaid"
|
||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from .simbrella import SimbrellaIntegration
|
||||||
|
from .kafka import KafkaIntegration
|
||||||
@@ -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()
|
||||||
@@ -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"}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .routes import api
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizeRequestSchema(Schema):
|
||||||
|
username = fields.Str(required=True)
|
||||||
|
password = fields.Str(required=True)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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}"
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .handlers import register_error_handlers
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
migrate = Migrate()
|
||||||
@@ -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']
|
||||||
@@ -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}>'
|
||||||
|
|
||||||
@@ -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}>'
|
||||||
@@ -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}>'
|
||||||
@@ -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}>'
|
||||||
@@ -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}>'
|
||||||
@@ -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)
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
Reference in New Issue
Block a user