25 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
Chinenye Nmoh 340132d8ad worked on balance 2025-06-24 22:21:58 +01:00
15 changed files with 447 additions and 127 deletions
+7 -1
View File
@@ -7,4 +7,10 @@ DATABASE_USER=firstadvance
DATABASE_PASSWORD=FirstAdvance! 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
+9 -3
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():
@@ -15,6 +16,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)
+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
@@ -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()
+142 -61
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,7 +195,7 @@ 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):
@@ -210,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,
@@ -265,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}")
logger.info(f"CollectLoan response: {response.json()}")
RepaymentService.set_repay_result(repayment_data['Id'], response.json().get('responseCode', ''), response.json().get('responseMessage', '')) 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)
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")
except Exception as e:
logger.info(f"Failed to call CollectLoan endpoint: {e}")
return ResponseHelper.error("Failed to call CollectLoan endpoint")
if result.get('responseCode') == '00':
amount_collected = Decimal(str(result.get('amountCollected', 0))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
logger.info(f"Amount collected: {amount_collected}")
@staticmethod if loan.balance is None or loan.balance <= 0:
def refresh_disbursement(data): logger.warning(f"Loan ID {loan.id} has no balance. Skipping loan update.")
return ResponseHelper.error("Loan has no balance. Skipping.")
try: try:
logger.info(f"Here is your Disbursement Request data ***** : {data}") 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")
return ResponseHelper.success(data, "Successful") 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'
)
return ResponseHelper.error("Unexpected shutdown detected", status_code=500, error=str(sys_exit))
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):
+91 -12
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
@@ -7,7 +6,9 @@ import logging
from sqlalchemy import and_, or_, not_ 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"
@@ -88,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
@@ -239,21 +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 @classmethod
def update_status(cls, loan_id, status): def update_status(cls, loan_id, status):
""" """
Update the status of the loan with the given loan_id. Update the status of the loan record with the given loan_id.
""" """
# Retrieve loan try:
loan = cls.query.get(loan_id) # 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()
if not loan: logger.info("Loan status updated and committed.")
raise ValueError(f"Loan with ID {loan_id} does not exist.") return loan.to_dict()
if loan.status == status: except Exception as e:
return 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)
# Update loan status and the updated_at timestamp if not loan:
loan.status = status 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)}")
+9 -5
View File
@@ -2,6 +2,8 @@ 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 app.enums.loan_status import LoanStatus
from sqlalchemy.exc import IntegrityError
class Repayment(db.Model): class Repayment(db.Model):
__tablename__ = "repayments" __tablename__ = "repayments"
@@ -51,7 +53,7 @@ class Repayment(db.Model):
@classmethod @classmethod
def create_repayment(cls, repayment_data): def create_repayment(cls, repayment_data):
if repayment_data["LoanStatus"] not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]: 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']})") raise ValueError(f"Repayment cannot be processed. Loan status: ({repayment_data['LoanStatus']})")
repayment = cls( repayment = cls(
@@ -70,9 +72,9 @@ class Repayment(db.Model):
db.session.commit() db.session.commit()
logger.info("Repayment record committed.") logger.info("Repayment record committed.")
return repayment return repayment
except InqtegrityError as err: except IntegrityError as err:
logger.error(f"Database integrity error: {err}") logger.error(f"Database integrity error: {err}")
return [q] return {"error": "Integrity error", "details": str(err)}
@classmethod @classmethod
@@ -148,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):
""" """
@@ -179,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 -9
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,9 +42,13 @@ class RepaymentsData(db.Model):
Add a new repayment data entry. Add a new repayment data entry.
""" """
try: try:
repaymentAmount = data.get('repaymentAmount', 0.0) repayment_amount = float(data.get('repaymentAmount', 0.0))
amountCollected = data.get('amountCollected',0.0) amount_collected = float(data.get('amountCollected', 0.0))
accountBalance = repaymentAmount - amountCollected
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'),
@@ -53,14 +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=amountCollected, amount_collected=amount_collected,
repayment_amount=repaymentAmount, repayment_amount=repayment_amount,
balance=round(float(accountBalance), 2) 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()
raise Exception(f"Error adding repayment data: {str(e)}") logger.error(f"Error adding repayment data: {e}")
raise Exception(f"Error adding repayment data: {str(e)}")
+75 -29
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
@@ -9,6 +10,8 @@ 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.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__)
@@ -124,22 +127,27 @@ def salary_detect():
if payload is None: if payload is None:
logger.warning("No payload received in request") logger.warning("No payload received in request")
#- Sometimes no paylod return ResponseHelper.error("Missing request payload", status_code=400) return ResponseHelper.error("Missing request payload", status_code=400)
if payload: # 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) # TODO - This will come as array of salaries - not just one
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}")
process_salary_list() # TODO - This should be threaded out or removed from here finally # 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(f"Finished processing List") logger.info("Finished processing List")
return ResponseHelper.success([], "AutoCall Add Salary Successful") return ResponseHelper.success([], "AutoCall Add Salary Successful")
@autocall_bp.route("/analytic-salary-process", methods=["POST"]) @autocall_bp.route("/analytic-salary-process", methods=["POST"])
def salary_process(): def salary_process():
response = process_salary_list() response = process_salary_list()
@@ -147,7 +155,7 @@ def salary_process():
def process_salary_list(): def process_salary_list():
# Step 2: Get all pending salaries # 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")
@@ -155,33 +163,32 @@ def process_salary_list():
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"Loan LOOP LoanID = :{loan.id}") logger.info(f"Processing Loan ID: {loan.id}")
try: try:
# loan_dict = loan.to_dict()
logger.info(f"loan_dict ==== Repayment Data:{loan}")
repayment_data = { repayment_data = {
"customerId": loan.customer_id, "customerId": loan.customer_id,
"loanId": loan.id, "loanId": loan.id,
@@ -192,21 +199,60 @@ def process_salary_list():
"LoanStatus": loan.status, "LoanStatus": loan.status,
} }
logger.info(f"Saving/Creating Repayment Data:{repayment_data}") logger.info(f"Creating repayment with data: {repayment_data}")
repayment = RepaymentService.create_repayment(repayment_data) repayment = RepaymentService.create_repayment(repayment_data)
LoanService.update_status(loan_id=repayment_data["loanId"],
status=LoanStatus.START_REPAY) # repay started 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 []
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))
+13
View File
@@ -78,6 +78,13 @@ 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 @classmethod
def update_status(cls, loan_id, status): def update_status(cls, loan_id, status):
@@ -86,3 +93,9 @@ class LoanService:
""" """
# Retrieve loan # Retrieve loan
return Loan.update_status(loan_id, status) 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)
+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"
+11 -3
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
@@ -200,4 +200,12 @@ paths:
example: "2025-01-01" example: "2025-01-01"
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