Compare commits

..

31 Commits

Author SHA1 Message Date
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
VivianDee f8da81d564 [add]: Loan schedule on provide loan endpoint 2025-10-15 12:01:29 +01: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
36 changed files with 905 additions and 611 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"
+30 -22
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,
@@ -19,37 +20,44 @@ 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__)
try:
# Load configuration
app.config.from_object(Config)
app = Flask(__name__)
CORS(app)
# Load configuration
app.config.from_object(Config)
JWTManager(app)
CORS(app, supports_credentials=True)
CORS(app)
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL")
JWTManager(app)
CORS(app, supports_credentials=True)
# Register blueprints
app.register_blueprint(api)
# 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
# Error Handlers
register_error_handlers(app)
# Database and Migrations
db.init_app(app)
from . import models
migrate.init_app(app, db)
# Initialize Flask-Mail
mail.init_app(app)
return 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}")
+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
+57 -2
View File
@@ -1,3 +1,5 @@
from sqlite3 import DatabaseError
from app.api.integrations.events_service import EventServiceIntegration
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,61 @@ 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"
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 1"))
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 Successful"
status = "failed"
errors.append(f"Events Service connection failed: {str(e)}")
response = {
"status": status,
"db_status": db_status,
"events_service_status": events_service_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)
+11 -7
View File
@@ -87,13 +87,10 @@ class OfferAnalysis:
if failed_true_rules or failed_false_rules or not salaries:
if failed_true_rules:
logger.warning(f"RAC analysis failed: TRUE rules failed: {failed_true_rules}")
if failed_false_rules:
logger.warning(f"RAC analysis failed: FALSE rules failed: {failed_false_rules}")
if not salaries:
logger.warning("RAC analysis failed: No salary records found in RAC response.")
raise ValueError("RAC analysis failed")
logger.warning(f"Failed TRUE rules: {failed_true_rules}")
logger.warning(f"Failed FALSE rules: {failed_false_rules}")
logger.warning("No salary records found in RAC response.")
raise ValueError(f"RAC analysis failed")
@@ -185,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,
@@ -226,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,
+57 -7
View File
@@ -13,9 +13,11 @@ from app.api.enums import LoanStatus
from app.extensions import db
from datetime import datetime, timezone
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, timezone, timedelta
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -157,26 +159,36 @@ 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
)
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 +207,42 @@ 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):
now = datetime.now(timezone.utc)
due_date = now + timedelta(days=offer.tenor)
charge_schedule_items = []
charge_schedule_items.append({
"id": 1,
"dueDate": due_date.isoformat(),
"amountDue": amount,
"componentName": "PRINCIPAL",
"startDate": now.isoformat(),
})
for idx, charge in enumerate(loan_charges, start=len(charge_schedule_items) + 1):
item = {
"id": idx,
"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(),
}
if charge.code.upper() == "INTEREST":
item["loanRef"] = loan_ref
charge_schedule_items.append(item)
return charge_schedule_items
+13 -2
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
@@ -79,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)
@@ -101,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
+17 -2
View File
@@ -24,7 +24,10 @@ class Config:
# Database Connection
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_DATABASE_URI = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
SQLALCHEMY_DATABASE_URI_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI_FULL", SQLALCHEMY_DATABASE_URI_INTERNAL)
#SQLALCHEMY_DATABASE_URI_FULL = 'oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv'
SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -43,6 +46,10 @@ 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")
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")
@@ -85,7 +92,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()
+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())
+44 -14
View File
@@ -20,12 +20,15 @@
{
"url": "http://api.dev.simbrellang.net:4500"
},
{
{
"url": "https://api.dev.simbrellang.net"
},
{
"url": "http://10.2.249.133:4500"
}
],
"tags": [
{
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
@@ -80,6 +83,10 @@
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Health",
"description": "System health check including DB status."
}
],
"paths": {
@@ -103,6 +110,41 @@
},
"/Repayment": {
"$ref": "swagger/paths/Repayment.json"
},
"/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": "healthy",
"error": []
}
}
}
},
"500": {
"description": "Health check failed",
"content": {
"application/json": {
"example": {
"status": "ok",
"db_status": "Connection Failed",
"events_service_status": "unhealthy",
"error":["could not connect to server: Connection refused"]
}
}
}
}
}
}
}
},
"components": {
@@ -131,18 +173,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": {
+107 -80
View File
@@ -1,83 +1,110 @@
{
"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
}
}
},
"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"
+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 ###
+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 ###
+3
View File
@@ -40,3 +40,6 @@ confluent-kafka==1.9.2
python-dateutil
Flask-Mail==0.10.0
pandas==2.1.3
openpyxl==3.1.5