Compare commits

...

18 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
9 changed files with 271 additions and 33 deletions
+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"
+27 -23
View File
@@ -20,40 +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)
# Initialize Flask-Mail
mail.init_app(app)
from . import models
# Database and Migrations
db.init_app(app)
# Initialize Flask-Mail
mail.init_app(app)
migrate.init_app(app, db)
# Database and Migrations
db.init_app(app)
return app
migrate.init_app(app, db)
return app
except Exception as e:
print(f"An unexpected error occurred: {e}")
+22 -2
View File
@@ -5,6 +5,7 @@ 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
@@ -13,7 +14,8 @@ class EventServiceIntegration:
"""
Calls the Direct Loan endpoint
"""
url = f"{EventServiceIntegration.BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_LOAN}"
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_LOAN}"
logger.info(f"Direct Loan URL: {url}")
payload = {"transactionId": str(transaction_id)}
headers = {
@@ -33,7 +35,8 @@ class EventServiceIntegration:
"""
Calls the Direct Repayment endpoint
"""
url = f"{EventServiceIntegration.BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_REPAYMENT}"
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_REPAYMENT}"
logger.info(f"Direct Repayment URL: {url}")
payload = {"transactionId": str(transaction_id)}
headers = {
@@ -47,3 +50,20 @@ class EventServiceIntegration:
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
+7
View File
@@ -182,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,
@@ -223,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,
+43 -5
View File
@@ -17,6 +17,7 @@ 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
@@ -158,20 +159,26 @@ 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,
}
@@ -207,4 +214,35 @@ class ProvideLoanService(BaseService):
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
+39
View File
@@ -83,6 +83,10 @@
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Health",
"description": "System health check including DB status."
}
],
"paths": {
@@ -106,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": {
+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",
@@ -25,6 +25,41 @@
"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"