added repayment_schedule

This commit is contained in:
Chinenye Nmoh
2025-08-27 12:00:42 +01:00
parent 524836f52a
commit fb460471fb
9 changed files with 330 additions and 131 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
from .transaction_type import TransactionType
from .loan_status import LoanStatus
from .loan_status import LoanStatus
from .repayment_schedule_status import RepaymentScheduleStatus
+5
View File
@@ -0,0 +1,5 @@
from enum import Enum
class RepaymentScheduleStatus(str, Enum):
PARTIALLY_PAID = "partially_paid"
REPAID = "repaid"
+48
View File
@@ -0,0 +1,48 @@
import random
import string
from app.services.repayment import RepaymentService
from app.services.loan import LoanService
from app.helpers.response_helper import ResponseHelper
from app.utils.logger import logger
class CollectLoanHelper:
@staticmethod
def _validate_repayment_and_loan(data):
repayment = RepaymentService.get_repayment_by_id(id=data['Id'])
if not repayment:
logger.info(f"Repayment id: {data['Id']}, was not found")
return None, None, ResponseHelper.error("Repayment not found")
repayment_data = repayment.to_dict()
loan = LoanService.get_loan_by_loan_id(loan_id=int(repayment_data['loanId']))
if not loan:
logger.info(f"Loan id: {repayment_data['loanId']}, was not found")
return None, None, ResponseHelper.error("Loan not found")
loan
return repayment_data, loan, None
@staticmethod
def _build_collect_loan_payload(loan_data, repayment_data, data, collectionMethod):
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
return {
"transactionId": t_id,
"fbnTransactionId": loan_data['transactionId'],
"debtId": debtId,
"customerId": repayment_data['customerId'],
"accountId": loan_data['accountId'],
"productId": repayment_data['productId'],
"collectAmount": (
data['overdueLoanScheduleAmount']
if data.get('overdueLoanScheduleAmount')
is not None else loan_data.get('balance', 0)
),
"penalCharge": 0,
"channel": "USSD",
"collectionMethod": collectionMethod,
"lienAmount": 0,
"countryId": "NG",
"comment": "COLLECT LOAN"
}
+37 -86
View File
@@ -6,7 +6,6 @@ from app.services.loan_repayment_schedule import LoanRepaymentScheduleService
from app.utils.auth import get_headers
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
@@ -17,11 +16,13 @@ from app.extensions import db
from app.services.repayments_data import RepaymentsData
from app.services.salary import SalaryService
from app.enums.loan_status import LoanStatus
from app.models.loan_repayment_schedule import LoanRepaymentSchedule
from decimal import Decimal, ROUND_HALF_UP
from requests.exceptions import SSLError, RequestException,Timeout
import sys
from requests.exceptions import ReadTimeout, ConnectTimeout
import socket
from app.helpers.collect_loan_helper import CollectLoanHelper
class SimbrellaClient:
@@ -243,19 +244,10 @@ class SimbrellaClient:
logger.info(f"Calling CollectLoan api_url==> : {api_url}")
logger.info(f"Calling CollectLoan endpoint with data: {data}")
repayment = RepaymentService.get_repayment_by_id(id=data['Id'])
if not repayment:
logger.info(f"Repayment with id: {data['Id']} not found")
return ResponseHelper.error("Repayment not found")
repayment_data = repayment.to_dict()
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")
return ResponseHelper.error("Loan not found")
repayment_data, loan, error = CollectLoanHelper._validate_repayment_and_loan(data)
if error:
return error
loan_data = loan.to_dict()
logger.info(f"Loan data: {loan_data}")
if repayment_data['repayDate'] is not None:
logger.info(f"Repayment already processed at {repayment_data['repayDate']}")
@@ -265,28 +257,7 @@ class SimbrellaClient:
repayment = RepaymentService.get_repayment_by_transaction_id(transaction_id=data['transactionId'])
repayment_data = repayment.to_dict()
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
collect_loan_data = {
"transactionId": t_id,
"fbnTransactionId": loan_data['transactionId'],
"debtId": debtId,
"customerId": repayment_data['customerId'],
"accountId": loan_data['accountId'],
"productId": repayment_data['productId'],
"collectAmount": (
data['overdueLoanScheduleAmount']
if data.get('overdueLoanScheduleAmount') is not None
else loan_data.get('balance', 0)
),
"penalCharge": 0,
"channel": "USSD",
"collectionMethod": collectionMethod,
"lienAmount": 0,
"countryId": "NG",
"comment": "COLLECT LOAN"
}
collect_loan_data = CollectLoanHelper._build_collect_loan_payload(loan_data, repayment_data, data, collectionMethod)
try:
logger.info(f"Sending CollectLoan request............ {collect_loan_data}")
response = requests.post(api_url, json=collect_loan_data, timeout=90, headers=get_headers())
@@ -299,6 +270,9 @@ class SimbrellaClient:
'404',
'Collection Service url not found'
)
if(data.get('overdueLoanScheduleId') is not None):
LoanRepaymentScheduleService.update_repayment_schedule_description(data['overdueLoanScheduleId'], 'Collection Service url not found')
logger.error("Received 404 from external service")
return ResponseHelper.error("Collection Service URL not found", status_code=404)
@@ -329,6 +303,7 @@ class SimbrellaClient:
else:
logger.warning("Failed to add repayment data")
updated_loan = None
response_message = result.get('responseMessage')
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}")
@@ -363,66 +338,32 @@ class SimbrellaClient:
logger.info(f'Updated loan with partial status: {partial}')
updated_loan = partial
try:
schedule_to_update = LoanRepaymentScheduleService.get_repayment_schedule_by_id_and_transaction_id(
data["overdueLoanScheduleId"], data["transactionId"]
)
logger.info(f"Schedule to update: {schedule_to_update}")
if schedule_to_update is None:
logger.warning(
f"Repayment schedule not found for ID {data['overdueLoanScheduleId']} "
f"and transaction ID {loan_data['transactionId']}"
)
else:
# Use DB installment amount as the reference
installment_amount = Decimal(str(schedule_to_update.installment_amount)).quantize(Decimal("0.01"))
if amount_collected >= installment_amount:
LoanRepaymentScheduleService.update_repayment_schedule_status(
schedule_to_update.id, paid=True)
logger.info(
f"Installment {data['overdueLoanScheduleId']} fully covered "
f"with {amount_collected}, marked as paid"
)
else:
logger.info(
f"Installment {data['overdueLoanScheduleId']} not fully covered. "
f"Collected={amount_collected}, "
f"Installment={installment_amount}"
)
except Exception as e:
logger.error(f"Failed to update repayment schedule installment: {e}")
except Exception as e:
db.session.rollback()
logger.error(f"Error while updating loan status for debtId {updated_loan['debtId']}: {e}")
# for now lets update the loan repayment schedule only when full payment has been made
# lets update the loan repayment schedule when full payment has been made
# get the repayment schedule by loan_id
logger.info(f"Updated loan status: {updated_loan.get('status')}")
if updated_loan and updated_loan.get('status') == LoanStatus.REPAID:
repayment_schedule = LoanRepaymentScheduleService.get_repayment_schedule_by_loan_id(updated_loan['debtId'])
logger.info(f'Loan repayment schedule: {repayment_schedule}')
# if repayment_schedule, loop through each installment
if repayment_schedule:
for installment in repayment_schedule:
try:
logger.info(f'Processing installment: {installment}')
# Update each installment as paid
LoanRepaymentScheduleService.update_repayment_schedule_status(installment.id, True)
logger.info(f'Updated installment {installment.id} as paid')
except Exception as e:
logger.error(f"Failed to update installment {installment.id}: {e}")
logger.info('All installments processed')
LoanRepaymentScheduleService.handle_schedule_updates(
updated_loan=updated_loan,
data=data,
amount_collected=amount_collected,
message=response_message,
loan_data=loan_data
)
return ResponseHelper.success(result, "Successful")
except SSLError as ssl_err:
db.session.rollback()
logger.exception(f"SSL error while calling Simbrella endpoint: {ssl_err}")
RepaymentService.set_repay_result(
repayment_data['Id'],
'502',
'SSL error occurred while calling Simbrella'
)
if(data.get('overdueLoanScheduleId') is not None):
LoanRepaymentScheduleService.update_repayment_schedule_description(data['overdueLoanScheduleId'], 'SSL error occurred')
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:
@@ -433,6 +374,9 @@ class SimbrellaClient:
'500',
'There was a timeout while calling Simbrella'
)
if(data.get('overdueLoanScheduleId') is not None):
LoanRepaymentScheduleService.update_repayment_schedule_description(data['overdueLoanScheduleId'], 'Timeout occurred')
return ResponseHelper.error("Connection to Simbrella timed out", status_code=504, error=str(timeout_err))
except RequestException as req_err:
db.session.rollback()
@@ -442,7 +386,8 @@ class SimbrellaClient:
'500',
'There was a request error while calling Simbrella'
)
return ResponseHelper.error("Connection to Simbrella failed", status_code=503, error=str(req_err))
if(data.get('overdueLoanScheduleId') is not None):
LoanRepaymentScheduleService.update_repayment_schedule_description(data['overdueLoanScheduleId'], 'Request error occurred')
except SystemExit as sys_exit:
db.session.rollback()
@@ -454,6 +399,9 @@ class SimbrellaClient:
)
if(data.get('overdueLoanScheduleId') is not None):
LoanRepaymentScheduleService.update_repayment_schedule_description(data['overdueLoanScheduleId'], 'Unexpected shutdown occurred')
return ResponseHelper.error("Unexpected shutdown detected", status_code=500, error=str(sys_exit))
except Exception as e:
@@ -464,6 +412,9 @@ class SimbrellaClient:
'500',
'Unexpected error while processing loan collection'
)
if(data.get('overdueLoanScheduleId') is not None):
LoanRepaymentScheduleService.update_repayment_schedule_description(data['overdueLoanScheduleId'], 'Unexpected error occurred')
return ResponseHelper.error("Unexpected error while processing loan collection", status_code=500, error=str(e))
+7 -2
View File
@@ -310,7 +310,7 @@ class Loan(db.Model):
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)
@@ -321,7 +321,12 @@ class Loan(db.Model):
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.")
# allow tiny rounding diff
if abs(amount_collected - balance) <= Decimal("0.01"):
amount_collected = balance
else:
raise ValueError("Repayment amount exceeds current loan balance.")
# Deduct the amount from the current balance
new_balance = balance - amount_collected
+137 -35
View File
@@ -2,8 +2,11 @@ from datetime import datetime, timezone
from app.extensions import db
from app.utils.logger import logger
from sqlalchemy.exc import SQLAlchemyError
# from dateutil.relativedelta import relativedelta
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
from decimal import Decimal, ROUND_HALF_UP
# from dateutil.relativedelta import relativedelta
class LoanRepaymentSchedule(db.Model):
__tablename__ = 'loan_repayment_schedules'
@@ -19,6 +22,9 @@ class LoanRepaymentSchedule(db.Model):
paid_at = db.Column(db.DateTime, nullable=True)
due_process_date = db.Column(db.DateTime, nullable=True)
due_process_count = db.Column(db.Integer, default=0)
paid_status = db.Column(db.String(20), nullable=True)
repay_description = db.Column(db.String(255), nullable=True)
partial_balance = db.Column(db.Float, default=0.0)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
@@ -37,6 +43,9 @@ class LoanRepaymentSchedule(db.Model):
'paid': self.paid,
'due_process_date': self.due_process_date.isoformat() if self.due_process_date else None,
'due_process_count': self.due_process_count,
'paid_status': self.paid_status,
'repay_description': self.repay_description,
'partial_balance': self.partial_balance,
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
@@ -46,15 +55,26 @@ class LoanRepaymentSchedule(db.Model):
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
@classmethod
def get_repayment_schedule_by_loan_id(cls, loan_id):
def get_repayment_schedule_by_loan_id(cls, loan_id, include_paid=True):
"""
Get repayment schedule by loan ID
Get repayment schedules by loan ID.
:param loan_id: Loan ID to filter by
:param include_paid: If True, include all schedules. If False, only unpaid ones.
:return: List of repayment schedules ordered by due_date
"""
try:
return cls.query.filter_by(loan_id=loan_id).all()
query = cls.query.filter_by(loan_id=loan_id)
if not include_paid:
query = query.filter_by(paid=False)
schedules = query.order_by(cls.due_date.asc()).all()
return schedules
except Exception as e:
logger.error(f"Error fetching repayment schedule for loan_id={loan_id}: {e}")
return []
logger.error(f"Error fetching repayment schedules for loan {loan_id}: {e}")
raise
@classmethod
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
"""
@@ -83,42 +103,124 @@ class LoanRepaymentSchedule(db.Model):
Get repayment schedule by transaction ID
"""
return cls.query.filter_by(transaction_id=transaction_id).all()
@classmethod
def update_repayment_schedule_status(cls, schedule_id, paid=False):
def update_repayment_schedule_description(cls, schedule_id, description):
"""
Update repayment schedule status.
Args:
schedule_id (int): ID of the repayment schedule.
paid (bool): Whether the repayment is marked as paid or not.
Returns:
schedule (RepaymentSchedule | None): The updated schedule object, or None if not found.
Update the repayment description for a specific schedule.
"""
try:
# Fetch the repayment schedule by ID
schedule = cls.query.get(schedule_id)
if not schedule:
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
if schedule:
# Update payment status
schedule.paid = paid
schedule.paid_at = datetime.now(timezone.utc) if paid else None
if schedule.due_process_count is None:
schedule.due_process_count = 0
schedule.due_process_count += 1
schedule.due_process_date = datetime.now(timezone.utc)
schedule.repay_description = description
schedule.updated_at = datetime.now(timezone.utc)
# Commit changes to the database
db.session.commit()
db.session.commit()
logger.info(f"Updated repayment description for schedule ID {schedule_id}")
return schedule
except SQLAlchemyError as e:
# Rollback changes if something goes wrong at the DB level
db.session.rollback()
logger.error(f"Database error updating repayment schedule {schedule_id}: {e}")
return None
return schedule.to_dict()
except Exception as e:
# Catch any other unexpected error
logger.error(f"Unexpected error updating repayment schedule {schedule_id}: {e}")
return None
db.session.rollback()
logger.error(f"Error updating repayment description for schedule {schedule_id}: {e}")
raise
@classmethod
def update_repayment_schedule_status(cls, schedule_id):
"""
Mark a repayment schedule as fully repaid when the parent loan is fully repaid.
This function does not take amount_collected because the loan is already cleared.
"""
try:
# Fetch schedule
schedule = cls.query.get(schedule_id)
if not schedule:
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
# Force balance to 0
schedule.partial_balance = 0.0
schedule.paid_status = RepaymentScheduleStatus.REPAID
schedule.paid = True
schedule.paid_at = datetime.now(timezone.utc)
# Track due processing
if schedule.due_process_count is None:
schedule.due_process_count = 0
schedule.due_process_count += 1
schedule.due_process_date = datetime.now(timezone.utc)
# Update timestamp
schedule.updated_at = datetime.now(timezone.utc)
# Commit changes
db.session.commit()
logger.info(f"Schedule {schedule_id} marked as REPAID since parent loan is fully repaid.")
return schedule.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error updating repayment schedule {schedule_id} after loan repayment: {e}")
raise
@classmethod
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
"""
Apply repayment to a loan schedule:
- Deduct from partial balance if partially paid.
- Otherwise deduct from installment amount.
- Update partial balance, paid status, timestamps, etc.
"""
try:
schedule = cls.query.get(schedule_id)
if not schedule:
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
# Normalize amount
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
if amount_collected <= Decimal("0.00"):
logger.info("Repayment amount must be greater than zero.")
return schedule.to_dict()
# Determine current balance
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID and (schedule.partial_balance or 0) > 0:
balance = Decimal(str(schedule.partial_balance))
else:
balance = Decimal(str(schedule.installment_amount))
# Deduct repayment
new_balance = balance - amount_collected
if new_balance < 0:
new_balance = Decimal("0.00") # prevent negatives
# Update schedule fields
schedule.partial_balance = float(new_balance) if new_balance > 0 else 0.0
schedule.updated_at = datetime.now(timezone.utc)
if new_balance == 0:
schedule.paid_status = RepaymentScheduleStatus.REPAID
schedule.paid = True
schedule.paid_at = datetime.now(timezone.utc)
else:
schedule.paid_status = RepaymentScheduleStatus.PARTIALLY_PAID
schedule.paid = False # not fully paid yet
# Track due processing
if schedule.due_process_count is None:
schedule.due_process_count = 0
schedule.due_process_count += 1
schedule.due_process_date = datetime.now(timezone.utc)
# Commit
db.session.commit()
logger.info(f"Repayment applied for schedule ID {schedule_id}. Remaining balance: {schedule.partial_balance}")
return schedule.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error applying repayment for schedule {schedule_id}: {e}")
raise
+7 -2
View File
@@ -11,6 +11,7 @@ from app.services.repayment import RepaymentService
from app.services.salary import SalaryService
from app.services.loan_repayment_schedule import LoanRepaymentScheduleService
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 app.config import settings
@@ -294,7 +295,7 @@ def process_overdue_loan(loan):
#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, paid=True)
LoanRepaymentScheduleService.update_repayment_schedule_status(loan.id)
logger.info(f"Updated Loan Repayment Schedule ID {loan.id} to PAID")
return
except Exception as e:
@@ -335,8 +336,12 @@ def process_overdue_loan(loan):
# 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["overdueLoanScheduleAmount"] = loan.installment_amount
repayment_data["Id"] = repayment.id
simbrella_response = SimbrellaClient.collect_loan_user_due_payment(repayment_data)
+2
View File
@@ -106,3 +106,5 @@ class LoanService:
Get all overdue loans.
"""
return Loan.get_overdue_loans()
+85 -5
View File
@@ -1,11 +1,13 @@
from app.models.loan_repayment_schedule import LoanRepaymentSchedule
from app.utils.logger import logger
from app.enums.loan_status import LoanStatus
class LoanRepaymentScheduleService:
@classmethod
def get_repayment_schedule_by_loan_id(cls, loan_id):
return LoanRepaymentSchedule.get_repayment_schedule_by_loan_id(loan_id)
def get_repayment_schedule_by_loan_id(cls, loan_id, include_paid=True):
return LoanRepaymentSchedule.get_repayment_schedule_by_loan_id(loan_id, include_paid=include_paid)
@classmethod
def get_overdue_repayment_schedule(cls):
return LoanRepaymentSchedule.get_overdue_repayment_schedule()
@@ -16,10 +18,88 @@ class LoanRepaymentScheduleService:
@classmethod
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
return LoanRepaymentSchedule.get_repayment_schedule_by_transaction_id(transaction_id)
@classmethod
def update_repayment_schedule_status(cls, schedule_id, paid):
def update_repayment_schedule_status(cls, schedule_id):
"""
Update repayment schedule status.
"""
return LoanRepaymentSchedule.update_repayment_schedule_status(schedule_id, paid)
return LoanRepaymentSchedule.update_repayment_schedule_status(schedule_id)
@classmethod
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
"""
Update repayment schedule balance.
"""
return LoanRepaymentSchedule.update_repayment_schedule_balance(schedule_id, amount_collected)
@classmethod
def update_repayment_schedule_description(cls, schedule_id, description):
"""
Update repayment schedule description.
"""
return LoanRepaymentSchedule.update_repayment_schedule_description(schedule_id, description)
@staticmethod
def handle_schedule_updates(updated_loan, data, amount_collected, message, loan_data):
"""
Handles updating loan repayment schedules depending on loan status
and overdue schedule data.
"""
try:
# Case 1: Loan fully repaid → mark all schedules paid
if updated_loan and updated_loan.get('status') == LoanStatus.REPAID:
repayment_schedule = LoanRepaymentScheduleService.get_repayment_schedule_by_loan_id(
updated_loan['debtId'], include_paid=False
)
logger.info(f'Loan repayment schedule: {repayment_schedule}')
if repayment_schedule:
for installment in repayment_schedule:
try:
logger.info(f'Processing installment: {installment}')
LoanRepaymentScheduleService.update_repayment_schedule_status(installment.id)
LoanRepaymentScheduleService.update_repayment_schedule_description(
installment.id,
message
)
logger.info(f'Updated installment {installment.id} as paid')
except Exception as e:
logger.error(f"Failed to update installment {installment.id}: {e}")
logger.info('All installments processed')
# Case 2: Partial repayment without overdueLoanScheduleId
elif updated_loan and updated_loan.get('status') == LoanStatus.ACTIVE_PARTIAL and not data.get('overdueLoanScheduleId'):
logger.info("Partial repayment detected, but no overdue schedule ID provided.")
# TODO: implement proportional installment updates
# Case 3: Overdue schedule repayment → update balance & description
elif data.get('overdueLoanScheduleId') is not None:
logger.info(f"Overdue loan schedule ID: {data['overdueLoanScheduleId']}")
try:
schedule_to_update = LoanRepaymentScheduleService.get_repayment_schedule_by_id_and_transaction_id(
data["overdueLoanScheduleId"], data["transactionId"]
)
logger.info(f"Schedule to update: {schedule_to_update}")
if schedule_to_update is None:
logger.warning(
f"Repayment schedule not found for ID {data['overdueLoanScheduleId']} "
f"and transaction ID {loan_data['transactionId']}"
)
else:
if not schedule_to_update.paid:
update_schedule_balance = LoanRepaymentScheduleService.update_repayment_schedule_balance(
schedule_to_update.id, amount_collected
)
logger.info(f"Updated loan schedule balance: {update_schedule_balance}")
LoanRepaymentScheduleService.update_repayment_schedule_description(
schedule_to_update.id,
message
)
except Exception as e:
logger.error(f"Failed to update repayment schedule installment: {e}")
except Exception as e:
logger.error(f"Unexpected error while handling schedule updates: {e}")