50 Commits

Author SHA1 Message Date
Chinenye Nmoh ca3ca1cac3 added eco integration 2025-07-25 11:53:05 +01:00
Chinenye Nmoh c50b69e852 added mail values in the env file 2025-07-17 12:42:22 +01:00
ameye cef23778d5 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-07-16 11:10:15 +00:00
Chinenye Nmoh 7ac9b8c061 added mail 2025-07-15 20:25:49 +01:00
ameye ccdb44f0d5 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-07-14 14:14:13 +00:00
Chinenye Nmoh 22ea12b3c4 resolved method not found error 2025-07-14 07:59:37 +01:00
ameye 71608091ab Merge branch 'oracle_migration' of DigiFi/digifi-EventManager into master 2025-07-11 10:27:08 +00:00
ameye 16758c3ae9 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-07-11 10:27:02 +00:00
VivianDee 38499d67ac Update .env.remote.example 2025-07-09 12:13:39 +01:00
Chinenye Nmoh 23574a433a corrected commit error 2025-07-08 10:49:04 +01:00
VivianDee 3f40385128 Update .env.local.example 2025-07-06 10:03:36 +01:00
VivianDee 4aaf07748e [add]: oracle migration 2025-07-06 09:53:52 +01:00
ameye 39d9abf5ec Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-07-04 10:35:05 +00:00
Chinenye Nmoh 0000907698 fixed restart error 2025-07-03 15:37:53 +01:00
ameye 501d0f2703 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-30 00:30:14 +00:00
Chinenye Nmoh 1e338c1a50 added catch for 404 2025-06-29 20:12:23 +01:00
ameye d79b71da58 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-26 20:11:57 +00:00
Chinenye Nmoh 1bf4d61554 debugging 2025-06-26 18:26:57 +01:00
Chinenye Nmoh 79109af695 balance 2025-06-26 13:36:49 +01:00
Chinenye Nmoh 9feab46016 balance 2025-06-26 13:31:51 +01:00
ameye d863285e9a Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-25 14:59:07 +00:00
Chinenye Nmoh 4a949d8b7e corrected update loan 2025-06-25 14:10:30 +01:00
ameye c4a89a0bd0 Merge branch 'calculat_loan_balance' of DigiFi/digifi-EventManager into master 2025-06-25 08:39:01 +00:00
ameye bb21a33014 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-25 08:38:38 +00:00
VivianDee 9831395e2a [add]: balance when calculating loan total 2025-06-25 08:23:11 +01:00
Chinenye Nmoh 340132d8ad worked on balance 2025-06-24 22:21:58 +01:00
ameye aa408cc720 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-23 10:42:04 +00:00
Chinenye Nmoh 35e4580313 changed balance to 2 decimal place 2025-06-23 11:09:28 +01:00
CHIEFSOFT\ameye 3d62cbc157 Loan status 2025-06-22 21:50:18 -04:00
CHIEFSOFT\ameye 3785196fb7 salary process apart 2025-06-21 11:55:54 -04:00
CHIEFSOFT\ameye 8249a1b6d8 kafka collect error 2025-06-21 11:11:45 -04:00
CHIEFSOFT\ameye caa77191c7 balace onm payment 2025-06-21 10:43:31 -04:00
CHIEFSOFT\ameye 3f1d87b88a Loan status fix 2025-06-20 22:31:15 -04:00
CHIEFSOFT\ameye 42a3b0fd73 except Exception as e: 2025-06-20 21:55:07 -04:00
CHIEFSOFT\ameye 43055ad010 LoanStatus 2025-06-20 21:51:43 -04:00
CHIEFSOFT\ameye 114a089bdb ["LoanStatus"] 2025-06-20 21:37:20 -04:00
CHIEFSOFT\ameye 48fb58c6ab repayment_data 2025-06-20 21:35:05 -04:00
CHIEFSOFT\ameye 306221f502 repayment_data.LoanStatus 2025-06-20 21:27:53 -04:00
CHIEFSOFT\ameye a310709c5e repayment bug 2025-06-20 21:24:14 -04:00
CHIEFSOFT\ameye a1b1caf5d7 variable bug 2025-06-20 21:22:06 -04:00
CHIEFSOFT\ameye b88a09376b loid 2025-06-20 21:19:33 -04:00
CHIEFSOFT\ameye ee077fb380 loan id 2025-06-20 20:52:08 -04:00
CHIEFSOFT\ameye 4e23ea4e91 customerId 2025-06-20 20:48:44 -04:00
CHIEFSOFT\ameye cb4383c10b loan_dict 2025-06-20 20:46:49 -04:00
CHIEFSOFT\ameye e9c29a8743 "customerId": loan_dict["customer_id"], 2025-06-20 20:45:08 -04:00
CHIEFSOFT\ameye 08a0c8a933 Repayment data 2025-06-20 20:39:37 -04:00
CHIEFSOFT\ameye 6200fc4dba fix ID 2025-06-20 20:23:05 -04:00
CHIEFSOFT\ameye 0d2ce16ff5 enums 2025-06-20 15:48:29 -04:00
CHIEFSOFT\ameye 7b21140b39 Repayment updates 2025-06-20 15:18:04 -04:00
ameye 190cd6611a Merge branch 'add_total_amount' of DigiFi/digifi-EventManager into master 2025-06-20 16:28:39 +00:00
21 changed files with 548 additions and 113 deletions
+6
View File
@@ -8,3 +8,9 @@ DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=10.20.30.60 DATABASE_HOST=10.20.30.60
DATABASE_PORT=5432 DATABASE_PORT=5432
DATABASE_NAME=firstadvancedev DATABASE_NAME=firstadvancedev
# DATABASE_USER=system
#DATABASE_PASSWORD=FIRSTADV_PASS
#DATABASE_HOST=10.10.33.65
#DATABASE_PORT=1521
#DATABASE_SID=FREE
+8 -2
View File
@@ -5,6 +5,12 @@ KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
DATABASE_USER=firstadvance DATABASE_USER=firstadvance
DATABASE_PASSWORD=FirstAdvance! DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=dev-data.simbrellang.net DATABASE_HOST=10.20.30.60
DATABASE_PORT=10532 DATABASE_PORT=5432
DATABASE_NAME=firstadvancedev DATABASE_NAME=firstadvancedev
# DATABASE_USER=system
#DATABASE_PASSWORD=FIRSTADV_PASS
#DATABASE_HOST=10.10.33.65
#DATABASE_PORT=1521
#DATABASE_SID=FREE
+1 -1
View File
@@ -18,4 +18,4 @@ ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0 ENV FLASK_RUN_HOST=0.0.0.0
# Run the application # Run the application
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "wsgi:wsgi_app"] CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--timeout", "120", "wsgi:wsgi_app"]
+5 -1
View File
@@ -1,9 +1,10 @@
from flask import Flask from flask import Flask
from flask_mail import Mail
from flask_cors import CORS from flask_cors import CORS
from app.config import Config from app.config import Config
from app.routes import auth_bp, autocall_bp from app.routes import auth_bp, autocall_bp
from app.response import (method_not_allowed, unsupported_media_type, not_found, bad_request) from app.response import (method_not_allowed, unsupported_media_type, not_found, bad_request)
from app.extensions import db from app.extensions import db, mail
def create_app(): def create_app():
@@ -16,6 +17,9 @@ def create_app():
# Setup CORS # Setup CORS
CORS(app) CORS(app)
# Initialize Flask-Mail
mail.init_app(app)
# Register blueprints # Register blueprints
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(autocall_bp, url_prefix="/autocall") app.register_blueprint(autocall_bp, url_prefix="/autocall")
+18 -2
View File
@@ -19,7 +19,7 @@ class Config:
) )
BANK_CALL_APP_ID = os.getenv("BANK_CALL_APP_ID", "app1") BANK_CALL_APP_ID = os.getenv("BANK_CALL_APP_ID", "app1")
BANK_CALL_API_KEY = os.getenv("BANK_CALL_API_KEY", "test-api-key-12345") BANK_CALL_API_KEY = os.getenv("BANK_CALL_API_KEY", "testtest-api-key-12345")
BANK_CALL_BASIC_AUTH_USERNAME = os.environ.get( BANK_CALL_BASIC_AUTH_USERNAME = os.environ.get(
"BANK_CALL_BASIC_AUTH_USERNAME", "user" "BANK_CALL_BASIC_AUTH_USERNAME", "user"
) )
@@ -32,11 +32,27 @@ class Config:
DATABASE_HOST = os.getenv("DATABASE_HOST") DATABASE_HOST = os.getenv("DATABASE_HOST")
DATABASE_NAME = os.getenv("DATABASE_NAME") DATABASE_NAME = os.getenv("DATABASE_NAME")
DATABASE_PORT = os.getenv("DATABASE_PORT", 10532) DATABASE_PORT = os.getenv("DATABASE_PORT", 10532)
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
#DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
#SQLALCHEMY_DATABASE_URI = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
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_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
# SQLALCHEMY_ECHO = True
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net")
MAIL_SERVER = os.getenv('MAIL_SERVER','smtp.zoho.com')
MAIL_PORT = os.getenv('MAIL_PORT', 587)
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'firstadvance@dynamikservices.tech')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'True').lower() in ('true', '1', 'yes')
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'False').lower() in ('true', '1', 'yes')
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'chinenyeumeaku@gmail.com')
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net/api")
BANK_CALL_SMS_BASE_URL= os.getenv("BANK_CALL_SMS_BASE_URL","https://first-advance-middleware-develop.fbn-devops-dev-asenv.appserviceenvironment.net/SMS") BANK_CALL_SMS_BASE_URL= os.getenv("BANK_CALL_SMS_BASE_URL","https://first-advance-middleware-develop.fbn-devops-dev-asenv.appserviceenvironment.net/SMS")
BANK_CALL_DISBURSE_LOAN_ENDPOINT = os.getenv("BANK_CALL_DISBURSE_LOAN_ENDPOINT","/DisburseLoan") BANK_CALL_DISBURSE_LOAN_ENDPOINT = os.getenv("BANK_CALL_DISBURSE_LOAN_ENDPOINT","/DisburseLoan")
BANK_CALL_COLLECT_LOAN_ENDPOINT = os.getenv("BANK_CALL_COLLECT_LOAN_ENDPOINT","/CollectLoan") BANK_CALL_COLLECT_LOAN_ENDPOINT = os.getenv("BANK_CALL_COLLECT_LOAN_ENDPOINT","/CollectLoan")
+2
View File
@@ -0,0 +1,2 @@
from .transaction_type import TransactionType
from .loan_status import LoanStatus
+8
View File
@@ -0,0 +1,8 @@
from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
ACTIVE_PARTIAL = "active_partial"
START_REPAY = "start_repay"
REPAID = "repaid"
+10
View File
@@ -0,0 +1,10 @@
from enum import Enum
class TransactionType(str, Enum):
ELIGIBILITY_CHECK = "eligibility_check"
CUSTOMER_CONSENT = "customer_consent"
LOAN_STATUS = "loan_status"
NOTIFICATION_CALLBACK = "notification_callback"
PROVIDE_LOAN = "provide_loan"
REPAYMENT = "repayment"
SELECT_OFFER = "select_offer"
+2
View File
@@ -1,3 +1,5 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail
mail = Mail()
db = SQLAlchemy() db = SQLAlchemy()
+2 -1
View File
@@ -146,10 +146,11 @@ class KafkaIntegration:
logger.info(f"Calling collect_loan service with message: {message}") logger.info(f"Calling collect_loan service with message: {message}")
try: try:
#Calling CollectLoan endpoint with data: {'transactionId': 'TRCVIC85641527829', 'customerId': 'ZX48440946', 'productId': 'AMPC', 'loanRef': 'TRCVIC85641527829USSDAMPC', 'debtId': '014231'}
response = SimbrellaClient.collect_loan_user_initiated(message) response = SimbrellaClient.collect_loan_user_initiated(message)
logger.info( logger.info(
f"Successfully sent message to collect_loan service: {response}" f"Successfully sent message to collect_loan service: {response}"
) )
except Exception as e: except Exception as e:
logger.info(f"Failed to call collect_loan service: {e}") logger.error(f"Failed to call collect_loan service: {e}")
# raise # raise
+152 -62
View File
@@ -4,6 +4,10 @@ from app.helpers.response_helper import ResponseHelper
from app.services.loan import LoanService from app.services.loan import LoanService
from app.utils.auth import get_headers from app.utils.auth import get_headers
from app.utils.extras import preprocess_loan_charges_data from app.utils.extras import preprocess_loan_charges_data
import random
import random
import string
from app.extensions import db
from app.utils.logger import logger from app.utils.logger import logger
from flask import jsonify, current_app from flask import jsonify, current_app
from app.services.transactions import TransactionService from app.services.transactions import TransactionService
@@ -11,6 +15,12 @@ from app.services.repayment import RepaymentService
from app.extensions import db from app.extensions import db
from app.services.repayments_data import RepaymentsData from app.services.repayments_data import RepaymentsData
from app.services.salary import SalaryService from app.services.salary import SalaryService
from app.enums.loan_status import LoanStatus
from decimal import Decimal, ROUND_HALF_UP
from requests.exceptions import SSLError, RequestException,Timeout
import sys
from requests.exceptions import ReadTimeout, ConnectTimeout
import socket
class SimbrellaClient: class SimbrellaClient:
@@ -23,7 +33,7 @@ class SimbrellaClient:
@staticmethod @staticmethod
def disburse_loan(data): def disburse_loan(data):
api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/{SimbrellaClient.BANK_CALL_DISBURSE_LOAN_ENDPOINT}" api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}{SimbrellaClient.BANK_CALL_DISBURSE_LOAN_ENDPOINT}"
logger.info(f"Calling DisburseLoan api_url==> : {api_url}") logger.info(f"Calling DisburseLoan api_url==> : {api_url}")
logger.info(f"Calling DisburseLoan endpoint with data: {data}") logger.info(f"Calling DisburseLoan endpoint with data: {data}")
@@ -68,9 +78,10 @@ class SimbrellaClient:
insurance_fee = loan_charges.get("INSURANCE")['amount'] insurance_fee = loan_charges.get("INSURANCE")['amount']
debtId = str(loan_data.get('debtId', "")).strip().zfill(6) debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
disbursement_data = { disbursement_data = {
"transactionId": loan_data.get('transactionId'), "transactionId": t_id,
"FbnTransactionId": loan_data.get('transactionId'), "FbnTransactionId": loan_data.get('transactionId'),
"debtId": debtId, "debtId": debtId,
"customerId": loan_data.get('customerId'), "customerId": loan_data.get('customerId'),
@@ -87,8 +98,12 @@ class SimbrellaClient:
try: try:
logger.info(f"Here is your Disbursement Request data ****** : {disbursement_data}") logger.info(f"Here is your Disbursement Request data ****** : {disbursement_data}")
response = requests.post(api_url, json=disbursement_data, timeout=10, headers=get_headers()) response = requests.post(api_url, json=disbursement_data, timeout=90, headers=get_headers())
if response.status_code == 404:
logger.error("Received 404 from external service")
return ResponseHelper.error("Disbursement Service url not found (404)", status_code=404)
logger.info(f"Disbursement response: {response.json()}") logger.info(f"Disbursement response: {response.json()}")
result = response.json() result = response.json()
LoanService.set_disbursement_result(loan_data['debtId'],result.get('responseCode', ''), result.get('responseMessage', '')) LoanService.set_disbursement_result(loan_data['debtId'],result.get('responseCode', ''), result.get('responseMessage', ''))
return ResponseHelper.success(response.json(), "Successful") return ResponseHelper.success(response.json(), "Successful")
@@ -152,6 +167,9 @@ class SimbrellaClient:
try: try:
logger.info(f"Here is your TransactionVerify Request data ****** : {verify_data}") logger.info(f"Here is your TransactionVerify Request data ****** : {verify_data}")
response = requests.post(api_url, json=verify_data, timeout=10, headers=get_headers()) response = requests.post(api_url, json=verify_data, timeout=10, headers=get_headers())
if response.status_code == 404:
logger.error("Received 404 from external service")
return ResponseHelper.error("Verify Service url not found (404)", status_code=404)
result = response.json() result = response.json()
logger.info(f"this is verify result, {result}") logger.info(f"this is verify result, {result}")
LoanService.set_disburse_verify_result(loan_data['debtId'],result.get('responseCode', ''), result.get('responseMessage', '')) LoanService.set_disburse_verify_result(loan_data['debtId'],result.get('responseCode', ''), result.get('responseMessage', ''))
@@ -161,8 +179,9 @@ class SimbrellaClient:
"unicode": True "unicode": True
} }
try: try:
sms_response = requests.post(sms_url, json=sms_data, timeout=10, headers=get_headers()) sms_response = requests.post(sms_url, json=sms_data, timeout=90, headers=get_headers())
sms_response.raise_for_status() # Raise an exception for 4xx or 5xx status codes sms_response.raise_for_status() # Raise an exception for 4xx or 5xx status codes
result = sms_response.json() result = sms_response.json()
logger.info(f"SMS Response JSON: {result}") logger.info(f"SMS Response JSON: {result}")
if result.get('isSuccess'): if result.get('isSuccess'):
@@ -176,13 +195,22 @@ class SimbrellaClient:
return 0 return 0
except Exception as e: except Exception as e:
logger.info(f"Failed to call TransactionVerify endpoint: {e}") logger.info(f"Failed to call TransactionVerify endpoint: {e}")
return 0 return ResponseHelper.error("Unexpected error while processing loan disbursement", status_code=500, error=str(e))
@staticmethod @staticmethod
def collect_loan_user_initiated(data): def collect_loan_user_initiated(data):
# InitiatedBy = USER_INITIATED # InitiatedBy = USER_INITIATED
logger.info(f"Calling CollectLoan collect_loan_user_initiated ******* endpoint with data: {data}") try:
return SimbrellaClient._collect_loan(data,"1") logger.info(f"Calling CollectLoan collect_loan_user_initiated ******* endpoint with data: {data}")
return SimbrellaClient._collect_loan(data, "1")
except Exception as e:
logger.error(f"Error in collect_loan_user_initiated: {e}")
# return ResponseHelper.error(
# message="Failed to collect loan for user initiated ",
# status_code=500,
# error=str(e)
# )
@staticmethod @staticmethod
def collect_loan_user_salary_detect(data): def collect_loan_user_salary_detect(data):
try: try:
@@ -201,53 +229,46 @@ class SimbrellaClient:
# InitiatedBy = REPAYMENT_DUE # InitiatedBy = REPAYMENT_DUE
return SimbrellaClient._collect_loan(data,"3") return SimbrellaClient._collect_loan(data,"3")
@staticmethod @staticmethod
def _collect_loan(data, collectionMethod: str): def _collect_loan(data, collectionMethod: str):
api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}{SimbrellaClient.BANK_CALL_COLLECT_LOAN_ENDPOINT}" api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}{SimbrellaClient.BANK_CALL_COLLECT_LOAN_ENDPOINT}"
logger.info(f"Calling CollectLoan api_url==> : {api_url}") logger.info(f"Calling CollectLoan api_url==> : {api_url}")
logger.info(f"Calling CollectLoan endpoint with data: {data}") logger.info(f"Calling CollectLoan endpoint with data: {data}")
# Check if the repayment exists
logger.info(f"Checking if repayment exists")
repayment = RepaymentService.get_repayment_by_id(id=data['Id']) repayment = RepaymentService.get_repayment_by_id(id=data['Id'])
logger.info(f"Repayment Response From Database ** : {repayment}")
if not repayment: if not repayment:
logger.info(f"Repayment with id: {data['Id']}, was not found") logger.info(f"Repayment with id: {data['Id']} not found")
return ResponseHelper.error("Repayment not found") return ResponseHelper.error("Repayment not found")
logger.info(f"Repayment Response From Database ** : {repayment.to_dict()}")
repayment_data = repayment.to_dict() repayment_data = repayment.to_dict()
loan = LoanService.get_loan_by_loan_id(loan_id=int(repayment_data['loanId'])) loan = LoanService.get_loan_by_loan_id(loan_id=int(repayment_data['loanId']))
if not loan:
logger.info(f"Loan with debtId: {repayment_data['loanId']} not found")
# If loan is not found
if loan is None:
logger.info(f"Loan with debtId: {repayment_data['loanId']}, was not found")
return ResponseHelper.error("Loan not found") return ResponseHelper.error("Loan not found")
loan_data = loan.to_dict() loan_data = loan.to_dict()
logger.info(f"loan dict : {loan_data}") logger.info(f"Loan data: {loan_data}")
if repayment_data['repayDate'] is not None: if repayment_data['repayDate'] is not None:
logger.info( logger.info(f"Repayment already processed at {repayment_data['repayDate']}")
f"Please call verify collection : {data['transactionId']} repayment send for processing at {repayment_data['repayDate']}")
return ResponseHelper.error("Repayment already processed") return ResponseHelper.error("Repayment already processed")
# let us set repay date
RepaymentService.set_repay_date(repayment_data['Id'], repayment_data['customerId']) RepaymentService.set_repay_date(repayment_data['Id'], repayment_data['customerId'])
repayment = RepaymentService.get_repayment_by_transaction_id(transaction_id=data['transactionId']) repayment = RepaymentService.get_repayment_by_transaction_id(transaction_id=data['transactionId'])
repayment_data = repayment.to_dict() repayment_data = repayment.to_dict()
logger.info(f"Here is your repayment data after setting repay date: {repayment_data}")
debtId = str(loan_data.get('debtId', "")).strip().zfill(6) debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
collect_loan_data = { collect_loan_data = {
"transactionId": repayment_data['transactionId'], "transactionId": t_id,
"fbnTransactionId": loan_data['transactionId'], "fbnTransactionId": loan_data['transactionId'],
"debtId": debtId, "debtId": debtId,
"customerId": repayment_data['customerId'], "customerId": repayment_data['customerId'],
"accountId": loan_data['accountId'], "accountId": loan_data['accountId'],
"productId": repayment_data['productId'], "productId": repayment_data['productId'],
"collectAmount": loan_data['repaymentAmount'] or 0, "collectAmount": loan_data['balance'] or 0,
"penalCharge": 5, "penalCharge": 0,
"channel": "USSD", "channel": "USSD",
"collectionMethod": collectionMethod, "collectionMethod": collectionMethod,
"lienAmount": 0, "lienAmount": 0,
@@ -256,59 +277,128 @@ class SimbrellaClient:
} }
try: try:
logger.info(f"Here is your CollectLoan Request data ***** : {collect_loan_data}") logger.info(f"Sending CollectLoan request............ {collect_loan_data}")
response = requests.post(api_url, json=collect_loan_data,timeout=30, headers=get_headers()) response = requests.post(api_url, json=collect_loan_data, timeout=90, headers=get_headers())
logger.info(f"HTTP response object: {response}")
if response.status_code == 404:
db.session.rollback()
RepaymentService.set_repay_result(
repayment_data['Id'],
'404',
'Collection Service url not found'
)
logger.error("Received 404 from external service")
return ResponseHelper.error("Collection Service URL not found", status_code=404)
logger.info(f"CollectLoan response: {response.json()}")
RepaymentService.set_repay_result(repayment_data['Id'], response.json().get('responseCode', ''), response.json().get('responseMessage', ''))
result = response.json() result = response.json()
logger.info(f"this is the result {result}") logger.info(f"CollectLoan response: {result}")
RepaymentService.set_repay_result(
repayment_data['Id'],
result.get('responseCode', ''),
result.get('responseMessage', '')
)
data_to_add = { data_to_add = {
"transactionId": result.get('transactionId') or collect_loan_data.get('transactionId'), "transactionId": result.get('transactionId') or collect_loan_data.get('transactionId'),
"fbnTransactionId": result.get('fbnTransactionId') or collect_loan_data.get('fbnTransactionId'), "fbnTransactionId": loan_data['transactionId'],
"accountId": result.get('accountId') or collect_loan_data.get('accountId'), "accountId": result.get('accountId') or collect_loan_data.get('accountId'),
"customerId": result.get('customerId') or collect_loan_data.get('customerId'), "customerId": result.get('customerId') or collect_loan_data.get('customerId'),
"amountCollected": result.get('amountCollected'), "amountCollected": float(result.get('amountCollected', 0)),
"repaymentAmount": collect_loan_data.get('collectAmount'), "repaymentAmount": collect_loan_data.get('collectAmount'),
"responseCode": result.get('responseCode'), "responseCode": result.get('responseCode'),
"responseDescr": result.get('responseMessage'), "responseDescr": result.get('responseMessage'),
"balance":result.get('lienAmount') "balance": round(float(result.get('lienAmount', 0)), 2)
} }
new_repayment_data = RepaymentsData.add_repayment_data(data_to_add) new_repayment_data = RepaymentsData.add_repayment_data(data_to_add)
logger.info(f"Repayment data added successfully: {new_repayment_data.to_dict()}") if new_repayment_data:
if not new_repayment_data: logger.info(f"Repayment data added: {new_repayment_data.to_dict()}")
logger.info(f"Failed to add repayment data") else:
logger.warning("Failed to add repayment data")
return ResponseHelper.success(response.json(), "Successful") if result.get('responseCode') == '00':
except Exception as e: amount_collected = Decimal(str(result.get('amountCollected', 0))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
logger.info(f"Failed to call CollectLoan endpoint: {e}") logger.info(f"Amount collected: {amount_collected}")
return ResponseHelper.error("Failed to call CollectLoan endpoint")
if loan.balance is None or loan.balance <= 0:
logger.warning(f"Loan ID {loan.id} has no balance. Skipping loan update.")
return ResponseHelper.error("Loan has no balance. Skipping.")
try:
logger.info(f"Updating loan balance for loan ID {loan_data['debtId']} with amount collected: {amount_collected}")
updated_loan = LoanService.update_loan_balance(int(loan_data['debtId']), amount_collected)
logger.info(f"Updated loan: {updated_loan}")
except Exception as ex:
db.session.rollback()
logger.error(f"Error updating loan balance for loan ID {loan.id}: {ex}")
return ResponseHelper.error("Error updating loan balance")
try:
updated_balance = Decimal(str(updated_loan['balance'])).quantize(Decimal('0.01'))
logger.info(f"Updated balance: {updated_balance}")
if updated_balance <= Decimal('0.00'):
logger.info('Loan fully repaid')
repaid = LoanService.update_status(updated_loan['debtId'], LoanStatus.REPAID)
logger.info(f'Updated loan with repaid status: {repaid}')
else:
logger.info('Loan partially repaid')
partial = LoanService.update_status(updated_loan['debtId'], LoanStatus.ACTIVE_PARTIAL)
logger.info(f'Updated loan with partial status: {partial}')
except Exception as e:
db.session.rollback()
logger.error(f"Error while updating loan status for debtId {updated_loan['debtId']}: {e}")
return ResponseHelper.success(result, "Successful")
except SSLError as ssl_err:
db.session.rollback()
logger.exception(f"SSL error while calling Simbrella endpoint: {ssl_err}")
return ResponseHelper.error("SSL handshake failed with Simbrella", status_code=502, error=str(ssl_err))
except (Timeout, ReadTimeout, ConnectTimeout, socket.timeout, TimeoutError) as timeout_err:
db.session.rollback()
logger.exception(f"Timeout while calling Simbrella: {timeout_err}")
RepaymentService.set_repay_result(
repayment_data['Id'],
'500',
'There was a timeout while calling Simbrella'
)
return ResponseHelper.error("Connection to Simbrella timed out", status_code=504, error=str(timeout_err))
except RequestException as req_err:
db.session.rollback()
logger.exception(f"RequestException while calling Simbrella: {req_err}")
RepaymentService.set_repay_result(
repayment_data['Id'],
'500',
'There was a request error while calling Simbrella'
)
return ResponseHelper.error("Connection to Simbrella failed", status_code=503, error=str(req_err))
except SystemExit as sys_exit:
db.session.rollback()
logger.error(f"SystemExit was triggered: {sys_exit}")
RepaymentService.set_repay_result(
repayment_data['Id'],
'500',
'There was a system error while calling Simbrella'
@staticmethod )
def refresh_disbursement(data): return ResponseHelper.error("Unexpected shutdown detected", status_code=500, error=str(sys_exit))
try:
logger.info(f"Here is your Disbursement Request data ***** : {data}")
return ResponseHelper.success(data, "Successful")
except Exception as e: except Exception as e:
logger.info(f"Failed to call Disbursement endpoint: {e}") db.session.rollback()
raise logger.exception(f"Unexpected error occurred while calling CollectLoan: {e}")
RepaymentService.set_repay_result(
repayment_data['Id'],
'500',
'Unexpected error while processing loan collection'
)
return ResponseHelper.error("Unexpected error while processing loan collection", status_code=500, error=str(e))
@staticmethod
def payment_callback(data):
try:
logger.info(f"Here is your Payment Callback Request data ***** : {data}")
return ResponseHelper.success(data, "Successful")
except Exception as e:
logger.info(f"Failed to call Payment Callback endpoint: {e}")
raise
@staticmethod @staticmethod
def penal_charge(data): def penal_charge(data):
+101 -3
View File
@@ -1,5 +1,4 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import timedelta from datetime import timedelta
@@ -8,6 +7,8 @@ from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.utils.logger import logger from app.utils.logger import logger
from app.extensions import db from app.extensions import db
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, timezone
class Loan(db.Model): class Loan(db.Model):
__tablename__ = "loans" __tablename__ = "loans"
@@ -30,6 +31,7 @@ class Loan(db.Model):
continuous_fee = db.Column(db.Float, default=0) continuous_fee = db.Column(db.Float, default=0)
upfront_fee = db.Column(db.Float, nullable=True, default=0.0) upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
repayment_amount = 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) installment_amount = db.Column(db.Float, nullable=True, default=0.0)
status = db.Column(db.String(20), default='pending') status = db.Column(db.String(20), default='pending')
tenor = db.Column(db.Integer, nullable=True) tenor = db.Column(db.Integer, nullable=True)
@@ -87,7 +89,9 @@ class Loan(db.Model):
'loanDate': self.created_at.isoformat() if self.created_at else None, 'loanDate': self.created_at.isoformat() if self.created_at else None,
'disburseDate': self.disburse_date.isoformat() if self.disburse_date else None, 'disburseDate': self.disburse_date.isoformat() if self.disburse_date else None,
'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None, 'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None,
'reference': self.reference 'reference': self.reference,
'balance': self.balance,
'tenor': self.tenor,
} }
@classmethod @classmethod
@@ -230,7 +234,7 @@ class Loan(db.Model):
raise ValueError(f"Customer with Id {customer_id} does not have any loan.") raise ValueError(f"Customer with Id {customer_id} does not have any loan.")
total_amount = ( total_amount = (
db.session.query(func.coalesce(func.sum(cls.current_loan_amount), 0.0)) cls.query.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
.filter_by(customer_id=customer_id) .filter_by(customer_id=customer_id)
.scalar() .scalar()
) )
@@ -238,3 +242,97 @@ class Loan(db.Model):
logger.info(f"Found {len(customer_loans)} loans for customer ID: {customer_id} with total amount: {total_amount}") logger.info(f"Found {len(customer_loans)} loans for customer ID: {customer_id} with total amount: {total_amount}")
return customer_loans, total_amount return customer_loans, total_amount
@classmethod
def get_customer_active_loans(cls, customer_id):
"""
Get customer's active loans and sum by customer_id.
"""
customer_loans = cls.query.filter(
cls.customer_id == customer_id,
cls.status != 'repaid'
).all()
if not customer_loans:
raise ValueError(f"Customer with Id {customer_id} does not have any active loan.")
total_amount = (
cls.query
.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
.filter(
cls.customer_id == customer_id,
cls.status != 'repaid'
)
.scalar()
)
logger.info(f"Found {len(customer_loans)} active loans for customer ID: {customer_id} with total amount: {total_amount}")
return customer_loans, total_amount
@classmethod
def update_status(cls, loan_id, status):
"""
Update the status of the loan record with the given loan_id.
"""
try:
# Retrieve loan record
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
if loan.status == status:
return loan.to_dict() # Still return the current state if no change
# Update status and timestamp
loan.status = status
loan.updated_at = datetime.now(timezone.utc)
db.session.commit()
logger.info("Loan status updated and committed.")
return loan.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error updating loan status: {e}")
raise Exception(f"Error updating loan status: {str(e)}")
@classmethod
def update_loan_balance(cls, loan_id, amount_collected):
"""
Update the balance of a loan after successful repayment.
"""
try:
# Fetch the loan record
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
# Convert to Decimal and round to 2 decimal places
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
balance = Decimal(str(loan.balance or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
# Ensure valid repayment amount
if amount_collected <= Decimal("0.00"):
raise ValueError("Repayment amount must be greater than zero.")
if balance <= Decimal("0.00"):
raise ValueError("There is no balance for this loan.")
if amount_collected > balance:
raise ValueError("Repayment amount exceeds current loan balance.")
# Deduct the amount from the current balance
new_balance = balance - amount_collected
loan.balance = float(new_balance)
loan.updated_at = datetime.now(timezone.utc)
db.session.commit()
logger.info(f"Loan balance updated for loan ID {loan_id}. New balance: {loan.balance}")
return loan.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error updating loan balance: {e}")
raise Exception(f"Error updating loan balance: {str(e)}")
+33 -2
View File
@@ -1,6 +1,9 @@
from app.extensions import db from app.extensions import db
from datetime import datetime, timezone from datetime import datetime, timezone
from app.utils.logger import logger from app.utils.logger import logger
from app.enums.loan_status import LoanStatus
from sqlalchemy.exc import IntegrityError
class Repayment(db.Model): class Repayment(db.Model):
__tablename__ = "repayments" __tablename__ = "repayments"
@@ -47,6 +50,32 @@ class Repayment(db.Model):
'VerifyDate': self.verify_date.isoformat() if self.verify_date else None, 'VerifyDate': self.verify_date.isoformat() if self.verify_date else None,
} }
@classmethod
def create_repayment(cls, repayment_data):
if repayment_data["LoanStatus"] not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY,LoanStatus.ACTIVE_PARTIAL]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({repayment_data['LoanStatus']})")
repayment = cls(
customer_id=repayment_data["customerId"],
loan_id=repayment_data["loanId"],
product_id=repayment_data["productId"],
transaction_id=repayment_data["transactionId"],
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
initiated_by= repayment_data["initiatedBy"],
salary_amount=repayment_data["salaryAmount"]
)
try:
db.session.add(repayment)
db.session.commit()
logger.info("Repayment record committed.")
return repayment
except IntegrityError as err:
logger.error(f"Database integrity error: {err}")
return {"error": "Integrity error", "details": str(err)}
@classmethod @classmethod
def add_repayment(cls, data: dict): def add_repayment(cls, data: dict):
@@ -121,10 +150,11 @@ class Repayment(db.Model):
try: try:
logger.info(f"Updating repay date for repayment ID {repayment_id} to {current_time}") logger.info(f"Updating repay date for repayment ID {repayment_id} to {current_time}")
db.session.commit() db.session.commit()
return repayment.to_dict()
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger.error(f"Failed to update repay date: {e}") logger.error(f"Failed to update repay date: {e}")
raise raise e
@classmethod @classmethod
def set_repay_verify_date(cls, repayment_id, customer_id): def set_repay_verify_date(cls, repayment_id, customer_id):
""" """
@@ -152,11 +182,12 @@ class Repayment(db.Model):
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger.error(f"Failed to update repay verify date: {e}") logger.error(f"Failed to update repay verify date: {e}")
raise raise e
@classmethod @classmethod
def set_repay_result(cls, repayment_id, result, description): def set_repay_result(cls, repayment_id, result, description):
logger.info("repay result called")
""" """
Update the repayment result and description of the repayment with the given repayment_id. Update the repayment result and description of the repayment with the given repayment_id.
""" """
+18 -4
View File
@@ -25,7 +25,7 @@ class RepaymentsData(db.Model):
"response_code": self.response_code, "response_code": self.response_code,
"response_descr": self.response_descr, "response_descr": self.response_descr,
"customerId": self.customer_id, "customerId": self.customer_id,
"accountId": self.customer_id, "accountId": self.account_id,
"fbnTransactionId": self.fbn_transaction_id, "fbnTransactionId": self.fbn_transaction_id,
"repaymentAmount": self.repayment_amount, "repaymentAmount": self.repayment_amount,
"amountCollected": self.amount_collected, "amountCollected": self.amount_collected,
@@ -42,6 +42,14 @@ class RepaymentsData(db.Model):
Add a new repayment data entry. Add a new repayment data entry.
""" """
try: try:
repayment_amount = float(data.get('repaymentAmount', 0.0))
amount_collected = float(data.get('amountCollected', 0.0))
if amount_collected < 0 or repayment_amount < 0:
raise ValueError("Amounts cannot be negative.")
account_balance = round(repayment_amount - amount_collected, 2)
new_data = cls( new_data = cls(
transaction_id=data.get('transactionId'), transaction_id=data.get('transactionId'),
response_code=data.get('responseCode'), response_code=data.get('responseCode'),
@@ -49,13 +57,19 @@ class RepaymentsData(db.Model):
fbn_transaction_id=data.get('fbnTransactionId'), fbn_transaction_id=data.get('fbnTransactionId'),
account_id=data.get('accountId'), account_id=data.get('accountId'),
customer_id=data.get('customerId'), customer_id=data.get('customerId'),
amount_collected=data.get('amountCollected'), amount_collected=amount_collected,
repayment_amount=data.get('repaymentAmount'), repayment_amount=repayment_amount,
balance=account_balance,
) )
db.session.add(new_data) db.session.add(new_data)
db.session.commit() db.session.commit()
logger.info(f"data has been commited ")
logger.info("Repayment data committed successfully")
return new_data return new_data
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger.error(f"Error adding repayment data: {e}")
raise Exception(f"Error adding repayment data: {str(e)}") raise Exception(f"Error adding repayment data: {str(e)}")
+1 -1
View File
@@ -41,7 +41,7 @@ class Salary(db.Model):
""" """
Add a new salary data entry. Add a new salary data entry.
""" """
logger.info(f"receieved data:{data}") logger.info(f"Received data:{data}")
try: try:
new_data = cls( new_data = cls(
customer_id=data.get('customerId'), customer_id=data.get('customerId'),
+86 -22
View File
@@ -1,5 +1,6 @@
from flask import Blueprint, request, jsonify, current_app from flask import Blueprint, request, jsonify, current_app
import requests import requests
from app.extensions import db
from app.config import settings from app.config import settings
from app.helpers.response_helper import ResponseHelper from app.helpers.response_helper import ResponseHelper
from app.utils.auth import get_headers from app.utils.auth import get_headers
@@ -8,6 +9,9 @@ from app.integrations.simbrella import SimbrellaClient
from app.services.loan import LoanService from app.services.loan import LoanService
from app.services.repayment import RepaymentService from app.services.repayment import RepaymentService
from app.services.salary import SalaryService from app.services.salary import SalaryService
from app.enums.loan_status import LoanStatus
from app.utils.mail import send_report_email, get_report_data
from app.config import settings
autocall_bp = Blueprint("autocall", __name__) autocall_bp = Blueprint("autocall", __name__)
@@ -127,13 +131,31 @@ def salary_detect():
# Step 1: Try to add new salary data # Step 1: Try to add new salary data
try: try:
new_salary = SalaryService.add_salary_data(payload) new_salary = SalaryService.add_salary_data(payload) # TODO - This will come as array of salaries - not just one
if new_salary: if new_salary:
logger.info(f"Salary added: {new_salary.id}") logger.info(f"Salary added: {new_salary.id}")
except Exception as e: except Exception as e:
logger.error(f"Failed to save salary: {e}") logger.error(f"Failed to save salary: {e}")
# Step 2: Get all pending salaries # Step 2: Try processing salary list
try:
process_salary_list()
except Exception as e:
logger.exception("Unhandled error occurred while processing salary list {e}")
return ResponseHelper.error("Failed to process salary list", status_code=500, error=str(e))
logger.info("Finished processing List")
return ResponseHelper.success([], "AutoCall Add Salary Successful")
@autocall_bp.route("/analytic-salary-process", methods=["POST"])
def salary_process():
response = process_salary_list()
return ResponseHelper.success([], "AutoCall Successful")
def process_salary_list():
# Step 1: Get all pending salaries
pending_salaries = SalaryService.get_pending_salaries() pending_salaries = SalaryService.get_pending_salaries()
if not pending_salaries: if not pending_salaries:
logger.info("No pending salaries found") logger.info("No pending salaries found")
@@ -141,54 +163,96 @@ def salary_detect():
logger.info(f"Found {len(pending_salaries)} pending salaries to process") logger.info(f"Found {len(pending_salaries)} pending salaries to process")
# Step 3: Process each salary
for pending_salary in pending_salaries: for pending_salary in pending_salaries:
logger.info(f"Processing salary ID: {pending_salary.id}") logger.info(f"Processing salary ID: {pending_salary.id}")
# Step 3.1: Update status to PROCESSING # Step 2: Update salary status to PROCESSING
try: try:
SalaryService.update_status(pending_salary.id, "PROCESSING") SalaryService.update_status(pending_salary.id, "PROCESSING")
except Exception as e: except Exception as e:
db.session.rollback()
logger.warning(f"Could not update status for salary ID {pending_salary.id}: {e}") logger.warning(f"Could not update status for salary ID {pending_salary.id}: {e}")
continue continue
# Step 3.2: Get loans # Step 3: Get customer's active loans
try: try:
loans, total_amount = LoanService.get_customer_loans(pending_salary.customer_id) loans, total_amount = LoanService.get_customer_active_loans(pending_salary.customer_id)
if not loans: if not loans:
logger.warning(f"No loans found for customer ID: {pending_salary.customer_id}") logger.warning(f"No loans found for customer ID: {pending_salary.customer_id}")
continue continue
except Exception as e: except Exception as e:
db.session.rollback()
logger.error(f"Error fetching loans for customer ID {pending_salary.customer_id}: {e}") logger.error(f"Error fetching loans for customer ID {pending_salary.customer_id}: {e}")
continue continue
# Step 3.3: Create repayments # Step 4: Create repayments for each loan
for loan in loans: for loan in loans:
logger.info(f"Processing Loan ID: {loan.id}")
try: try:
loan_dict = loan.to_dict()
repayment_data = { repayment_data = {
"customerId": pending_salary.customer_id, "customerId": loan.customer_id,
"loanId": loan_dict["debtId"], "loanId": loan.id,
"productId": loan_dict["productId"], "productId": loan.product_id,
"transactionId": loan_dict["transactionId"], "transactionId": loan.transaction_id,
"initiatedBy": "SALARY_DETECT", "initiatedBy": "SALARY_DETECT",
"salaryAmount": pending_salary.amount, "salaryAmount": pending_salary.amount,
"LoanStatus": loan.status,
} }
logger.info(f"Creating repayment for loan ID {loan_dict['debtId']}")
repayment = RepaymentService.add_repayment(repayment_data) logger.info(f"Creating repayment with data: {repayment_data}")
repayment = RepaymentService.create_repayment(repayment_data)
if not repayment or isinstance(repayment, dict) and "error" in repayment:
db.session.rollback() # important in case create_repayment failed mid-way
logger.error(f"Repayment creation failed for loan ID {loan.id}: {repayment}")
continue
try:
LoanService.update_status(loan_id=loan.id, status=LoanStatus.START_REPAY)
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update loan status for loan ID {loan.id}: {e}")
logger.info(f"Created repayment ID: {repayment.id}") logger.info(f"Created repayment ID: {repayment.id}")
# Step 5: Call Simbrella
try:
simbrella_response = SimbrellaClient.collect_loan_user_salary_detect(repayment.to_dict())
if isinstance(simbrella_response, tuple):
simbrella_response, status_code = simbrella_response
logger.warning(f"Simbrella returned tuple: status={status_code}, response={simbrella_response}")
if isinstance(simbrella_response, dict):
if simbrella_response.get("status") != "success":
logger.warning(f"Simbrella failed for repayment ID {repayment.id}: {simbrella_response}")
else:
logger.warning(f"Unexpected Simbrella response: {type(simbrella_response)}")
except Exception as e:
logger.error(f"Failed to call Simbrella for repayment ID {repayment.id}: {e}")
except Exception as e: except Exception as e:
db.session.rollback()
logger.error(f"Error creating repayment for loan ID {loan.id}: {e}") logger.error(f"Error creating repayment for loan ID {loan.id}: {e}")
continue continue
# Step 4: Simbrella integration call after all processing
try:
SimbrellaClient.collect_loan_user_salary_detect(repayment.to_dict())
except Exception as e:
logger.error(f"Failed to call Simbrella client: {e}")
logger.info(f"Finished processing salary ID: {pending_salary.id}") logger.info(f"Finished processing salary ID: {pending_salary.id}")
return ResponseHelper.success([], "AutoCall Successful")
return ResponseHelper.success([], "Processed all pending salaries")
@autocall_bp.route("/report", methods=["GET"])
def report():
try:
report_data = get_report_data()
logger.info(f"Generated report data: {report_data}")
send_report_email(
report_data,
recipients = [email.strip() for email in settings.MAIL_RECEIVER.split(",")])
logger.info(f"Report sent successfully")
return ResponseHelper.success(message="Report sent successfully",status_code=200)
except Exception as e:
logger.error(f"Error generating or sending report: {e}")
return ResponseHelper.error("Failed to send report", status_code=500, error=str(e))
+21
View File
@@ -78,3 +78,24 @@ class LoanService:
""" """
return Loan.get_customer_loans(customer_id=customer_id) return Loan.get_customer_loans(customer_id=customer_id)
@classmethod
def get_customer_active_loans(cls, customer_id):
"""
Get customer's active loans by customer_id.
"""
return Loan.get_customer_active_loans(customer_id=customer_id)
@classmethod
def update_status(cls, loan_id, status):
"""
Update the status of the loan with the given loan_id.
"""
# Retrieve loan
return Loan.update_status(loan_id, status)
@classmethod
def update_loan_balance(cls,loan_id,amount_collected):
"""
update the loan balance after successful repayment
"""
return Loan.update_loan_balance(loan_id,amount_collected)
+8
View File
@@ -62,9 +62,17 @@ class RepaymentService:
Get the latest repayment with a repay date and no verification date. Get the latest repayment with a repay date and no verification date.
""" """
return Repayment.get_latest_loan_with_repay_date() return Repayment.get_latest_loan_with_repay_date()
@classmethod @classmethod
def add_repayment(cls, data): def add_repayment(cls, data):
""" """
Add a new repayment entry. Add a new repayment entry.
""" """
return Repayment.add_repayment(data) return Repayment.add_repayment(data)
@classmethod
def create_repayment(cls, repayment_data):
"""
Add a new repayment entry.
"""
return Repayment.create_repayment(repayment_data)
+42
View File
@@ -0,0 +1,42 @@
from flask_mail import Message
from flask import current_app
from app.extensions import mail
import pandas as pd
from io import BytesIO
from app.utils.logger import logger
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"
+10 -2
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: Event Manager API title: Eco Integration Event Manager API
description: The documentation for Event Manager API description: The documentation for Eco Event Manager API
version: 1.0.0 version: 1.0.0
contact: contact:
name: API Support name: API Support
@@ -201,3 +201,11 @@ paths:
responses: responses:
200: 200:
description: A successful response description: A successful response
/autocall/report:
get:
summary: Generate and send a report
responses:
200:
description: A successful response
+4
View File
@@ -10,3 +10,7 @@ flask-sqlalchemy
psycopg2-binary psycopg2-binary
alembic alembic
python-dateutil python-dateutil
oracledb
Flask-Mail==0.10.0
pandas==2.1.3
openpyxl==3.1.5