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
+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))