Compare commits

...

92 Commits

Author SHA1 Message Date
VivianDee e5b2d6e7b6 Update provide_loan.py 2025-11-11 07:15:21 +01:00
CHIEFSOFT\ameye 2a184d134d eligibility clean up 2025-11-07 18:35:52 -05:00
CHIEFSOFT\ameye 5e5f1b83ad checks 2025-11-07 18:17:16 -05:00
CHIEFSOFT\ameye 2413446107 has loan 2025-11-07 18:12:08 -05:00
CHIEFSOFT\ameye ff5cbcc49d 3MPC check 2025-11-07 18:02:35 -05:00
ameye 9f2daad7c8 Merge branch 'get_active_loans_fix' of DigiFi/digifi-BankToProductCore into master 2025-11-05 14:01:35 +00:00
VivianDee 9f7435227f [fix]: Get active loans 2025-11-05 14:23:54 +01:00
CHIEFSOFT\ameye 0961a65b19 added logger 2025-11-03 12:52:01 -05:00
CHIEFSOFT\ameye 798d264748 Handle duplicate repayment attempts 2025-11-03 12:47:17 -05:00
CHIEFSOFT\ameye 5b1d867d49 aloow partial to repay 2025-11-03 10:04:15 -05:00
ameye c4b2df714c Merge branch 'loan_schedules_update' of DigiFi/digifi-BankToProductCore into master 2025-10-30 13:52:45 +00:00
VivianDee 6138085c0e Update provide_loan.py 2025-10-30 14:47:54 +01:00
VivianDee fc9f7fe175 [fix]: swagger and health check response 2025-10-30 14:47:53 +01:00
ameye 2aee3d08ed Merge branch 'bank_Call_authorization' of DigiFi/digifi-BankToProductCore into master 2025-10-27 18:48:35 +00:00
VivianDee d99640345a [fix]: swagger and health check response 2025-10-27 19:42:23 +01:00
VivianDee 6221447353 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore 2025-10-27 19:24:41 +01:00
VivianDee 54e52c639b [add]: token expiry 2025-10-27 19:24:08 +01:00
VivianDee bb85c8f166 [add]: bank call auth endpoint 2025-10-27 19:14:59 +01:00
ameye 6077c78840 Merge branch 'update_loan_status_response' of DigiFi/digifi-BankToProductCore into master 2025-10-23 10:42:23 +00:00
VivianDee 9cbc824661 [fix]: settled amount 2025-10-23 09:17:59 +01:00
VivianDee 2e08c636a2 [fix]: Current loan amount 2025-10-23 09:00:29 +01:00
VivianDee ad043ba3f8 [add]: Summary to loan status response 2025-10-23 08:52:28 +01:00
CHIEFSOFT\ameye 63da7e8292 Addded marker on name 2025-10-22 13:23:05 -04:00
CHIEFSOFT\ameye b73bf9a234 changed swagger name 2025-10-22 13:20:24 -04:00
ameye e8abb3c668 Merge branch 'add_dafaults_to_provide_loan_response' of DigiFi/digifi-BankToProductCore into master 2025-10-22 09:44:00 +00:00
VivianDee 396516b941 [update]: added interest to response and fixed defaults amounts due 2025-10-22 09:49:42 +01:00
ameye c59068d3bb Merge branch 'add_dafaults_to_provide_loan_response' of DigiFi/digifi-BankToProductCore into master 2025-10-20 16:27:04 +00:00
VivianDee ccd5b12f2c Update provide_loan.py 2025-10-20 16:57:57 +01:00
VivianDee 9913f6500c Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore 2025-10-20 16:49:11 +01:00
VivianDee 17c760981a [add]: routes and defaults 2025-10-20 16:47:47 +01:00
CHIEFSOFT\ameye 68aca1407c SELECT table_name FROM user_tables ORDER BY table_name 2025-10-17 19:48:58 -04:00
CHIEFSOFT\ameye 550895b8ef rearrange for parts 2025-10-17 19:22:29 -04:00
CHIEFSOFT\ameye b7e3527f35 added network 2025-10-16 23:49:38 -04:00
ameye 1bab78ec1a Merge branch 'add_db_uri' of DigiFi/digifi-BankToProductCore into master 2025-10-16 10:12:34 +00:00
VivianDee fe5d3fbc6e Update AuthorizeRefresh.json 2025-10-15 12:54:49 +01:00
VivianDee c516c3f52c [fix]: Provide loan charge scheduleand swagger doc 2025-10-15 12:46:13 +01:00
ameye 55140efed8 Merge branch 'add_db_uri' of DigiFi/digifi-BankToProductCore into master 2025-10-15 11:30:52 +00:00
VivianDee f8da81d564 [add]: Loan schedule on provide loan endpoint 2025-10-15 12:01:29 +01:00
ameye 0faf01bcfa Merge branch 'add_db_uri' of DigiFi/digifi-BankToProductCore into master 2025-10-10 16:52:35 +00:00
VivianDee 9da259900c [add]: DB URI 2025-10-10 16:21:28 +01:00
CHIEFSOFT\ameye 4da0b8c716 SQLALCHEMY_DATABASE_URI 2025-10-09 19:17:57 -04:00
CHIEFSOFT\ameye ddb08d8063 rem remove swagger file path 2025-10-09 16:14:30 -04:00
CHIEFSOFT\ameye 8c405c5d2e Try catch at start 2025-10-09 16:00:57 -04:00
vivian.d.simbrellang.com 4c95112bde Merge branch 'health_check' of DigiFi/digifi-BankToProductCore into master 2025-10-06 17:28:49 +00:00
VivianDee 244f648974 [add]: Events Service health check 2025-10-06 18:22:49 +01:00
ameye 324293ee96 Merge branch 'health_check' of DigiFi/digifi-BankToProductCore into master 2025-10-03 15:28:18 +00:00
VivianDee 06b266c3a7 [fix]: status 2025-10-03 15:00:37 +01:00
VivianDee ab9330bb23 [add]: Database health check 2025-10-03 14:54:56 +01:00
CHIEFSOFT\ameye f5cf4e5bdd sample config 2025-09-30 07:35:02 -04:00
CHIEFSOFT\ameye 0b78698db9 added logger 2025-09-26 06:49:41 -04:00
CHIEFSOFT\ameye d8abd0ff2a Fix URL 2025-09-26 06:35:18 -04:00
CHIEFSOFT\ameye c6acd9e73a returned eligibility amount checks 2025-09-26 06:01:31 -04:00
CHIEFSOFT\ameye 18dda45fa1 bank minimum amount 2025-09-25 10:01:36 -04:00
ameye 363c6b498a Merge branch 'direct_loan_repayment' of DigiFi/digifi-BankToProductCore into master 2025-09-13 20:25:30 +00:00
VivianDee 2a12215931 Update config.py 2025-09-12 13:13:13 +01:00
VivianDee 2f3b589420 [add]: Documentation Update 2025-09-12 13:06:41 +01:00
CHIEFSOFT\ameye f0679b8c1e http://10.2.249.133:4500/ added 2025-09-01 07:29:52 -04:00
ameye c8e21be48c Merge branch 'add-repament-status,-description-and-balance' of DigiFi/digifi-BankToProductCore into master 2025-08-26 16:28:07 +00:00
VivianDee f1e9e39fe5 Update loan_repayment_schedule.py 2025-08-26 14:01:23 +01:00
VivianDee b412313fc5 [add]: Repayment status, balance and description 2025-08-26 13:58:49 +01:00
ameye cba6eac501 Merge branch 'process_date_and_count_on_repayment_schedules' of DigiFi/digifi-BankToProductCore into master 2025-08-22 10:48:18 +00:00
VivianDee 443c6262b7 [add]: process date and count 2025-08-21 14:31:05 +01:00
CHIEFSOFT\ameye 2ba97cee4a sql full added to env sample 2025-08-14 07:15:32 -04:00
CHIEFSOFT\ameye 8c714cb45c SQL switch 2025-08-13 19:57:53 -04:00
ameye c93e1a8bdd Merge branch 'mail_implementation' of DigiFi/digifi-BankToProductCore into master 2025-07-17 10:48:35 +00:00
VivianDee 6d8fe24718 [add]: send email in base service 2025-07-16 14:24:17 +01:00
VivianDee deaddd8132 [add]: mail 2025-07-16 14:19:10 +01:00
ameye 6e08b0680f Merge branch 'oracle_migration' of DigiFi/digifi-BankToProductCore into master 2025-07-09 23:30:59 +00:00
VivianDee 3c0e00d9e4 Merge branch 'oracle_migration' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore into oracle_migration 2025-07-09 12:34:54 +01:00
VivianDee b502c128f9 [fix]: migration 2025-07-09 12:33:50 +01:00
VivianDee c81a029e20 Update 48c62b4da905_.py 2025-07-09 12:33:50 +01:00
VivianDee 469d94cea1 [add]: migration 2025-07-09 12:33:50 +01:00
VivianDee d6aabb959e [add]: MIgration to oracle database 2025-07-09 12:33:50 +01:00
VivianDee 3a970816f5 [fix]: migration 2025-07-06 09:41:44 +01:00
VivianDee 65c1608b92 Update 48c62b4da905_.py 2025-07-06 09:33:04 +01:00
VivianDee 2493c075c0 [add]: balance to loan table 2025-07-06 09:26:59 +01:00
VivianDee 4ea06de2bc [add]: migration 2025-07-03 14:56:55 +01:00
VivianDee 1aa6a1710c [add]: MIgration to oracle database 2025-07-03 05:02:08 +01:00
ameye dff000dbb2 Merge branch 'add_balance_to_loan' of DigiFi/digifi-BankToProductCore into master 2025-06-23 10:41:24 +00:00
VivianDee 617738b785 [add]: balance to loan table 2025-06-23 11:13:40 +01:00
CHIEFSOFT\ameye 9db3b68b13 ACTIVE_PARTIAL = "active_partial" 2025-06-22 21:46:44 -04:00
CHIEFSOFT\ameye 1795db35be Revert "mercore starter"
This reverts commit 8bb5ce69e2.
2025-06-22 20:14:37 -04:00
CHIEFSOFT\ameye 8bb5ce69e2 mercore starter 2025-06-22 20:11:32 -04:00
CHIEFSOFT\ameye 5087afcca6 repayment id 2025-06-21 11:27:53 -04:00
CHIEFSOFT\ameye 11b52357ba reapyment is needed 2025-06-21 11:18:36 -04:00
ameye e8361668e5 Merge branch 'salary_table_fix' of DigiFi/digifi-BankToProductCore into master 2025-06-19 09:51:38 +00:00
VivianDee f659fa9cf2 [add]: salary model fix 2025-06-19 04:42:11 +01:00
CHIEFSOFT\ameye 23e340be27 Data adjust 2025-06-18 23:00:15 -04:00
ameye adc2498044 Merge branch 'add_salary_table' of DigiFi/digifi-BankToProductCore into master 2025-06-18 12:32:12 +00:00
VivianDee eb7f783b18 [add]: Salary table 2025-06-18 12:34:28 +01:00
ameye 5040002c54 Merge branch 'update_repayments_data_table' of DigiFi/digifi-BankToProductCore into master 2025-06-17 15:11:06 +00:00
ameye 4822de764a Merge branch 'update_repayments_data_table' of DigiFi/digifi-BankToProductCore into master 2025-06-16 14:34:40 +00:00
81 changed files with 1633 additions and 2038 deletions
+1
View File
@@ -12,6 +12,7 @@ DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=dev-data.simbrellang.net
DATABASE_PORT=10532
DATABASE_NAME=firstadvancedev
#SQLALCHEMY_DATABASE_URI_FULL="oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv"
# DATABASE_HOST=10.20.30.60
# DATABASE_USER=firstadvance
+15
View File
@@ -0,0 +1,15 @@
from flask import Flask
from app.extensions import mail
from app.utils.mail import send_report_email, get_report_data
from app.config import settings
app = Flask(__name__)
app.config.from_object(settings)
mail.init_app(app)
with app.app_context():
report_data = get_report_data()
recipients = ["vdagbue@gmail.com"]
result = send_report_email(report_data, recipients)
print(result)
+40
View File
@@ -0,0 +1,40 @@
# Environment Variables ======================================================
BASIC_AUTH_USERNAME=user
BASIC_AUTH_PASSWORD=password
SWAGGER_URL="/documentation"
API_URL="/swagger.json"
# Flask Configuration =========================================================
FLASK_APP=wsgi.py
FLASK_ENV=development
APP_PORT=4500
#Database Configuration =======================================================
DATABASE_USER=FIRSTADVSTG
DATABASE_PASSWORD=Pchanged_56789
DATABASE_HOST=ig-x6-uat-scan
DATABASE_PORT=1521
DATABASE_NAME=FIRSTADVSTG
DATABASE_SID=firstadv
SQLALCHEMY_DATABASE_URI_FULL="oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv"
# Event Bus =====================================================================
KAFKA_BROKER="10.2.110.20:9082"
#Bank Calls =====================================================================
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
SIMBRELLA_APP_ID="app1"
SIMBRELLA_API_KEY="testtest-api-key-12345"
#Events Direct Location =========================================================
EVENTS_SERVICE_BASE_URL="http://10.2.24.133:5000"
ENDPOINT_DIRECT_LOAN="/autocall/direct/loan"
ENDPOINT_DIRECT_REPAYMENT="/autocall/direct/repayment"
#EVENTS_SERVICE_BASE_URL2="https://event-core.simbrellang.net"
#EVENTS_SERVICE_BASE_URL="http://10.10.11.17:14700"
+32 -17
View File
@@ -1,4 +1,5 @@
from flask import Flask
from flask_mail import Mail
import os
from flask_swagger_ui import get_swaggerui_blueprint
from flask_cors import CORS
@@ -7,7 +8,7 @@ from app.api.routes import api
from app.errors import register_error_handlers
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from app.extensions import db, migrate
from app.extensions import db, migrate, mail
from flask_jwt_extended import (
JWTManager,
jwt_required,
@@ -18,34 +19,48 @@ from flask_jwt_extended import (
def create_app():
"""Factory function to create a Flask app instance"""
# import oracledb
# oracledb.init_oracle_client(lib_dir=None)
app = Flask(__name__)
# Load configuration
app.config.from_object(Config)
CORS(app)
JWTManager(app)
CORS(app, supports_credentials=True)
CORS(app, supports_credentials=True)
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL")
# Register blueprints
app.register_blueprint(api)
try:
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL")
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
# Register blueprints
app.register_blueprint(api)
# Error Handlers
register_error_handlers(app)
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
from . import models
except Exception as e:
print(f"Swagger Unexpected error occurred: {e}")
# Database and Migrations
db.init_app(app)
migrate.init_app(app, db)
try:
# Error Handlers
register_error_handlers(app)
return app
from . import models
# Initialize Flask-Mail
mail.init_app(app)
# Database and Migrations
db.init_app(app)
migrate.init_app(app, db)
return app
except Exception as e:
print(f"An unexpected error occurred: {e}")
+1
View File
@@ -3,5 +3,6 @@ from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
ACTIVE_PARTIAL = "active_partial"
START_REPAY = "start_repay"
REPAID = "repaid"
+2 -1
View File
@@ -1,2 +1,3 @@
from .simbrella import SimbrellaIntegration
from .kafka import KafkaIntegration
from .kafka import KafkaIntegration
from .events_service import EventServiceIntegration
+69
View File
@@ -0,0 +1,69 @@
import httpx
from app.utils.logger import logger
from app.config import settings
class EventServiceIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
EVENTS_SERVICE_BASE_URL = settings.EVENTS_SERVICE_BASE_URL
ENDPOINT_DIRECT_LOAN = settings.ENDPOINT_DIRECT_LOAN
ENDPOINT_DIRECT_REPAYMENT = settings.ENDPOINT_DIRECT_REPAYMENT
@staticmethod
def direct_loan(transaction_id: str):
"""
Calls the Direct Loan endpoint
"""
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_LOAN}"
logger.info(f"Direct Loan URL: {url}")
payload = {"transactionId": str(transaction_id)}
headers = {
"Content-Type": "application/json"
}
try:
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.info(f"Loan Response: {response.text}")
return response
except Exception as e:
logger.error(f"Direct Loan API call failed: {str(e)}", exc_info=True)
raise
@staticmethod
def direct_repayment(transaction_id: str):
"""
Calls the Direct Repayment endpoint
"""
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_REPAYMENT}"
logger.info(f"Direct Repayment URL: {url}")
payload = {"transactionId": str(transaction_id)}
headers = {
"Content-Type": "application/json",
}
try:
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.info(f"Repayment Response: {response.text}")
return response
except Exception as e:
logger.error(f"Direct Repayment API call failed: {str(e)}", exc_info=True)
raise
@staticmethod
def health_check():
"""
Health check for Events Service
"""
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}/health"
logger.info(f"Health Check URL: {url}")
try:
response = httpx.get(url, timeout=5.0)
logger.info(f"Health Check Response: {response.text}")
return response
except Exception as e:
logger.error(f"Health Check API call failed: {str(e)}", exc_info=True)
raise
+83 -8
View File
@@ -1,13 +1,64 @@
from os import access
import httpx
import json
import time
from app.utils.logger import logger
from app.config import settings
import logging
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
HEALTH_ENDPOINT = settings.SIMBRELLA_HEALTH
AUTH_ENDPOINT = settings.BANK_CALL_AUTH_ENDPOINT
_access_token = None # cache token in memory
_token_expiry = 0
@staticmethod
def generate_token():
"""
Generate a new access token using the username and password from settings.
"""
url = f"{SimbrellaIntegration.BASE_URL}{SimbrellaIntegration.AUTH_ENDPOINT}"
payload = {
"username": settings.BANK_CALL_USERNAME,
"password": settings.BANK_CALL_PASSWORD,
"grant_type": "password"
}
headers = {"Content-Type": "application/json"}
try:
logger.info(f"Requesting Bank token from {url}")
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
response.raise_for_status()
data = response.json()
expires_in = data.get("expires_in", 1800)
SimbrellaIntegration._access_token = data.get("access_token")
SimbrellaIntegration._token_expiry = time.time() + expires_in - 60
if not SimbrellaIntegration._access_token:
raise Exception("Access token not found in Bank Authorization response")
logger.info("Successfully retrieved Bank access token")
return SimbrellaIntegration._access_token
except Exception as e:
logger.error(f"Token generation failed: {str(e)}", exc_info=True)
raise Exception(f"Token generation failed: {str(e)}")
@staticmethod
def _get_token():
"""
Return a valid token, refreshing if expired or missing
"""
if not SimbrellaIntegration._access_token or time.time() >= SimbrellaIntegration._token_expiry:
return SimbrellaIntegration.generate_token()
return SimbrellaIntegration._access_token
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
@@ -26,13 +77,14 @@ class SimbrellaIntegration:
"channel": "USSD"
}
headers = {
"Content-Type": "application/json",
"x-api-key": f"{settings.VALID_API_KEY}",
"App-Id": f"{settings.VALID_APP_ID}",
}
try:
access_token = SimbrellaIntegration._get_token()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.info(f"This is Response: {str(response)}", exc_info=True)
@@ -42,4 +94,27 @@ class SimbrellaIntegration:
except Exception as e:
logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True)
raise Exception(f"RACCheck API call failed: {str(e)}")
@staticmethod
def health_check():
"""
Health check for Bank Service
"""
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.HEALTH_ENDPOINT}"
logger.info(f"Bank Health Check URL: {url}")
try:
access_token = SimbrellaIntegration._get_token()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
response = httpx.get(url, headers=headers, timeout=10.0)
logger.info(f"Bank Health Check Response: {response.text}")
return response
except Exception as e:
logger.error(f"Bank Health Check API call failed: {str(e)}", exc_info=True)
raise Exception(f"Bank Health Check API call failed: {str(e)}")
+73 -2
View File
@@ -1,3 +1,5 @@
from app.api.integrations.events_service import EventServiceIntegration
from app.api.integrations.simbrella import SimbrellaIntegration
from flask import Blueprint, request, jsonify, send_from_directory
from app.api.services import (
EligibilityCheckService,
@@ -19,6 +21,9 @@ from flask_jwt_extended import (
get_jwt_identity,
create_refresh_token,
)
from sqlalchemy import text
from app.extensions import db
from app.config import settings
api = Blueprint("api", __name__)
@@ -113,11 +118,77 @@ def notification_callback():
response = NotificationCallbackService.process_request(data)
return response
# Health Check Endpoint
@api.route("/health", methods=["GET"])
def health_check():
return {"status": "ok"}, 200
SQLALCHEMY_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI
response = {}
db_status = "Connection Successful"
events_service_status = "Connection Successful"
bank_status = "Connection Successful"
errors = []
status = "ok"
# Extract the database URI
try:
db_uri = db.engine.url.render_as_string(hide_password=False)
db_uri = db_uri
except Exception as e:
db_uri = "Unavailable"
# Check database connection
try:
logger.info(f"Database Health == : {SQLALCHEMY_DATABASE_URI}")
db.session.execute(text("SELECT table_name FROM user_tables ORDER BY table_name"))
except Exception as e:
db_status = "Connection Failed"
errors.append(f"Database Error: {str(e)}")
status = "failed"
# Check Events Service health
try:
events_service_response = EventServiceIntegration.health_check()
if events_service_response.status_code != 200:
events_service_status = "Connection Failed"
status = "failed"
errors.append(f"Events Service response: {events_service_response.text}")
except Exception as e:
events_service_status = "Connection Failed"
status = "failed"
errors.append(f"Events Service connection failed: {str(e)}")
# Check Bank health
try:
emulator_response = SimbrellaIntegration.health_check()
if emulator_response.status_code != 200:
bank_status = "Connection Failed"
status = "failed"
errors.append(f"Bank Connection response: {emulator_response.text}")
except Exception as e:
bank_status = "Connection Failed"
status = "failed"
errors.append(f"Connection to Bank failed: {str(e)}")
response = {
"status": status,
"db_status": db_status,
"events_service_status": events_service_status,
"bank_status": bank_status,
"db_uri": db_uri,
"errors": errors or None
}
return jsonify(response), 200 if status == "ok" else 500
# Authorize endpoint
+4
View File
@@ -4,6 +4,7 @@ from flask import jsonify
from marshmallow import ValidationError
import logging
from app.api.integrations import KafkaIntegration
from app.utils.mail import send_report_email, get_report_data
logger = logging.getLogger(__name__)
@@ -173,3 +174,6 @@ class BaseService:
# return {"rate": 0, "fee": 0, "due_days": 0}
@classmethod
def send_mail(cls, report_data, recipients):
send_report_email(report_data, recipients)
+13 -1
View File
@@ -51,6 +51,13 @@ class EligibilityCheckService(BaseService):
db.session.flush()
# Determine if there is any loan of 3MPC active
current_loan = EligibilityCheckService.get_current_active_loans_by_account_id(account_id = account_id)
if current_loan:
logger.info(f"Account {current_loan.account_id} has active loan {current_loan}")
if current_loan.product_id =='3MPC':
return ResponseHelper.error(result_description="Max loan count for 3MPC reached")
# Determine Loan count
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
@@ -167,7 +174,12 @@ class EligibilityCheckService(BaseService):
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
@staticmethod
def get_current_active_loans_by_account_id(account_id):
current_loan = Loan.get_current_active_loans_by_account_id(account_id)
return current_loan
@staticmethod
def check_loan_limits(customer_id):
+55 -22
View File
@@ -3,15 +3,15 @@ from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.models import Customer
from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.api.enums import TransactionType
from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class LoanStatusService(BaseService):
TRANSACTION_TYPE = TransactionType.LOAN_STATUS
TRANSACTION_TYPE = TransactionType.LOAN_STATUS
@staticmethod
def process_request(data):
@@ -27,31 +27,57 @@ class LoanStatusService(BaseService):
try:
with db.session.begin():
# Validate data
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
validated_data = LoanStatusService.validate_data(
data, LoanStatusSchema()
)
customer_id = validated_data.get('customerId')
customer_id = validated_data.get("customerId")
logger.info(f"Looking for customer *** {customer_id}")
customer = Customer.get_customer_with_loan_list(customer_id)
transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId')
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transactionId = validated_data.get("transactionId")
account_id = validated_data.get("accountId")
if LoanStatusService.validate_account_ownership(
account_id=account_id, customer_id=customer_id
):
# Get loans
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
customer_loans = customer.loans
loans = [
loan.to_dict()
for loan in customer_loans
if loan.status in [LoanStatus.ACTIVE, LoanStatus.START_REPAY, LoanStatus.ACTIVE_PARTIAL]
]
transaction = LoanStatusService.log_transaction(
validated_data=validated_data
)
if not transaction:
logger.error(f"Failed to log transaction")
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0
for loan in loans
return ResponseHelper.error(
result_description="Failed to log transaction."
)
else:
return ResponseHelper.error(
result_description="Invalid Customer or Account"
)
# CONFIRM IF THE TOTAL DEBT IF FOR ONLY ACTIVE LOANS OR ALL LOANS
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0 for loan in loans
)
total_outstanding_amount = sum(
loan.get("currentLoanAmount") or 0 for loan in loans
)
total_active_loan_amount = sum(
loan.get("repaymentAmount") or 0 for loan in loans
)
total_settled_amount = total_active_loan_amount - total_outstanding_amount
# Simulated processing logic
response_data = {
"customerId": customer_id,
@@ -59,6 +85,11 @@ class LoanStatusService(BaseService):
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": total_debt_amount,
"summary": {
"totalSettledAmount": total_settled_amount,
"totalOutstandingAmount": total_outstanding_amount,
"totalActiveLoanAmount": total_active_loan_amount,
}
}
db.session.commit()
@@ -68,9 +99,11 @@ class LoanStatusService(BaseService):
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
return ResponseHelper.unprocessable_entity(
result_description="Validation exception"
)
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
@@ -78,4 +111,4 @@ class LoanStatusService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
return ResponseHelper.internal_server_error()
+10 -2
View File
@@ -40,6 +40,7 @@ class OfferAnalysis:
original_transaction = transaction_id
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response, offer):
logger.info(f"This is PayLoad for ANALYSYS ***** : {str(rack_response)}", exc_info=True)
@@ -93,7 +94,7 @@ class OfferAnalysis:
logger.info(f"These are the salarie amounts ***** : {str(salaries)}", exc_info=True)
logger.info(f"These are the salary amounts ***** : {str(salaries)}", exc_info=True)
#Least salary in the last 6 months
min_salary = min(salaries)
@@ -113,7 +114,7 @@ class OfferAnalysis:
else: # Income is not consistent
eligible_amount = 0
logger.info("Applying np percentage on least salary due unstable income.")
logger.info("Applying no percentage on least salary due unstable income.")
@@ -181,6 +182,10 @@ class OfferAnalysis:
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
# if real_eligible_amount < 100:
# logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
# raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
@@ -222,6 +227,9 @@ class OfferAnalysis:
logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
# if approved_amount < 100:
# logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
# raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
+108 -12
View File
@@ -1,3 +1,4 @@
from gettext import install
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.integrations.kafka import KafkaIntegration
@@ -11,11 +12,13 @@ from threading import Thread
from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck
from app.api.enums import LoanStatus
from app.extensions import db
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from dateutil.relativedelta import relativedelta
from app.api.integrations import EventServiceIntegration
from app.models import LoanRepaymentSchedule
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
from datetime import datetime
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -111,9 +114,12 @@ class ProvideLoanService(BaseService):
management = charges["management"]
insurance = charges["insurance"]
vat = charges["vat"]
# Generate Loan Reference
loan_ref = f"SIM{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
padded_id = str(transaction_id).zfill(12)
loan_ref = f"{padded_id}{channel}{offer.product_id}"
# padded_id = str(transaction_id).zfill(12)
# loan_ref = f"{padded_id}{channel}{offer.product_id}"
# Save the loan details
@@ -142,10 +148,10 @@ class ProvideLoanService(BaseService):
db.session.flush()
current_product_id = offer.product_id
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
schedules = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedule:
if not schedules:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
@@ -157,26 +163,37 @@ class ProvideLoanService(BaseService):
loan_charges = LoanCharge.create_charges_for_loan(loan_id = loan_id, transaction_id = transaction_id, referenced_amount = 800, charges = charges)
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
charge_schedule_items = ProvideLoanService.get_charge_schedule_items(
loan_charges=loan_charges,
offer=offer,
loan_ref=loan_ref,
amount=amount,
schedules=schedules
)
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"loanRef": loan_ref,
"customerId": customer_id,
"accountId": account_id,
"msisdn": customer.msisdn
"msisdn": customer.msisdn,
"schedule": charge_schedule_items,
}
event_thread = Thread(target=ProvideLoanService.trigger_loan_disbursement, args=(transaction_id,))
event_thread.start()
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# Call Kafka in a background thread
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start()
kafka_thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
kafka_thread.start()
db.session.commit()
return ResponseHelper.success(data=response_data)
@@ -195,4 +212,83 @@ class ProvideLoanService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
return ResponseHelper.internal_server_error()
@classmethod
def trigger_loan_disbursement(cls, transaction_id: str):
response = EventServiceIntegration.direct_loan(transaction_id=transaction_id)
return response
@classmethod
def get_charge_schedule_items(cls, loan_charges, offer, loan_ref, amount, schedules):
now = datetime.now(timezone.utc)
due_date = now + timedelta(days=offer.tenor)
id_counter = 1
charge_schedule_items = []
charge_schedule_items.append({
"id": id_counter,
"dueDate": due_date.isoformat(),
"amountDue": amount,
"componentName": "PRINCIPAL",
"startDate": now.isoformat(),
})
interest_amount = 0.0
for charge in loan_charges:
code = charge.code.upper()
if code == "INTEREST":
interest_amount = float(charge.amount)
continue
item = {
"id": id_counter,
"dueDate": charge.due_date.isoformat(),
"amountDue": float(charge.amount),
"componentName": charge.code.upper(), # e.g. INTEREST, MGMT_FEE, VAT_FEE
"startDate": charge.created_at.isoformat(),
}
charge_schedule_items.append(item)
id_counter += 1
num_schedules = len(schedules)
if num_schedules > 0:
principal_per_schedule = amount / num_schedules
else:
principal_per_schedule = 0.0
for schedule in schedules:
default = {
"id": id_counter,
"installmentNo": schedule.installment_number,
"dueDate": schedule.due_date.isoformat(),
"amountDue": round(principal_per_schedule, 2),
"componentName": "DEFAULT",
"startDate": schedule.created_at.isoformat(),
}
charge_schedule_items.append(default)
id_counter += 1
interest = {
"id": id_counter,
"dueDate": schedule.due_date.isoformat(),
"amountDue": round(interest_amount, 2),
"componentName": "INTEREST",
"startDate": schedule.created_at.isoformat()
}
charge_schedule_items.append(interest)
id_counter += 1
return charge_schedule_items
+18 -3
View File
@@ -11,6 +11,7 @@ from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from threading import Thread
from app.extensions import db
from app.api.integrations import EventServiceIntegration
class RepaymentService(BaseService):
TRANSACTION_TYPE = TransactionType.REPAYMENT
@@ -56,7 +57,7 @@ class RepaymentService(BaseService):
return ResponseHelper.error(result_description="Failed to save repayment details.")
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started by user
transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction:
@@ -67,7 +68,11 @@ class RepaymentService(BaseService):
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Simulated processing logic
# TODO start using repayment_id instead if id or Id
response_data = {
"Id": repayment.id,
"repayment_id": repayment.id,
"initiated_by": repayment.initiated_by,
"transactionId": transaction_id,
"customerId": customer_id,
"productId": loan.product_id,
@@ -75,9 +80,12 @@ class RepaymentService(BaseService):
"debtId": loan_id
}
event_thread = Thread(target=RepaymentService.trigger_loan_repayment, args=(transaction_id,))
event_thread.start()
# Call Kafka in a background thread
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start()
kafka_thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
kafka_thread.start()
db.session.commit()
return ResponseHelper.success(data=response_data)
@@ -97,3 +105,10 @@ class RepaymentService(BaseService):
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
@classmethod
def trigger_loan_repayment(cls, transaction_id: str):
response = EventServiceIntegration.direct_repayment(transaction_id=transaction_id)
return response
+1
View File
@@ -38,6 +38,7 @@ class SelectOfferService(BaseService):
transaction_id = validated_data.get("transactionId")
request_id = validated_data.get("requestId")
offer_id = int(transaction_offer_id[5:]) # The last part is int
#"offerId": "SAL30001129",
+31 -3
View File
@@ -16,10 +16,19 @@ class Config:
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME")
DATABASE_NAME = os.environ.get("DATABASE_NAME", "firstadvancedev")
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})))"
# Database Connection
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
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_FULL = 'oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv'
SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -37,6 +46,17 @@ class Config:
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "test-api-key-12345")
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
SIMBRELLA_HEALTH = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/system-health-check")
BANK_CALL_AUTH_ENDPOINT = os.getenv("BANK_CALL_AUTH_ENDPOINT", "/api/Auth/generate-token")
BANK_CALL_USERNAME = os.getenv("BANK_CALL_USERNAME", "simbrella")
BANK_CALL_PASSWORD = os.getenv("BANK_CALL_PASSWORD", "G7$k9@pL2!qR")
EVENTS_SERVICE_BASE_URL = os.getenv("EVENTS_SERVICE_BASE_URL","https://event-core.simbrellang.net")
ENDPOINT_DIRECT_LOAN = os.getenv("ENDPOINT_DIRECT_LOAN","/autocall/direct/loan")
ENDPOINT_DIRECT_REPAYMENT = os.getenv("ENDPOINT_DIRECT_REPAYMENT","/autocall/direct/repayment")
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
@@ -79,7 +99,15 @@ class Config:
"salarypaymenT_6"
]
MAIL_SERVER = os.getenv('MAIL_SERVER','smtp.zoho.com')
MAIL_PORT = 587
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'firstadvance@dynamikservices.tech')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'vdagbue@gmail.com')
settings = Config()
+2
View File
@@ -1,5 +1,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail
mail = Mail()
db = SQLAlchemy()
migrate = Migrate()
+2 -1
View File
@@ -10,6 +10,7 @@ from .rac_checks import RACCheck
from .loan_repayment_schedule import LoanRepaymentSchedule
from .transaction_offers import TransactionOffer
from .repayments_data import RepaymentsData
from .salary import Salary
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData']
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData', 'Salary']
+26 -4
View File
@@ -1,5 +1,6 @@
from datetime import datetime, timezone, timedelta
from itertools import product
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from app.models.customer import Customer
from app.models.account import Account
@@ -35,6 +36,7 @@ class Loan(db.Model):
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)
@@ -112,6 +114,7 @@ class Loan(db.Model):
current_loan_amount = initial_loan_amount,
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
balance = repayment_amount,
installment_amount = installment_amount,
due_date=due_date,
tenor = tenor,
@@ -190,13 +193,32 @@ class Loan(db.Model):
Get all active loans with the same original_transaction ID.
"""
active_loans = cls.query.filter_by(
original_transaction=original_transaction_id,
# status='active'
active_loans = cls.query.filter(
cls.original_transaction == original_transaction_id,
or_(
cls.status == LoanStatus.ACTIVE.value,
cls.status == LoanStatus.START_REPAY.value,
cls.status == LoanStatus.ACTIVE_PARTIAL.value,
)
).all()
return active_loans
@classmethod
def get_current_active_loans_by_account_id(cls, account_id):
"""
Get the first active loan based on the accountID.
"""
first_active_loan = cls.query.filter(
cls.account_id == account_id,
or_(
cls.status == LoanStatus.ACTIVE.value,
cls.status == LoanStatus.START_REPAY.value,
cls.status == LoanStatus.ACTIVE_PARTIAL.value,
)
).order_by(cls.id.desc()).first()
return first_active_loan
@classmethod
def update_status(cls, loan_id, status):
@@ -244,7 +266,7 @@ class Loan(db.Model):
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount,
'currentLoanAmount': self.balance,
'defaultPenaltyFee': self.default_penalty_fee,
'continuousFee': self.continuous_fee,
'collectionType': self.collection_type,
+5
View File
@@ -17,6 +17,11 @@ class LoanRepaymentSchedule(db.Model):
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(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
+2 -2
View File
@@ -7,8 +7,8 @@ from sqlalchemy.sql import func
class Offer(db.Model):
__tablename__ = 'offers'
id = db.Column(db.String, primary_key=True)
product_id = db.Column(db.String, nullable=False)
id = db.Column(db.String(50), primary_key=True)
product_id = db.Column(db.String(50), nullable=False)
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
tenor = db.Column(db.Integer, nullable=False)
+18 -5
View File
@@ -3,6 +3,7 @@ from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.exc import IntegrityError
from uuid import uuid4
import json
from sqlalchemy.types import JSON
from sqlalchemy.sql import func
@@ -11,11 +12,22 @@ class RACCheck(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String, nullable=False)
account_id = db.Column(db.String, nullable=False)
rac_response = db.Column(db.JSON, nullable=False)
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=False)
rac_response = db.Column(db.Text, nullable=False)
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())
@property
def rac_response_data(self):
return json.loads(self.rac_response)
@rac_response_data.setter
def rac_response_data(self, value):
self.rac_response = json.dumps(value)
@classmethod
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
@@ -25,10 +37,11 @@ class RACCheck(db.Model):
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction_id,
rac_response = data,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
rac_check.rac_response_data = data or {}
try:
db.session.add(rac_check)
@@ -66,7 +79,7 @@ class RACCheck(db.Model):
"transactionId": str(self.transaction_id),
"customerId": self.customer_id,
"accountId": self.account_id,
"racResponse": self.rac_response,
"racResponse": self.rac_response_data,
"createdAt": self.created_at.isoformat(),
"updatedAt": self.updated_at.isoformat() if self.updated_at else None
}
+34 -5
View File
@@ -21,20 +21,23 @@ class Repayment(db.Model):
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())
transaction_id = db.Column(db.String(50), nullable=True)
repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
# repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
repay_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_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
# verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
verify_date = db.Column(db.DateTime, nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
initiated_by = db.Column(db.String(50), nullable=True)
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
@classmethod
def create_repayment(cls, customer_id, loan, transaction_id):
# Check that the loan is active
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY, LoanStatus.ACTIVE_PARTIAL]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
@@ -42,7 +45,8 @@ class Repayment(db.Model):
product_id=loan.product_id,
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
updated_at=datetime.now(timezone.utc),
initiated_by='USER_INITIATED'
)
try:
@@ -52,5 +56,30 @@ class Repayment(db.Model):
return repayment
# Get loan repayments
@classmethod
def get_repayments_by_id(cls, loan_id):
return cls.query.filter_by(loan_id=loan_id).all()
def to_dict(self):
return {
"id": self.id,
"loan_id": self.loan_id,
"customer_id": self.customer_id,
"product_id": self.product_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"transaction_id": self.transaction_id,
"repay_date": self.repay_date.isoformat() if self.repay_date else None,
"repay_result": self.repay_result,
"repay_description": self.repay_description,
"verify_date": self.verify_date.isoformat() if self.verify_date else None,
"verify_result": self.verify_result,
"verify_description": self.verify_description,
"initiated_by": self.initiated_by,
"salary_amount": self.salary_amount
}
def __repr__(self):
return f'<Repayment {self.id}>'
+2
View File
@@ -15,6 +15,7 @@ class RepaymentsData(db.Model):
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)
balance = db.Column(db.Float, nullable=True, default=0.0)
def to_dict(self):
return {
@@ -28,6 +29,7 @@ class RepaymentsData(db.Model):
"added_date": self.added_date.isoformat() if self.added_date else None,
"response_code": self.response_code,
"response_descr": self.response_descr,
"balance": self.balance,
}
def __repr__(self):
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.sql import func
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=True)
status = db.Column(db.String(20), default='active')
amount = db.Column(db.Float, nullable=False, default=0.0)
salary_date = db.Column(db.DateTime(timezone=False), server_default=func.now())
created_at = db.Column(db.DateTime(timezone=False), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
def to_dict(self):
return {
"id": self.id,
"customer_id": self.customer_id,
"account_id": self.account_id,
"status": self.status,
"amount": self.amount,
"salary_date": self.salary_date.isoformat() if self.salary_date 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'<Salary {self.id} - {self.amount}>'
+21 -11
View File
@@ -4,6 +4,12 @@ from app.models import account
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
from app.api.enums import TransactionType
import logging
logger = logging.getLogger(__name__)
class Transaction(db.Model):
__tablename__ = 'transactions'
@@ -20,7 +26,7 @@ class Transaction(db.Model):
phone_number = db.Column(db.String(50), 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())
def __repr__(self):
return f'<Transaction {self.id}>'
@@ -30,17 +36,21 @@ class Transaction(db.Model):
# if cls.query.filter_by(transaction_id=transaction_id).first():
# raise ValueError("Duplicate Transaction")
if cls.query.filter( and_( cls.transaction_id ==transaction_id, cls.type==type) ).first():
raise ValueError("Duplicate Transaction")
if cls.query.filter(and_(cls.transaction_id == transaction_id, cls.type == type)).first():
if type == TransactionType.REPAYMENT:
logger.info('Repayment transaction already exists :::: But we like to continue.')
now = datetime.now()
type = TransactionType.REPAYMENT + '.'+ now.strftime("%Y%m%d%H%M%S")
logger.info('Modify Type :::: {0}'.format(type))
else:
raise ValueError("Duplicate Transaction")
transaction = cls(
transaction_id = transaction_id,
customer_id = customer_id,
account_id = account_id,
type = type,
channel = channel,
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)
)
@@ -54,4 +64,4 @@ class Transaction(db.Model):
@classmethod
def get_transaction_by_id(cls, transaction_id):
return cls.query.get(transaction_id)
return cls.query.get(transaction_id)
+49 -15
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.3",
"info": {
"title": "Swagger Bank Channel to Simbrella FirstAdvance - OpenAPI 3.0",
"title": "Swagger Simbrella Core [10/22/2025] - OpenAPI 3.0",
"description": "This is a Simbrella FirstAdvance Backend Server with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
"termsOfService": "http://swagger.io/terms/",
"contact": {
@@ -20,12 +20,19 @@
{
"url": "http://api.dev.simbrellang.net:4500"
},
{
{
"url": "https://api.dev.simbrellang.net"
},
{
"url": "http://10.2.249.133:4500"
}
],
"tags": [
{
{
"name": "Health",
"description": "System health check including DB status."
},
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
@@ -83,6 +90,45 @@
}
],
"paths": {
"/health": {
"get": {
"tags": ["Health"],
"summary": "Health Check",
"description": "Returns service health information including DB connection status.",
"responses": {
"200": {
"description": "Health check successful",
"content": {
"application/json": {
"example": {
"status": "ok",
"db_status": "Connection Successful",
"events_service_status":"Connection Successful",
"bank_status":"Connection Successful",
"db_uri": "postgresql://user:****@localhost:5432/digifi_db",
"error": []
}
}
}
},
"500": {
"description": "Health check failed",
"content": {
"application/json": {
"example": {
"status": "failed",
"db_status": "Connection Failed",
"events_service_status":"Connection Failed",
"bank_status":"Connection Failed",
"db_uri": "Unavailable",
"error":["could not connect to server: Connection refused"]
}
}
}
}
}
}
},
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
@@ -131,18 +177,6 @@
"RepaymentResponse": {
"$ref": "swagger/schemas/RepaymentResponse.json"
},
"CustomerConsentRequest": {
"$ref": "swagger/schemas/CustomerConsentRequest.json"
},
"CustomerConsentResponse": {
"$ref": "swagger/schemas/CustomerConsentResponse.json"
},
"NotificationCallbackRequest": {
"$ref": "swagger/schemas/NotificationCallbackRequest.json"
},
"NotificationCallbackResponse": {
"$ref": "swagger/schemas/NotificationCallbackResponse.json"
},
"ApiResponse": {
"$ref": "swagger/schemas/ApiResponse.json"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"post": {
"tags": ["Authorize Refresh"],
"tags": ["AuthorizeRefresh"],
"summary": "Customer Authorize Refresh Request",
"description": "Customer Authorize Refresh Request",
"operationId": "AuthorizeRefresh",
-56
View File
@@ -1,56 +0,0 @@
{
"post": {
"tags": [
"CustomerConsent"
],
"summary": "Customer Consent Request",
"description": "Customer Consent Request",
"operationId": "CustomerConsent",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/CustomerConsentResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/CustomerConsentResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
@@ -1,57 +0,0 @@
{
"post": {
"tags": [
"NotificationCallback"
],
"summary": "Loan Information Request ",
"description": "Loan Information Request",
"operationId": "NotificationCallback",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/NotificationCallbackResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/NotificationCallbackResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
@@ -1,37 +0,0 @@
{
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "CustomerConsentRequest"
},
"transactionId": {
"type": "string",
"example": "20171209232177"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"requestTime": {
"type": "string",
"format": "date-time",
"example": "2019-10-18 14:26:21.063"
},
"consentType": {
"type": "string",
"example": "Revoke"
},
"channel": {
"type": "string",
"example": "USSD"
}
},
"xml": {
"name": "CustomerConsentRequest"
}
}
@@ -1,16 +0,0 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Request is received"
}
},
"xml": {
"name": "CustomerConsentResponse"
}
}
@@ -3,7 +3,7 @@
"properties": {
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
"example": "TRX201712RK9232P115"
},
"countryCode": {
"type": "string",
@@ -11,19 +11,19 @@
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "8093451342"
},
"channel": {
"type": "string",
"example": "100"
"example": "5268548"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
"example": "4348094226"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
},
"channel": {
"type": "string",
"example": "USSD"
}
},
"xml": {
@@ -3,19 +3,19 @@
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
"example": "5268548"
},
"transactionId": {
"type": "string",
"example": "TX12345"
"example": "TRX201712RK9232P115"
},
"countryCode": {
"type": "string",
"example": "NG"
"example": "NGR"
},
"msisdn": {
"type": "string",
"example": "3451342"
"example": "2348093451342"
},
"eligibleOffers": {
"type": "array",
@@ -24,42 +24,42 @@
"properties": {
"offerId": {
"type": "string",
"example": "Offer1"
"example": "SAL90000204"
},
"productId": {
"type": "string",
"example": "Product1"
"example": "3MPC"
},
"minAamount": {
"type": "number",
"format": "decimal",
"example": 100.00
"example": 20000.00
},
"maxAamount": {
"type": "number",
"format": "decimal",
"example": 1000.00
"example": 31257.00
},
"tenor": {
"type": "integer",
"example": 12
"example": 90
}
}
},
"example": [
{
"offerId": "Offer1",
"productId": "Product1",
"minAamount": 100.00,
"maxAamount": 1000.00,
"tenor": 12
{
"max_amount": "31257.00",
"min_amount": 20000.0,
"offerId": "SAL90000204",
"product_id": "3MPC",
"tenor": 90
},
{
"offerId": "Offer2",
"productId": "Product2",
"minAamount": 200.00,
"maxAamount": 2000.00,
"tenor": 24
"max_amount": "20838.00",
"min_amount": 5000.0,
"offerId": "SAL30000205",
"product_id": "AMPC",
"tenor": 30
}
]
},
+4 -4
View File
@@ -3,15 +3,15 @@
"properties": {
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
"example": "TRCVIC73089465966"
},
"customerId": {
"type": "string",
"example": "CN621868"
"example": "ZX48440946"
},
"msisdn": {
"type": "string",
"example": "3451342"
"example": "2348093451342"
},
"channel": {
"type": "string",
@@ -19,7 +19,7 @@
},
"accountId": {
"type": "string",
"example": "ACN8263457"
"example": "361005323"
}
},
"xml": {
+130 -80
View File
@@ -1,83 +1,133 @@
{
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
},
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"loans": {
"type": "array",
"items": {
"type": "object",
"properties": {
"debtId": {
"type": "string",
"example": "123456789"
},
"loanDate": {
"type": "string",
"format": "date-time",
"example": "2019-10-18 14:26:21.063"
},
"dueDate": {
"type": "string",
"format": "date-time",
"example": "2019-11-20 14:26:21.063"
},
"currentLoanAmount": {
"type": "integer",
"example": 8500
},
"initialLoanAmount": {
"type": "integer",
"example": 10000
},
"defaultPenaltyFee": {
"type": "integer",
"example": 0
},
"continuousFee": {
"type": "integer",
"example": 0
},
"productId": {
"type": "string",
"example": "101"
},
"installment": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"repaymentDate": {
"type": "string",
"example": "2025-04-24 10:31:"
}
}
}
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"xml": {
"name": "LoanStatusResponse"
"transactionId": {
"type": "string",
"example": "TRCVIC49381378037"
},
"accountId": {
"type": "string",
"example": "361005323"
},
"loans": {
"type": "array",
"items": {
"type": "object",
"properties": {
"debtId": {
"type": "integer",
"example": 80
},
"loanDate": {
"type": "string",
"format": "date-time",
"example": "2025-09-12T11:58:58"
},
"dueDate": {
"type": "string",
"format": "date-time",
"example": "2025-12-11T11:58:58"
},
"currentLoanAmount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"initialLoanAmount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"defaultPenaltyFee": {
"type": "number",
"format": "float",
"example": 0.0
},
"continuousFee": {
"type": "number",
"format": "float",
"example": 0.0
},
"productId": {
"type": "string",
"example": "3MPC"
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 10900.0
},
"repaymentAmount": {
"type": "number",
"format": "float",
"example": 32700.0
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
},
"status": {
"type": "string",
"example": "active"
},
"tenor": {
"type": "integer",
"example": 90
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"upfrontFee": {
"type": "number",
"format": "float",
"example": 622.5
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
},
"totalDebtAmount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"summary": {
"type": "object",
"properties": {
"totalOutstandingAmount": {
"type": "number",
"format": "float",
"example": 114450.0,
"description": "Total amount still owed across all unpaid loans."
},
"totalActiveLoanAmount": {
"type": "number",
"format": "float",
"example": 40000.0,
"description": "Total principal amount of currently active loans."
},
"totalSettledAmount": {
"type": "number",
"format": "float",
"example": 80000.0,
"description": "Total amount that has been fully repaid."
}
}
}
}
},
"xml": {
"name": "LoanStatusResponse"
}
}
@@ -1,50 +0,0 @@
{
"type": "object",
"properties": {
"fbnTransactionId": {
"type": "string",
"example": "123456789"
},
"transactionId": {
"type": "string",
"example": "123456789"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"debtId": {
"type": "string",
"example": "987654321"
},
"transactionType": {
"type": "string",
"example": "Disbursement"
},
"amountProvided": {
"type": "number",
"format": "float",
"example": 1000.00
},
"amountCollected": {
"type": "number",
"format": "float",
"example": 0.00
},
"responseCode": {
"type": "string",
"example": "00"
},
"responseDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "NotificationCallbackRequest"
}
}
@@ -1,16 +0,0 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "NotificationCallbackResponse"
}
}
+9 -9
View File
@@ -3,40 +3,40 @@
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
"example": "RQID11170001371256908"
},
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
"example": "TRCVIC73089465966"
},
"customerId": {
"type": "string",
"example": "CN621868"
"example": "ZX48440946"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
"example": "361005323"
},
"msisdn": {
"type": "string",
"example": "3451342"
"example": "2348093451342"
},
"requestedAmount": {
"type": "number",
"format": "decimal",
"example": 900
"example": 20000
},
"collectionType": {
"type": "integer",
"example": 1
"example": 0
},
"offerId": {
"type": "integer",
"example": 1127
"example": "SAL900004543304"
},
"channel": {
"type": "string",
"example": "100"
"example": "USSD"
}
},
"xml": {
+72 -37
View File
@@ -1,40 +1,75 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"loanRef": {
"type": "string",
"example": "1620029887USSDAMPC"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"msisdn": {
"type": "string",
"example": "3451342"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "81757678335583"
},
"xml": {
"name": "ProvideLoanResponse"
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"accountId": {
"type": "string",
"example": "361005323"
},
"msisdn": {
"type": "string",
"example": "98016510058"
},
"schedule": {
"type": "array",
"description": "List of loan repayment components with due dates and amounts.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"amountDue": {
"type": "number",
"example": 2000.0
},
"componentName": {
"type": "string",
"example": "INTEREST"
},
"dueDate": {
"type": "string",
"format": "date-time",
"example": "2026-01-13T11:36:39.890747+00:00"
},
"startDate": {
"type": "string",
"format": "date-time",
"example": "2025-10-15T11:36:39.890747+00:00"
},
"loanRef": {
"type": "string",
"example": "TRX1760528156816285USSD3MPC"
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
}
},
"xml": {
"name": "ProvideLoanResponse"
}
}
+7 -7
View File
@@ -3,27 +3,27 @@
"properties": {
"msisdn": {
"type": "string",
"example": "3451342"
"example": "2348093451342"
},
"debtId": {
"type": "string",
"example": "10"
"type": "number",
"example": 80
},
"transactionId": {
"type": "string",
"example": "20171209232115"
"example": "TRCVIC73089465966"
},
"customerId": {
"type": "string",
"example": "CID0000025585"
"example": "ZX48440946"
},
"loanRef": {
"type": "string",
"example": "Trx5847365252USSD3MPC"
"example": "TRCVIC73089465966USSD3MPC"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
"example": "361005323"
}
},
"xml": {
+45 -25
View File
@@ -1,28 +1,48 @@
{
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
},
"productId": {
"type": "string",
"example": "101"
},
"debtId": {
"type": "string",
"example": "273194670"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
"type": "object",
"properties": {
"Id": {
"type": "integer",
"example": 195
},
"xml": {
"name": "RepaymentResponse"
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"debtId": {
"type": "string",
"example": "80"
},
"initiated_by": {
"type": "string",
"example": "USER_INITIATED"
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
},
"productId": {
"type": "string",
"example": "3MPC"
},
"repayment_id": {
"type": "integer",
"example": 195
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
}
},
"xml": {
"name": "RepaymentResponse"
}
}
+15 -15
View File
@@ -3,40 +3,40 @@
"properties": {
"requestId": {
"type": "string",
"example": "202111170001371256908"
"example": "RQID11170001371256908"
},
"transactionId": {
"type": "string",
"example": "1231231321232"
"example": "TRX1231231321232"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "123456789"
},
"requestedAmount": {
"type": "number",
"format": "double",
"example": 10000.55
"example": "CN6215268548868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
"example": "4348094226"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
},
"requestedAmount": {
"type": "number",
"example": 20000
},
"productId": {
"type": "string",
"example": "3MPC"
},
"offerId": {
"type": "string",
"example": "101"
"example": "SAL900004543304"
},
"channel": {
"type": "string",
"example": ""
"example": "USSD"
}
},
"xml": {
+121 -113
View File
@@ -1,121 +1,129 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "81757678225025"
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"accountId": {
"type": "string",
"example": "361005323"
},
"loan": {
"type": "array",
"items": {
"type": "object",
"properties": {
"offerId": {
"type": "string",
"example": "202111170001371256908"
},
"transactionId": {
"example": "SAL90000204"
},
"productId": {
"type": "string",
"example": "1231231321232"
},
"customerId": {
"type": "string",
"example": "1256907"
},
"accountId": {
"type": "string",
"example": "5948306019"
},
"loan": {
"example": "3MPC"
},
"amount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"upfrontPayment": {
"type": "number",
"format": "float",
"example": 622.5
},
"interestRate": {
"type": "number",
"format": "float",
"example": 3.0
},
"interestFee": {
"type": "number",
"format": "float",
"example": 2700.0
},
"managementRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"managementFee": {
"type": "number",
"format": "float",
"example": 300.0
},
"insuranceRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"insuranceFee": {
"type": "number",
"format": "float",
"example": 300.0
},
"VATRate": {
"type": "number",
"format": "float",
"example": 7.5
},
"VATAmount": {
"type": "number",
"format": "float",
"example": 22.5
},
"recommendedRepaymentDates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"offerId": {
"type": "string",
"example": "14451"
},
"productId": {
"type": "string",
"example": "3MPC"
},
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"dueDate": {
"type": "string",
"example": "2025-04-24 10:31:"
},
"upfrontPayment": {
"type": "number",
"format": "float",
"example": 1000.0
},
"interestRate": {
"type": "number",
"format": "float",
"example": 3.0
},
"interestFee": {
"type": "number",
"format": "float",
"example": 3000.00
},
"ManagementRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"ManagementFee": {
"type": "number",
"format": "float",
"example": 1.0
},
"InsuranceRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"InsuranceFee": {
"type": "number",
"format": "float",
"example": 100.0
},
"VATRate": {
"type": "number",
"format": "float",
"example": 7.5
},
"VATamount": {
"type": "number",
"format": "float",
"example": 100.0
},
"installmentRepaymentDates": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"2022-11-30"
]
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 11000.0
},
"totalRepaymentAmount": {
"type": "number",
"format": "float",
"example": 11000.0
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
"type": "string"
},
"example": [
"2025-10-12",
"2025-11-12",
"2025-12-12"
]
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 10900.0
},
"repaymentAmount": {
"type": "number",
"format": "float",
"example": 32700.0
},
"totalRepaymentAmount": {
"type": "number",
"format": "float",
"example": 33322.5
}
}
}
},
"xml": {
"name": "SelectOffersResponse"
"outstandingDebtAmount": {
"type": "number",
"format": "float",
"example": 0
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
}
},
"xml": {
"name": "SelectOffersResponse"
}
}
+40
View File
@@ -0,0 +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)
return "Report email sent"
+12 -1
View File
@@ -1,3 +1,4 @@
version: '3.8'
services:
digifi-bank-to-product-core:
build: .
@@ -11,4 +12,14 @@ services:
- DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
volumes:
- .:/app
restart: always
restart: always
networks:
- my_custom_network
networks:
my_custom_network:
driver: bridge
ipam:
config:
- subnet: 10.244.0.0/26
+31
View File
@@ -0,0 +1,31 @@
"""empty message
Revision ID: 05b5494ad406
Revises: 33e09efd85e3
Create Date: 2025-07-06 09:28:26.264927
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "05b5494ad406"
down_revision = "33e09efd85e3"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("loans", sa.Column("balance", sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("loans", "balance")
# ### end Alembic commands ###
@@ -1,35 +0,0 @@
"""Migration on Tue Jun 10 08:41:00 WAT 2025
Revision ID: 0acd553309a1
Revises: 45790fd659fb
Create Date: 2025-06-10 08:41:45.222513
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0acd553309a1'
down_revision = '45790fd659fb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('repayments_data',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('added_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('response_code', sa.String(length=10), nullable=True),
sa.Column('response_descr', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('repayments_data')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Thu Apr 10 21:50:01 UTC 2025
Revision ID: 1340e7e578b9
Revises: b8f6fd76ead8
Create Date: 2025-04-10 21:50:32.113149
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1340e7e578b9'
down_revision = 'b8f6fd76ead8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('ref_model')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Sat May 10 09:54:34 UTC 2025
Revision ID: 173ea45db189
Revises: 3105abd795d4
Create Date: 2025-05-10 09:54:39.380499
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '173ea45db189'
down_revision = '3105abd795d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -1,53 +0,0 @@
"""Migration on Thu Apr 24 17:42:25 UTC 2025
Revision ID: 1b2339f43824
Revises: de9ad96ba34e
Create Date: 2025-04-24 17:43:09.589626
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1b2339f43824'
down_revision = 'de9ad96ba34e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('rac_checks',
sa.Column('id', sa.String(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(), nullable=False),
sa.Column('account_id', sa.String(), nullable=False),
sa.Column('rac_response', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.NUMERIC(precision=10, scale=2),
type_=sa.Float(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('installment_amount')
batch_op.drop_column('repayment_amount')
batch_op.drop_column('upfront_fee')
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.Float(),
type_=sa.NUMERIC(precision=10, scale=2),
existing_nullable=True)
op.drop_table('rac_checks')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Wed Apr 16 18:35:18 UTC 2025
Revision ID: 287ecb02d3d7
Revises: a4847b997191
Create Date: 2025-04-16 18:36:04.632791
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '287ecb02d3d7'
down_revision = 'a4847b997191'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
# ### end Alembic commands ###
@@ -1,42 +0,0 @@
"""Migration on Fri Apr 25 15:01:00 UTC 2025
Revision ID: 2a45dd99c9cb
Revises: 2cf0c177ca02
Create Date: 2025-04-25 15:01:51.129681
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a45dd99c9cb'
down_revision = '2cf0c177ca02'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('total_repayment_amount', sa.Float(), nullable=True))
batch_op.drop_column('principal_amount')
batch_op.drop_column('interest_amount')
batch_op.drop_column('total_installment')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('total_installment', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('interest_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('principal_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.drop_column('total_repayment_amount')
batch_op.drop_column('installment_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -1,41 +0,0 @@
"""Migration on Fri Apr 25 14:02:01 UTC 2025
Revision ID: 2cf0c177ca02
Revises: 1b2339f43824
Create Date: 2025-04-25 14:02:42.244146
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2cf0c177ca02'
down_revision = '1b2339f43824'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('principal_amount', sa.Float(), nullable=True),
sa.Column('interest_amount', sa.Float(), nullable=True),
sa.Column('total_installment', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('loan_repayment_schedules')
# ### end Alembic commands ###
-250
View File
@@ -1,250 +0,0 @@
"""empty message
Revision ID: 2eee4157505f
Revises: 565bc3d0ba6e
Create Date: 2025-05-16 13:24:41.914400
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '2eee4157505f'
down_revision = '565bc3d0ba6e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
# ### end Alembic commands ###
+45
View File
@@ -0,0 +1,45 @@
"""empty message
Revision ID: 30b45df851fa
Revises: d59bfb9ead82
Create Date: 2025-08-26 13:48:27.458593
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '30b45df851fa'
down_revision = 'd59bfb9ead82'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
'loan_repayment_schedules',
sa.Column('paid_status', sa.String(length=20), nullable=True),
)
op.add_column(
'loan_repayment_schedules',
sa.Column('repay_description', sa.String(length=255), nullable=True),
)
op.add_column(
'loan_repayment_schedules',
sa.Column('partial_balance', sa.Float(), nullable=True),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('loan_repayment_schedules', 'partial_balance')
op.drop_column('loan_repayment_schedules', 'repay_description')
op.drop_column('loan_repayment_schedules', 'paid_status')
# ### end Alembic commands ###
-52
View File
@@ -1,52 +0,0 @@
"""empty message
Revision ID: 3105abd795d4
Revises: 95a52be203c4
Create Date: 2025-05-07 11:44:18.483694
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3105abd795d4'
down_revision = '95a52be203c4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
# Step 1: Drop the default value
batch_op.alter_column('id',
server_default=None,
existing_type=sa.VARCHAR(),
existing_nullable=False
)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
# Step 2: Change the column type
batch_op.alter_column('id',
existing_type=sa.VARCHAR(),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
postgresql_using='id::integer'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("''::character varying"))
# ### end Alembic commands ###
+280
View File
@@ -0,0 +1,280 @@
"""empty message
Revision ID: 33e09efd85e3
Revises:
Create Date: 2025-07-03 14:07:14.424548
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import literal_column
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = '33e09efd85e3'
down_revision = None
branch_labels = None
depends_on = None
sequences_and_triggers = [
("transactions", "transactions_seq", "trg_transactions_id"),
("transaction_offers", "transaction_offers_seq", "trg_transaction_offers_id"),
("salaries", "salaries_seq", "trg_salaries_id"),
("repayments_data", "repayments_data_seq", "trg_repayments_data_id"),
("repayments", "repayments_seq", "trg_repayments_id"),
("rac_checks", "rac_checks_seq", "trg_rac_checks_id"),
("loans", "loans_seq", "trg_loans_id"),
(
"loan_repayment_schedules",
"loan_repayment_schedules_seq",
"trg_loan_repayment_schedules_id",
),
("loan_charges", "loan_charges_seq", "trg_loan_charges_id"),
("charges", "charges_seq", "trg_charges_id"),
]
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('accounts',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_type', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('lien_amount', sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('offer_id', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('customers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('msisdn', sa.String(length=20), nullable=False),
sa.Column('country_code', sa.String(length=3), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('msisdn')
)
op.create_table('loan_charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('installment_amount', sa.Float(), nullable=True),
sa.Column('total_repayment_amount', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('loans',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('original_transaction', sa.String(length=50), nullable=True),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('collection_type', sa.String(length=20), nullable=True),
sa.Column('current_loan_amount', sa.Float(), nullable=True),
sa.Column('initial_loan_amount', sa.Float(), nullable=False),
sa.Column('default_penalty_fee', sa.Float(), nullable=True),
sa.Column('continuous_fee', sa.Float(), nullable=True),
sa.Column('upfront_fee', sa.Float(), nullable=True),
sa.Column('repayment_amount', sa.Float(), nullable=True),
sa.Column('installment_amount', sa.Float(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('due_date', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('disburse_date', sa.DateTime(), nullable=True),
sa.Column('disburse_verify', sa.DateTime(), nullable=True),
sa.Column('reference', sa.String(length=50), nullable=True),
sa.Column('disburse_result', sa.String(length=10), nullable=True),
sa.Column('disburse_description', sa.String(length=100), nullable=True),
sa.Column('verify_result', sa.String(length=10), nullable=True),
sa.Column('verify_description', sa.String(length=100), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('offers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=50), nullable=False),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('tenor', sa.Integer(), nullable=False),
sa.Column('schedule', sa.Integer(), nullable=True),
sa.Column('interest_rate', sa.Float(), nullable=True),
sa.Column('management_rate', sa.Float(), nullable=True),
sa.Column('insurance_rate', sa.Float(), nullable=True),
sa.Column('vat_rate', sa.Float(), nullable=True),
sa.Column('list_order', sa.Integer(), nullable=True),
sa.Column('max_daily_loans', sa.Integer(), nullable=True),
sa.Column('max_active_loans', sa.Integer(), nullable=True),
sa.Column('max_life_loans', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('rac_checks',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('rac_response', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('repay_date', sa.DateTime(), nullable=True),
sa.Column('repay_result', sa.String(length=10), nullable=True),
sa.Column('repay_description', sa.String(length=100), nullable=True),
sa.Column('verify_date', sa.DateTime(), nullable=True),
sa.Column('verify_result', sa.String(length=10), nullable=True),
sa.Column('verify_description', sa.String(length=100), nullable=True),
sa.Column('initiated_by', sa.String(length=50), nullable=True),
sa.Column('salary_amount', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('repayments_data',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True),
sa.Column('customer_id', sa.String(length=50), nullable=True),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('repayment_amount', sa.Float(), nullable=True),
sa.Column('amount_collected', sa.Float(), nullable=True),
sa.Column('added_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('response_code', sa.String(length=10), nullable=True),
sa.Column('response_descr', sa.String(length=255), nullable=True),
sa.Column('balance', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('salaries',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('amount', sa.Float(), nullable=False),
sa.Column('salary_date', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transaction_offers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('original_transaction', sa.String(length=50), nullable=True),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('customer_id', sa.String(length=50), nullable=True),
sa.Column('type', sa.String(length=50), nullable=False),
sa.Column('channel', sa.String(length=50), nullable=False),
sa.Column('phone_number', sa.String(length=50), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
for table, seq, trg in sequences_and_triggers:
op.execute(
text(
f"""
BEGIN
EXECUTE IMMEDIATE 'CREATE SEQUENCE {seq} START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
"""
)
)
op.execute(
text(
f"""
CREATE OR REPLACE TRIGGER {trg}
BEFORE INSERT ON {table}
FOR EACH ROW
BEGIN
IF ||':'||'NEW.id IS NULL THEN
SELECT {seq}.NEXTVAL INTO '||':'||'NEW.id FROM dual;
END IF;
END;
"""
)
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('transactions')
op.drop_table('transaction_offers')
op.drop_table('salaries')
op.drop_table('repayments_data')
op.drop_table('repayments')
op.drop_table('rac_checks')
op.drop_table('offers')
op.drop_table('loans')
op.drop_table('loan_repayment_schedules')
op.drop_table('loan_charges')
op.drop_table('customers')
op.drop_table('charges')
op.drop_table('accounts')
# ### end Alembic commands ###
for table, seq, trg in sequences_and_triggers:
op.execute(text(f"DROP TRIGGER {trg}"))
op.execute(text(f"DROP SEQUENCE {seq}"))
@@ -1,32 +0,0 @@
"""Migration for mloan table
Revision ID: 38acee611d55
Revises: f1e83a993034
Create Date: 2025-04-30 09:55:30.552838
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '38acee611d55'
down_revision = 'f1e83a993034'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('tenor', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('tenor')
# ### end Alembic commands ###
-54
View File
@@ -1,54 +0,0 @@
"""empty message
Revision ID: 45790fd659fb
Revises: b3a5e10bc77e
Create Date: 2025-06-04 12:37:48.180736
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '45790fd659fb'
down_revision = 'b3a5e10bc77e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('disburse_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('disburse_description', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('verify_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('verify_description', sa.String(length=100), nullable=True))
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.add_column(sa.Column('repay_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('repay_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('repay_description', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('verify_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('verify_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('verify_description', sa.String(length=100), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.drop_column('verify_description')
batch_op.drop_column('verify_result')
batch_op.drop_column('verify_date')
batch_op.drop_column('repay_description')
batch_op.drop_column('repay_result')
batch_op.drop_column('repay_date')
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('verify_description')
batch_op.drop_column('verify_result')
batch_op.drop_column('disburse_description')
batch_op.drop_column('disburse_result')
# ### end Alembic commands ###
@@ -1,34 +0,0 @@
"""Migration on Sat May 10 12:54:52 UTC 2025
Revision ID: 565bc3d0ba6e
Revises: 173ea45db189
Create Date: 2025-05-10 12:54:56.683215
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '565bc3d0ba6e'
down_revision = '173ea45db189'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('disburse_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('disburse_verify', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('disburse_verify')
batch_op.drop_column('disburse_date')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Fri Apr 11 14:15:19 UTC 2025
Revision ID: 610b7e9d15a6
Revises: 9bb0367eb486
Create Date: 2025-04-11 14:16:12.533227
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610b7e9d15a6'
down_revision = '9bb0367eb486'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Mon Apr 14 15:15:05 UTC 2025
Revision ID: 783a023a477f
Revises: f6cd1bfc8832
Create Date: 2025-04-14 15:15:36.991148
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '783a023a477f'
down_revision = 'f6cd1bfc8832'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('customer_id')
# ### end Alembic commands ###
-41
View File
@@ -1,41 +0,0 @@
"""empty message
Revision ID: 86e701febdda
Revises: eb99c7fb9e09
Create Date: 2025-04-29 07:59:33.305967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '86e701febdda'
down_revision = 'eb99c7fb9e09'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('transaction_offers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('transaction_offers')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Sat Apr 26 12:50:46 UTC 2025
Revision ID: 89759cebb9c6
Revises: 2a45dd99c9cb
Create Date: 2025-04-26 12:50:49.771355
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '89759cebb9c6'
down_revision = '2a45dd99c9cb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('interest_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('management_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('insurance_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('vat_rate', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.drop_column('vat_rate')
batch_op.drop_column('insurance_rate')
batch_op.drop_column('management_rate')
batch_op.drop_column('interest_rate')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Sat May 3 21:53:29 UTC 2025
Revision ID: 95a52be203c4
Revises: 38acee611d55
Create Date: 2025-05-03 21:53:32.154029
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '95a52be203c4'
down_revision = '38acee611d55'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('eligible_amount', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('eligible_amount')
# ### end Alembic commands ###
@@ -1,40 +0,0 @@
"""Migration on Fri Apr 11 12:48:01 UTC 2025
Revision ID: 9bb0367eb486
Revises: fd447d78b161
Create Date: 2025-04-11 12:48:36.145311
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9bb0367eb486'
down_revision = 'fd447d78b161'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('current_loan_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('default_penalty_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('continuous_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('continuous_fee')
batch_op.drop_column('default_penalty_fee')
batch_op.drop_column('current_loan_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -1,57 +0,0 @@
"""Migration on Wed Apr 16 17:42:49 UTC 2025
Revision ID: a4847b997191
Revises: 783a023a477f
Create Date: 2025-04-16 17:43:22.509659
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a4847b997191'
down_revision = '783a023a477f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('offers',
sa.Column('id', sa.String(), nullable=False),
sa.Column('product_id', sa.String(), nullable=False),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('tenor', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('product_id')
op.drop_table('offers')
op.drop_table('loan_charges')
# ### end Alembic commands ###
-32
View File
@@ -1,32 +0,0 @@
"""empty message
Revision ID: b3a5e10bc77e
Revises: e8dd9b841ad7
Create Date: 2025-05-27 01:52:48.538333
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b3a5e10bc77e'
down_revision = 'e8dd9b841ad7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('reference', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('reference')
# ### end Alembic commands ###
-46
View File
@@ -1,46 +0,0 @@
"""empty message
Revision ID: b54422fb31e0
Revises: 0acd553309a1
Create Date: 2025-06-16 12:24:09.159498
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b54422fb31e0'
down_revision = '0acd553309a1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
batch_op.add_column(sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('account_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('repayment_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('amount_collected', sa.Float(), nullable=True))
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('phone_number', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('phone_number')
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
batch_op.drop_column('amount_collected')
batch_op.drop_column('repayment_amount')
batch_op.drop_column('account_id')
batch_op.drop_column('customer_id')
batch_op.drop_column('fbn_transaction_id')
# ### end Alembic commands ###
@@ -1,86 +0,0 @@
"""Migration on Thu Apr 10 16:21:45 UTC 2025
Revision ID: b8f6fd76ead8
Revises:
Create Date: 2025-04-10 16:22:15.946157
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'b8f6fd76ead8'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.VARCHAR(length=50),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('channel',
existing_type=sa.VARCHAR(length=8),
type_=sa.String(length=50),
existing_nullable=False)
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.drop_constraint('transactions_id_key', type_='unique')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.create_unique_constraint('transactions_id_key', ['id'])
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('channel',
existing_type=sa.String(length=50),
type_=sa.VARCHAR(length=8),
existing_nullable=False)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(length=50),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
op.drop_table('repayments')
# ### end Alembic commands ###
+38
View File
@@ -0,0 +1,38 @@
"""empty message
Revision ID: d59bfb9ead82
Revises: 05b5494ad406
Create Date: 2025-08-21 14:22:19.220158
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd59bfb9ead82'
down_revision = '05b5494ad406'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
'loan_repayment_schedules',
sa.Column('due_process_date', sa.DateTime(), nullable=True)
)
# Add due_process_count column
op.add_column(
'loan_repayment_schedules',
sa.Column('due_process_count', sa.Integer(), nullable=True)
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('loan_repayment_schedules', 'due_process_date')
op.drop_column('loan_repayment_schedules', 'due_process_count')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Thu Apr 17 14:15:36 UTC 2025
Revision ID: de9ad96ba34e
Revises: ec8d97f9b584
Create Date: 2025-04-17 14:16:16.537466
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'de9ad96ba34e'
down_revision = 'ec8d97f9b584'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('offer_id', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('charges')
# ### end Alembic commands ###
-36
View File
@@ -1,36 +0,0 @@
"""empty message
Revision ID: e8dd9b841ad7
Revises: 2eee4157505f
Create Date: 2025-05-19 11:46:19.204637
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e8dd9b841ad7'
down_revision = '2eee4157505f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('max_daily_loans', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('max_active_loans', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('max_life_loans', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.drop_column('max_life_loans')
batch_op.drop_column('max_active_loans')
batch_op.drop_column('max_daily_loans')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Sat Apr 26 19:02:17 UTC 2025
Revision ID: eb99c7fb9e09
Revises: 89759cebb9c6
Create Date: 2025-04-26 19:02:20.443678
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'eb99c7fb9e09'
down_revision = '89759cebb9c6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -1,34 +0,0 @@
"""Migration on Thu Apr 17 10:40:05 UTC 2025
Revision ID: ec8d97f9b584
Revises: 287ecb02d3d7
Create Date: 2025-04-17 10:40:34.751272
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ec8d97f9b584'
down_revision = '287ecb02d3d7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Tue Apr 29 20:43:35 UTC 2025
Revision ID: f1e83a993034
Revises: 86e701febdda
Create Date: 2025-04-29 20:43:38.595543
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f1e83a993034'
down_revision = '86e701febdda'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,34 +0,0 @@
"""Migration on Fri Apr 11 14:34:36 UTC 2025
Revision ID: f6cd1bfc8832
Revises: 610b7e9d15a6
Create Date: 2025-04-11 14:35:07.093967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f6cd1bfc8832'
down_revision = '610b7e9d15a6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('collection_type', sa.String(length=20), nullable=True))
batch_op.drop_column('product_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
batch_op.drop_column('collection_type')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Fri Apr 11 12:02:45 UTC 2025
Revision ID: fd447d78b161
Revises: 1340e7e578b9
Create Date: 2025-04-11 12:03:28.346671
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fd447d78b161'
down_revision = '1340e7e578b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=True)
batch_op.drop_column('ref_model')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=False)
# ### end Alembic commands ###
+4
View File
@@ -6,6 +6,7 @@ flask-sqlalchemy
flask-migrate
psycopg2-binary
alembic
oracledb
# Schema for validations
Flask-Marshmallow==0.15.0
@@ -39,3 +40,6 @@ confluent-kafka==1.9.2
python-dateutil
Flask-Mail==0.10.0
pandas==2.1.3
openpyxl==3.1.5