Files
2026-04-05 16:53:02 +01:00

785 lines
32 KiB
Python

import time as time_module
from flask import Blueprint, request, jsonify, current_app
import requests
from app.extensions import db
from app.config import settings
from app.helpers.response_helper import ResponseHelper
from app.helpers.collect_loan_helper import CollectLoanHelper
from app.utils.auth import get_headers
from app.utils.logger import logger
from app.integrations.simbrella import SimbrellaClient
from app.services.loan import LoanService
from app.services.repayment import RepaymentService
from app.services.salary import SalaryService
from app.services.loan_repayment_schedule import LoanRepaymentScheduleService
from app.services.loan_charge import LoanChargesService
from app.enums.loan_status import LoanStatus
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
from app.utils.mail import send_report_email, get_report_data
from datetime import datetime, timezone, timedelta
from app.config import settings
autocall_bp = Blueprint("autocall", __name__)
#refresh-verify-disbursement
#
#
@autocall_bp.route("/refresh-verify-disbursement", methods=["GET"])
def verify_transaction():
"""
Get the latest loan with disbursement date and no verification date.
Then call the verify transaction endpoint with the loan data.
This is called after disbursement to ensure the loan was actually disbursed
Then a verification date is set
"""
logger.info(f"Calling VerifyTransaction Components")
loan = LoanService.get_latest_loan_with_disburse_date()
if not loan:
logger.info(f"No loan found without disbursement date")
return 0
logger.info(f"Calling VerifyTransaction endpoint with data: {loan}")
loan_data = loan.to_dict()
data = {
"transactionId": loan_data.get('transactionId'),
"fbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.verify_transaction(data)
return response
@autocall_bp.route("/refresh-disbursement", methods=["GET"])
def disbursement():
"""
Get the latest loan without disbursement date.
Then call the disburse loan endpoint with the loan data.
"""
# data = request.json()
logger.info(f"Calling Disbursement Components")
loan = LoanService.get_latest_loan_without_disburse_date()
if not loan:
logger.info(f"No loan found without disbursement date")
return 0
logger.info(f"Calling DisburseLoan endpoint with data: {loan}")
loan_data = loan.to_dict()
data = {
"transactionId": loan_data.get('transactionId'),
"FbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.disburse_loan(data)
return response
@autocall_bp.route("/retry-disbursement", methods=["POST"])
def retry_disbursement():
"""
This takes in a transaction id as input and
retries the disbursement for the loan with that transaction id.
This is to be used in cases where the disbursement failed due to network issues
or other transient issues. It will call the disbursement endpoint
with the loan data for the loan with the given transaction id.
"""
try:
data = request.get_json()
logger.info(f"Retry Transaction ID Data Received for :::: {data}")
transactionId = data["transactionId"]
logger.info(f"Starting Transaction ID Data Received for :::: {transactionId}")
logger.info(f"Calling Disbursement Components for Retry Transaction ID Data Received for :::: {transactionId}")
loan = LoanService.get_loan_by_transaction_id(transactionId)
if not loan:
logger.info(f"No loan found without disbursement date")
return 0
logger.info(f"Calling DisburseLoan endpoint with data: {loan}")
loan_data = loan.to_dict()
data = {
"transactionId": loan_data.get('transactionId'),
"FbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.disburse_loan(data)
# return response
logger.info(f"Retry Disbursement Transaction ID Result Received for :::: {response}")
return ResponseHelper.success(message="Retry Disbursement Request Sent Successfully", status_code=200)
except Exception as e:
logger.error(f"Failed to call retry disbursement {data}: {e}")
@autocall_bp.route("/retry-failed-disbursements", methods=["GET"])
def retry_failed_disbursements():
"""
This endpoint is for retrying failed disbursements.
It will get the list of failed disbursements and call the disbursement endpoint for each of them.
This is to be used in cases where the disbursement failed due to network issues or other transient issues. It will call the disbursement endpoint
with the loan data for the loan with the given transaction id.
"""
try:
logger.info(f"Calling Retry Failed Disbursements Components")
failed_disbursements = LoanService.get_failed_disbursements()
if not failed_disbursements:
logger.info(f"No failed disbursements found")
return ResponseHelper.success(message="No failed disbursements found", status_code=200)
logger.info(f"Found {len(failed_disbursements)} failed disbursements to retry")
#get batch size from settings
# Safe config values
batch_size = max(1, settings.FAILED_DISBURSEMENT_BATCH_SIZE or 1)
delay_seconds = max(0, settings.FAILED_DISBURSEMENT_DELAY_SECONDS or 0)
batch_delay = max(0, settings.FAILED_DISBURSEMENT_BATCH_DELAY_SECONDS or 0)
for i in range(0, len(failed_disbursements), batch_size):
batch = failed_disbursements[i:i + batch_size]
for loan in batch:
logger.info(f"Retrying disbursement for loan ID {loan.id}")
loan_data = loan.to_dict()
data = {
"transactionId": loan_data.get('transactionId'),
"FbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.disburse_loan(data)
logger.info(f"Retry Disbursement Result Received for loan ID {loan.id}: {response}")
time_module.sleep(delay_seconds)
time_module.sleep(batch_delay)
return ResponseHelper.success(message="Retry Failed Disbursements Request Sent Successfully", status_code=200)
except Exception as e:
logger.error(f"Failed to retry disbursements: {e}")
return ResponseHelper.error("Failed to retry disbursements", status_code=500, error=str(e))
@autocall_bp.route("/direct/loan", methods=["POST"])
def direct_loan():
"""
This endpoint is for directly calling the disbursement endpoint with a transaction id.
This is to be used in cases where the disbursement failed due to network issues
or other transient issues. It will call the disbursement endpoint
with the loan data for the loan with the given transaction id.
"""
data = request.get_json()
logger.info(f"Data received: {data}")
REQUIRED_KEYS = [
"transactionId"
]
# Check for missing keys
missing_keys = [key for key in REQUIRED_KEYS if key not in data or data[key] is None]
if missing_keys:
logger.warning(f"Missing required keys: {missing_keys}")
return jsonify({
"status": "error",
"message": f"Missing required fields: {', '.join(missing_keys)}"
}), 400
# Check if the loan exists
logger.info(f"Checking if loan with transaction id {data['transactionId']} exists")
transaction_id = data["transactionId"].strip()
loan = LoanService.get_loan_by_transaction_id(transaction_id=transaction_id)
if not loan:
logger.warning(f"Loan with transaction id {transaction_id} does not exist")
return jsonify({
"status": "error",
"message": f"Loan with transaction id {transaction_id} does not exist"
}), 400
loan_data = loan.to_dict()
# Prevent double disbursement
if loan_data.get('disburseDate') is not None:
return jsonify({
"status": "error",
"message": f"Loan with transaction id {data['transactionId']} has already been processed"
}), 400
data_to_process = {
"transactionId": loan_data.get('transactionId'),
"FbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.disburse_loan(data_to_process)
return response
@autocall_bp.route("/direct/repayment", methods=["POST"])
def direct_repayment():
"""
This endpoint is for directly calling the collect loan endpoint with a transaction id.
"""
data = request.get_json()
logger.info(f"Data received: {data}")
REQUIRED_KEYS = ["transactionId"]
# Check for missing keys
missing_keys = [key for key in REQUIRED_KEYS if key not in data or data[key] is None]
if missing_keys:
logger.warning(f"Missing required keys: {missing_keys}")
return jsonify({
"status": "error",
"message": f"Missing required fields: {', '.join(missing_keys)}"
}), 400
# Check if the loan exists
logger.info(f"Checking if loan with transaction id {data['transactionId']} exists")
loan = LoanService.get_loan_by_transaction_id(transaction_id=data['transactionId'])
if not loan:
logger.info(f"Loan with transaction id {data['transactionId']} does not exist")
return jsonify({
"status": "error",
"message": f"Loan with transaction id {data['transactionId']} does not exist"
}), 400
loan_data = loan.to_dict()
# check if loan has been repaid
if loan_data.get("status") == LoanStatus.REPAID and loan_data.get("balance") <= 0:
logger.info(f"Loan with Id {loan_data.get('debtId')} has been repaid")
return jsonify({
"status": "error",
"message": f"loan with Id {loan_data.get('debtId')} has been repaid"
}), 400
repayment_data = {
"customerId": loan_data.get("customerId"),
"loanId": loan_data.get("debtId"),
"productId": loan_data.get("productId"),
"transactionId": loan_data.get("transactionId"),
"initiatedBy": "USER INITIATED",
"salaryAmount": 0,
"LoanStatus": loan_data.get("status"),
}
logger.info(f"Creating repayment with data: {repayment_data}")
try:
repayment = RepaymentService.create_repayment(repayment_data)
logger.info(f"Repayment created: {repayment}")
except Exception as e:
db.session.rollback()
logger.error(f"Repayment creation raised exception: {e}")
return jsonify({
"status": "error",
"message": "Failed to create repayment"
}), 500
if not repayment or (isinstance(repayment, dict) and "error" in repayment):
db.session.rollback()
logger.error(f"Repayment creation failed for loan ID {loan_data.get('debtId')}: {repayment}")
try:
if loan_data.get('status') == LoanStatus.ACTIVE:
LoanService.update_status(loan_id=loan_data.get('debtId'), status=LoanStatus.START_REPAY)
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update loan status for loan ID {loan_data.get('debtId')}: {e}")
repayment_data_dict = repayment.to_dict()
data_to_process = {
"transactionId": repayment_data_dict['transactionId'],
"debtId": repayment_data_dict['loanId'],
"customerId": repayment_data_dict['customerId'],
"productId": repayment_data_dict['productId'],
"Id":repayment_data_dict['Id']
}
response = SimbrellaClient.collect_loan_user_initiated(data_to_process)
return response
@autocall_bp.route("/refresh-verify-collection", methods=["GET"])
def refresh_verify_collection():
"""
This endpoint is for directly calling the verify collection endpoint.
"""
data = request.get_json()
logger.info(f"Calling Verify Collection")
response = SimbrellaClient.collect_loan(data)
return response
@autocall_bp.route("/refresh-collection", methods=["GET"])
def refresh_collection():
"""
This endpoint is for directly calling the collect loan endpoint.
It will get the latest repayment with no repay date and call the collect loan endpoint with that repayment data.
This is to be used in cases where the collection failed due to network issues or other transient issues. It will call the collection endpoint
"""
#data = request.get_json()
logger.info(f"Calling Collection ")
#grab the last repayments with repay date is none
repayment = RepaymentService.get_latest_repayment_without_repay_date()
#repayment = RepaymentService.get_latest_repayment_with_loanId(13735)
if not repayment:
logger.info(f"No repayment found without repay date")
return 0
logger.info(f"Calling repay loan endpoint with data: {repayment}")
repayment_data = repayment.to_dict()
logger.info(f"here is the dict form of repayment {repayment_data}")
data = {
"transactionId": repayment_data['transactionId'],
"debtId": repayment_data['loanId'],
"customerId": repayment_data['customerId'],
"productId": repayment_data['productId'],
"Id":repayment_data['Id']
}
logger.info(f"Data being sent to Simbrella: {data}")
logger.info(f"calling simbrella")
response = SimbrellaClient.collect_loan_user_initiated(data)
return response
@autocall_bp.route("/payment-callback", methods=["POST"])
def payment_callback():
data = request.get_json()
logger.info(f"Calling Callback Components")
response = SimbrellaClient.payment_callback(data)
return response
@autocall_bp.route("/penal-charge", methods=["POST"])
def penal_charge():
"""
This endpoint is for directly calling the penal charge endpoint.
"""
data = request.get_json()
logger.info(f"Calling Penal Charge Endpoint")
try:
response = SimbrellaClient.penal_charge(data[0])
return response
except Exception as e:
logger.error(f"Error in Penal Charge: {e}")
return ResponseHelper.error("Penal charge failed")
@autocall_bp.route("/analytic-salary-detect", methods=["POST"])
def salary_detect():
"""
Creates new salary data
Gets the salary list not yet processed
then, gets the loan list for each salary and
creates repayments for each loan with the salary data,
then calls the collect loan endpoint for each repayment created.
"""
payload = request.get_json()
logger.info("Calling Salary Detect endpoint")
if payload is None:
logger.warning("No payload received in request")
return ResponseHelper.error("Missing request payload", status_code=400)
# Step 1: Try to add new salary data
try:
new_salary = SalaryService.add_salary_data(payload) # TODO - This will come as array of salaries - not just one
if new_salary:
logger.info(f"Salary added: {new_salary.id}")
except Exception as e:
logger.error(f"Failed to save salary: {e}")
# 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()
if not pending_salaries:
logger.info("No pending salaries found")
return ResponseHelper.success([], "No pending salaries")
logger.info(f"Found {len(pending_salaries)} pending salaries to process")
for pending_salary in pending_salaries:
logger.info(f"Processing salary ID: {pending_salary.id}")
# Step 2: Update salary status to PROCESSING
try:
SalaryService.update_status(pending_salary.id, "PROCESSING")
except Exception as e:
db.session.rollback()
logger.warning(f"Could not update status for salary ID {pending_salary.id}: {e}")
continue
# Step 3: Get customer's active loans
try:
loans, total_amount = LoanService.get_customer_active_loans(pending_salary.customer_id)
if not loans:
logger.warning(f"No loans found for customer ID: {pending_salary.customer_id}")
continue
except Exception as e:
db.session.rollback()
logger.error(f"Error fetching loans for customer ID {pending_salary.customer_id}: {e}")
continue
# Step 4: Create repayments for each loan
for loan in loans:
logger.info(f"Processing Loan ID: {loan.id}")
#check if the loan has been repaid
if loan.status in [LoanStatus.REPAID] and loan.balance <= 0:
logger.info(f"Skipping loan ID {loan.id} because it is already repaid/closed")
continue
try:
repayment_data = {
"customerId": loan.customer_id,
"loanId": loan.id,
"productId": loan.product_id,
"transactionId": loan.transaction_id,
"initiatedBy": "SALARY_DETECT",
"salaryAmount": pending_salary.amount,
"LoanStatus": loan.status,
}
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:
if loan.status == LoanStatus.ACTIVE:
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}")
# 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:
db.session.rollback()
logger.error(f"Error creating repayment for loan ID {loan.id}: {e}")
continue
logger.info(f"Finished processing salary ID: {pending_salary.id}")
return ResponseHelper.success([], "Processed all pending salaries")
@autocall_bp.route("/report", methods=["GET"])
def report():
"""This endpoint is for generating the report and sending the email."""
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))
@autocall_bp.route("/process-penal-charges", methods=["GET"])
def process_penal_charges():
"""
This endpoint is for processing penal charges for overdue loans schedule with grace period.
It will check for overdue loans, calculate the penal charge,
create a new penal charge record, update the loan and repayment schedule with the new penal charge, and call the Simbrella endpoint to update the penal charge on their system.
"""
try:
OVERDUE_GRACE_PERIOD_DAYS = settings.OVERDUE_GRACE_PERIOD_DAYS
OVERDUE_PROCESSING_LIST_LIMIT = settings.OVERDUE_PROCESSING_LIST_LIMIT
PENAL_CHARGE_MAXIMUM_COUNT = settings.PENAL_CHARGE_MAXIMUM_COUNT
PENAL_CHARGE_INTERVAL_DAYS = settings.PENAL_CHARGE_INTERVAL_DAYS
now = datetime.now(timezone.utc)
overdue_schedules = (
LoanRepaymentScheduleService
.get_overdue_repayment_schedule_with_grace_period(
OVERDUE_GRACE_PERIOD_DAYS,
OVERDUE_PROCESSING_LIST_LIMIT
)
)
logger.info(f"Found {len(overdue_schedules)} overdue loan schedule.")
if not overdue_schedules:
return ResponseHelper.success(
message="No overdue loan schedule found",
status_code=200
)
processed_loans = []
for schedule in overdue_schedules:
loan = LoanService.get_loan_by_loan_id(schedule.loan_id)
if not loan:
logger.info(f"Loan with id {schedule.loan_id} not found")
continue
penal_count = schedule.penal_count or 0
# MAX PENAL CHECK
if penal_count >= PENAL_CHARGE_MAXIMUM_COUNT:
logger.info(
f"Penal count for schedule {schedule.id} has reached the maximum limit."
)
continue
# INTERVAL CHECK (PER SCHEDULE)
if schedule.last_penal_date:
# ensure last_penal_date is timezone-aware
last_penal = schedule.last_penal_date
if last_penal.tzinfo is None:
last_penal = last_penal.replace(tzinfo=timezone.utc)
next_allowed_date = last_penal + timedelta(days=PENAL_CHARGE_INTERVAL_DAYS)
if now < next_allowed_date:
logger.info(
f"Penal interval for schedule {schedule.id} has not passed yet."
)
continue
# NEXT PENAL NUMBER
next_penal_no = penal_count + 1
# CALCULATE PENAL
penal_amount = LoanRepaymentScheduleService.calculate_penal_charge(schedule)
# CREATE PENAL CHARGE
new_penal_charge = LoanChargesService.create_penal_charges_for_loan(
loan_id=schedule.loan_id,
transaction_id=schedule.transaction_id,
percent=settings.PENAL_CHARGE_PERCENTAGE,
penal_no=next_penal_no,
schedule_number=schedule.installment_number,
penal_amount=penal_amount
)
if not new_penal_charge:
logger.error(f"Failed to create penal charge for loan ID: {loan.id}")
continue
logger.info(f"Penal charge created: {new_penal_charge.to_dict()}")
# UPDATE SCHEDULE
LoanRepaymentScheduleService.apply_penal_to_schedule(
schedule.id,
penal_amount
)
logger.info(f"Penal charge applied to schedule {schedule.id}")
# UPDATE LOAN TOTAL
LoanService.apply_penal_to_loan(
loan.id,
penal_amount
)
logger.info(f"Penal charge applied to loan {loan.id}")
processed_loans.append(loan.to_dict())
return ResponseHelper.success(
message="Penal Charges Processed Successfully",
status_code=200,
data=processed_loans
)
except Exception as e:
logger.exception(f"Error processing penal charges: {e}")
return ResponseHelper.error(
"Failed to process penal charges",
status_code=500,
error=str(e)
)
@autocall_bp.route("/overdue-loans", methods=["GET"])
def overdue_loans():
"""
This endpoint is for processing overdue loans.
It will get all active overdue loan schedules,
and then for each loan schedule, it will create a repayment, update the loan status, and call the Simbrella endpoint to collect the loan.
"""
try:
# Step 1: Get all active overdue loans
overdue_loans = LoanRepaymentScheduleService.get_active_overdue_repayment_schedule()
logger.info(f"Found {len(overdue_loans)} overdue loans.")
if not overdue_loans:
logger.info("No overdue loans found.")
return ResponseHelper.success(message="No overdue loans found", status_code=200)
#get batch size from settings
loan_delay_seconds = max(0, settings.OVERDUE_LOAN_DELAY_SECONDS)
batch_delay_seconds = max(0, settings.OVERDUE_LOAN_BATCH_DELAY_SECONDS)
batch_size = max(1, settings.OVERDUE_LOAN_BATCH_SIZE)
loan_chunks = list(CollectLoanHelper.chunk_list(overdue_loans, batch_size))
logger.info(f"Found {len(loan_chunks)} loan chunks to process.")
# Step 2: Process each loan
for chunk_index, loan_chunk in enumerate(loan_chunks):
logger.info(f"Processing chunk {chunk_index + 1} of {len(loan_chunks)} with {len(loan_chunk)} loans.")
for loan in loan_chunk:
try:
process_overdue_loan(loan)
except Exception:
logger.exception(f"Failed processing loan {loan.id}")
finally:
time_module.sleep(loan_delay_seconds)
if chunk_index < len(loan_chunks) - 1:
logger.info(f"Waiting {batch_delay_seconds} seconds before processing next chunk...")
time_module.sleep(batch_delay_seconds) # Delay between chunks
return ResponseHelper.success(message="Processed overdue loans successfully", status_code=200)
except Exception as e:
logger.exception(f"Error fetching overdue loans: {e}")
return ResponseHelper.error("Failed to fetch overdue loans", status_code=500, error=str(e))
def process_overdue_loan(loan):
"""
Handles repayment creation, loan status update, and Simbrella call
for a single overdue loan.
"""
logger.info(f"Processing Loan ID: {loan.loan_id}")
full_loan_data = LoanService.get_loan_by_loan_id(loan.loan_id)
logger.info(f"full loan details: {full_loan_data.to_dict()}")
if not full_loan_data:
logger.warning(f"Full Loan ID {loan.loan_id} not found in database")
else:
#lets check if the loan with the repayment has been repaid, then update the loan schedule to paid
if full_loan_data.to_dict().get("status") == LoanStatus.REPAID and full_loan_data.to_dict().get("balance") == 0:
try:
LoanRepaymentScheduleService.update_repayment_schedule_status(loan.id)
logger.info(f"Updated Loan Repayment Schedule ID {loan.id} to PAID")
return
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update Loan Repayment Schedule ID {loan.id} to PAID: {e}")
customer_id = full_loan_data.to_dict().get("customerId")
loan_status = full_loan_data.to_dict().get("status")
try:
repayment_data = {
"customerId": customer_id,
"loanId": loan.loan_id,
"productId": loan.product_id,
"transactionId": loan.transaction_id,
"initiatedBy": "SYSTEM", # To be reviewed
"salaryAmount": 0,
"LoanStatus": loan_status,
}
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.loan_id}: {repayment}")
return
# Update loan status
try:
logger.info(f"Updating loan status for loan ID {loan.loan_id}")
LoanService.update_status(loan_id=loan.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.loan_id}: {e}")
logger.info(f"Created repayment ID: {repayment.id}")
# Step 3: Call Simbrella
try:
#lets add the overdue loan schedule id and amount we are currently processing to the repayment data
if loan.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID:
amount = loan.partial_balance or 0
else:
amount = loan.installment_amount
repayment_data["overdueLoanScheduleAmount"] = amount
repayment_data["overdueLoanScheduleId"] = loan.id
repayment_data["Id"] = repayment.id
logger.info(f"Calling Simbrella for with repayment data: {repayment_data}")
simbrella_response = SimbrellaClient.collect_loan_user_due_payment(repayment_data)
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:
db.session.rollback()
logger.error(f"Error creating repayment for loan ID {loan.id}: {e}")
finally:
logger.info(f"Finished processing loan ID: {loan.id}")