Compare commits

..

5 Commits

Author SHA1 Message Date
CHIEFSOFT\ameye dd0f39428a SQLALCHEMY_DATABASE_URI 2025-10-10 16:17:35 +01:00
CHIEFSOFT\ameye 50eaf0099b rem remove swagger file path 2025-10-10 16:17:35 +01:00
CHIEFSOFT\ameye 6377158c3c Try catch at start 2025-10-10 16:17:35 +01:00
VivianDee ce6b0684c8 [add]: DB_URI 2025-10-10 16:15:41 +01:00
VivianDee b6ca91153f Update routes.py 2025-10-06 18:29:47 +01:00
14 changed files with 92 additions and 334 deletions
+11 -14
View File
@@ -22,17 +22,19 @@ def create_app():
# 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)
try:
app = Flask(__name__)
# Load configuration
app.config.from_object(Config)
CORS(app)
JWTManager(app)
CORS(app, supports_credentials=True)
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL")
@@ -43,11 +45,6 @@ def create_app():
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
except Exception as e:
print(f"Swagger Unexpected error occurred: {e}")
try:
# Error Handlers
register_error_handlers(app)
-18
View File
@@ -8,7 +8,6 @@ import logging
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
HEALTH_ENDPOINT = settings.SIMBRELLA_HEALTH
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
@@ -43,21 +42,4 @@ 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 Simbrella Service
"""
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.HEALTH_ENDPOINT}"
logger.info(f"Simbrella Health Check URL: {url}")
try:
response = httpx.get(url, timeout=10.0)
logger.info(f"Simbrella Health Check Response: {response.text}")
return response
except Exception as e:
logger.error(f"Simbrella Health Check API call failed: {str(e)}", exc_info=True)
raise
+9 -27
View File
@@ -1,7 +1,4 @@
from re import S
from sqlite3 import DatabaseError
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,
@@ -26,6 +23,7 @@ from flask_jwt_extended import (
from sqlalchemy import text
from app.extensions import db
from app.config import settings
from urllib.parse import urlparse
api = Blueprint("api", __name__)
@@ -44,10 +42,10 @@ def swagger_json():
return send_from_directory(swagger_dir, "digifi_swagger.json")
@api.route("/swagger/<path:filename>")
def serve_paths(filename):
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, filename)
# @api.route("/swagger/<path:filename>")
# def serve_paths(filename):
# swagger_dir = os.path.join("swagger")
# return send_from_directory(swagger_dir, filename)
# EligibilityCheck Endpoint
@@ -120,6 +118,7 @@ def notification_callback():
response = NotificationCallbackService.process_request(data)
return response
# Health Check Endpoint
@api.route("/health", methods=["GET"])
def health_check():
@@ -127,11 +126,9 @@ def health_check():
response = {}
db_status = "Connection Successful"
events_service_status = "Connection Successful"
emulator_status = "Connection Successful"
errors = []
status = "ok"
# Extract the database URI
try:
db_uri = db.engine.url.render_as_string(hide_password=False)
@@ -143,7 +140,7 @@ def health_check():
# 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"))
db.session.execute(text("SELECT 1"))
except Exception as e:
db_status = "Connection Failed"
errors.append(f"Database Error: {str(e)}")
@@ -161,31 +158,16 @@ def health_check():
except Exception as e:
events_service_status = "Connection Failed"
events_service_status = "Connection Successful"
status = "failed"
errors.append(f"Events Service connection failed: {str(e)}")
# Check Emulator health
try:
emulator_response = SimbrellaIntegration.health_check()
if emulator_response.status_code != 200:
emulator_status = "Connection Failed"
status = "failed"
errors.append(f"Emulator response: {emulator_response.text}")
except Exception as e:
emulator_status = "Connection Failed"
status = "failed"
errors.append(f"Emulator connection failed: {str(e)}")
response = {
"status": status,
"db_status": db_status,
"events_service_status": events_service_status,
"emulator_status": emulator_status,
"db_uri": db_uri,
"events_service_status": events_service_status,
"errors": errors or None
}
+21 -54
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,56 +27,30 @@ 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')
transactionId = validated_data.get("transactionId")
account_id = validated_data.get("accountId")
if LoanStatusService.validate_account_ownership(
account_id=account_id, customer_id=customer_id
):
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans
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
)
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
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"
)
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
loan.get("currentLoanAmount") or 0
for loan in loans
)
# Simulated processing logic
response_data = {
@@ -85,11 +59,6 @@ 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()
@@ -99,11 +68,9 @@ 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))
@@ -111,4 +78,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()
+8 -92
View File
@@ -1,4 +1,3 @@
from gettext import install
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.integrations.kafka import KafkaIntegration
@@ -12,7 +11,7 @@ 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, timedelta
from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta
from app.api.integrations import EventServiceIntegration
from app.models import LoanRepaymentSchedule
@@ -144,10 +143,10 @@ class ProvideLoanService(BaseService):
db.session.flush()
current_product_id = offer.product_id
schedules = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedules:
if not schedule:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
@@ -159,27 +158,20 @@ 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,
"schedule": charge_schedule_items,
"msisdn": customer.msisdn
}
@@ -215,80 +207,4 @@ 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, 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(),
}
if charge.code.upper() == "VAT":
item["loanRef"] = loan_ref
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(),
"loanRef": loan_ref
}
charge_schedule_items.append(interest)
id_counter += 1
return charge_schedule_items
-1
View File
@@ -38,7 +38,6 @@ 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",
-6
View File
@@ -44,14 +44,8 @@ class Config:
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
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")
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")
+1 -1
View File
@@ -246,7 +246,7 @@ class Loan(db.Model):
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.balance,
'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee,
'continuousFee': self.continuous_fee,
'collectionType': self.collection_type,
-6
View File
@@ -56,12 +56,6 @@ class Repayment(db.Model):
raise ValueError(f"Database integrity error: {err}")
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 {
+40 -44
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.3",
"info": {
"title": "Swagger Simbrella Core [10/22/2025] - OpenAPI 3.0",
"title": "Swagger Bank Channel to Simbrella FirstAdvance - OpenAPI 3.0",
"description": "This is a Simbrella FirstAdvance Backend Server with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
"termsOfService": "http://swagger.io/terms/",
"contact": {
@@ -28,10 +28,6 @@
}
],
"tags": [
{
"name": "Health",
"description": "System health check including DB status."
},
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
@@ -87,48 +83,13 @@
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Health",
"description": "System health check including DB status."
}
],
"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",
"emulator_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",
"emulator_status":"Connection Failed",
"db_uri": "Unavailable",
"error":["could not connect to server: Connection refused"]
}
}
}
}
}
}
},
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
@@ -149,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": ["AuthorizeRefresh"],
"tags": ["Authorize Refresh"],
"summary": "Customer Authorize Refresh Request",
"description": "Customer Authorize Refresh Request",
"operationId": "AuthorizeRefresh",
@@ -102,29 +102,6 @@
"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": {
@@ -25,41 +25,6 @@
"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"
+1 -12
View File
@@ -1,4 +1,3 @@
version: '3.8'
services:
digifi-bank-to-product-core:
build: .
@@ -12,14 +11,4 @@ services:
- DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
volumes:
- .:/app
restart: always
networks:
- my_custom_network
networks:
my_custom_network:
driver: bridge
ipam:
config:
- subnet: 10.244.0.0/26
restart: always