Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d2659462 | |||
| 569d4c45d7 | |||
| bfb35e0285 | |||
| 1a0e1f324a | |||
| 6593aedc56 | |||
| 818f968935 | |||
| 84648d8242 | |||
| 80a41d5ee1 | |||
| cf3a96ad98 | |||
| d0dccbf1ec | |||
| 5c8ffc5bbc | |||
| 9d7c3cfb32 | |||
| f6f8e369c4 | |||
| 39ea231fa0 | |||
| da05ba0f3d | |||
| 0f4455e738 | |||
| 53a843d129 | |||
| 03c12fd9b5 | |||
| a338441086 | |||
| f048dd99ba | |||
| 46b3c856b1 | |||
| 1cbb55fae6 | |||
| 38dbb32579 | |||
| af14baead5 | |||
| fbc3f090a0 | |||
| 675c38462e |
+15
-15
@@ -1,16 +1,16 @@
|
||||
|
||||
KAFKA_TIMEOUT=1000.0
|
||||
KAFKA_BROKER="10.20.30.50:9092"
|
||||
KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
|
||||
|
||||
# DATABASE_USER=firstadvance
|
||||
# DATABASE_PASSWORD=FirstAdvance!
|
||||
# DATABASE_HOST=10.20.30.60
|
||||
# DATABASE_PORT=5432
|
||||
# DATABASE_NAME=firstadvancedev
|
||||
|
||||
DATABASE_USER=system
|
||||
DATABASE_PASSWORD=FIRSTADV_PASS
|
||||
DATABASE_HOST=10.10.33.65
|
||||
DATABASE_PORT=1521
|
||||
|
||||
KAFKA_TIMEOUT=1000.0
|
||||
KAFKA_BROKER="10.20.30.50:9092"
|
||||
KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
|
||||
|
||||
# DATABASE_USER=firstadvance
|
||||
# DATABASE_PASSWORD=FirstAdvance!
|
||||
# DATABASE_HOST=10.20.30.60
|
||||
# DATABASE_PORT=5432
|
||||
# DATABASE_NAME=firstadvancedev
|
||||
|
||||
DATABASE_USER=system
|
||||
DATABASE_PASSWORD=FIRSTADV_PASS
|
||||
DATABASE_HOST=10.10.33.65
|
||||
DATABASE_PORT=1521
|
||||
DATABASE_SID=FREE
|
||||
+15
-15
@@ -1,16 +1,16 @@
|
||||
|
||||
KAFKA_TIMEOUT=1000.0
|
||||
KAFKA_BROKER="dev-events.simbrellang.net:9085"
|
||||
KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
|
||||
|
||||
# DATABASE_USER=firstadvance
|
||||
# DATABASE_PASSWORD=FirstAdvance!
|
||||
# DATABASE_HOST=dev-data.simbrellang.net
|
||||
# DATABASE_PORT=10532
|
||||
# DATABASE_NAME=firstadvancedev
|
||||
|
||||
DATABASE_USER=system
|
||||
DATABASE_PASSWORD=FIRSTADV_PASS
|
||||
DATABASE_HOST=10.10.33.65
|
||||
DATABASE_PORT=1521
|
||||
|
||||
KAFKA_TIMEOUT=1000.0
|
||||
KAFKA_BROKER="dev-events.simbrellang.net:9085"
|
||||
KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
|
||||
|
||||
# DATABASE_USER=firstadvance
|
||||
# DATABASE_PASSWORD=FirstAdvance!
|
||||
# DATABASE_HOST=dev-data.simbrellang.net
|
||||
# DATABASE_PORT=10532
|
||||
# DATABASE_NAME=firstadvancedev
|
||||
|
||||
DATABASE_USER=system
|
||||
DATABASE_PASSWORD=FIRSTADV_PASS
|
||||
DATABASE_HOST=10.10.33.65
|
||||
DATABASE_PORT=1521
|
||||
DATABASE_SID=FREE
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
.vscode/
|
||||
__pycache__/
|
||||
*/__pycache__/
|
||||
.env
|
||||
app.log
|
||||
.vscode/
|
||||
__pycache__/
|
||||
*/__pycache__/
|
||||
.env
|
||||
app.log
|
||||
.idea/
|
||||
+21
-21
@@ -1,21 +1,21 @@
|
||||
# Use Python base image
|
||||
FROM python:3.10
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy files to container
|
||||
COPY . /app
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Expose port 5000
|
||||
EXPOSE 5000
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
|
||||
# Run the application
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--timeout", "120", "wsgi:wsgi_app"]
|
||||
# Use Python base image
|
||||
FROM python:3.10
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy files to container
|
||||
COPY . /app
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Expose port 5000
|
||||
EXPOSE 5000
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
|
||||
# Run the application
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--timeout", "120", "wsgi:wsgi_app"]
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
# Flask API with Swagger in Docker
|
||||
|
||||
This repository contains a Flask API with Swagger documentation, running inside Docker containers.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Ensure you have the following installed:
|
||||
|
||||
- [Docker](https://www.docker.com/get-started)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
```sh
|
||||
git clone <your-repo-url>
|
||||
cd <your-repo-folder>
|
||||
```
|
||||
|
||||
### Setup and Run the Application
|
||||
|
||||
#### 1. Build and Start the Containers
|
||||
|
||||
```sh
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Build the Flask API container.
|
||||
- Start the Swagger UI container separately.
|
||||
|
||||
#### 2. Verify the Containers are Running
|
||||
|
||||
```sh
|
||||
docker ps
|
||||
```
|
||||
|
||||
#### 3. Access the API
|
||||
|
||||
- The **Flask API** will be available at:
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
- The **Swagger UI** will be available at:
|
||||
```
|
||||
http://localhost:9000
|
||||
```
|
||||
You can interact with the API documentation and test endpoints from here.
|
||||
|
||||
## Making API Requests
|
||||
|
||||
You can send requests using tools like `curl` or Postman:
|
||||
|
||||
```sh
|
||||
curl -X GET http://localhost:5000/hello
|
||||
```
|
||||
|
||||
## Stopping the Application
|
||||
|
||||
To stop the running containers, use:
|
||||
|
||||
```sh
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If there are permission issues, try running Docker commands with `sudo`.
|
||||
- If ports are in use, change them in `docker-compose.yml`.
|
||||
- To rebuild the containers, use:
|
||||
```sh
|
||||
docker-compose up --build -d
|
||||
```
|
||||
# Flask API with Swagger in Docker
|
||||
|
||||
This repository contains a Flask API with Swagger documentation, running inside Docker containers.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Ensure you have the following installed:
|
||||
|
||||
- [Docker](https://www.docker.com/get-started)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
```sh
|
||||
git clone <your-repo-url>
|
||||
cd <your-repo-folder>
|
||||
```
|
||||
|
||||
### Setup and Run the Application
|
||||
|
||||
#### 1. Build and Start the Containers
|
||||
|
||||
```sh
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Build the Flask API container.
|
||||
- Start the Swagger UI container separately.
|
||||
|
||||
#### 2. Verify the Containers are Running
|
||||
|
||||
```sh
|
||||
docker ps
|
||||
```
|
||||
|
||||
#### 3. Access the API
|
||||
|
||||
- The **Flask API** will be available at:
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
- The **Swagger UI** will be available at:
|
||||
```
|
||||
http://localhost:9000
|
||||
```
|
||||
You can interact with the API documentation and test endpoints from here.
|
||||
|
||||
## Making API Requests
|
||||
|
||||
You can send requests using tools like `curl` or Postman:
|
||||
|
||||
```sh
|
||||
curl -X GET http://localhost:5000/hello
|
||||
```
|
||||
|
||||
## Stopping the Application
|
||||
|
||||
To stop the running containers, use:
|
||||
|
||||
```sh
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If there are permission issues, try running Docker commands with `sudo`.
|
||||
- If ports are in use, change them in `docker-compose.yml`.
|
||||
- To rebuild the containers, use:
|
||||
```sh
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
|
||||
+36
-36
@@ -1,36 +1,36 @@
|
||||
from flask import Flask
|
||||
from flask_mail import Mail
|
||||
from flask_cors import CORS
|
||||
from app.config import Config
|
||||
from app.routes import auth_bp, autocall_bp
|
||||
from app.response import (method_not_allowed, unsupported_media_type, not_found, bad_request)
|
||||
from app.extensions import db, mail
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Factory function to create a Flask app instance"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Load configuration
|
||||
app.config.from_object(Config)
|
||||
|
||||
# Setup CORS
|
||||
CORS(app)
|
||||
|
||||
# Initialize Flask-Mail
|
||||
mail.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(autocall_bp, url_prefix="/autocall")
|
||||
|
||||
# Error Handlers
|
||||
app.register_error_handler(405, method_not_allowed)
|
||||
app.register_error_handler(415, unsupported_media_type)
|
||||
app.register_error_handler(404, not_found)
|
||||
app.register_error_handler(400, bad_request)
|
||||
|
||||
# Database
|
||||
db.init_app(app)
|
||||
|
||||
return app
|
||||
from flask import Flask
|
||||
from flask_mail import Mail
|
||||
from flask_cors import CORS
|
||||
from app.config import Config
|
||||
from app.routes import auth_bp, autocall_bp
|
||||
from app.response import (method_not_allowed, unsupported_media_type, not_found, bad_request)
|
||||
from app.extensions import db, mail
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Factory function to create a Flask app instance"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Load configuration
|
||||
app.config.from_object(Config)
|
||||
|
||||
# Setup CORS
|
||||
CORS(app)
|
||||
|
||||
# Initialize Flask-Mail
|
||||
mail.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(autocall_bp, url_prefix="/autocall")
|
||||
|
||||
# Error Handlers
|
||||
app.register_error_handler(405, method_not_allowed)
|
||||
app.register_error_handler(415, unsupported_media_type)
|
||||
app.register_error_handler(404, not_found)
|
||||
app.register_error_handler(400, bad_request)
|
||||
|
||||
# Database
|
||||
db.init_app(app)
|
||||
|
||||
return app
|
||||
|
||||
+83
-69
@@ -1,69 +1,83 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Config:
|
||||
"""Base configuration for Flask app"""
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey")
|
||||
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your_jwt_secret")
|
||||
DEBUG = True
|
||||
|
||||
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
|
||||
KAFKA_TOPICS = [topic.strip() for topic in os.getenv("KAFKA_TOPICS", "PROCESS_PAYMENT,LOAN_REPAYMENT").split(",") if topic.strip()]
|
||||
KAFKA_TIMEOUT = float( os.getenv("KAFKA_TIMEOUT", 1000.0) )
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
BANK_CALL_APP_ID = os.getenv("BANK_CALL_APP_ID", "app1")
|
||||
BANK_CALL_API_KEY = os.getenv("BANK_CALL_API_KEY", "testtest-api-key-12345")
|
||||
BANK_CALL_BASIC_AUTH_USERNAME = os.environ.get(
|
||||
"BANK_CALL_BASIC_AUTH_USERNAME", "simbrella"
|
||||
)
|
||||
BANK_CALL_BASIC_AUTH_PASSWORD = os.environ.get(
|
||||
"BANK_CALL_BASIC_AUTH_PASSWORD", "G7$k9@pL2!qR"
|
||||
)
|
||||
|
||||
DATABASE_USER = os.getenv("DATABASE_USER")
|
||||
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD")
|
||||
DATABASE_HOST = os.getenv("DATABASE_HOST")
|
||||
DATABASE_NAME = os.getenv("DATABASE_NAME")
|
||||
DATABASE_PORT = os.getenv("DATABASE_PORT", 10532)
|
||||
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
|
||||
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
|
||||
|
||||
|
||||
SQLALCHEMY_DATABASE_URI_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
|
||||
#SQLALCHEMY_DATABASE_URI_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI_FULL", SQLALCHEMY_DATABASE_URI_INTERNAL)
|
||||
|
||||
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
# SQLALCHEMY_ECHO = True
|
||||
OVERRIDE_COLLECTION_TRANCATION_ID = int(os.getenv("OVERRIDE_COLLECTION_TRANCATION_ID", 100))
|
||||
|
||||
|
||||
MAIL_SERVER = os.getenv('MAIL_SERVER','smtp.zoho.com')
|
||||
MAIL_PORT = os.getenv('MAIL_PORT', 587)
|
||||
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'firstadvance@dynamikservices.tech')
|
||||
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
||||
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'True').lower() in ('true', '1', 'yes')
|
||||
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'False').lower() in ('true', '1', 'yes')
|
||||
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
|
||||
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'chinenyeumeaku@gmail.com,umeakuchinenye@gmail.com')
|
||||
|
||||
BANK_CALL_API_TIME_OUT = os.getenv("BANK_CALL_API_TIME_OUT", 100)
|
||||
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net/api")
|
||||
BANK_CALL_SMS_BASE_URL= os.getenv("BANK_CALL_SMS_BASE_URL","https://first-advance-middleware-develop.fbn-devops-dev-asenv.appserviceenvironment.net/SMS")
|
||||
BANK_CALL_DISBURSE_LOAN_ENDPOINT = os.getenv("BANK_CALL_DISBURSE_LOAN_ENDPOINT","/DisburseLoan")
|
||||
BANK_CALL_COLLECT_LOAN_ENDPOINT = os.getenv("BANK_CALL_COLLECT_LOAN_ENDPOINT","/CollectLoan")
|
||||
BANK_CALL_TRANSACTION_VERIFY = os.getenv("BANK_CALL_TRANSACTION_VERIFY", "/TransactionVerify")
|
||||
BANK_HEALTH_CHECK_ENDPOINT = os.getenv("BANK_HEALTH_CHECK_ENDPOINT", "/system-health-check")
|
||||
BANK_CALL_AUTH_ENDPOINT = os.getenv("BANK_CALL_AUTH_ENDPOINT", "/Auth/generate-token")
|
||||
BANK_GRANT_TYPE = os.getenv("BANK_GRANT_TYPE", "password")
|
||||
TEST_NO = os.getenv("TEST_NO", "2347038224367")
|
||||
|
||||
settings = Config()
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Config:
|
||||
"""Base configuration for Flask app"""
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey")
|
||||
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your_jwt_secret")
|
||||
DEBUG = True
|
||||
|
||||
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
|
||||
KAFKA_TOPICS = [topic.strip() for topic in os.getenv("KAFKA_TOPICS", "PROCESS_PAYMENT,LOAN_REPAYMENT").split(",") if topic.strip()]
|
||||
KAFKA_TIMEOUT = float( os.getenv("KAFKA_TIMEOUT", 1000.0) )
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
BANK_CALL_APP_ID = os.getenv("BANK_CALL_APP_ID", "app1")
|
||||
BANK_CALL_API_KEY = os.getenv("BANK_CALL_API_KEY", "testtest-api-key-12345")
|
||||
BANK_CALL_BASIC_AUTH_USERNAME = os.environ.get(
|
||||
"BANK_CALL_BASIC_AUTH_USERNAME", "simbrella"
|
||||
)
|
||||
BANK_CALL_BASIC_AUTH_PASSWORD = os.environ.get(
|
||||
"BANK_CALL_BASIC_AUTH_PASSWORD", "G7$k9@pL2!qR"
|
||||
)
|
||||
|
||||
DATABASE_USER = os.getenv("DATABASE_USER")
|
||||
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD")
|
||||
DATABASE_HOST = os.getenv("DATABASE_HOST")
|
||||
DATABASE_NAME = os.getenv("DATABASE_NAME")
|
||||
DATABASE_PORT = os.getenv("DATABASE_PORT", 10532)
|
||||
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
|
||||
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
|
||||
|
||||
|
||||
SQLALCHEMY_DATABASE_URI_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
|
||||
#SQLALCHEMY_DATABASE_URI_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI_FULL", SQLALCHEMY_DATABASE_URI_INTERNAL)
|
||||
|
||||
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
# SQLALCHEMY_ECHO = True
|
||||
OVERRIDE_COLLECTION_TRANCATION_ID = int(os.getenv("OVERRIDE_COLLECTION_TRANCATION_ID", 100))
|
||||
|
||||
|
||||
MAIL_SERVER = os.getenv('MAIL_SERVER','smtp.zoho.com')
|
||||
MAIL_PORT = os.getenv('MAIL_PORT', 587)
|
||||
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'firstadvance@dynamikservices.tech')
|
||||
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
||||
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'True').lower() in ('true', '1', 'yes')
|
||||
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'False').lower() in ('true', '1', 'yes')
|
||||
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
|
||||
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'chinenyeumeaku@gmail.com,umeakuchinenye@gmail.com')
|
||||
|
||||
|
||||
# Processing Overdue LOANS sections
|
||||
OVERDUE_LOAN_BATCH_SIZE = int(os.getenv("OVERDUE_LOAN_BATCH_SIZE", 10))
|
||||
OVERDUE_LOAN_DELAY_SECONDS = int(os.getenv("OVERDUE_LOAN_DELAY_SECONDS", 5))
|
||||
OVERDUE_LOAN_BATCH_DELAY_SECONDS = int(
|
||||
os.getenv("OVERDUE_LOAN_BATCH_DELAY_SECONDS", 5)
|
||||
)
|
||||
OVERDUE_GRACE_PERIOD_DAYS = int(os.getenv("OVERDUE_GRACE_PERIOD_DAYS", 30))
|
||||
OVERDUE_PROCESSING_LIST_LIMIT = int(os.getenv("OVERDUE_PROCESSING_LIST_LIMIT", 100))
|
||||
PENAL_CHARGE_PERCENTAGE = os.getenv("PENAL_CHARGE_PERCENTAGE", 1)
|
||||
PENAL_CHARGE_INTERVAL_DAYS = os.getenv("PENAL_CHARGE_INTERVAL_DAYS", 30)
|
||||
PENAL_CHARGE_MAXIMUM_COUNT = os.getenv("PENAL_CHARGE_MAXIMUM_COUNT", 6)
|
||||
|
||||
|
||||
BANK_CALL_API_TIME_OUT = os.getenv("BANK_CALL_API_TIME_OUT", 100)
|
||||
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net/api")
|
||||
BANK_CALL_SMS_BASE_URL= os.getenv("BANK_CALL_SMS_BASE_URL","https://first-advance-middleware-develop.fbn-devops-dev-asenv.appserviceenvironment.net/SMS")
|
||||
BANK_CALL_DISBURSE_LOAN_ENDPOINT = os.getenv("BANK_CALL_DISBURSE_LOAN_ENDPOINT","/DisburseLoan")
|
||||
BANK_CALL_COLLECT_LOAN_ENDPOINT = os.getenv("BANK_CALL_COLLECT_LOAN_ENDPOINT","/CollectLoan")
|
||||
BANK_CALL_TRANSACTION_VERIFY = os.getenv("BANK_CALL_TRANSACTION_VERIFY", "/TransactionVerify")
|
||||
BANK_HEALTH_CHECK_ENDPOINT = os.getenv("BANK_HEALTH_CHECK_ENDPOINT", "/system-health-check")
|
||||
BANK_CALL_AUTH_ENDPOINT = os.getenv("BANK_CALL_AUTH_ENDPOINT", "/Auth/generate-token")
|
||||
BANK_GRANT_TYPE = os.getenv("BANK_GRANT_TYPE", "password")
|
||||
TEST_NO = os.getenv("TEST_NO", "2347038224367")
|
||||
|
||||
settings = Config()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .transaction_type import TransactionType
|
||||
from .loan_status import LoanStatus
|
||||
from .repayment_schedule_status import RepaymentScheduleStatus
|
||||
from .transaction_type import TransactionType
|
||||
from .loan_status import LoanStatus
|
||||
from .repayment_schedule_status import RepaymentScheduleStatus
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from enum import Enum
|
||||
|
||||
class LoanStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
ACTIVE = "active"
|
||||
ACTIVE_PARTIAL = "active_partial"
|
||||
START_REPAY = "start_repay"
|
||||
from enum import Enum
|
||||
|
||||
class LoanStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
ACTIVE = "active"
|
||||
ACTIVE_PARTIAL = "active_partial"
|
||||
START_REPAY = "start_repay"
|
||||
REPAID = "repaid"
|
||||
@@ -1,5 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
class RepaymentScheduleStatus(str, Enum):
|
||||
PARTIALLY_PAID = "partially_paid"
|
||||
REPAID = "repaid"
|
||||
from enum import Enum
|
||||
|
||||
class RepaymentScheduleStatus(str, Enum):
|
||||
PARTIALLY_PAID = "partially_paid"
|
||||
REPAID = "repaid"
|
||||
ACTIVE = "active"
|
||||
OVERDUE = "overdue"
|
||||
@@ -1,10 +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"
|
||||
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"
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_mail import Mail
|
||||
|
||||
mail = Mail()
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_mail import Mail
|
||||
|
||||
mail = Mail()
|
||||
db = SQLAlchemy()
|
||||
@@ -1,57 +1,63 @@
|
||||
import random
|
||||
import string
|
||||
from app.services.repayment import RepaymentService
|
||||
from app.services.loan import LoanService
|
||||
from app.helpers.response_helper import ResponseHelper
|
||||
from app.utils.logger import logger
|
||||
from app.config import settings
|
||||
|
||||
OVERRIDE_COLLECTION_TRANCATION_ID = settings.OVERRIDE_COLLECTION_TRANCATION_ID
|
||||
|
||||
class CollectLoanHelper:
|
||||
@staticmethod
|
||||
def _validate_repayment_and_loan(data):
|
||||
repayment = RepaymentService.get_repayment_by_id(id=data['Id'])
|
||||
if not repayment:
|
||||
logger.info(f"Repayment id: {data['Id']}, was not found")
|
||||
return None, None, ResponseHelper.error("Repayment not found")
|
||||
|
||||
repayment_data = repayment.to_dict()
|
||||
loan = LoanService.get_loan_by_loan_id(loan_id=int(repayment_data['loanId']))
|
||||
if not loan:
|
||||
logger.info(f"Loan id: {repayment_data['loanId']}, was not found")
|
||||
return None, None, ResponseHelper.error("Loan not found")
|
||||
|
||||
loan
|
||||
return repayment_data, loan, None
|
||||
|
||||
@staticmethod
|
||||
def _build_collect_loan_payload(loan_data, repayment_data, data, collectionMethod):
|
||||
logger.info(f"building CollectLoan endpoint with data: {loan_data}")
|
||||
|
||||
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
|
||||
#this can be overridden based on config
|
||||
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
|
||||
if OVERRIDE_COLLECTION_TRANCATION_ID == 100:
|
||||
t_id = loan_data['transactionId']
|
||||
return {
|
||||
"transactionId": t_id,
|
||||
"fbnTransactionId": loan_data['transactionId'],
|
||||
"debtId": debtId,
|
||||
"customerId": repayment_data['customerId'],
|
||||
"accountId": loan_data['accountId'],
|
||||
"productId": repayment_data['productId'],
|
||||
"collectAmount": (
|
||||
data['overdueLoanScheduleAmount']
|
||||
if data.get('overdueLoanScheduleAmount')
|
||||
is not None else loan_data.get('balance', 0)
|
||||
),
|
||||
"penalCharge": 0,
|
||||
"channel": "USSD",
|
||||
"collectionMethod": collectionMethod,
|
||||
"lienAmount": 0,
|
||||
"countryId": "NG",
|
||||
"comment": "COLLECT LOAN"
|
||||
}
|
||||
|
||||
import random
|
||||
import string
|
||||
from app.services.repayment import RepaymentService
|
||||
from app.services.loan import LoanService
|
||||
from app.helpers.response_helper import ResponseHelper
|
||||
from app.utils.logger import logger
|
||||
from app.config import settings
|
||||
|
||||
OVERRIDE_COLLECTION_TRANCATION_ID = settings.OVERRIDE_COLLECTION_TRANCATION_ID
|
||||
|
||||
class CollectLoanHelper:
|
||||
@staticmethod
|
||||
def _validate_repayment_and_loan(data):
|
||||
repayment = RepaymentService.get_repayment_by_id(id=data['Id'])
|
||||
if not repayment:
|
||||
logger.info(f"Repayment id: {data['Id']}, was not found")
|
||||
return None, None, ResponseHelper.error("Repayment not found")
|
||||
|
||||
repayment_data = repayment.to_dict()
|
||||
loan = LoanService.get_loan_by_loan_id(loan_id=int(repayment_data['loanId']))
|
||||
if not loan:
|
||||
logger.info(f"Loan id: {repayment_data['loanId']}, was not found")
|
||||
return None, None, ResponseHelper.error("Loan not found")
|
||||
|
||||
loan
|
||||
return repayment_data, loan, None
|
||||
|
||||
@staticmethod
|
||||
def _build_collect_loan_payload(loan_data, repayment_data, data, collectionMethod):
|
||||
logger.info(f"building CollectLoan endpoint with data: {loan_data}")
|
||||
|
||||
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
|
||||
#this can be overridden based on config
|
||||
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
|
||||
if OVERRIDE_COLLECTION_TRANCATION_ID == 100:
|
||||
t_id = loan_data['transactionId']
|
||||
return {
|
||||
"transactionId": t_id,
|
||||
"fbnTransactionId": loan_data['transactionId'],
|
||||
"debtId": debtId,
|
||||
"customerId": repayment_data['customerId'],
|
||||
"accountId": loan_data['accountId'],
|
||||
"productId": repayment_data['productId'],
|
||||
"collectAmount": (
|
||||
data['overdueLoanScheduleAmount']
|
||||
if data.get('overdueLoanScheduleAmount')
|
||||
is not None else loan_data.get('balance', 0)
|
||||
),
|
||||
"penalCharge": 0,
|
||||
"channel": "USSD",
|
||||
"collectionMethod": collectionMethod,
|
||||
"lienAmount": 0,
|
||||
"countryId": "NG",
|
||||
"comment": "COLLECT LOAN"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def chunk_list(data, chunk_size):
|
||||
"""Yield successive chunk_size chunks from data."""
|
||||
for i in range(0, len(data), chunk_size):
|
||||
yield data[i:i + chunk_size]
|
||||
|
||||
|
||||
+250
-250
@@ -1,251 +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.
|
||||
"""
|
||||
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)
|
||||
@@ -1,3 +1,3 @@
|
||||
from .kafka import KafkaIntegration
|
||||
from .simbrella import SimbrellaClient
|
||||
from .kafka import KafkaIntegration
|
||||
from .simbrella import SimbrellaClient
|
||||
from .bank_service import BankService
|
||||
@@ -1,25 +1,25 @@
|
||||
import requests
|
||||
from app.config import settings
|
||||
from app.utils.auth import get_headers
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class BankService:
|
||||
BANK_CALL_BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
BANK_HEALTH_CHECK_ENDPOINT = settings.BANK_HEALTH_CHECK_ENDPOINT
|
||||
BANK_CALL_APP_ID = settings.BANK_CALL_APP_ID
|
||||
|
||||
@staticmethod
|
||||
def health_check():
|
||||
api_url = f"{BankService.BANK_CALL_BASE_URL}{BankService.BANK_HEALTH_CHECK_ENDPOINT}"
|
||||
logger.info(f"Calling Health Check endpoint: {api_url}")
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, timeout=5, headers=get_headers())
|
||||
logger.info(f"Health Check response status code: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Health Check API call failed: {str(e)}", exc_info=True)
|
||||
import requests
|
||||
from app.config import settings
|
||||
from app.utils.auth import get_headers
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class BankService:
|
||||
BANK_CALL_BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
BANK_HEALTH_CHECK_ENDPOINT = settings.BANK_HEALTH_CHECK_ENDPOINT
|
||||
BANK_CALL_APP_ID = settings.BANK_CALL_APP_ID
|
||||
|
||||
@staticmethod
|
||||
def health_check():
|
||||
api_url = f"{BankService.BANK_CALL_BASE_URL}{BankService.BANK_HEALTH_CHECK_ENDPOINT}"
|
||||
logger.info(f"Calling Health Check endpoint: {api_url}")
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, timeout=5, headers=get_headers())
|
||||
logger.info(f"Health Check response status code: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Health Check API call failed: {str(e)}", exc_info=True)
|
||||
raise
|
||||
+156
-156
@@ -1,156 +1,156 @@
|
||||
from confluent_kafka import Consumer, Producer
|
||||
import json
|
||||
from app.utils.logger import logger
|
||||
from app.config import settings
|
||||
import requests
|
||||
from app.integrations.simbrella import SimbrellaClient
|
||||
|
||||
|
||||
class KafkaIntegration:
|
||||
|
||||
BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
|
||||
_consumer = None
|
||||
|
||||
_consumer_config = {
|
||||
"bootstrap.servers": settings.KAFKA_BROKER,
|
||||
"group.id": "loan-service-consumer",
|
||||
"auto.offset.reset": "earliest",
|
||||
"enable.auto.commit": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_consumer():
|
||||
"""Kafka consumer"""
|
||||
if not KafkaIntegration._consumer:
|
||||
KafkaIntegration._consumer = Consumer(KafkaIntegration._consumer_config)
|
||||
logger.info(
|
||||
f"Consumer connected to Kafka broker at {KafkaIntegration._consumer_config['bootstrap.servers']}"
|
||||
)
|
||||
return KafkaIntegration._consumer
|
||||
|
||||
@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 receive_messages(topics, timeout):
|
||||
"""
|
||||
Receive messages from a Kafka topic.
|
||||
|
||||
:param topics: The Kafka topics to subscribe to
|
||||
:param timeout: Time to wait for a message (in seconds)
|
||||
:return: The message value (decoded) or None if no message is received
|
||||
"""
|
||||
consumer = KafkaIntegration._get_consumer()
|
||||
consumer.subscribe(topics)
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Waiting for messages from topic {topics} with this timeout: {timeout}..."
|
||||
)
|
||||
message = []
|
||||
try:
|
||||
msg = consumer.poll(timeout=timeout)
|
||||
|
||||
logger.info(str(msg.value))
|
||||
|
||||
logger.info(
|
||||
f"Received message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: "
|
||||
)
|
||||
|
||||
if msg is None:
|
||||
logger.info(f"No message received from topic {topics} within timeout")
|
||||
return None
|
||||
if msg.error():
|
||||
logger.info(f"Consumer error: {msg.error()}")
|
||||
raise RuntimeError(f"Consumer error: {msg.error()}")
|
||||
|
||||
# Decode and return the message value
|
||||
message_value= {"value":''}
|
||||
if msg.value() != "":
|
||||
message_value = msg.value().decode("utf-8")
|
||||
logger.info(
|
||||
f"Received message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: {message_value}"
|
||||
)
|
||||
|
||||
# Invalid Json will stop this process
|
||||
try:
|
||||
message = json.loads(message_value) if message_value else None
|
||||
except ValueError as e:
|
||||
return message # Invalid JSON JUST TURN BACK HERE
|
||||
else:
|
||||
pass # valid json
|
||||
|
||||
# Call the endpoint if provided
|
||||
if message:
|
||||
logger.info(
|
||||
f"Received message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: {message}"
|
||||
)
|
||||
current_topic = msg.topic()
|
||||
|
||||
if current_topic=="PROCESS_PAYMENT":
|
||||
KafkaIntegration._call_disbursement_service(message)
|
||||
|
||||
if current_topic=="LOAN_REPAYMENT":
|
||||
KafkaIntegration._call_collect_loan_service(message)
|
||||
logger.info(
|
||||
f"Loan Repayment message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: {message}"
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while receiving message: {e}")
|
||||
raise
|
||||
|
||||
#return []
|
||||
|
||||
@staticmethod
|
||||
def close_consumer():
|
||||
"""Shutdown consumer"""
|
||||
consumer = KafkaIntegration._get_consumer()
|
||||
consumer.close()
|
||||
logger.info("Kafka consumer closed")
|
||||
|
||||
@staticmethod
|
||||
def _call_disbursement_service(message):
|
||||
"""Call the disbursement service with the received message"""
|
||||
logger.info(f"Calling disbursement service with message: {message}")
|
||||
|
||||
try:
|
||||
response = SimbrellaClient.disburse_loan(message)
|
||||
logger.info(
|
||||
f"Successfully sent message to disbursement service: {response}"
|
||||
)
|
||||
|
||||
# LoanService.set_disbursement_date(loan_id=loan_data['debtId'],
|
||||
# customer_id=customerId) # must mark it on way out
|
||||
#
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Failed to call disbursement service: {e}")
|
||||
#raise
|
||||
|
||||
@staticmethod
|
||||
def _call_collect_loan_service(message):
|
||||
"""Call the collect loan service with the received message"""
|
||||
logger.info(f"Calling collect_loan service with message: {message}")
|
||||
|
||||
try:
|
||||
#Calling CollectLoan endpoint with data: {'transactionId': 'TRCVIC85641527829', 'customerId': 'ZX48440946', 'productId': 'AMPC', 'loanRef': 'TRCVIC85641527829USSDAMPC', 'debtId': '014231'}
|
||||
response = SimbrellaClient.collect_loan_user_initiated(message)
|
||||
logger.info(
|
||||
f"Successfully sent message to collect_loan service: {response}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to call collect_loan service: {e}")
|
||||
# raise
|
||||
from confluent_kafka import Consumer, Producer
|
||||
import json
|
||||
from app.utils.logger import logger
|
||||
from app.config import settings
|
||||
import requests
|
||||
from app.integrations.simbrella import SimbrellaClient
|
||||
|
||||
|
||||
class KafkaIntegration:
|
||||
|
||||
BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
|
||||
_consumer = None
|
||||
|
||||
_consumer_config = {
|
||||
"bootstrap.servers": settings.KAFKA_BROKER,
|
||||
"group.id": "loan-service-consumer",
|
||||
"auto.offset.reset": "earliest",
|
||||
"enable.auto.commit": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_consumer():
|
||||
"""Kafka consumer"""
|
||||
if not KafkaIntegration._consumer:
|
||||
KafkaIntegration._consumer = Consumer(KafkaIntegration._consumer_config)
|
||||
logger.info(
|
||||
f"Consumer connected to Kafka broker at {KafkaIntegration._consumer_config['bootstrap.servers']}"
|
||||
)
|
||||
return KafkaIntegration._consumer
|
||||
|
||||
@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 receive_messages(topics, timeout):
|
||||
"""
|
||||
Receive messages from a Kafka topic.
|
||||
|
||||
:param topics: The Kafka topics to subscribe to
|
||||
:param timeout: Time to wait for a message (in seconds)
|
||||
:return: The message value (decoded) or None if no message is received
|
||||
"""
|
||||
consumer = KafkaIntegration._get_consumer()
|
||||
consumer.subscribe(topics)
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Waiting for messages from topic {topics} with this timeout: {timeout}..."
|
||||
)
|
||||
message = []
|
||||
try:
|
||||
msg = consumer.poll(timeout=timeout)
|
||||
|
||||
logger.info(str(msg.value))
|
||||
|
||||
logger.info(
|
||||
f"Received message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: "
|
||||
)
|
||||
|
||||
if msg is None:
|
||||
logger.info(f"No message received from topic {topics} within timeout")
|
||||
return None
|
||||
if msg.error():
|
||||
logger.info(f"Consumer error: {msg.error()}")
|
||||
raise RuntimeError(f"Consumer error: {msg.error()}")
|
||||
|
||||
# Decode and return the message value
|
||||
message_value= {"value":''}
|
||||
if msg.value() != "":
|
||||
message_value = msg.value().decode("utf-8")
|
||||
logger.info(
|
||||
f"Received message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: {message_value}"
|
||||
)
|
||||
|
||||
# Invalid Json will stop this process
|
||||
try:
|
||||
message = json.loads(message_value) if message_value else None
|
||||
except ValueError as e:
|
||||
return message # Invalid JSON JUST TURN BACK HERE
|
||||
else:
|
||||
pass # valid json
|
||||
|
||||
# Call the endpoint if provided
|
||||
if message:
|
||||
logger.info(
|
||||
f"Received message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: {message}"
|
||||
)
|
||||
current_topic = msg.topic()
|
||||
|
||||
if current_topic=="PROCESS_PAYMENT":
|
||||
KafkaIntegration._call_disbursement_service(message)
|
||||
|
||||
if current_topic=="LOAN_REPAYMENT":
|
||||
KafkaIntegration._call_collect_loan_service(message)
|
||||
logger.info(
|
||||
f"Loan Repayment message from {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}: {message}"
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while receiving message: {e}")
|
||||
raise
|
||||
|
||||
#return []
|
||||
|
||||
@staticmethod
|
||||
def close_consumer():
|
||||
"""Shutdown consumer"""
|
||||
consumer = KafkaIntegration._get_consumer()
|
||||
consumer.close()
|
||||
logger.info("Kafka consumer closed")
|
||||
|
||||
@staticmethod
|
||||
def _call_disbursement_service(message):
|
||||
"""Call the disbursement service with the received message"""
|
||||
logger.info(f"Calling disbursement service with message: {message}")
|
||||
|
||||
try:
|
||||
response = SimbrellaClient.disburse_loan(message)
|
||||
logger.info(
|
||||
f"Successfully sent message to disbursement service: {response}"
|
||||
)
|
||||
|
||||
# LoanService.set_disbursement_date(loan_id=loan_data['debtId'],
|
||||
# customer_id=customerId) # must mark it on way out
|
||||
#
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Failed to call disbursement service: {e}")
|
||||
#raise
|
||||
|
||||
@staticmethod
|
||||
def _call_collect_loan_service(message):
|
||||
"""Call the collect loan service with the received message"""
|
||||
logger.info(f"Calling collect_loan service with message: {message}")
|
||||
|
||||
try:
|
||||
#Calling CollectLoan endpoint with data: {'transactionId': 'TRCVIC85641527829', 'customerId': 'ZX48440946', 'productId': 'AMPC', 'loanRef': 'TRCVIC85641527829USSDAMPC', 'debtId': '014231'}
|
||||
response = SimbrellaClient.collect_loan_user_initiated(message)
|
||||
logger.info(
|
||||
f"Successfully sent message to collect_loan service: {response}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to call collect_loan service: {e}")
|
||||
# raise
|
||||
|
||||
+545
-535
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
from .transactions import Transaction
|
||||
from .repayment import Repayment
|
||||
from .loan import Loan
|
||||
from .loan_charge import LoanCharge
|
||||
from .customer import Customer
|
||||
from .account import Account
|
||||
from .repayments_data import RepaymentsData
|
||||
from .salary import Salary
|
||||
|
||||
from .transactions import Transaction
|
||||
from .repayment import Repayment
|
||||
from .loan import Loan
|
||||
from .loan_charge import LoanCharge
|
||||
from .customer import Customer
|
||||
from .account import Account
|
||||
from .repayments_data import RepaymentsData
|
||||
from .salary import Salary
|
||||
|
||||
__all__ = ['Transaction', 'Repayment', 'Loan', 'LoanCharge', 'Customer', 'Account', 'RepaymentsData','Salary']
|
||||
+25
-25
@@ -1,25 +1,25 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Account {self.id}>'
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Account {self.id}>'
|
||||
|
||||
+41
-41
@@ -1,41 +1,41 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
loans = relationship(
|
||||
"Loan",
|
||||
primaryjoin="Customer.id == Loan.customer_id",
|
||||
foreign_keys="Loan.customer_id",
|
||||
back_populates="customer",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_customer(cls, customer_id):
|
||||
"""
|
||||
Get customer by ID.
|
||||
"""
|
||||
customer = cls.query.filter_by(id=customer_id).first()
|
||||
|
||||
if not customer:
|
||||
raise ValueError(f"Customer does not exist")
|
||||
return customer
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Customer {self.id}>'
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
loans = relationship(
|
||||
"Loan",
|
||||
primaryjoin="Customer.id == Loan.customer_id",
|
||||
foreign_keys="Loan.customer_id",
|
||||
back_populates="customer",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_customer(cls, customer_id):
|
||||
"""
|
||||
Get customer by ID.
|
||||
"""
|
||||
customer = cls.query.filter_by(id=customer_id).first()
|
||||
|
||||
if not customer:
|
||||
raise ValueError(f"Customer does not exist")
|
||||
return customer
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Customer {self.id}>'
|
||||
|
||||
+415
-390
@@ -1,390 +1,415 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from sqlalchemy import and_, or_, not_
|
||||
from sqlalchemy.sql import func
|
||||
from app.utils.logger import logger
|
||||
from app.extensions import db
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import datetime, timezone
|
||||
|
||||
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)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
original_transaction = db.Column(db.String(50), nullable=True)
|
||||
account_id = db.Column(db.String(50), nullable=False)
|
||||
offer_id = db.Column(db.String(20), nullable=False)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
collection_type = db.Column(db.String(20), nullable=True)
|
||||
current_loan_amount = db.Column(db.Float, nullable=True)
|
||||
initial_loan_amount = db.Column(db.Float, nullable=False)
|
||||
default_penalty_fee = db.Column(db.Float, default=0)
|
||||
continuous_fee = db.Column(db.Float, default=0)
|
||||
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
|
||||
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
tenor = db.Column(db.Integer, nullable=True)
|
||||
due_date = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
disburse_date = db.Column(db.DateTime, nullable=True)
|
||||
disburse_verify = db.Column(db.DateTime, nullable=True)
|
||||
disburse_result = db.Column(db.String(10), nullable=True)
|
||||
disburse_description = db.Column(db.String(100), nullable=True)
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
reference = db.Column(db.String(50), nullable=True)
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
primaryjoin="Customer.id == Loan.customer_id",
|
||||
foreign_keys=[customer_id],
|
||||
back_populates="loans",
|
||||
)
|
||||
|
||||
loan_charges = relationship(
|
||||
"LoanCharge",
|
||||
primaryjoin="Loan.id == LoanCharge.loan_id",
|
||||
foreign_keys="LoanCharge.loan_id",
|
||||
back_populates="loan",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Loan {self.id}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Loan object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'debtId': self.id,
|
||||
"customerId": self.customer_id,
|
||||
'initialLoanAmount': self.initial_loan_amount,
|
||||
'currentLoanAmount': self.current_loan_amount,
|
||||
'defaultPenaltyFee': self.default_penalty_fee,
|
||||
'continuousFee': self.continuous_fee,
|
||||
'collectionType': self.collection_type,
|
||||
'repaymentAmount':self.repayment_amount,
|
||||
'status': self.status,
|
||||
'productId': self.product_id,
|
||||
'disburseResult': self.disburse_result,
|
||||
'disburseDescription': self.disburse_description,
|
||||
'verifyResult': self.verify_result,
|
||||
'verifyDescription': self.verify_description,
|
||||
'transactionId': self.transaction_id,
|
||||
'accountId':self.account_id,
|
||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||
'disburseDate': self.disburse_date.isoformat() if self.disburse_date else None,
|
||||
'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None,
|
||||
'reference': self.reference,
|
||||
'balance': self.balance,
|
||||
'tenor': self.tenor,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_transaction_id(cls, transaction_id):
|
||||
return cls.query.filter_by(transaction_id=transaction_id).first()
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_loan_id(cls, loan_id):
|
||||
return cls.query.filter_by(id=loan_id).first()
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disburse date 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.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if loan.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the loan's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update loan disburse_date
|
||||
loan.disburse_date = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse date for loan ID {loan_id} to {current_time}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disburse date: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def set_disburse_verify_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disburse verify date 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.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if loan.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the loan's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update loan verify_date
|
||||
loan.disburse_verify = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse verify date for loan ID {loan_id} to {current_time}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disburse verify date: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_message(cls, loan_id, description):
|
||||
"""
|
||||
Update the disburse result and description 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.")
|
||||
|
||||
# Update disburse description only
|
||||
loan.disburse_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse result for loan ID {loan_id} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disbursement result: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the disburse result and description 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.")
|
||||
|
||||
# Update disburse result and description
|
||||
loan.disburse_result = result
|
||||
loan.disburse_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse result for loan ID {loan_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disbursement result: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disburse_verify_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the verify result and description 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.")
|
||||
|
||||
# Update disburse result and description
|
||||
loan.verify_result = result
|
||||
loan.verify_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating verify result for loan ID {loan_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update verify result: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def get_latest_loan_without_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan without a disbursement date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.disburse_date.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan with a disbursement date and no verification date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.disburse_date.isnot(None),
|
||||
cls.disburse_verify.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
|
||||
@classmethod
|
||||
def get_customer_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans and sum by customer_id.
|
||||
"""
|
||||
customer_loans = cls.query.filter_by( customer_id = customer_id).all()
|
||||
if not customer_loans:
|
||||
raise ValueError(f"Customer with Id {customer_id} does not have any loan.")
|
||||
|
||||
total_amount = (
|
||||
cls.query.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
|
||||
.filter_by(customer_id=customer_id)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(customer_loans)} loans for customer ID: {customer_id} with total amount: {total_amount}")
|
||||
|
||||
return customer_loans, total_amount
|
||||
@classmethod
|
||||
def get_customer_active_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans and sum by customer_id.
|
||||
"""
|
||||
customer_loans = cls.query.filter(
|
||||
cls.customer_id == customer_id,
|
||||
cls.status != 'repaid'
|
||||
).all()
|
||||
|
||||
if not customer_loans:
|
||||
raise ValueError(f"Customer with Id {customer_id} does not have any active loan.")
|
||||
|
||||
total_amount = (
|
||||
cls.query
|
||||
.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
|
||||
.filter(
|
||||
cls.customer_id == customer_id,
|
||||
cls.status != 'repaid'
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(customer_loans)} active loans for customer ID: {customer_id} with total amount: {total_amount}")
|
||||
|
||||
return customer_loans, total_amount
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
Update the status of the loan record with the given loan_id.
|
||||
"""
|
||||
try:
|
||||
# Retrieve loan record
|
||||
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 loan.to_dict() # Still return the current state if no change
|
||||
|
||||
# Update status and timestamp
|
||||
loan.status = status
|
||||
loan.updated_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Loan status updated and committed.")
|
||||
return loan.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating loan status: {e}")
|
||||
raise Exception(f"Error updating loan status: {str(e)}")
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_loan_balance(cls, loan_id, amount_collected):
|
||||
"""
|
||||
Update the balance of a loan after successful repayment.
|
||||
"""
|
||||
try:
|
||||
# Fetch the loan record
|
||||
loan = cls.query.get(loan_id)
|
||||
|
||||
if not loan:
|
||||
raise ValueError(f"Loan with ID {loan_id} does not exist.")
|
||||
|
||||
# Convert to Decimal and round to 2 decimal places
|
||||
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
balance = Decimal(str(loan.balance or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# Ensure valid repayment amount
|
||||
if amount_collected <= Decimal("0.00"):
|
||||
logger.info(f"Repayment amount is less than or equal to 0: {amount_collected}. Must be greater than 0.00")
|
||||
if balance <= Decimal("0.00"):
|
||||
raise ValueError("There is no balance for this loan.")
|
||||
if amount_collected > balance:
|
||||
# allow tiny rounding diff
|
||||
if abs(amount_collected - balance) <= Decimal("0.01"):
|
||||
amount_collected = balance
|
||||
else:
|
||||
raise ValueError("Repayment amount exceeds current loan balance.")
|
||||
|
||||
|
||||
# Deduct the amount from the current balance
|
||||
new_balance = balance - amount_collected
|
||||
loan.balance = float(new_balance)
|
||||
loan.updated_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Loan balance updated for loan ID {loan_id}. New balance: {loan.balance}")
|
||||
return loan.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating loan balance: {e}")
|
||||
raise Exception(f"Error updating loan balance: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def get_overdue_loans(cls):
|
||||
"""
|
||||
Get all overdue loans.
|
||||
"""
|
||||
try:
|
||||
overdue_loans = cls.query.filter(
|
||||
cls.due_date < datetime.now(timezone.utc),
|
||||
cls.status != 'repaid'
|
||||
).all()
|
||||
|
||||
if not overdue_loans:
|
||||
logger.info("No overdue loans found.")
|
||||
return []
|
||||
|
||||
logger.info(f"Found {len(overdue_loans)} overdue loans.")
|
||||
return overdue_loans
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overdue loans: {e}")
|
||||
return []
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from sqlalchemy import and_, or_, not_
|
||||
from sqlalchemy.sql import func
|
||||
from app.utils.logger import logger
|
||||
from app.extensions import db
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import datetime, timezone
|
||||
|
||||
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)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
original_transaction = db.Column(db.String(50), nullable=True)
|
||||
account_id = db.Column(db.String(50), nullable=False)
|
||||
offer_id = db.Column(db.String(20), nullable=False)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
collection_type = db.Column(db.String(20), nullable=True)
|
||||
current_loan_amount = db.Column(db.Float, nullable=True)
|
||||
initial_loan_amount = db.Column(db.Float, nullable=False)
|
||||
default_penalty_fee = db.Column(db.Float, default=0)
|
||||
continuous_fee = db.Column(db.Float, default=0)
|
||||
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
|
||||
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
tenor = db.Column(db.Integer, nullable=True)
|
||||
due_date = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
disburse_date = db.Column(db.DateTime, nullable=True)
|
||||
disburse_verify = db.Column(db.DateTime, nullable=True)
|
||||
disburse_result = db.Column(db.String(10), nullable=True)
|
||||
disburse_description = db.Column(db.String(100), nullable=True)
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
reference = db.Column(db.String(50), nullable=True)
|
||||
|
||||
total_penal_charge = db.Column(db.Float, default=0.0)
|
||||
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
primaryjoin="Customer.id == Loan.customer_id",
|
||||
foreign_keys=[customer_id],
|
||||
back_populates="loans",
|
||||
)
|
||||
|
||||
loan_charges = relationship(
|
||||
"LoanCharge",
|
||||
primaryjoin="Loan.id == LoanCharge.loan_id",
|
||||
foreign_keys="LoanCharge.loan_id",
|
||||
back_populates="loan",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Loan {self.id}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Loan object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'debtId': self.id,
|
||||
"customerId": self.customer_id,
|
||||
'initialLoanAmount': self.initial_loan_amount,
|
||||
'currentLoanAmount': self.current_loan_amount,
|
||||
'defaultPenaltyFee': self.default_penalty_fee,
|
||||
'continuousFee': self.continuous_fee,
|
||||
'collectionType': self.collection_type,
|
||||
'repaymentAmount':self.repayment_amount,
|
||||
'status': self.status,
|
||||
'productId': self.product_id,
|
||||
'disburseResult': self.disburse_result,
|
||||
'disburseDescription': self.disburse_description,
|
||||
'verifyResult': self.verify_result,
|
||||
'verifyDescription': self.verify_description,
|
||||
'transactionId': self.transaction_id,
|
||||
'accountId':self.account_id,
|
||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||
'disburseDate': self.disburse_date.isoformat() if self.disburse_date else None,
|
||||
'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None,
|
||||
'reference': self.reference,
|
||||
'balance': self.balance,
|
||||
'tenor': self.tenor,
|
||||
'totalPenalCharge': self.total_penal_charge,
|
||||
'lastPenalDate': self.last_penal_date
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_transaction_id(cls, transaction_id):
|
||||
return cls.query.filter_by(transaction_id=transaction_id).first()
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_loan_id(cls, loan_id):
|
||||
return cls.query.filter_by(id=loan_id).first()
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disburse date 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.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if loan.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the loan's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update loan disburse_date
|
||||
loan.disburse_date = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse date for loan ID {loan_id} to {current_time}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disburse date: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def set_disburse_verify_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disburse verify date 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.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if loan.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the loan's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update loan verify_date
|
||||
loan.disburse_verify = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse verify date for loan ID {loan_id} to {current_time}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disburse verify date: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_message(cls, loan_id, description):
|
||||
"""
|
||||
Update the disburse result and description 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.")
|
||||
|
||||
# Update disburse description only
|
||||
loan.disburse_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse result for loan ID {loan_id} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disbursement result: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the disburse result and description 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.")
|
||||
|
||||
# Update disburse result and description
|
||||
loan.disburse_result = result
|
||||
loan.disburse_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating disburse result for loan ID {loan_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update disbursement result: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disburse_verify_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the verify result and description 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.")
|
||||
|
||||
# Update disburse result and description
|
||||
loan.verify_result = result
|
||||
loan.verify_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating verify result for loan ID {loan_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update verify result: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def get_latest_loan_without_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan without a disbursement date.
|
||||
"""
|
||||
logger.info("Fetching latest loan without disburse date")
|
||||
|
||||
try:
|
||||
return cls.query.filter(
|
||||
cls.disburse_date.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching latest loan without disburse date: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan with a disbursement date and no verification date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.disburse_date.isnot(None),
|
||||
cls.disburse_verify.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
|
||||
@classmethod
|
||||
def get_customer_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans and sum by customer_id.
|
||||
"""
|
||||
customer_loans = cls.query.filter_by( customer_id = customer_id).all()
|
||||
if not customer_loans:
|
||||
raise ValueError(f"Customer with Id {customer_id} does not have any loan.")
|
||||
|
||||
total_amount = (
|
||||
cls.query.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
|
||||
.filter_by(customer_id=customer_id)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(customer_loans)} loans for customer ID: {customer_id} with total amount: {total_amount}")
|
||||
|
||||
return customer_loans, total_amount
|
||||
@classmethod
|
||||
def get_customer_active_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans and sum by customer_id.
|
||||
"""
|
||||
customer_loans = cls.query.filter(
|
||||
cls.customer_id == customer_id,
|
||||
cls.status != 'repaid'
|
||||
).all()
|
||||
|
||||
if not customer_loans:
|
||||
raise ValueError(f"Customer with Id {customer_id} does not have any active loan.")
|
||||
|
||||
total_amount = (
|
||||
cls.query
|
||||
.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
|
||||
.filter(
|
||||
cls.customer_id == customer_id,
|
||||
cls.status != 'repaid'
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(customer_loans)} active loans for customer ID: {customer_id} with total amount: {total_amount}")
|
||||
|
||||
return customer_loans, total_amount
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
Update the status of the loan record with the given loan_id.
|
||||
"""
|
||||
try:
|
||||
# Retrieve loan record
|
||||
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 loan.to_dict() # Still return the current state if no change
|
||||
|
||||
# Update status and timestamp
|
||||
loan.status = status
|
||||
loan.updated_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Loan status updated and committed.")
|
||||
return loan.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating loan status: {e}")
|
||||
raise Exception(f"Error updating loan status: {str(e)}")
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_loan_balance(cls, loan_id, amount_collected):
|
||||
"""
|
||||
Update the balance of a loan after successful repayment.
|
||||
"""
|
||||
try:
|
||||
# Fetch the loan record
|
||||
loan = cls.query.get(loan_id)
|
||||
|
||||
if not loan:
|
||||
raise ValueError(f"Loan with ID {loan_id} does not exist.")
|
||||
|
||||
# Convert to Decimal and round to 2 decimal places
|
||||
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
balance = Decimal(str(loan.balance or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# Ensure valid repayment amount
|
||||
if amount_collected <= Decimal("0.00"):
|
||||
logger.info(f"Repayment amount is less than or equal to 0: {amount_collected}. Must be greater than 0.00")
|
||||
if balance <= Decimal("0.00"):
|
||||
raise ValueError("There is no balance for this loan.")
|
||||
if amount_collected > balance:
|
||||
# allow tiny rounding diff
|
||||
if abs(amount_collected - balance) <= Decimal("0.01"):
|
||||
amount_collected = balance
|
||||
else:
|
||||
raise ValueError("Repayment amount exceeds current loan balance.")
|
||||
|
||||
|
||||
# Deduct the amount from the current balance
|
||||
new_balance = balance - amount_collected
|
||||
loan.balance = float(new_balance)
|
||||
loan.updated_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Loan balance updated for loan ID {loan_id}. New balance: {loan.balance}")
|
||||
return loan.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating loan balance: {e}")
|
||||
raise Exception(f"Error updating loan balance: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def get_overdue_loans(cls):
|
||||
"""
|
||||
Get all overdue loans.
|
||||
"""
|
||||
try:
|
||||
overdue_loans = cls.query.filter(
|
||||
cls.due_date < datetime.now(timezone.utc),
|
||||
cls.status != 'repaid'
|
||||
).all()
|
||||
|
||||
if not overdue_loans:
|
||||
logger.info("No overdue loans found.")
|
||||
return []
|
||||
|
||||
logger.info(f"Found {len(overdue_loans)} overdue loans.")
|
||||
return overdue_loans
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overdue loans: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def apply_penal_to_loan(cls, loan_id, penal_amount):
|
||||
|
||||
loan = cls.query.get(loan_id)
|
||||
|
||||
if not loan:
|
||||
raise ValueError("Loan not found")
|
||||
penal_amount = Decimal(str(penal_amount))
|
||||
|
||||
loan.total_penal_charge = Decimal(str(loan.total_penal_charge or 0)) + penal_amount
|
||||
loan.last_penal_date = datetime.now(timezone.utc)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
+127
-49
@@ -1,49 +1,127 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from os.path import devnull
|
||||
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class LoanCharge(db.Model):
|
||||
__tablename__ = 'loan_charges'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
loan_id = db.Column(db.Integer, nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
code = db.Column(db.String(50), nullable=False)
|
||||
amount = db.Column(db.Float, default=0.0)
|
||||
percent = db.Column(db.Float, default=0.0)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
due = db.Column(db.Integer, nullable=False)
|
||||
due_date = db.Column(db.DateTime, 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))
|
||||
|
||||
loan = relationship(
|
||||
"Loan",
|
||||
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||
foreign_keys=[loan_id],
|
||||
back_populates="loan_charges",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LoanCharge {self.id} - Loan {self.loan_id} - {self.code}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Loan charge object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'loanId': self.loan_id,
|
||||
'transactionId': self.transaction_id,
|
||||
'code': self.code,
|
||||
'amount': self.amount,
|
||||
'percent': self.percent,
|
||||
'description': self.description,
|
||||
'due': self.due
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_loan_charge_by_debt_id(cls, debt_id):
|
||||
return cls.query.filter_by(loan_id=debt_id)
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from os.path import devnull
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class LoanCharge(db.Model):
|
||||
__tablename__ = 'loan_charges'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
loan_id = db.Column(db.Integer, nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
code = db.Column(db.String(50), nullable=False)
|
||||
amount = db.Column(db.Float, default=0.0)
|
||||
percent = db.Column(db.Float, default=0.0)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
due = db.Column(db.Integer, nullable=False)
|
||||
due_date = db.Column(db.DateTime, 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))
|
||||
|
||||
loan = relationship(
|
||||
"Loan",
|
||||
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||
foreign_keys=[loan_id],
|
||||
back_populates="loan_charges",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LoanCharge {self.id} - Loan {self.loan_id} - {self.code}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Loan charge object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'loanId': self.loan_id,
|
||||
'transactionId': self.transaction_id,
|
||||
'code': self.code,
|
||||
'amount': self.amount,
|
||||
'percent': self.percent,
|
||||
'description': self.description,
|
||||
'due': self.due
|
||||
}
|
||||
#get last penal
|
||||
@classmethod
|
||||
def get_last_penal_no(cls, loan_id):
|
||||
"""
|
||||
Returns the last penal number created for a loan.
|
||||
Example:
|
||||
PENAL1 -> returns 1
|
||||
PENAL3 -> returns 3
|
||||
If none exists, returns 0.
|
||||
"""
|
||||
last_penal = (
|
||||
cls.query
|
||||
.filter(cls.loan_id == loan_id)
|
||||
.filter(cls.code.like("PENAL%"))
|
||||
.order_by(cls.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not last_penal:
|
||||
return 0
|
||||
|
||||
try:
|
||||
return int(last_penal.code.replace("PENAL", ""))
|
||||
except ValueError:
|
||||
return 0
|
||||
@classmethod
|
||||
def get_penal_charges_by_loan_id(cls, loan_id):
|
||||
"""
|
||||
Returns all penal charges for a specific loan.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.loan_id == loan_id,
|
||||
cls.code.like("PENAL%")
|
||||
).all()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_charge_by_debt_id(cls, debt_id):
|
||||
return cls.query.filter_by(loan_id=debt_id)
|
||||
|
||||
#create penal charge
|
||||
@classmethod
|
||||
def create_penal_charges_for_loan(cls, loan_id, transaction_id, percent, penal_no, schedule_number, penal_amount=0.0):
|
||||
"""
|
||||
Create a penal charge for a given loan and schedule.
|
||||
"""
|
||||
|
||||
if loan_id is None:
|
||||
raise ValueError("loan_id cannot be None")
|
||||
|
||||
code = f"PENAL{penal_no:02d}-SCHEDULE{schedule_number:02d}"
|
||||
|
||||
# Check if this penal charge already exists
|
||||
existing = cls.query.filter_by(
|
||||
loan_id=loan_id,
|
||||
code=code
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
penal_charge = cls(
|
||||
loan_id=loan_id,
|
||||
transaction_id=transaction_id,
|
||||
code=code,
|
||||
amount=penal_amount,
|
||||
percent=percent,
|
||||
description=f"Penal Charge {penal_no} for loan {loan_id} schedule {schedule_number}",
|
||||
due=True,
|
||||
due_date=now
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(penal_charge)
|
||||
db.session.commit()
|
||||
except IntegrityError as err:
|
||||
db.session.rollback()
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
|
||||
return penal_charge
|
||||
@@ -1,226 +1,348 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from app.utils.logger import logger
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
# from dateutil.relativedelta import relativedelta
|
||||
|
||||
class LoanRepaymentSchedule(db.Model):
|
||||
__tablename__ = 'loan_repayment_schedules'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
loan_id = db.Column(db.Integer, nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
installment_number = db.Column(db.Integer, nullable=False)
|
||||
due_date = db.Column(db.DateTime, nullable=False)
|
||||
installment_amount= db.Column(db.Float, default=0.0)
|
||||
total_repayment_amount = db.Column(db.Float, default=0.0)
|
||||
paid = db.Column(db.Boolean, default=False)
|
||||
paid_at = db.Column(db.DateTime, nullable=True)
|
||||
due_process_date = db.Column(db.DateTime, nullable=True)
|
||||
due_process_count = db.Column(db.Integer, default=0)
|
||||
paid_status = db.Column(db.String(20), nullable=True)
|
||||
repay_description = db.Column(db.String(255), nullable=True)
|
||||
partial_balance = 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))
|
||||
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'loan_id': self.loan_id,
|
||||
'product_id': self.product_id,
|
||||
'transaction_id': self.transaction_id,
|
||||
'installment_number': self.installment_number,
|
||||
'due_date': self.due_date.isoformat() if self.due_date else None,
|
||||
'installment_amount': self.installment_amount,
|
||||
'total_repayment_amount': self.total_repayment_amount,
|
||||
'paid': self.paid,
|
||||
'due_process_date': self.due_process_date.isoformat() if self.due_process_date else None,
|
||||
'due_process_count': self.due_process_count,
|
||||
'paid_status': self.paid_status,
|
||||
'repay_description': self.repay_description,
|
||||
'partial_balance': self.partial_balance,
|
||||
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_loan_id(cls, loan_id, include_paid=True):
|
||||
"""
|
||||
Get repayment schedules by loan ID.
|
||||
|
||||
:param loan_id: Loan ID to filter by
|
||||
:param include_paid: If True, include all schedules. If False, only unpaid ones.
|
||||
:return: List of repayment schedules ordered by due_date
|
||||
"""
|
||||
try:
|
||||
query = cls.query.filter_by(loan_id=loan_id)
|
||||
if not include_paid:
|
||||
query = query.filter_by(paid=False)
|
||||
|
||||
schedules = query.order_by(cls.due_date.asc()).all()
|
||||
return schedules
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repayment schedules for loan {loan_id}: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
|
||||
"""
|
||||
Get repayment schedule by ID and transaction ID
|
||||
"""
|
||||
try:
|
||||
return cls.query.filter_by(id=id, transaction_id=transaction_id).first()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repayment schedule for id={id}, transaction_id={transaction_id}: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_overdue_repayment_schedule(cls):
|
||||
"""
|
||||
Get all overdue repayment schedules.
|
||||
"""
|
||||
try:
|
||||
return cls.query.filter(cls.due_date < datetime.now(timezone.utc), cls.paid == False).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overdue repayment schedules: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
|
||||
"""
|
||||
Get repayment schedule by transaction ID
|
||||
"""
|
||||
return cls.query.filter_by(transaction_id=transaction_id).all()
|
||||
@classmethod
|
||||
def update_repayment_schedule_description(cls, schedule_id, description):
|
||||
"""
|
||||
Update the repayment description for a specific schedule.
|
||||
"""
|
||||
try:
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
|
||||
schedule.repay_description = description
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Updated repayment description for schedule ID {schedule_id}")
|
||||
|
||||
return schedule.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating repayment description for schedule {schedule_id}: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def update_repayment_schedule_status(cls, schedule_id):
|
||||
"""
|
||||
Mark a repayment schedule as fully repaid when the parent loan is fully repaid.
|
||||
This function does not take amount_collected because the loan is already cleared.
|
||||
"""
|
||||
try:
|
||||
# Fetch schedule
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
|
||||
# Force balance to 0
|
||||
schedule.partial_balance = 0.0
|
||||
schedule.paid_status = RepaymentScheduleStatus.REPAID
|
||||
schedule.paid = True
|
||||
schedule.paid_at = datetime.now(timezone.utc)
|
||||
|
||||
# Track due processing
|
||||
if schedule.due_process_count is None:
|
||||
schedule.due_process_count = 0
|
||||
schedule.due_process_count += 1
|
||||
schedule.due_process_date = datetime.now(timezone.utc)
|
||||
|
||||
# Update timestamp
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# Commit changes
|
||||
db.session.commit()
|
||||
logger.info(f"Schedule {schedule_id} marked as REPAID since parent loan is fully repaid.")
|
||||
|
||||
return schedule.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating repayment schedule {schedule_id} after loan repayment: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
||||
"""
|
||||
Apply repayment to a loan schedule:
|
||||
- Deduct from partial balance if partially paid.
|
||||
- Otherwise deduct from installment amount.
|
||||
- Update partial balance, paid status, timestamps, etc.
|
||||
"""
|
||||
try:
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
|
||||
# Normalize amount
|
||||
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
if amount_collected <= Decimal("0.00"):
|
||||
logger.info("Repayment amount must be greater than zero.")
|
||||
return schedule.to_dict()
|
||||
|
||||
# Determine current balance
|
||||
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID and (schedule.partial_balance or 0) > 0:
|
||||
balance = Decimal(str(schedule.partial_balance))
|
||||
else:
|
||||
balance = Decimal(str(schedule.installment_amount))
|
||||
|
||||
# Deduct repayment
|
||||
new_balance = balance - amount_collected
|
||||
if new_balance < 0:
|
||||
new_balance = Decimal("0.00") # prevent negatives
|
||||
|
||||
# Update schedule fields
|
||||
schedule.partial_balance = float(new_balance) if new_balance > 0 else 0.0
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
if new_balance == 0:
|
||||
schedule.paid_status = RepaymentScheduleStatus.REPAID
|
||||
schedule.paid = True
|
||||
schedule.paid_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
schedule.paid_status = RepaymentScheduleStatus.PARTIALLY_PAID
|
||||
schedule.paid = False # not fully paid yet
|
||||
|
||||
# Track due processing
|
||||
if schedule.due_process_count is None:
|
||||
schedule.due_process_count = 0
|
||||
schedule.due_process_count += 1
|
||||
schedule.due_process_date = datetime.now(timezone.utc)
|
||||
|
||||
# Commit
|
||||
db.session.commit()
|
||||
logger.info(f"Repayment applied for schedule ID {schedule_id}. Remaining balance: {schedule.partial_balance}")
|
||||
|
||||
return schedule.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error applying repayment for schedule {schedule_id}: {e}")
|
||||
raise
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from app.extensions import db
|
||||
from app.utils.logger import logger
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import or_
|
||||
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
|
||||
from app.config import settings
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
# from dateutil.relativedelta import relativedelta
|
||||
|
||||
class LoanRepaymentSchedule(db.Model):
|
||||
__tablename__ = 'loan_repayment_schedules'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
loan_id = db.Column(db.Integer, nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
installment_number = db.Column(db.Integer, nullable=False)
|
||||
due_date = db.Column(db.DateTime, nullable=False)
|
||||
installment_amount= db.Column(db.Float, default=0.0)
|
||||
total_repayment_amount = db.Column(db.Float, default=0.0)
|
||||
paid = db.Column(db.Boolean, default=False)
|
||||
paid_at = db.Column(db.DateTime, nullable=True)
|
||||
due_process_date = db.Column(db.DateTime, nullable=True)
|
||||
due_process_count = db.Column(db.Integer, default=0)
|
||||
paid_status = db.Column(db.String(20), nullable=True)
|
||||
repay_description = db.Column(db.String(255), nullable=True)
|
||||
partial_balance = 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))
|
||||
penal_charge = db.Column(db.Float, default=0.0)
|
||||
penal_count = db.Column(db.Integer, default=0)
|
||||
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'loan_id': self.loan_id,
|
||||
'product_id': self.product_id,
|
||||
'transaction_id': self.transaction_id,
|
||||
'installment_number': self.installment_number,
|
||||
'due_date': self.due_date.isoformat() if self.due_date else None,
|
||||
'installment_amount': self.installment_amount,
|
||||
'total_repayment_amount': self.total_repayment_amount,
|
||||
'paid': self.paid,
|
||||
'due_process_date': self.due_process_date.isoformat() if self.due_process_date else None,
|
||||
'due_process_count': self.due_process_count,
|
||||
'paid_status': self.paid_status,
|
||||
'repay_description': self.repay_description,
|
||||
'partial_balance': self.partial_balance,
|
||||
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'penal_charge': self.penal_charge,
|
||||
'penal_count': self.penal_count,
|
||||
'last_penal_date': self.last_penal_date.isoformat() if self.last_penal_date else None
|
||||
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_loan_id(cls, loan_id, include_paid=True):
|
||||
"""
|
||||
Get repayment schedules by loan ID.
|
||||
|
||||
:param loan_id: Loan ID to filter by
|
||||
:param include_paid: If True, include all schedules. If False, only unpaid ones.
|
||||
:return: List of repayment schedules ordered by due_date
|
||||
"""
|
||||
try:
|
||||
query = cls.query.filter_by(loan_id=loan_id)
|
||||
if not include_paid:
|
||||
query = query.filter_by(paid=False)
|
||||
|
||||
schedules = query.order_by(cls.due_date.asc()).all()
|
||||
return schedules
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repayment schedules for loan {loan_id}: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
|
||||
"""
|
||||
Get repayment schedule by ID and transaction ID
|
||||
"""
|
||||
try:
|
||||
return cls.query.filter_by(id=id, transaction_id=transaction_id).first()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repayment schedule for id={id}, transaction_id={transaction_id}: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_overdue_repayment_schedule(cls):
|
||||
"""
|
||||
Get all overdue repayment schedules that are not repaid.
|
||||
"""
|
||||
try:
|
||||
return cls.query.filter(cls.due_date < datetime.now(timezone.utc), cls.paid == False).order_by(cls.due_date.asc()).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overdue repayment schedules: {e}")
|
||||
return []
|
||||
@classmethod
|
||||
def get_active_overdue_repayment_schedule(cls):
|
||||
"""
|
||||
Get all overdue repayment schedules that are active.
|
||||
"""
|
||||
try:
|
||||
return (
|
||||
cls.query
|
||||
.filter(
|
||||
cls.due_date < datetime.now(timezone.utc),
|
||||
cls.paid_status == RepaymentScheduleStatus.ACTIVE
|
||||
)
|
||||
.order_by(cls.due_date.asc())
|
||||
.all()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching active overdue repayment schedules: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_overdue_repayment_schedule_with_grace_period(cls, grace_period_days, limit=None):
|
||||
try:
|
||||
now = datetime.now(timezone.utc)
|
||||
grace_period_date = now - timedelta(days=grace_period_days)
|
||||
penal_interval = timedelta(days=settings.PENAL_CHARGE_INTERVAL_DAYS)
|
||||
|
||||
return cls.query.filter(
|
||||
cls.due_date < grace_period_date,
|
||||
cls.paid == False,
|
||||
or_(
|
||||
cls.last_penal_date == None, # never penalized before
|
||||
cls.last_penal_date < now - penal_interval
|
||||
)
|
||||
).order_by(cls.due_date.asc()).limit(limit).all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overdue repayment schedules with grace period: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_partially_paid_overdue_repayment_schedule(cls):
|
||||
"""
|
||||
Get all overdue repayment schedules that are partially paid.
|
||||
"""
|
||||
try:
|
||||
return (
|
||||
cls.query
|
||||
.filter(
|
||||
cls.due_date < datetime.now(timezone.utc),
|
||||
cls.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID
|
||||
)
|
||||
.order_by(cls.due_date.asc())
|
||||
.all()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching partially paid overdue repayment schedules: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
|
||||
"""
|
||||
Get repayment schedule by transaction ID
|
||||
"""
|
||||
return cls.query.filter_by(transaction_id=transaction_id).all()
|
||||
@classmethod
|
||||
def update_repayment_schedule_description(cls, schedule_id, description):
|
||||
"""
|
||||
Update the repayment description for a specific schedule.
|
||||
"""
|
||||
try:
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
|
||||
schedule.repay_description = description
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Updated repayment description for schedule ID {schedule_id}")
|
||||
|
||||
return schedule.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating repayment description for schedule {schedule_id}: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def update_repayment_schedule_status(cls, schedule_id):
|
||||
"""
|
||||
Mark a repayment schedule as fully repaid when the parent loan is fully repaid.
|
||||
This function does not take amount_collected because the loan is already cleared.
|
||||
"""
|
||||
try:
|
||||
# Fetch schedule
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
|
||||
# Force balance to 0
|
||||
schedule.partial_balance = 0.0
|
||||
schedule.paid_status = RepaymentScheduleStatus.REPAID
|
||||
schedule.paid = True
|
||||
schedule.paid_at = datetime.now(timezone.utc)
|
||||
|
||||
# Track due processing
|
||||
if schedule.due_process_count is None:
|
||||
schedule.due_process_count = 0
|
||||
schedule.due_process_count += 1
|
||||
schedule.due_process_date = datetime.now(timezone.utc)
|
||||
|
||||
# Update timestamp
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# Commit changes
|
||||
db.session.commit()
|
||||
logger.info(f"Schedule {schedule_id} marked as REPAID since parent loan is fully repaid.")
|
||||
|
||||
return schedule.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating repayment schedule {schedule_id} after loan repayment: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_status_to_active(cls, schedule_id):
|
||||
"""
|
||||
Update repayment schedule status to ACTIVE.
|
||||
"""
|
||||
try:
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
schedule.paid_status = RepaymentScheduleStatus.ACTIVE
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
logger.info(f"Updated repayment schedule ID {schedule_id} status to ACTIVE")
|
||||
return schedule.to_dict()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating repayment schedule status for schedule {schedule_id}: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
||||
"""
|
||||
Apply repayment to a loan schedule:
|
||||
- Deduct from partial balance if partially paid.
|
||||
- Otherwise deduct from installment amount.
|
||||
- Update partial balance, paid status, timestamps, etc.
|
||||
"""
|
||||
try:
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
|
||||
# Normalize amount
|
||||
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
if amount_collected <= Decimal("0.00"):
|
||||
logger.info("Repayment amount must be greater than zero.")
|
||||
return schedule.to_dict()
|
||||
|
||||
# Determine current balance
|
||||
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID and (schedule.partial_balance or 0) > 0:
|
||||
balance = Decimal(str(schedule.partial_balance))
|
||||
else:
|
||||
balance = Decimal(str(schedule.installment_amount))
|
||||
|
||||
# Deduct repayment
|
||||
new_balance = balance - amount_collected
|
||||
if new_balance < 0:
|
||||
new_balance = Decimal("0.00") # prevent negatives
|
||||
|
||||
# Update schedule fields
|
||||
schedule.partial_balance = float(new_balance) if new_balance > 0 else 0.0
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
if new_balance == 0:
|
||||
schedule.paid_status = RepaymentScheduleStatus.REPAID
|
||||
schedule.paid = True
|
||||
schedule.paid_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
schedule.paid_status = RepaymentScheduleStatus.PARTIALLY_PAID
|
||||
schedule.paid = False # not fully paid yet
|
||||
|
||||
# Track due processing
|
||||
if schedule.due_process_count is None:
|
||||
schedule.due_process_count = 0
|
||||
schedule.due_process_count += 1
|
||||
schedule.due_process_date = datetime.now(timezone.utc)
|
||||
|
||||
# Commit
|
||||
db.session.commit()
|
||||
logger.info(f"Repayment applied for schedule ID {schedule_id}. Remaining balance: {schedule.partial_balance}")
|
||||
|
||||
return schedule.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error applying repayment for schedule {schedule_id}: {e}")
|
||||
raise
|
||||
from decimal import Decimal
|
||||
|
||||
@classmethod
|
||||
def apply_penal_to_schedule(cls, schedule_id, penal_amount):
|
||||
|
||||
schedule = cls.query.get(schedule_id)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
penal_amount = Decimal(str(penal_amount))
|
||||
|
||||
current_penal = Decimal(str(schedule.penal_charge)) if schedule.penal_charge else Decimal("0")
|
||||
|
||||
schedule.penal_count = (schedule.penal_count or 0) + 1
|
||||
schedule.penal_charge = current_penal + penal_amount
|
||||
schedule.last_penal_date = now
|
||||
schedule.due_process_date = now
|
||||
schedule.updated_at = now
|
||||
|
||||
db.session.commit()
|
||||
# Calculate penal charge
|
||||
@classmethod
|
||||
def calculate_penal_charge(cls, schedule):
|
||||
|
||||
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID:
|
||||
outstanding = Decimal(str(schedule.partial_balance))
|
||||
else:
|
||||
outstanding = Decimal(str(schedule.installment_amount))
|
||||
|
||||
rate = Decimal(str(settings.PENAL_CHARGE_PERCENTAGE)) / 100
|
||||
|
||||
penal_charge = (outstanding * rate).quantize(
|
||||
Decimal("0.01"),
|
||||
rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
return penal_charge
|
||||
|
||||
|
||||
+259
-259
@@ -1,260 +1,260 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
from app.utils.logger import logger
|
||||
from app.enums.loan_status import LoanStatus
|
||||
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)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
initiated_by = db.Column(db.String(50), nullable=True)
|
||||
salary_amount = db.Column(db.Float, nullable=True, 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))
|
||||
repay_date = db.Column(db.DateTime, nullable=True)
|
||||
verify_date = db.Column(db.DateTime, nullable=True)
|
||||
repay_result = db.Column(db.String(10), nullable=True)
|
||||
repay_description = db.Column(db.String(100), nullable=True)
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
def __repr__(self):
|
||||
return f'<Repayment {self.id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Repayment object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'Id': self.id,
|
||||
"customerId": self.customer_id,
|
||||
'loanId': self.loan_id,
|
||||
'productId': self.product_id,
|
||||
'repayResult': self.repay_result,
|
||||
'repayDescription': self.repay_description,
|
||||
'verifyResult': self.verify_result,
|
||||
'verifyDescription': self.verify_description,
|
||||
'transactionId': self.transaction_id,
|
||||
'initiatedBy':self.initiated_by,
|
||||
'salaryAmount':self.salary_amount,
|
||||
'repayDate': self.repay_date.isoformat() if self.repay_date else None,
|
||||
'VerifyDate': self.verify_date.isoformat() if self.verify_date else None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_repayment(cls, repayment_data):
|
||||
|
||||
if repayment_data["LoanStatus"] not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY,LoanStatus.ACTIVE_PARTIAL]:
|
||||
raise ValueError(f"Repayment cannot be processed. Loan status: ({repayment_data['LoanStatus']})")
|
||||
|
||||
repayment = cls(
|
||||
customer_id=repayment_data["customerId"],
|
||||
loan_id=repayment_data["loanId"],
|
||||
product_id=repayment_data["productId"],
|
||||
transaction_id=repayment_data["transactionId"],
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
initiated_by= repayment_data["initiatedBy"],
|
||||
salary_amount=repayment_data["salaryAmount"]
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(repayment)
|
||||
db.session.commit()
|
||||
logger.info("Repayment record committed.")
|
||||
return repayment
|
||||
except IntegrityError as err:
|
||||
logger.error(f"Database integrity error: {err}")
|
||||
return {"error": "Integrity error", "details": str(err)}
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_repayment(cls, data: dict):
|
||||
"""
|
||||
Create and persist a new repayment record.
|
||||
"""
|
||||
logger.info(f"Received repayment data: {data}")
|
||||
|
||||
try:
|
||||
new_repayment = cls(
|
||||
loan_id=data["loanId"],
|
||||
customer_id=data["customerId"],
|
||||
product_id=data.get("productId"),
|
||||
transaction_id=data["transactionId"],
|
||||
initiated_by=data.get("initiatedBy"),
|
||||
salary_amount=float(data.get("salaryAmount", 0.0)),
|
||||
repay_date=(
|
||||
datetime.strptime(data["repayDate"], "%Y-%m-%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
if data.get("repayDate")
|
||||
else None
|
||||
),
|
||||
repay_result=data.get("repayResult"),
|
||||
repay_description=data.get("repayDescription"),
|
||||
verify_result=data.get("verifyResult"),
|
||||
verify_description=data.get("verifyDescription"),
|
||||
verify_date=(
|
||||
datetime.strptime(data["verifyDate"], "%Y-%m-%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
if data.get("verifyDate")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
db.session.add(new_repayment)
|
||||
db.session.commit()
|
||||
logger.info("Repayment record committed.")
|
||||
return new_repayment
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error adding repayment data: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def get_repayment_by_transaction_id(cls, transaction_id):
|
||||
return cls.query.filter_by(transaction_id=transaction_id).first()
|
||||
@classmethod
|
||||
def get_repayment_by_id(cls, id):
|
||||
return cls.query.filter_by(id=id).first()
|
||||
|
||||
@classmethod
|
||||
def set_repay_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the repay date of the loan with the given loan_id.
|
||||
"""
|
||||
# Retrieve repayment
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if repayment.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the repayment's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update repayment date
|
||||
repayment.repay_date = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating repay date for repayment ID {repayment_id} to {current_time}")
|
||||
db.session.commit()
|
||||
return repayment.to_dict()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update repay date: {e}")
|
||||
raise e
|
||||
@classmethod
|
||||
def set_repay_verify_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the repayment verify date of the loan with the given repayment_id.
|
||||
"""
|
||||
# Retrieve repayment
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if repayment.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the repayment's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update repayment verify_date
|
||||
repayment.verify_date = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating repay verify date for repayment ID {repayment_id} to {current_time}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update repay verify date: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_repay_result(cls, repayment_id, result, description):
|
||||
logger.info("repay result called")
|
||||
"""
|
||||
Update the repayment result and description of the repayment with the given repayment_id.
|
||||
"""
|
||||
# Retrieve loan
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Update repayment result and description
|
||||
repayment.repay_result = result
|
||||
repayment.repay_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating repayment result for repayment ID {repayment_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update repayment result: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def set_verify_date_result(cls, repayment_id, result, description):
|
||||
"""
|
||||
Update the verify result and description of the repayment with the given repayment_id.
|
||||
"""
|
||||
# Retrieve repayment
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Update disburse result and description
|
||||
repayment.verify_result = result
|
||||
repayment.verify_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating verify result for repayment ID {repayment_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update verify result: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def get_latest_repayment_without_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment without a repay date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.repay_date.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
@classmethod
|
||||
def get_latest_repayment_with_loanId(cls, loan_id):
|
||||
"""
|
||||
Get the latest repayment with loan Id.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.loan_id == loan_id
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment with a repay date and no verification date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.repay_date.isnot(None),
|
||||
cls.verify_date.is_(None)
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
from app.utils.logger import logger
|
||||
from app.enums.loan_status import LoanStatus
|
||||
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)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
initiated_by = db.Column(db.String(50), nullable=True)
|
||||
salary_amount = db.Column(db.Float, nullable=True, 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))
|
||||
repay_date = db.Column(db.DateTime, nullable=True)
|
||||
verify_date = db.Column(db.DateTime, nullable=True)
|
||||
repay_result = db.Column(db.String(10), nullable=True)
|
||||
repay_description = db.Column(db.String(100), nullable=True)
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
def __repr__(self):
|
||||
return f'<Repayment {self.id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Repayment object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'Id': self.id,
|
||||
"customerId": self.customer_id,
|
||||
'loanId': self.loan_id,
|
||||
'productId': self.product_id,
|
||||
'repayResult': self.repay_result,
|
||||
'repayDescription': self.repay_description,
|
||||
'verifyResult': self.verify_result,
|
||||
'verifyDescription': self.verify_description,
|
||||
'transactionId': self.transaction_id,
|
||||
'initiatedBy':self.initiated_by,
|
||||
'salaryAmount':self.salary_amount,
|
||||
'repayDate': self.repay_date.isoformat() if self.repay_date else None,
|
||||
'VerifyDate': self.verify_date.isoformat() if self.verify_date else None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_repayment(cls, repayment_data):
|
||||
|
||||
if repayment_data["LoanStatus"] not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY,LoanStatus.ACTIVE_PARTIAL]:
|
||||
raise ValueError(f"Repayment cannot be processed. Loan status: ({repayment_data['LoanStatus']})")
|
||||
|
||||
repayment = cls(
|
||||
customer_id=repayment_data["customerId"],
|
||||
loan_id=repayment_data["loanId"],
|
||||
product_id=repayment_data["productId"],
|
||||
transaction_id=repayment_data["transactionId"],
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
initiated_by= repayment_data["initiatedBy"],
|
||||
salary_amount=repayment_data["salaryAmount"]
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(repayment)
|
||||
db.session.commit()
|
||||
logger.info("Repayment record committed.")
|
||||
return repayment
|
||||
except IntegrityError as err:
|
||||
logger.error(f"Database integrity error: {err}")
|
||||
return {"error": "Integrity error", "details": str(err)}
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_repayment(cls, data: dict):
|
||||
"""
|
||||
Create and persist a new repayment record.
|
||||
"""
|
||||
logger.info(f"Received repayment data: {data}")
|
||||
|
||||
try:
|
||||
new_repayment = cls(
|
||||
loan_id=data["loanId"],
|
||||
customer_id=data["customerId"],
|
||||
product_id=data.get("productId"),
|
||||
transaction_id=data["transactionId"],
|
||||
initiated_by=data.get("initiatedBy"),
|
||||
salary_amount=float(data.get("salaryAmount", 0.0)),
|
||||
repay_date=(
|
||||
datetime.strptime(data["repayDate"], "%Y-%m-%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
if data.get("repayDate")
|
||||
else None
|
||||
),
|
||||
repay_result=data.get("repayResult"),
|
||||
repay_description=data.get("repayDescription"),
|
||||
verify_result=data.get("verifyResult"),
|
||||
verify_description=data.get("verifyDescription"),
|
||||
verify_date=(
|
||||
datetime.strptime(data["verifyDate"], "%Y-%m-%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
if data.get("verifyDate")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
db.session.add(new_repayment)
|
||||
db.session.commit()
|
||||
logger.info("Repayment record committed.")
|
||||
return new_repayment
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error adding repayment data: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def get_repayment_by_transaction_id(cls, transaction_id):
|
||||
return cls.query.filter_by(transaction_id=transaction_id).first()
|
||||
@classmethod
|
||||
def get_repayment_by_id(cls, id):
|
||||
return cls.query.filter_by(id=id).first()
|
||||
|
||||
@classmethod
|
||||
def set_repay_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the repay date of the loan with the given loan_id.
|
||||
"""
|
||||
# Retrieve repayment
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if repayment.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the repayment's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update repayment date
|
||||
repayment.repay_date = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating repay date for repayment ID {repayment_id} to {current_time}")
|
||||
db.session.commit()
|
||||
return repayment.to_dict()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update repay date: {e}")
|
||||
raise e
|
||||
@classmethod
|
||||
def set_repay_verify_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the repayment verify date of the loan with the given repayment_id.
|
||||
"""
|
||||
# Retrieve repayment
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Check if customer_id matches
|
||||
if repayment.customer_id != customer_id:
|
||||
raise ValueError(f"Customer ID {customer_id} does not match the repayment's customer ID.")
|
||||
|
||||
current_time = datetime.now()
|
||||
logger.info(f"What is now ======= ==== ==> : {current_time}")
|
||||
# Update repayment verify_date
|
||||
repayment.verify_date = current_time
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating repay verify date for repayment ID {repayment_id} to {current_time}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update repay verify date: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_repay_result(cls, repayment_id, result, description):
|
||||
logger.info("repay result called")
|
||||
"""
|
||||
Update the repayment result and description of the repayment with the given repayment_id.
|
||||
"""
|
||||
# Retrieve loan
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Update repayment result and description
|
||||
repayment.repay_result = result
|
||||
repayment.repay_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating repayment result for repayment ID {repayment_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update repayment result: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def set_verify_date_result(cls, repayment_id, result, description):
|
||||
"""
|
||||
Update the verify result and description of the repayment with the given repayment_id.
|
||||
"""
|
||||
# Retrieve repayment
|
||||
repayment = cls.query.get(repayment_id)
|
||||
|
||||
if not repayment:
|
||||
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
|
||||
|
||||
# Update disburse result and description
|
||||
repayment.verify_result = result
|
||||
repayment.verify_description = description
|
||||
|
||||
# Commit changes to database
|
||||
try:
|
||||
logger.info(f"Updating verify result for repayment ID {repayment_id} to {result} with description {description}")
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Failed to update verify result: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def get_latest_repayment_without_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment without a repay date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.repay_date.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
@classmethod
|
||||
def get_latest_repayment_with_loanId(cls, loan_id):
|
||||
"""
|
||||
Get the latest repayment with loan Id.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.loan_id == loan_id
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment with a repay date and no verification date.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.repay_date.isnot(None),
|
||||
cls.verify_date.is_(None)
|
||||
).order_by(cls.created_at.desc()).first()
|
||||
@@ -1,75 +1,75 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from app.utils.logger import logger
|
||||
|
||||
class RepaymentsData(db.Model):
|
||||
__tablename__ = 'repayments_data'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
|
||||
response_code = db.Column(db.String(10), nullable=True)
|
||||
response_descr = db.Column(db.String(255), nullable=True)
|
||||
fbn_transaction_id = db.Column(db.String(255),nullable=True)
|
||||
account_id = db.Column(db.String(50), nullable=True)
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
repayment_amount = db.Column(db.Float, nullable=True)
|
||||
amount_collected = db.Column(db.Float, nullable=True)
|
||||
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"transaction_id": self.transaction_id,
|
||||
"added_date": self.added_date.isoformat() if self.added_date else None,
|
||||
"response_code": self.response_code,
|
||||
"response_descr": self.response_descr,
|
||||
"customerId": self.customer_id,
|
||||
"accountId": self.account_id,
|
||||
"fbnTransactionId": self.fbn_transaction_id,
|
||||
"repaymentAmount": self.repayment_amount,
|
||||
"amountCollected": self.amount_collected,
|
||||
"balance": self.balance
|
||||
}
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
|
||||
|
||||
@classmethod
|
||||
def add_repayment_data(cls, data):
|
||||
"""
|
||||
Add a new repayment data entry.
|
||||
"""
|
||||
try:
|
||||
repayment_amount = float(data.get('repaymentAmount', 0.0))
|
||||
amount_collected = float(data.get('amountCollected', 0.0))
|
||||
|
||||
if amount_collected < 0 or repayment_amount < 0:
|
||||
raise ValueError("Amounts cannot be negative.")
|
||||
|
||||
account_balance = round(repayment_amount - amount_collected, 2)
|
||||
|
||||
new_data = cls(
|
||||
transaction_id=data.get('transactionId'),
|
||||
response_code=data.get('responseCode'),
|
||||
response_descr=data.get('responseDescr'),
|
||||
fbn_transaction_id=data.get('fbnTransactionId'),
|
||||
account_id=data.get('accountId'),
|
||||
customer_id=data.get('customerId'),
|
||||
amount_collected=amount_collected,
|
||||
repayment_amount=repayment_amount,
|
||||
balance=account_balance,
|
||||
)
|
||||
|
||||
db.session.add(new_data)
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Repayment data committed successfully")
|
||||
return new_data
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error adding repayment data: {e}")
|
||||
raise Exception(f"Error adding repayment data: {str(e)}")
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from app.utils.logger import logger
|
||||
|
||||
class RepaymentsData(db.Model):
|
||||
__tablename__ = 'repayments_data'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
|
||||
response_code = db.Column(db.String(10), nullable=True)
|
||||
response_descr = db.Column(db.String(255), nullable=True)
|
||||
fbn_transaction_id = db.Column(db.String(255),nullable=True)
|
||||
account_id = db.Column(db.String(50), nullable=True)
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
repayment_amount = db.Column(db.Float, nullable=True)
|
||||
amount_collected = db.Column(db.Float, nullable=True)
|
||||
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"transaction_id": self.transaction_id,
|
||||
"added_date": self.added_date.isoformat() if self.added_date else None,
|
||||
"response_code": self.response_code,
|
||||
"response_descr": self.response_descr,
|
||||
"customerId": self.customer_id,
|
||||
"accountId": self.account_id,
|
||||
"fbnTransactionId": self.fbn_transaction_id,
|
||||
"repaymentAmount": self.repayment_amount,
|
||||
"amountCollected": self.amount_collected,
|
||||
"balance": self.balance
|
||||
}
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
|
||||
|
||||
@classmethod
|
||||
def add_repayment_data(cls, data):
|
||||
"""
|
||||
Add a new repayment data entry.
|
||||
"""
|
||||
try:
|
||||
repayment_amount = float(data.get('repaymentAmount', 0.0))
|
||||
amount_collected = float(data.get('amountCollected', 0.0))
|
||||
|
||||
if amount_collected < 0 or repayment_amount < 0:
|
||||
raise ValueError("Amounts cannot be negative.")
|
||||
|
||||
account_balance = round(repayment_amount - amount_collected, 2)
|
||||
|
||||
new_data = cls(
|
||||
transaction_id=data.get('transactionId'),
|
||||
response_code=data.get('responseCode'),
|
||||
response_descr=data.get('responseDescr'),
|
||||
fbn_transaction_id=data.get('fbnTransactionId'),
|
||||
account_id=data.get('accountId'),
|
||||
customer_id=data.get('customerId'),
|
||||
amount_collected=amount_collected,
|
||||
repayment_amount=repayment_amount,
|
||||
balance=account_balance,
|
||||
)
|
||||
|
||||
db.session.add(new_data)
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Repayment data committed successfully")
|
||||
return new_data
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error adding repayment data: {e}")
|
||||
raise Exception(f"Error adding repayment data: {str(e)}")
|
||||
|
||||
+98
-98
@@ -1,98 +1,98 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
from app.utils.logger import logger
|
||||
|
||||
class Salary(db.Model):
|
||||
__tablename__ = "salaries"
|
||||
|
||||
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)
|
||||
amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
status = db.Column(db.String(20), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
salary_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Salary {self.id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Salary object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'customerId': self.customer_id,
|
||||
'accountId' : self.account_id,
|
||||
'salaryAmount': self.amount,
|
||||
'status': self.status,
|
||||
'createdAt': self.created_at.isoformat() if self.created_at else None,
|
||||
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'salaryDate': self.salary_date.isoformat() if self.salary_date else None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def add_salary_data(cls, data):
|
||||
"""
|
||||
Add a new salary data entry.
|
||||
"""
|
||||
logger.info(f"Received data:{data}")
|
||||
try:
|
||||
new_data = cls(
|
||||
customer_id=data.get('customerId'),
|
||||
amount=data.get('salaryAmount', 0.0),
|
||||
status='START',
|
||||
salary_date = datetime.strptime(data.get('salaryDate'), "%Y-%m-%d").date() if data.get('salaryDate') else None,
|
||||
account_id=data.get('accountId')
|
||||
)
|
||||
db.session.add(new_data)
|
||||
db.session.commit()
|
||||
logger.info("Salary data has been committed.")
|
||||
return new_data
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.info(f"error : {str(e)}")
|
||||
raise Exception(f"Error adding salary data: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def get_pending_salaries(cls):
|
||||
"""
|
||||
Retrieve all salary entries with status 'START', ordered by ID ascending.
|
||||
"""
|
||||
try:
|
||||
return cls.query.filter_by(status='START').order_by(cls.id.asc()).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching pending salaries: {str(e)}")
|
||||
return []
|
||||
@classmethod
|
||||
def update_status(cls, salary_id, status):
|
||||
"""
|
||||
Update the status of the salary record with the given salary_id.
|
||||
"""
|
||||
try:
|
||||
# Retrieve salary record
|
||||
salary = cls.query.get(salary_id)
|
||||
|
||||
if not salary:
|
||||
raise ValueError(f"Salary with ID {salary_id} does not exist.")
|
||||
|
||||
if salary.status == status:
|
||||
return salary.to_dict() # Still return the current state if no change
|
||||
|
||||
# Update status and timestamp
|
||||
salary.status = status
|
||||
salary.updated_at = datetime.now(timezone.utc) # Manually update timestamp if not auto-updating
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Salary status updated and committed.")
|
||||
return salary.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating salary status: {e}")
|
||||
raise Exception(f"Error updating salary status: {str(e)}")
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
from app.utils.logger import logger
|
||||
|
||||
class Salary(db.Model):
|
||||
__tablename__ = "salaries"
|
||||
|
||||
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)
|
||||
amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
status = db.Column(db.String(20), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
salary_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Salary {self.id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Salary object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'customerId': self.customer_id,
|
||||
'accountId' : self.account_id,
|
||||
'salaryAmount': self.amount,
|
||||
'status': self.status,
|
||||
'createdAt': self.created_at.isoformat() if self.created_at else None,
|
||||
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'salaryDate': self.salary_date.isoformat() if self.salary_date else None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def add_salary_data(cls, data):
|
||||
"""
|
||||
Add a new salary data entry.
|
||||
"""
|
||||
logger.info(f"Received data:{data}")
|
||||
try:
|
||||
new_data = cls(
|
||||
customer_id=data.get('customerId'),
|
||||
amount=data.get('salaryAmount', 0.0),
|
||||
status='START',
|
||||
salary_date = datetime.strptime(data.get('salaryDate'), "%Y-%m-%d").date() if data.get('salaryDate') else None,
|
||||
account_id=data.get('accountId')
|
||||
)
|
||||
db.session.add(new_data)
|
||||
db.session.commit()
|
||||
logger.info("Salary data has been committed.")
|
||||
return new_data
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.info(f"error : {str(e)}")
|
||||
raise Exception(f"Error adding salary data: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def get_pending_salaries(cls):
|
||||
"""
|
||||
Retrieve all salary entries with status 'START', ordered by ID ascending.
|
||||
"""
|
||||
try:
|
||||
return cls.query.filter_by(status='START').order_by(cls.id.asc()).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching pending salaries: {str(e)}")
|
||||
return []
|
||||
@classmethod
|
||||
def update_status(cls, salary_id, status):
|
||||
"""
|
||||
Update the status of the salary record with the given salary_id.
|
||||
"""
|
||||
try:
|
||||
# Retrieve salary record
|
||||
salary = cls.query.get(salary_id)
|
||||
|
||||
if not salary:
|
||||
raise ValueError(f"Salary with ID {salary_id} does not exist.")
|
||||
|
||||
if salary.status == status:
|
||||
return salary.to_dict() # Still return the current state if no change
|
||||
|
||||
# Update status and timestamp
|
||||
salary.status = status
|
||||
salary.updated_at = datetime.now(timezone.utc) # Manually update timestamp if not auto-updating
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Salary status updated and committed.")
|
||||
return salary.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating salary status: {e}")
|
||||
raise Exception(f"Error updating salary status: {str(e)}")
|
||||
|
||||
+67
-67
@@ -1,68 +1,68 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.utils.logger import logger
|
||||
from sqlalchemy import and_, or_, not_
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id = db.Column(
|
||||
db.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
account_id = db.Column(db.String(50), nullable=True)
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
type = db.Column(db.String(50), nullable=False)
|
||||
channel = db.Column(db.String(50), nullable=False)
|
||||
phone_number = db.Column(db.String(50), 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_transaction(cls, transaction_id, account_id, customer_id, type, channel):
|
||||
logger.error(f"**Setting Transaction {transaction_id} for Type {type}")
|
||||
if cls.query.filter( and_( cls.transaction_id ==transaction_id, cls.type==type) ).first():
|
||||
logger.error(f"Transaction already exists for {type}")
|
||||
return '' # dont raise - do not crash beacause of this
|
||||
|
||||
transaction = cls(
|
||||
transaction_id = transaction_id,
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
type = type,
|
||||
channel = channel,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
except IntegrityError as err:
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
|
||||
return transaction
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Transaction {self.id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Transaction object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'transaction_id': self.transaction_id,
|
||||
'account_id': self.account_id,
|
||||
'customer_id': self.customer_id,
|
||||
'phone_number':self.phone_number,
|
||||
'type': self.type,
|
||||
'channel': self.channel,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_transaction_by_transaction_id(cls, transaction_id):
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.utils.logger import logger
|
||||
from sqlalchemy import and_, or_, not_
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id = db.Column(
|
||||
db.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
account_id = db.Column(db.String(50), nullable=True)
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
type = db.Column(db.String(50), nullable=False)
|
||||
channel = db.Column(db.String(50), nullable=False)
|
||||
phone_number = db.Column(db.String(50), 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_transaction(cls, transaction_id, account_id, customer_id, type, channel):
|
||||
logger.error(f"**Setting Transaction {transaction_id} for Type {type}")
|
||||
if cls.query.filter( and_( cls.transaction_id ==transaction_id, cls.type==type) ).first():
|
||||
logger.error(f"Transaction already exists for {type}")
|
||||
return '' # dont raise - do not crash beacause of this
|
||||
|
||||
transaction = cls(
|
||||
transaction_id = transaction_id,
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
type = type,
|
||||
channel = channel,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
except IntegrityError as err:
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
|
||||
return transaction
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Transaction {self.id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Transaction object to a dictionary format for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'transaction_id': self.transaction_id,
|
||||
'account_id': self.account_id,
|
||||
'customer_id': self.customer_id,
|
||||
'phone_number':self.phone_number,
|
||||
'type': self.type,
|
||||
'channel': self.channel,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_transaction_by_transaction_id(cls, transaction_id):
|
||||
return cls.query.filter_by(transaction_id=transaction_id).first()
|
||||
@@ -1,2 +1,2 @@
|
||||
from .handlers import (method_not_allowed, unsupported_media_type,
|
||||
from .handlers import (method_not_allowed, unsupported_media_type,
|
||||
not_found, bad_request, success, created, updated)
|
||||
+30
-30
@@ -1,30 +1,30 @@
|
||||
from flask import jsonify
|
||||
from app.helpers.response_helper import ResponseHelper
|
||||
|
||||
|
||||
def method_not_allowed(error):
|
||||
return ResponseHelper.method_not_allowed(message="Method Not Allowed")
|
||||
|
||||
|
||||
def not_found(error):
|
||||
return ResponseHelper.not_found(message="URL Not Found")
|
||||
|
||||
|
||||
def bad_request(error):
|
||||
return ResponseHelper.bad_request(message="Bad Request")
|
||||
|
||||
|
||||
def unsupported_media_type(error):
|
||||
return ResponseHelper.error(message="Unsupported Media Type", status_code=415)
|
||||
|
||||
|
||||
def success(data):
|
||||
return ResponseHelper.success(data=data)
|
||||
|
||||
|
||||
def created(data):
|
||||
return ResponseHelper.created(data=data)
|
||||
|
||||
|
||||
def updated(data):
|
||||
return ResponseHelper.updated(data=data)
|
||||
from flask import jsonify
|
||||
from app.helpers.response_helper import ResponseHelper
|
||||
|
||||
|
||||
def method_not_allowed(error):
|
||||
return ResponseHelper.method_not_allowed(message="Method Not Allowed")
|
||||
|
||||
|
||||
def not_found(error):
|
||||
return ResponseHelper.not_found(message="URL Not Found")
|
||||
|
||||
|
||||
def bad_request(error):
|
||||
return ResponseHelper.bad_request(message="Bad Request")
|
||||
|
||||
|
||||
def unsupported_media_type(error):
|
||||
return ResponseHelper.error(message="Unsupported Media Type", status_code=415)
|
||||
|
||||
|
||||
def success(data):
|
||||
return ResponseHelper.success(data=data)
|
||||
|
||||
|
||||
def created(data):
|
||||
return ResponseHelper.created(data=data)
|
||||
|
||||
|
||||
def updated(data):
|
||||
return ResponseHelper.updated(data=data)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
from .authentication import auth_bp
|
||||
from .autocall import autocall_bp
|
||||
from .authentication import auth_bp
|
||||
from .autocall import autocall_bp
|
||||
|
||||
+137
-137
@@ -1,137 +1,137 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
import requests
|
||||
from app.extensions import db
|
||||
from sqlalchemy import text
|
||||
from app.utils.auth import get_headers
|
||||
from app.config import settings
|
||||
from app.utils.logger import logger
|
||||
from app.integrations.bank_service import BankService
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
|
||||
|
||||
@auth_bp.route("/health", methods=["GET"])
|
||||
def health():
|
||||
logger.info("Health check endpoint called")
|
||||
errors = [] # collect all errors
|
||||
|
||||
try:
|
||||
# Detect database type
|
||||
dialect = db.engine.dialect.name.lower()
|
||||
logger.info(f"Database dialect detected: {dialect}")
|
||||
|
||||
# Build correct query based on DB type
|
||||
if "oracle" in dialect:
|
||||
query = text("SELECT 1 FROM dual")
|
||||
else:
|
||||
query = text("SELECT 1")
|
||||
|
||||
# Test database connection
|
||||
try:
|
||||
db.session.execute(query)
|
||||
logger.info("Database connection successful.")
|
||||
except Exception as db_err:
|
||||
logger.error(f"Database connection failed: {str(db_err)}")
|
||||
errors.append(f"Database connection failed: {str(db_err)}")
|
||||
|
||||
# Check Bank Service health
|
||||
try:
|
||||
bank_response = BankService.health_check()
|
||||
logger.info(f"Bank Service health check response: {bank_response}")
|
||||
except Exception as bank_err:
|
||||
logger.error(f"Bank Service health check failed: {str(bank_err)}")
|
||||
errors.append(f"Bank Service health check failed: {str(bank_err)}")
|
||||
|
||||
# Build final response
|
||||
if errors:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"database": dialect,
|
||||
"errors": errors
|
||||
}), 500
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"database": dialect,
|
||||
"db_status": "connected",
|
||||
"bank_service_status": "operational",
|
||||
"message": "All systems operational"
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error during health check")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"errors": [str(e)]
|
||||
}), 500
|
||||
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["POST"])
|
||||
def login():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/login"
|
||||
|
||||
response = requests.post(api_url, json=data)
|
||||
if response.status_code == 200:
|
||||
return jsonify(response.json()), 200
|
||||
return jsonify({"error": "Invalid credentials"}), response.status_code
|
||||
|
||||
|
||||
@auth_bp.route("/status-call", methods=["POST"])
|
||||
def status_call():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/StatusCall"
|
||||
|
||||
# response = requests.post(api_url, json=data, headers=get_headers())
|
||||
# return jsonify(response.json()), response.status_code
|
||||
response = {
|
||||
"transactionId": "24110114545374721",
|
||||
"data": {
|
||||
"transactionId": "241101",
|
||||
"providedAmount": 1000,
|
||||
"collectedAmount": 0,
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Loan Provision is successful",
|
||||
},
|
||||
"resultCode": "00",
|
||||
"resultDescription": "SUCCESS",
|
||||
}
|
||||
|
||||
return jsonify(response), 200
|
||||
|
||||
|
||||
@auth_bp.route("/sms", methods=["POST"])
|
||||
def sms():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/SMS"
|
||||
|
||||
# response = requests.post(api_url, json=data, headers=get_headers())
|
||||
# return jsonify(response.json()), response.status_code
|
||||
response = {
|
||||
"data": "",
|
||||
"statusCode": 200,
|
||||
"IsSuccessful": True,
|
||||
"errorMessage": None,
|
||||
}
|
||||
|
||||
return jsonify(response), 200
|
||||
|
||||
|
||||
@auth_bp.route("/bulk-sms", methods=["POST"])
|
||||
def bulk_sms():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/BulkSMS"
|
||||
|
||||
# response = requests.post(api_url, json=data, headers=get_headers())
|
||||
# return jsonify(response.json()), response.status_code
|
||||
response = {
|
||||
"data": "",
|
||||
"statusCode": 200,
|
||||
"IsSuccessful": True,
|
||||
"errorMessage": None,
|
||||
}
|
||||
|
||||
return jsonify(response), 200
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
import requests
|
||||
from app.extensions import db
|
||||
from sqlalchemy import text
|
||||
from app.utils.auth import get_headers
|
||||
from app.config import settings
|
||||
from app.utils.logger import logger
|
||||
from app.integrations.bank_service import BankService
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
|
||||
|
||||
@auth_bp.route("/health", methods=["GET"])
|
||||
def health():
|
||||
logger.info("Health check endpoint called")
|
||||
errors = [] # collect all errors
|
||||
|
||||
try:
|
||||
# Detect database type
|
||||
dialect = db.engine.dialect.name.lower()
|
||||
logger.info(f"Database dialect detected: {dialect}")
|
||||
|
||||
# Build correct query based on DB type
|
||||
if "oracle" in dialect:
|
||||
query = text("SELECT 1 FROM dual")
|
||||
else:
|
||||
query = text("SELECT 1")
|
||||
|
||||
# Test database connection
|
||||
try:
|
||||
db.session.execute(query)
|
||||
logger.info("Database connection successful.")
|
||||
except Exception as db_err:
|
||||
logger.error(f"Database connection failed: {str(db_err)}")
|
||||
errors.append(f"Database connection failed: {str(db_err)}")
|
||||
|
||||
# Check Bank Service health
|
||||
try:
|
||||
bank_response = BankService.health_check()
|
||||
logger.info(f"Bank Service health check response: {bank_response}")
|
||||
except Exception as bank_err:
|
||||
logger.error(f"Bank Service health check failed: {str(bank_err)}")
|
||||
errors.append(f"Bank Service health check failed: {str(bank_err)}")
|
||||
|
||||
# Build final response
|
||||
if errors:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"database": dialect,
|
||||
"errors": errors
|
||||
}), 500
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"database": dialect,
|
||||
"db_status": "connected",
|
||||
"bank_service_status": "operational",
|
||||
"message": "All systems operational"
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error during health check")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"errors": [str(e)]
|
||||
}), 500
|
||||
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["POST"])
|
||||
def login():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/login"
|
||||
|
||||
response = requests.post(api_url, json=data)
|
||||
if response.status_code == 200:
|
||||
return jsonify(response.json()), 200
|
||||
return jsonify({"error": "Invalid credentials"}), response.status_code
|
||||
|
||||
|
||||
@auth_bp.route("/status-call", methods=["POST"])
|
||||
def status_call():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/StatusCall"
|
||||
|
||||
# response = requests.post(api_url, json=data, headers=get_headers())
|
||||
# return jsonify(response.json()), response.status_code
|
||||
response = {
|
||||
"transactionId": "24110114545374721",
|
||||
"data": {
|
||||
"transactionId": "241101",
|
||||
"providedAmount": 1000,
|
||||
"collectedAmount": 0,
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Loan Provision is successful",
|
||||
},
|
||||
"resultCode": "00",
|
||||
"resultDescription": "SUCCESS",
|
||||
}
|
||||
|
||||
return jsonify(response), 200
|
||||
|
||||
|
||||
@auth_bp.route("/sms", methods=["POST"])
|
||||
def sms():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/SMS"
|
||||
|
||||
# response = requests.post(api_url, json=data, headers=get_headers())
|
||||
# return jsonify(response.json()), response.status_code
|
||||
response = {
|
||||
"data": "",
|
||||
"statusCode": 200,
|
||||
"IsSuccessful": True,
|
||||
"errorMessage": None,
|
||||
}
|
||||
|
||||
return jsonify(response), 200
|
||||
|
||||
|
||||
@auth_bp.route("/bulk-sms", methods=["POST"])
|
||||
def bulk_sms():
|
||||
data = request.get_json()
|
||||
api_url = f"{BASE_URL}/BulkSMS"
|
||||
|
||||
# response = requests.post(api_url, json=data, headers=get_headers())
|
||||
# return jsonify(response.json()), response.status_code
|
||||
response = {
|
||||
"data": "",
|
||||
"statusCode": 200,
|
||||
"IsSuccessful": True,
|
||||
"errorMessage": None,
|
||||
}
|
||||
|
||||
return jsonify(response), 200
|
||||
|
||||
+684
-670
File diff suppressed because it is too large
Load Diff
+147
-144
@@ -1,145 +1,148 @@
|
||||
from app.models import Loan, LoanCharge
|
||||
from app.utils.logger import logger
|
||||
from app.enums.loan_status import LoanStatus
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from app.services.loan_repayment_schedule import LoanRepaymentScheduleService
|
||||
|
||||
class LoanService:
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_transaction_id(cls, transaction_id):
|
||||
"""
|
||||
Get the loan by transaction ID
|
||||
"""
|
||||
return Loan.get_loan_by_transaction_id(transaction_id)
|
||||
@classmethod
|
||||
def get_loan_by_loan_id(cls, loan_id):
|
||||
"""
|
||||
Get the loan by ID
|
||||
"""
|
||||
return Loan.get_loan_by_loan_id(loan_id)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_debt_id(cls, debt_id):
|
||||
"""
|
||||
Get the loan by transaction ID
|
||||
"""
|
||||
return Loan.get_loan_by_debt_id(debt_id)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_charge_by_debt_id(cls, debt_id):
|
||||
"""
|
||||
Get the loan charge by debt ID
|
||||
"""
|
||||
return LoanCharge.get_loan_charge_by_debt_id(debt_id)
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disbursement status of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disbursement_date(loan_id, customer_id)
|
||||
@classmethod
|
||||
def set_disburse_verify_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disburse verify date of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disburse_verify_date(loan_id, customer_id)
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the disbursement result of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disbursement_result(loan_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_loan_description(cls,loan_id,description):
|
||||
|
||||
return Loan.set_disbursement_message(loan_id, result, description)
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disburse_verify_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the disburse verify result of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disburse_verify_result(loan_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_without_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan without a disbursement date.
|
||||
"""
|
||||
return Loan.get_latest_loan_without_disburse_date()
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan without a disbursement date.
|
||||
"""
|
||||
return Loan.get_latest_loan_with_disburse_date()
|
||||
|
||||
@classmethod
|
||||
def get_customer_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans by customer_id.
|
||||
"""
|
||||
|
||||
return Loan.get_customer_loans(customer_id=customer_id)
|
||||
@classmethod
|
||||
def get_customer_active_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans by customer_id.
|
||||
"""
|
||||
|
||||
return Loan.get_customer_active_loans(customer_id=customer_id)
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
Update the status of the loan with the given loan_id.
|
||||
"""
|
||||
# Retrieve loan
|
||||
return Loan.update_status(loan_id, status)
|
||||
@classmethod
|
||||
def update_loan_balance(cls,loan_id,amount_collected):
|
||||
"""
|
||||
update the loan balance after successful repayment
|
||||
"""
|
||||
return Loan.update_loan_balance(loan_id,amount_collected)
|
||||
|
||||
@classmethod
|
||||
def get_overdue_loans(cls):
|
||||
"""
|
||||
Get all overdue loans.
|
||||
"""
|
||||
return Loan.get_overdue_loans()
|
||||
@staticmethod
|
||||
def _update_loan_after_collection(loan, loan_data, updated_loan, amount_collected, data, response_message):
|
||||
if loan.balance is None or loan.balance <= 0:
|
||||
logger.warning(f"Loan ID {loan.id} has no balance. Skipping loan update.")
|
||||
updated_loan = loan.to_dict()
|
||||
else:
|
||||
updated_loan = LoanService.update_loan_balance(int(loan_data['debtId']), amount_collected)
|
||||
updated_balance = Decimal(str(updated_loan['balance'])).quantize(Decimal('0.01'))
|
||||
|
||||
if updated_balance <= Decimal('0.00'):
|
||||
updated_loan = LoanService.update_status(updated_loan['debtId'], LoanStatus.REPAID)
|
||||
else:
|
||||
updated_loan = LoanService.update_status(updated_loan['debtId'], LoanStatus.ACTIVE_PARTIAL)
|
||||
logger.info(f"Updated loan status: {updated_loan.get('status')}")
|
||||
# lets update the loan repayment schedule
|
||||
LoanRepaymentScheduleService.handle_schedule_updates(
|
||||
updated_loan=updated_loan,
|
||||
data=data,
|
||||
amount_collected=amount_collected,
|
||||
message=response_message,
|
||||
loan_data=loan_data
|
||||
)
|
||||
return updated_loan
|
||||
|
||||
|
||||
from app.models import Loan, LoanCharge
|
||||
from app.utils.logger import logger
|
||||
from app.enums.loan_status import LoanStatus
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from app.services.loan_repayment_schedule import LoanRepaymentScheduleService
|
||||
|
||||
class LoanService:
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_transaction_id(cls, transaction_id):
|
||||
"""
|
||||
Get the loan by transaction ID
|
||||
"""
|
||||
return Loan.get_loan_by_transaction_id(transaction_id)
|
||||
@classmethod
|
||||
def get_loan_by_loan_id(cls, loan_id):
|
||||
"""
|
||||
Get the loan by ID
|
||||
"""
|
||||
return Loan.get_loan_by_loan_id(loan_id)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_debt_id(cls, debt_id):
|
||||
"""
|
||||
Get the loan by transaction ID
|
||||
"""
|
||||
return Loan.get_loan_by_debt_id(debt_id)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_charge_by_debt_id(cls, debt_id):
|
||||
"""
|
||||
Get the loan charge by debt ID
|
||||
"""
|
||||
return LoanCharge.get_loan_charge_by_debt_id(debt_id)
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disbursement status of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disbursement_date(loan_id, customer_id)
|
||||
@classmethod
|
||||
def set_disburse_verify_date(cls, loan_id, customer_id):
|
||||
"""
|
||||
Update the disburse verify date of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disburse_verify_date(loan_id, customer_id)
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the disbursement result of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disbursement_result(loan_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def set_disbursement_loan_description(cls,loan_id,description):
|
||||
|
||||
return Loan.set_disbursement_message(loan_id, description)
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disburse_verify_result(cls, loan_id, result, description):
|
||||
"""
|
||||
Update the disburse verify result of the loan with the given loan_id.
|
||||
"""
|
||||
return Loan.set_disburse_verify_result(loan_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_without_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan without a disbursement date.
|
||||
"""
|
||||
return Loan.get_latest_loan_without_disburse_date()
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_disburse_date(cls):
|
||||
"""
|
||||
Get the latest loan without a disbursement date.
|
||||
"""
|
||||
return Loan.get_latest_loan_with_disburse_date()
|
||||
|
||||
@classmethod
|
||||
def get_customer_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans by customer_id.
|
||||
"""
|
||||
|
||||
return Loan.get_customer_loans(customer_id=customer_id)
|
||||
@classmethod
|
||||
def get_customer_active_loans(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans by customer_id.
|
||||
"""
|
||||
|
||||
return Loan.get_customer_active_loans(customer_id=customer_id)
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
Update the status of the loan with the given loan_id.
|
||||
"""
|
||||
# Retrieve loan
|
||||
return Loan.update_status(loan_id, status)
|
||||
@classmethod
|
||||
def update_loan_balance(cls,loan_id,amount_collected):
|
||||
"""
|
||||
update the loan balance after successful repayment
|
||||
"""
|
||||
return Loan.update_loan_balance(loan_id,amount_collected)
|
||||
|
||||
@classmethod
|
||||
def get_overdue_loans(cls):
|
||||
"""
|
||||
Get all overdue loans.
|
||||
"""
|
||||
return Loan.get_overdue_loans()
|
||||
@classmethod
|
||||
def apply_penal_to_loan(cls,loan_id,penal_charge):
|
||||
return Loan.apply_penal_to_loan(loan_id,penal_charge)
|
||||
@staticmethod
|
||||
def _update_loan_after_collection(loan, loan_data, updated_loan, amount_collected, data, response_message):
|
||||
if loan.balance is None or loan.balance <= 0:
|
||||
logger.warning(f"Loan ID {loan.id} has no balance. Skipping loan update.")
|
||||
updated_loan = loan.to_dict()
|
||||
else:
|
||||
updated_loan = LoanService.update_loan_balance(int(loan_data['debtId']), amount_collected)
|
||||
updated_balance = Decimal(str(updated_loan['balance'])).quantize(Decimal('0.01'))
|
||||
|
||||
if updated_balance <= Decimal('0.00'):
|
||||
updated_loan = LoanService.update_status(updated_loan['debtId'], LoanStatus.REPAID)
|
||||
else:
|
||||
updated_loan = LoanService.update_status(updated_loan['debtId'], LoanStatus.ACTIVE_PARTIAL)
|
||||
logger.info(f"Updated loan status: {updated_loan.get('status')}")
|
||||
# lets update the loan repayment schedule
|
||||
LoanRepaymentScheduleService.handle_schedule_updates(
|
||||
updated_loan=updated_loan,
|
||||
data=data,
|
||||
amount_collected=amount_collected,
|
||||
message=response_message,
|
||||
loan_data=loan_data
|
||||
)
|
||||
return updated_loan
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from app.models.loan_charge import LoanCharge
|
||||
|
||||
class LoanChargesService:
|
||||
@classmethod
|
||||
def create_penal_charges_for_loan(cls, loan_id, transaction_id, percent, penal_no, schedule_number, penal_amount=0.0,):
|
||||
return LoanCharge.create_penal_charges_for_loan(loan_id, transaction_id, percent, penal_no,schedule_number, penal_amount)
|
||||
@classmethod
|
||||
def get_last_penal_no(cls,loan_id):
|
||||
return LoanCharge.get_last_penal_no(loan_id)
|
||||
|
||||
@classmethod
|
||||
def get_penal_charges_by_loan_id(cls,loan_id):
|
||||
return LoanCharge.get_penal_charges_by_loan_id(loan_id)
|
||||
@@ -1,110 +1,136 @@
|
||||
from app.models.loan_repayment_schedule import LoanRepaymentSchedule
|
||||
from app.utils.logger import logger
|
||||
from app.enums.loan_status import LoanStatus
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
|
||||
class LoanRepaymentScheduleService:
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_loan_id(cls, loan_id, include_paid=True):
|
||||
return LoanRepaymentSchedule.get_repayment_schedule_by_loan_id(loan_id, include_paid=include_paid)
|
||||
@classmethod
|
||||
def get_overdue_repayment_schedule(cls):
|
||||
return LoanRepaymentSchedule.get_overdue_repayment_schedule()
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
|
||||
return LoanRepaymentSchedule.get_repayment_schedule_by_id_and_transaction_id(id, transaction_id)
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
|
||||
return LoanRepaymentSchedule.get_repayment_schedule_by_transaction_id(transaction_id)
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_status(cls, schedule_id):
|
||||
"""
|
||||
Update repayment schedule status.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_status(schedule_id)
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
||||
"""
|
||||
Update repayment schedule balance.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_balance(schedule_id, amount_collected)
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_description(cls, schedule_id, description):
|
||||
"""
|
||||
Update repayment schedule description.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_description(schedule_id, description)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def handle_schedule_updates(updated_loan, data, amount_collected, message, loan_data):
|
||||
"""
|
||||
Handles updating loan repayment schedules depending on loan status
|
||||
and overdue schedule data.
|
||||
"""
|
||||
try:
|
||||
# Case 1: Loan fully repaid → mark all schedules paid
|
||||
if updated_loan and updated_loan.get('status') == LoanStatus.REPAID:
|
||||
repayment_schedule = LoanRepaymentScheduleService.get_repayment_schedule_by_loan_id(
|
||||
updated_loan['debtId'], include_paid=False
|
||||
)
|
||||
logger.info(f'Loan repayment schedule: {repayment_schedule}')
|
||||
|
||||
if repayment_schedule:
|
||||
for installment in repayment_schedule:
|
||||
try:
|
||||
logger.info(f'Processing installment: {installment}')
|
||||
LoanRepaymentScheduleService.update_repayment_schedule_status(installment.id)
|
||||
LoanRepaymentScheduleService.update_repayment_schedule_description(
|
||||
installment.id,
|
||||
message
|
||||
)
|
||||
logger.info(f'Updated installment {installment.id} as paid')
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update installment {installment.id}: {e}")
|
||||
logger.info('All installments processed')
|
||||
|
||||
# Case 2: Partial repayment made on a full loan without overdueLoanScheduleId
|
||||
elif updated_loan and updated_loan.get('status') == LoanStatus.ACTIVE_PARTIAL and not data.get('overdueLoanScheduleId'):
|
||||
logger.info("Partial repayment detected, but no overdue schedule ID provided.")
|
||||
# TODO: implement proportional installment updates
|
||||
|
||||
# Case 3: when we are processing Overdue schedule repayment → update balance & description
|
||||
elif data.get('overdueLoanScheduleId') is not None:
|
||||
logger.info(f"Overdue loan schedule ID: {data['overdueLoanScheduleId']}")
|
||||
try:
|
||||
schedule_to_update = LoanRepaymentScheduleService.get_repayment_schedule_by_id_and_transaction_id(
|
||||
data["overdueLoanScheduleId"], data["transactionId"]
|
||||
)
|
||||
logger.info(f"Schedule to update: {schedule_to_update}")
|
||||
|
||||
if schedule_to_update is None:
|
||||
logger.warning(
|
||||
f"Repayment schedule not found for ID {data['overdueLoanScheduleId']} "
|
||||
f"and transaction ID {loan_data['transactionId']}"
|
||||
)
|
||||
else:
|
||||
if not schedule_to_update.paid:
|
||||
update_schedule_balance = LoanRepaymentScheduleService.update_repayment_schedule_balance(
|
||||
schedule_to_update.id, amount_collected
|
||||
)
|
||||
logger.info(f"Updated loan schedule balance: {update_schedule_balance}")
|
||||
LoanRepaymentScheduleService.update_repayment_schedule_description(
|
||||
schedule_to_update.id,
|
||||
message
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update repayment schedule installment: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error while handling schedule updates: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
from app.models.loan_repayment_schedule import LoanRepaymentSchedule
|
||||
from app.utils.logger import logger
|
||||
from app.enums.loan_status import LoanStatus
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
|
||||
class LoanRepaymentScheduleService:
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_loan_id(cls, loan_id, include_paid=True):
|
||||
return LoanRepaymentSchedule.get_repayment_schedule_by_loan_id(loan_id, include_paid=include_paid)
|
||||
@classmethod
|
||||
def get_overdue_repayment_schedule(cls):
|
||||
return LoanRepaymentSchedule.get_overdue_repayment_schedule()
|
||||
@classmethod
|
||||
def get_active_overdue_repayment_schedule(cls):
|
||||
return LoanRepaymentSchedule.get_active_overdue_repayment_schedule()
|
||||
@classmethod
|
||||
def get_partially_paid_overdue_repayment_schedule(cls):
|
||||
return LoanRepaymentSchedule.get_partially_paid_overdue_repayment_schedule()
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
|
||||
return LoanRepaymentSchedule.get_repayment_schedule_by_id_and_transaction_id(id, transaction_id)
|
||||
|
||||
@classmethod
|
||||
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
|
||||
return LoanRepaymentSchedule.get_repayment_schedule_by_transaction_id(transaction_id)
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_status(cls, schedule_id):
|
||||
"""
|
||||
Update repayment schedule status.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_status(schedule_id)
|
||||
@classmethod
|
||||
def update_repayment_schedule_status_to_active(cls, schedule_id):
|
||||
"""
|
||||
Update repayment schedule status.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_status_to_active(schedule_id)
|
||||
@classmethod
|
||||
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
||||
"""
|
||||
Update repayment schedule balance.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_balance(schedule_id, amount_collected)
|
||||
@classmethod
|
||||
def calculate_penal_charge(cls, schedule):
|
||||
"""
|
||||
Calculate penal charge for a repayment schedule.
|
||||
"""
|
||||
return LoanRepaymentSchedule.calculate_penal_charge(schedule)
|
||||
|
||||
@classmethod
|
||||
def update_repayment_schedule_description(cls, schedule_id, description):
|
||||
"""
|
||||
Update repayment schedule description.
|
||||
"""
|
||||
return LoanRepaymentSchedule.update_repayment_schedule_description(schedule_id, description)
|
||||
|
||||
@classmethod
|
||||
def get_overdue_repayment_schedule_with_grace_period(cls, grace_period_days, limit=None):
|
||||
return LoanRepaymentSchedule.get_overdue_repayment_schedule_with_grace_period(grace_period_days, limit=limit)
|
||||
|
||||
@classmethod
|
||||
def apply_penal_to_schedule(cls, schedule_id, penal_amount):
|
||||
"""
|
||||
Apply penal charge to a repayment schedule.
|
||||
"""
|
||||
return LoanRepaymentSchedule.apply_penal_to_schedule(schedule_id, penal_amount)
|
||||
@staticmethod
|
||||
def handle_schedule_updates(updated_loan, data, amount_collected, message, loan_data):
|
||||
"""
|
||||
Handles updating loan repayment schedules depending on loan status
|
||||
and overdue schedule data.
|
||||
"""
|
||||
try:
|
||||
# Case 1: Loan fully repaid → mark all schedules paid
|
||||
if updated_loan and updated_loan.get('status') == LoanStatus.REPAID:
|
||||
repayment_schedule = LoanRepaymentScheduleService.get_repayment_schedule_by_loan_id(
|
||||
updated_loan['debtId'], include_paid=False
|
||||
)
|
||||
logger.info(f'Loan repayment schedule: {repayment_schedule}')
|
||||
|
||||
if repayment_schedule:
|
||||
for installment in repayment_schedule:
|
||||
try:
|
||||
logger.info(f'Processing installment: {installment}')
|
||||
LoanRepaymentScheduleService.update_repayment_schedule_status(installment.id)
|
||||
LoanRepaymentScheduleService.update_repayment_schedule_description(
|
||||
installment.id,
|
||||
message
|
||||
)
|
||||
logger.info(f'Updated installment {installment.id} as paid')
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update installment {installment.id}: {e}")
|
||||
logger.info('All installments processed')
|
||||
|
||||
# Case 2: Partial repayment made on a full loan without overdueLoanScheduleId
|
||||
elif updated_loan and updated_loan.get('status') == LoanStatus.ACTIVE_PARTIAL and not data.get('overdueLoanScheduleId'):
|
||||
logger.info("Partial repayment detected, but no overdue schedule ID provided.")
|
||||
# TODO: implement proportional installment updates
|
||||
|
||||
# Case 3: when we are processing Overdue schedule repayment → update balance & description
|
||||
elif data.get('overdueLoanScheduleId') is not None:
|
||||
logger.info(f"Overdue loan schedule ID: {data['overdueLoanScheduleId']}")
|
||||
try:
|
||||
schedule_to_update = LoanRepaymentScheduleService.get_repayment_schedule_by_id_and_transaction_id(
|
||||
data["overdueLoanScheduleId"], data["transactionId"]
|
||||
)
|
||||
logger.info(f"Schedule to update: {schedule_to_update}")
|
||||
|
||||
if schedule_to_update is None:
|
||||
logger.warning(
|
||||
f"Repayment schedule not found for ID {data['overdueLoanScheduleId']} "
|
||||
f"and transaction ID {loan_data['transactionId']}"
|
||||
)
|
||||
else:
|
||||
if not schedule_to_update.paid:
|
||||
update_schedule_balance = LoanRepaymentScheduleService.update_repayment_schedule_balance(
|
||||
schedule_to_update.id, amount_collected
|
||||
)
|
||||
logger.info(f"Updated loan schedule balance: {update_schedule_balance}")
|
||||
LoanRepaymentScheduleService.update_repayment_schedule_description(
|
||||
schedule_to_update.id,
|
||||
message
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update repayment schedule installment: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error while handling schedule updates: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+77
-77
@@ -1,78 +1,78 @@
|
||||
from app.models import Repayment
|
||||
|
||||
class RepaymentService:
|
||||
|
||||
@staticmethod
|
||||
def get_repayment_by_transaction_id(transaction_id):
|
||||
"""
|
||||
Get the repayment by transaction ID
|
||||
"""
|
||||
return Repayment.get_repayment_by_transaction_id(transaction_id)
|
||||
@staticmethod
|
||||
def get_repayment_by_id(id):
|
||||
"""
|
||||
Get the repayment by ID
|
||||
"""
|
||||
return Repayment.get_repayment_by_id(id)
|
||||
|
||||
@classmethod
|
||||
def set_repay_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the repay status of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_repay_date(repayment_id, customer_id)
|
||||
|
||||
@classmethod
|
||||
def set_repay_verify_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the verify date of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_repay_verify_date(repayment_id, customer_id)
|
||||
|
||||
@classmethod
|
||||
def set_repay_result(cls, repayment_id, result, description):
|
||||
"""
|
||||
Update the repay result of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_repay_result(repayment_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def set_verify_date_result(cls, repayment_id, result, description):
|
||||
"""
|
||||
Update the verify result of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_verify_date_result(repayment_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def get_latest_repayment_without_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment without a repay date.
|
||||
"""
|
||||
return Repayment.get_latest_repayment_without_repay_date()
|
||||
@classmethod
|
||||
def get_latest_repayment_with_loanId(cls,loan_id):
|
||||
"""
|
||||
Get the latest repayment with loan id.
|
||||
"""
|
||||
return Repayment.get_latest_repayment_with_loanId(loan_id)
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment with a repay date and no verification date.
|
||||
"""
|
||||
return Repayment.get_latest_loan_with_repay_date()
|
||||
|
||||
@classmethod
|
||||
def add_repayment(cls, data):
|
||||
"""
|
||||
Add a new repayment entry.
|
||||
"""
|
||||
return Repayment.add_repayment(data)
|
||||
|
||||
@classmethod
|
||||
def create_repayment(cls, repayment_data):
|
||||
"""
|
||||
Add a new repayment entry.
|
||||
"""
|
||||
from app.models import Repayment
|
||||
|
||||
class RepaymentService:
|
||||
|
||||
@staticmethod
|
||||
def get_repayment_by_transaction_id(transaction_id):
|
||||
"""
|
||||
Get the repayment by transaction ID
|
||||
"""
|
||||
return Repayment.get_repayment_by_transaction_id(transaction_id)
|
||||
@staticmethod
|
||||
def get_repayment_by_id(id):
|
||||
"""
|
||||
Get the repayment by ID
|
||||
"""
|
||||
return Repayment.get_repayment_by_id(id)
|
||||
|
||||
@classmethod
|
||||
def set_repay_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the repay status of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_repay_date(repayment_id, customer_id)
|
||||
|
||||
@classmethod
|
||||
def set_repay_verify_date(cls, repayment_id, customer_id):
|
||||
"""
|
||||
Update the verify date of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_repay_verify_date(repayment_id, customer_id)
|
||||
|
||||
@classmethod
|
||||
def set_repay_result(cls, repayment_id, result, description):
|
||||
"""
|
||||
Update the repay result of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_repay_result(repayment_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def set_verify_date_result(cls, repayment_id, result, description):
|
||||
"""
|
||||
Update the verify result of the repayment with the given repayment_id.
|
||||
"""
|
||||
return Repayment.set_verify_date_result(repayment_id, result, description)
|
||||
|
||||
@classmethod
|
||||
def get_latest_repayment_without_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment without a repay date.
|
||||
"""
|
||||
return Repayment.get_latest_repayment_without_repay_date()
|
||||
@classmethod
|
||||
def get_latest_repayment_with_loanId(cls,loan_id):
|
||||
"""
|
||||
Get the latest repayment with loan id.
|
||||
"""
|
||||
return Repayment.get_latest_repayment_with_loanId(loan_id)
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_repay_date(cls):
|
||||
"""
|
||||
Get the latest repayment with a repay date and no verification date.
|
||||
"""
|
||||
return Repayment.get_latest_loan_with_repay_date()
|
||||
|
||||
@classmethod
|
||||
def add_repayment(cls, data):
|
||||
"""
|
||||
Add a new repayment entry.
|
||||
"""
|
||||
return Repayment.add_repayment(data)
|
||||
|
||||
@classmethod
|
||||
def create_repayment(cls, repayment_data):
|
||||
"""
|
||||
Add a new repayment entry.
|
||||
"""
|
||||
return Repayment.create_repayment(repayment_data)
|
||||
@@ -1,11 +1,11 @@
|
||||
from app.models import RepaymentsData
|
||||
|
||||
class RepaymentService:
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_repayment_data(cls,data):
|
||||
"""
|
||||
Add a new repayment data entry.
|
||||
"""
|
||||
from app.models import RepaymentsData
|
||||
|
||||
class RepaymentService:
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_repayment_data(cls,data):
|
||||
"""
|
||||
Add a new repayment data entry.
|
||||
"""
|
||||
return RepaymentsData.add_repayment_data(data)
|
||||
+23
-23
@@ -1,24 +1,24 @@
|
||||
from app.models import Salary
|
||||
|
||||
class SalaryService:
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_salary_data(cls,data):
|
||||
"""
|
||||
Add a new salary data entry.
|
||||
"""
|
||||
return Salary.add_salary_data(data)
|
||||
|
||||
@classmethod
|
||||
def get_pending_salaries(cls):
|
||||
"""
|
||||
Get the pending salary for a given customer.
|
||||
"""
|
||||
return Salary.get_pending_salaries()
|
||||
@classmethod
|
||||
def update_status(cls, salary_id, status):
|
||||
"""
|
||||
Update the status of the salary with the given salary_id.
|
||||
"""
|
||||
from app.models import Salary
|
||||
|
||||
class SalaryService:
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_salary_data(cls,data):
|
||||
"""
|
||||
Add a new salary data entry.
|
||||
"""
|
||||
return Salary.add_salary_data(data)
|
||||
|
||||
@classmethod
|
||||
def get_pending_salaries(cls):
|
||||
"""
|
||||
Get the pending salary for a given customer.
|
||||
"""
|
||||
return Salary.get_pending_salaries()
|
||||
@classmethod
|
||||
def update_status(cls, salary_id, status):
|
||||
"""
|
||||
Update the status of the salary with the given salary_id.
|
||||
"""
|
||||
return Salary.update_status(salary_id, status)
|
||||
@@ -1,17 +1,17 @@
|
||||
from app.models import Transaction
|
||||
|
||||
class TransactionService:
|
||||
|
||||
@staticmethod
|
||||
def get_transaction_by_transaction_id(transaction_id):
|
||||
"""
|
||||
Get the transaction by ID
|
||||
"""
|
||||
return Transaction.get_transaction_by_transaction_id(transaction_id)
|
||||
|
||||
@staticmethod
|
||||
def create_transaction(transaction_id, account_id, customer_id, type, channel):
|
||||
"""
|
||||
Create Transaction Entry
|
||||
"""
|
||||
return Transaction.create_transaction(transaction_id, account_id, customer_id, type, channel)
|
||||
from app.models import Transaction
|
||||
|
||||
class TransactionService:
|
||||
|
||||
@staticmethod
|
||||
def get_transaction_by_transaction_id(transaction_id):
|
||||
"""
|
||||
Get the transaction by ID
|
||||
"""
|
||||
return Transaction.get_transaction_by_transaction_id(transaction_id)
|
||||
|
||||
@staticmethod
|
||||
def create_transaction(transaction_id, account_id, customer_id, type, channel):
|
||||
"""
|
||||
Create Transaction Entry
|
||||
"""
|
||||
return Transaction.create_transaction(transaction_id, account_id, customer_id, type, channel)
|
||||
|
||||
+45
-45
@@ -1,45 +1,45 @@
|
||||
from app.config import settings
|
||||
import requests
|
||||
from app.utils.logger import logger
|
||||
|
||||
def get_headers():
|
||||
BANK_CALL_BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
BANK_CALL_AUTH_ENDPOINT = settings.BANK_CALL_AUTH_ENDPOINT
|
||||
BANK_CALL_BASIC_AUTH_USERNAME = settings.BANK_CALL_BASIC_AUTH_USERNAME
|
||||
BANK_CALL_BASIC_AUTH_PASSWORD = settings.BANK_CALL_BASIC_AUTH_PASSWORD
|
||||
BANK_GRANT_TYPE = settings.BANK_GRANT_TYPE
|
||||
#authenticate
|
||||
url = f"{BANK_CALL_BASE_URL}{BANK_CALL_AUTH_ENDPOINT}"
|
||||
data = {
|
||||
"grant_type": BANK_GRANT_TYPE,
|
||||
"username": BANK_CALL_BASIC_AUTH_USERNAME,
|
||||
"password": "G7$k9@pL2!qR"
|
||||
}
|
||||
logger.info(f"Calling Bank Call-Auth Endpoint: {url}")
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
response.raise_for_status() # Raises HTTPError for 4xx/5xx
|
||||
result = response.json()
|
||||
|
||||
|
||||
# Check if access_token is present
|
||||
if 'access_token' not in result:
|
||||
logger.error("No access_token found in Bank Call Auth response")
|
||||
return {"error": "Authentication failed: no access_token returned"}
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.BANK_CALL_API_KEY,
|
||||
"App-Id": settings.BANK_CALL_APP_ID,
|
||||
"Authorization": f"Bearer {result['access_token']}"
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to get auth token: {e}")
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to parse auth response JSON: {e}")
|
||||
raise
|
||||
from app.config import settings
|
||||
import requests
|
||||
from app.utils.logger import logger
|
||||
|
||||
def get_headers():
|
||||
BANK_CALL_BASE_URL = settings.BANK_CALL_BASE_URL
|
||||
BANK_CALL_AUTH_ENDPOINT = settings.BANK_CALL_AUTH_ENDPOINT
|
||||
BANK_CALL_BASIC_AUTH_USERNAME = settings.BANK_CALL_BASIC_AUTH_USERNAME
|
||||
BANK_CALL_BASIC_AUTH_PASSWORD = settings.BANK_CALL_BASIC_AUTH_PASSWORD
|
||||
BANK_GRANT_TYPE = settings.BANK_GRANT_TYPE
|
||||
#authenticate
|
||||
url = f"{BANK_CALL_BASE_URL}{BANK_CALL_AUTH_ENDPOINT}"
|
||||
data = {
|
||||
"grant_type": BANK_GRANT_TYPE,
|
||||
"username": BANK_CALL_BASIC_AUTH_USERNAME,
|
||||
"password": "G7$k9@pL2!qR"
|
||||
}
|
||||
logger.info(f"Calling Bank Call-Auth Endpoint: {url}")
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
response.raise_for_status() # Raises HTTPError for 4xx/5xx
|
||||
result = response.json()
|
||||
|
||||
|
||||
# Check if access_token is present
|
||||
if 'access_token' not in result:
|
||||
logger.error("No access_token found in Bank Call Auth response")
|
||||
return {"error": "Authentication failed: no access_token returned"}
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.BANK_CALL_API_KEY,
|
||||
"App-Id": settings.BANK_CALL_APP_ID,
|
||||
"Authorization": f"Bearer {result['access_token']}"
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to get auth token: {e}")
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to parse auth response JSON: {e}")
|
||||
raise
|
||||
|
||||
+15
-15
@@ -1,16 +1,16 @@
|
||||
def preprocess_loan_charges_data(data):
|
||||
"""
|
||||
Preprocesses the data into a dictionary for efficient lookups by 'code'.
|
||||
|
||||
Args:
|
||||
data: A list of dictionaries.
|
||||
|
||||
Returns:
|
||||
A dictionary where keys are 'code' values and values are the corresponding dictionaries from the input data.
|
||||
If multiple items have the same code, the last one encountered will be stored.
|
||||
"""
|
||||
preprocessed = {}
|
||||
for item in data:
|
||||
if 'code' in item:
|
||||
preprocessed[item['code']] = item
|
||||
def preprocess_loan_charges_data(data):
|
||||
"""
|
||||
Preprocesses the data into a dictionary for efficient lookups by 'code'.
|
||||
|
||||
Args:
|
||||
data: A list of dictionaries.
|
||||
|
||||
Returns:
|
||||
A dictionary where keys are 'code' values and values are the corresponding dictionaries from the input data.
|
||||
If multiple items have the same code, the last one encountered will be stored.
|
||||
"""
|
||||
preprocessed = {}
|
||||
for item in data:
|
||||
if 'code' in item:
|
||||
preprocessed[item['code']] = item
|
||||
return preprocessed
|
||||
+13
-13
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
# logging.StreamHandler(), # Log to console
|
||||
logging.FileHandler("app.log", mode='a') # Log to file
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger("DetectionService")
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
# logging.StreamHandler(), # Log to console
|
||||
logging.FileHandler("app.log", mode='a') # Log to file
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger("DetectionService")
|
||||
|
||||
+39
-39
@@ -1,40 +1,40 @@
|
||||
from flask_mail import Message
|
||||
from flask import current_app
|
||||
from app.extensions import mail
|
||||
import pandas as pd
|
||||
from io import BytesIO
|
||||
|
||||
def get_report_data():
|
||||
"""
|
||||
Fetch and return loan summary data.
|
||||
"""
|
||||
return [
|
||||
{"Type": "Disbursement", "Count": 45},
|
||||
{"Type": "Repayment", "Count": 32},
|
||||
]
|
||||
def send_report_email(report_data: list, recipients: list):
|
||||
"""
|
||||
Sends an HTML + Excel report to the given email recipients.
|
||||
"""
|
||||
df = pd.DataFrame(report_data)
|
||||
output = BytesIO()
|
||||
df.to_excel(output, index=False)
|
||||
output.seek(0)
|
||||
|
||||
html_table = df.to_html(index=False, border=1)
|
||||
|
||||
msg = Message(
|
||||
subject="Loan Report Summary",
|
||||
recipients=recipients,
|
||||
html=f"<h3>Loan Report Summary</h3>{html_table}",
|
||||
)
|
||||
msg.attach(
|
||||
"loan_report.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
output.read()
|
||||
)
|
||||
|
||||
with current_app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
from flask_mail import Message
|
||||
from flask import current_app
|
||||
from app.extensions import mail
|
||||
import pandas as pd
|
||||
from io import BytesIO
|
||||
|
||||
def get_report_data():
|
||||
"""
|
||||
Fetch and return loan summary data.
|
||||
"""
|
||||
return [
|
||||
{"Type": "Disbursement", "Count": 45},
|
||||
{"Type": "Repayment", "Count": 32},
|
||||
]
|
||||
def send_report_email(report_data: list, recipients: list):
|
||||
"""
|
||||
Sends an HTML + Excel report to the given email recipients.
|
||||
"""
|
||||
df = pd.DataFrame(report_data)
|
||||
output = BytesIO()
|
||||
df.to_excel(output, index=False)
|
||||
output.seek(0)
|
||||
|
||||
html_table = df.to_html(index=False, border=1)
|
||||
|
||||
msg = Message(
|
||||
subject="Loan Report Summary",
|
||||
recipients=recipients,
|
||||
html=f"<h3>Loan Report Summary</h3>{html_table}",
|
||||
)
|
||||
msg.attach(
|
||||
"loan_report.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
output.read()
|
||||
)
|
||||
|
||||
with current_app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
return "Report email sent"
|
||||
+32
-32
@@ -1,32 +1,32 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
flask:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- FLASK_APP=app.py
|
||||
- FLASK_RUN_HOST=0.0.0.0
|
||||
volumes:
|
||||
- .:/app
|
||||
restart: always
|
||||
networks:
|
||||
- digital
|
||||
|
||||
swagger:
|
||||
image: swaggerapi/swagger-ui:v5.1.0
|
||||
ports:
|
||||
- "9000:8080"
|
||||
volumes:
|
||||
- ./openapi.yml:/usr/local/openapi.yml
|
||||
environment:
|
||||
- SWAGGER_JSON=/usr/local/openapi.yml
|
||||
restart: always
|
||||
networks:
|
||||
- digital
|
||||
networks:
|
||||
digital:
|
||||
driver: bridge
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
flask:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- FLASK_APP=app.py
|
||||
- FLASK_RUN_HOST=0.0.0.0
|
||||
volumes:
|
||||
- .:/app
|
||||
restart: always
|
||||
networks:
|
||||
- digital
|
||||
|
||||
swagger:
|
||||
image: swaggerapi/swagger-ui:v5.1.0
|
||||
ports:
|
||||
- "9000:8080"
|
||||
volumes:
|
||||
- ./openapi.yml:/usr/local/openapi.yml
|
||||
environment:
|
||||
- SWAGGER_JSON=/usr/local/openapi.yml
|
||||
restart: always
|
||||
networks:
|
||||
- digital
|
||||
networks:
|
||||
digital:
|
||||
driver: bridge
|
||||
|
||||
+258
-252
@@ -1,253 +1,259 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Event Manager API
|
||||
description: The documentation for Event Manager API
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: API Support
|
||||
email: support@example.com
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
|
||||
servers:
|
||||
- url: http://localhost:5000
|
||||
description: Local development server
|
||||
- url: http://www.simbrellang.net:5000
|
||||
description: Remote Temporary development server
|
||||
- url: https://event-core.simbrellang.net
|
||||
description: Remote development server
|
||||
- url: http://10.2.249.133:5000
|
||||
description: Internal development server
|
||||
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: Returns a health message
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/status-call:
|
||||
post:
|
||||
summary: Perform a status call
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
example: "R02802"
|
||||
countryCode:
|
||||
type: string
|
||||
example: "NGR"
|
||||
transactionId:
|
||||
type: string
|
||||
example: "Tr201712RK9232P115"
|
||||
debtId:
|
||||
type: string
|
||||
example: "173021"
|
||||
transactionType:
|
||||
type: string
|
||||
example: "Disbursement"
|
||||
customerId:
|
||||
type: string
|
||||
example: "CN621868"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/sms:
|
||||
post:
|
||||
summary: Send a SMS
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
example: "This is a test message for SMS request method."
|
||||
dest:
|
||||
type: string
|
||||
example: "+2348039409144"
|
||||
unicode:
|
||||
type: boolean
|
||||
example: false
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/bulk-sms:
|
||||
post:
|
||||
summary: Send a bulk SMS
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
example: "This is a test message for SMS request method."
|
||||
dest:
|
||||
type: string
|
||||
example: "+2348039409144"
|
||||
unicode:
|
||||
type: boolean
|
||||
example: true
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-verify-disbursement:
|
||||
get:
|
||||
summary: Refresh the disbursement to verify
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-disbursement:
|
||||
get:
|
||||
summary: Refresh the disbursement
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-verify-collection:
|
||||
get:
|
||||
summary: Refresh the disbursement to verify
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-collection:
|
||||
get:
|
||||
summary: Refresh the disbursement
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/payment-callback:
|
||||
get:
|
||||
summary: The Payment callback
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/penal-charge:
|
||||
post:
|
||||
summary: Penal Charge Request
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
example: "T004"
|
||||
fbnTransactionId:
|
||||
type: string
|
||||
example: "Tr201712RK9232P115"
|
||||
debtId:
|
||||
type: string
|
||||
example: "273194670"
|
||||
customerId:
|
||||
type: string
|
||||
example: "CN621868"
|
||||
accountId:
|
||||
type: string
|
||||
example: "2017821799"
|
||||
penalCharge:
|
||||
type: number
|
||||
example: "1.2"
|
||||
lienAmount:
|
||||
type: number
|
||||
example: "101.2"
|
||||
countryId:
|
||||
type: string
|
||||
example: "01"
|
||||
comment:
|
||||
type: string
|
||||
example: "Testing PenalCharge"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/analytic-salary-detect:
|
||||
post:
|
||||
summary: Salary Detect Endpoint
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
customerId:
|
||||
type: string
|
||||
example: "CN621868"
|
||||
accountId:
|
||||
type: string
|
||||
example: "OP621868"
|
||||
status:
|
||||
type: string
|
||||
salaryAmount:
|
||||
type: number
|
||||
example: 200000
|
||||
salaryDate:
|
||||
type: string
|
||||
example: "2025-01-01"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
|
||||
/autocall/report:
|
||||
get:
|
||||
summary: Generate and send a report
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/overdue-loans:
|
||||
get:
|
||||
summary: Get all overdue loans
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/direct/loan:
|
||||
post:
|
||||
summary: Direct call for loan disbursement
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
example: "TXN123456"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/direct/repayment:
|
||||
post:
|
||||
summary: Direct call for loan repayment
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
example: "TXN123456"
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Event Manager API
|
||||
description: The documentation for Event Manager API
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: API Support
|
||||
email: support@example.com
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
|
||||
servers:
|
||||
- url: http://localhost:5000
|
||||
description: Local development server
|
||||
- url: http://www.simbrellang.net:5000
|
||||
description: Remote Temporary development server
|
||||
- url: https://event-core.simbrellang.net
|
||||
description: Remote development server
|
||||
- url: http://10.2.249.133:5000
|
||||
description: Internal development server
|
||||
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: Returns a health message
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/status-call:
|
||||
post:
|
||||
summary: Perform a status call
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
example: "R02802"
|
||||
countryCode:
|
||||
type: string
|
||||
example: "NGR"
|
||||
transactionId:
|
||||
type: string
|
||||
example: "Tr201712RK9232P115"
|
||||
debtId:
|
||||
type: string
|
||||
example: "173021"
|
||||
transactionType:
|
||||
type: string
|
||||
example: "Disbursement"
|
||||
customerId:
|
||||
type: string
|
||||
example: "CN621868"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/sms:
|
||||
post:
|
||||
summary: Send a SMS
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
example: "This is a test message for SMS request method."
|
||||
dest:
|
||||
type: string
|
||||
example: "+2348039409144"
|
||||
unicode:
|
||||
type: boolean
|
||||
example: false
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/bulk-sms:
|
||||
post:
|
||||
summary: Send a bulk SMS
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
example: "This is a test message for SMS request method."
|
||||
dest:
|
||||
type: string
|
||||
example: "+2348039409144"
|
||||
unicode:
|
||||
type: boolean
|
||||
example: true
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-verify-disbursement:
|
||||
get:
|
||||
summary: Refresh the disbursement to verify
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-disbursement:
|
||||
get:
|
||||
summary: Refresh the disbursement
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-verify-collection:
|
||||
get:
|
||||
summary: Refresh the disbursement to verify
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-collection:
|
||||
get:
|
||||
summary: Refresh the disbursement
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/payment-callback:
|
||||
get:
|
||||
summary: The Payment callback
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/penal-charge:
|
||||
post:
|
||||
summary: Penal Charge Request
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
example: "T004"
|
||||
fbnTransactionId:
|
||||
type: string
|
||||
example: "Tr201712RK9232P115"
|
||||
debtId:
|
||||
type: string
|
||||
example: "273194670"
|
||||
customerId:
|
||||
type: string
|
||||
example: "CN621868"
|
||||
accountId:
|
||||
type: string
|
||||
example: "2017821799"
|
||||
penalCharge:
|
||||
type: number
|
||||
example: "1.2"
|
||||
lienAmount:
|
||||
type: number
|
||||
example: "101.2"
|
||||
countryId:
|
||||
type: string
|
||||
example: "01"
|
||||
comment:
|
||||
type: string
|
||||
example: "Testing PenalCharge"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/analytic-salary-detect:
|
||||
post:
|
||||
summary: Salary Detect Endpoint
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
customerId:
|
||||
type: string
|
||||
example: "CN621868"
|
||||
accountId:
|
||||
type: string
|
||||
example: "OP621868"
|
||||
status:
|
||||
type: string
|
||||
salaryAmount:
|
||||
type: number
|
||||
example: 200000
|
||||
salaryDate:
|
||||
type: string
|
||||
example: "2025-01-01"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
|
||||
/autocall/report:
|
||||
get:
|
||||
summary: Generate and send a report
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/overdue-loans:
|
||||
get:
|
||||
summary: Get all overdue loans
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/process-penal-charges:
|
||||
get:
|
||||
summary: Get all overdue loans with grace period
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/direct/loan:
|
||||
post:
|
||||
summary: Direct call for loan disbursement
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
example: "TXN123456"
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/direct/repayment:
|
||||
post:
|
||||
summary: Direct call for loan repayment
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
example: "TXN123456"
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
|
||||
|
||||
+16
-16
@@ -1,16 +1,16 @@
|
||||
# Flask and Extensions
|
||||
Flask==2.3.3
|
||||
Flask-Marshmallow==0.15.0
|
||||
marshmallow==3.19.0
|
||||
Flask-Cors==3.0.10
|
||||
gunicorn
|
||||
requests
|
||||
confluent-kafka==1.9.2
|
||||
flask-sqlalchemy
|
||||
psycopg2-binary
|
||||
alembic
|
||||
python-dateutil
|
||||
oracledb
|
||||
Flask-Mail==0.10.0
|
||||
pandas==2.1.3
|
||||
openpyxl==3.1.5
|
||||
# Flask and Extensions
|
||||
Flask==2.3.3
|
||||
Flask-Marshmallow==0.15.0
|
||||
marshmallow==3.19.0
|
||||
Flask-Cors==3.0.10
|
||||
gunicorn
|
||||
requests
|
||||
confluent-kafka==1.9.2
|
||||
flask-sqlalchemy
|
||||
psycopg2-binary
|
||||
alembic
|
||||
python-dateutil
|
||||
oracledb
|
||||
Flask-Mail==0.10.0
|
||||
pandas==2.1.3
|
||||
openpyxl==3.1.5
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import threading
|
||||
from app import create_app
|
||||
from app.integrations import KafkaIntegration
|
||||
from app.config import settings
|
||||
from app.utils.logger import logger
|
||||
|
||||
app = create_app()
|
||||
kafka = KafkaIntegration()
|
||||
|
||||
def start_kafka_consumer(app):
|
||||
with app.app_context():
|
||||
logger.info("Starting Kafka consumer...")
|
||||
while True:
|
||||
try:
|
||||
|
||||
message = kafka.receive_messages(
|
||||
topics=settings.KAFKA_TOPICS, timeout=settings.KAFKA_TIMEOUT
|
||||
)
|
||||
|
||||
if message:
|
||||
logger.info(f"Processed message: {message}")
|
||||
else:
|
||||
logger.info("No message received within timeout")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while receiving message: {e}")
|
||||
|
||||
|
||||
|
||||
if __name__ != "__main__":
|
||||
|
||||
# Expose WSGI app instance for Gunicorn
|
||||
wsgi_app = app
|
||||
|
||||
# Start kafka in a thread
|
||||
import threading
|
||||
from app import create_app
|
||||
from app.integrations import KafkaIntegration
|
||||
from app.config import settings
|
||||
from app.utils.logger import logger
|
||||
|
||||
app = create_app()
|
||||
kafka = KafkaIntegration()
|
||||
|
||||
def start_kafka_consumer(app):
|
||||
with app.app_context():
|
||||
logger.info("Starting Kafka consumer...")
|
||||
while True:
|
||||
try:
|
||||
|
||||
message = kafka.receive_messages(
|
||||
topics=settings.KAFKA_TOPICS, timeout=settings.KAFKA_TIMEOUT
|
||||
)
|
||||
|
||||
if message:
|
||||
logger.info(f"Processed message: {message}")
|
||||
else:
|
||||
logger.info("No message received within timeout")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while receiving message: {e}")
|
||||
|
||||
|
||||
|
||||
if __name__ != "__main__":
|
||||
|
||||
# Expose WSGI app instance for Gunicorn
|
||||
wsgi_app = app
|
||||
|
||||
# Start kafka in a thread
|
||||
threading.Thread(target=start_kafka_consumer, args=(app,), daemon=True).start()
|
||||
Reference in New Issue
Block a user