Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d2659462 | |||
| 569d4c45d7 | |||
| bfb35e0285 | |||
| 1a0e1f324a | |||
| 6593aedc56 | |||
| 818f968935 | |||
| 84648d8242 | |||
| 80a41d5ee1 | |||
| cf3a96ad98 | |||
| d0dccbf1ec | |||
| 5c8ffc5bbc | |||
| 9d7c3cfb32 | |||
| f6f8e369c4 | |||
| 39ea231fa0 | |||
| da05ba0f3d | |||
| 0f4455e738 | |||
| 53a843d129 | |||
| 03c12fd9b5 | |||
| a338441086 | |||
| f048dd99ba | |||
| 46b3c856b1 | |||
| 1cbb55fae6 | |||
| 38dbb32579 | |||
| af14baead5 | |||
| fbc3f090a0 | |||
| 675c38462e |
@@ -55,6 +55,20 @@ class Config:
|
|||||||
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
|
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
|
||||||
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'chinenyeumeaku@gmail.com,umeakuchinenye@gmail.com')
|
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'chinenyeumeaku@gmail.com,umeakuchinenye@gmail.com')
|
||||||
|
|
||||||
|
|
||||||
|
# Processing Overdue LOANS sections
|
||||||
|
OVERDUE_LOAN_BATCH_SIZE = int(os.getenv("OVERDUE_LOAN_BATCH_SIZE", 10))
|
||||||
|
OVERDUE_LOAN_DELAY_SECONDS = int(os.getenv("OVERDUE_LOAN_DELAY_SECONDS", 5))
|
||||||
|
OVERDUE_LOAN_BATCH_DELAY_SECONDS = int(
|
||||||
|
os.getenv("OVERDUE_LOAN_BATCH_DELAY_SECONDS", 5)
|
||||||
|
)
|
||||||
|
OVERDUE_GRACE_PERIOD_DAYS = int(os.getenv("OVERDUE_GRACE_PERIOD_DAYS", 30))
|
||||||
|
OVERDUE_PROCESSING_LIST_LIMIT = int(os.getenv("OVERDUE_PROCESSING_LIST_LIMIT", 100))
|
||||||
|
PENAL_CHARGE_PERCENTAGE = os.getenv("PENAL_CHARGE_PERCENTAGE", 1)
|
||||||
|
PENAL_CHARGE_INTERVAL_DAYS = os.getenv("PENAL_CHARGE_INTERVAL_DAYS", 30)
|
||||||
|
PENAL_CHARGE_MAXIMUM_COUNT = os.getenv("PENAL_CHARGE_MAXIMUM_COUNT", 6)
|
||||||
|
|
||||||
|
|
||||||
BANK_CALL_API_TIME_OUT = os.getenv("BANK_CALL_API_TIME_OUT", 100)
|
BANK_CALL_API_TIME_OUT = os.getenv("BANK_CALL_API_TIME_OUT", 100)
|
||||||
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net/api")
|
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")
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ from enum import Enum
|
|||||||
class RepaymentScheduleStatus(str, Enum):
|
class RepaymentScheduleStatus(str, Enum):
|
||||||
PARTIALLY_PAID = "partially_paid"
|
PARTIALLY_PAID = "partially_paid"
|
||||||
REPAID = "repaid"
|
REPAID = "repaid"
|
||||||
|
ACTIVE = "active"
|
||||||
|
OVERDUE = "overdue"
|
||||||
@@ -54,4 +54,10 @@ class CollectLoanHelper:
|
|||||||
"comment": "COLLECT LOAN"
|
"comment": "COLLECT LOAN"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def chunk_list(data, chunk_size):
|
||||||
|
"""Yield successive chunk_size chunks from data."""
|
||||||
|
for i in range(0, len(data), chunk_size):
|
||||||
|
yield data[i:i + chunk_size]
|
||||||
|
|
||||||
|
|
||||||
@@ -92,9 +92,8 @@ class SimbrellaClient:
|
|||||||
vat_fee = loan_charges.get("VAT")['amount']
|
vat_fee = loan_charges.get("VAT")['amount']
|
||||||
interest_fee = loan_charges.get("INTEREST")['amount']
|
interest_fee = loan_charges.get("INTEREST")['amount']
|
||||||
insurance_fee = loan_charges.get("INSURANCE")['amount']
|
insurance_fee = loan_charges.get("INSURANCE")['amount']
|
||||||
|
product_id = str(loan_data.get('productId', ""))
|
||||||
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
|
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
|
||||||
|
|
||||||
disbursement_data = {
|
disbursement_data = {
|
||||||
"transactionId": loan_data.get('transactionId'),
|
"transactionId": loan_data.get('transactionId'),
|
||||||
"fbnTransactionId": loan_data.get('transactionId'),
|
"fbnTransactionId": loan_data.get('transactionId'),
|
||||||
@@ -103,7 +102,7 @@ class SimbrellaClient:
|
|||||||
"accountId": loan_data.get('accountId'),
|
"accountId": loan_data.get('accountId'),
|
||||||
"productId": str(loan_data.get('productId', "")),
|
"productId": str(loan_data.get('productId', "")),
|
||||||
"provideAmount": loan_data.get('currentLoanAmount'),
|
"provideAmount": loan_data.get('currentLoanAmount'),
|
||||||
"collectAmountInterest": interest_fee,
|
"collectAmountInterest": interest_fee if product_id != '3MPC' else 0,
|
||||||
"collectAmountMgtFee": mgt_fee,
|
"collectAmountMgtFee": mgt_fee,
|
||||||
"collectAmountInsurance": insurance_fee,
|
"collectAmountInsurance": insurance_fee,
|
||||||
"collectAmountVAT": vat_fee,
|
"collectAmountVAT": vat_fee,
|
||||||
@@ -148,7 +147,17 @@ class SimbrellaClient:
|
|||||||
result.get('responseMessage', ''))
|
result.get('responseMessage', ''))
|
||||||
reload_loan = LoanService.get_loan_by_transaction_id(transaction_id=data['transactionId'])
|
reload_loan = LoanService.get_loan_by_transaction_id(transaction_id=data['transactionId'])
|
||||||
reload_loan_data = reload_loan.to_dict()
|
reload_loan_data = reload_loan.to_dict()
|
||||||
|
#mark repayment schedule as active
|
||||||
|
repayment_schedule = LoanRepaymentScheduleService.get_repayment_schedule_by_loan_id(
|
||||||
|
reload_loan_data['debtId'], include_paid=False)
|
||||||
|
logger.info(f'Loan repayment schedule: {repayment_schedule}')
|
||||||
|
if repayment_schedule:
|
||||||
|
for schedule in repayment_schedule:
|
||||||
|
logger.info(f"Updating repayment schedule ID {schedule.id} status to ACTIVE")
|
||||||
|
LoanRepaymentScheduleService.update_repayment_schedule_status_to_active(schedule.id)
|
||||||
|
|
||||||
SimbrellaClient.verify_disbursement_transaction(reload_loan_data)
|
SimbrellaClient.verify_disbursement_transaction(reload_loan_data)
|
||||||
|
|
||||||
return ResponseHelper.success(response.json(), "Successful")
|
return ResponseHelper.success(response.json(), "Successful")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -279,6 +288,7 @@ class SimbrellaClient:
|
|||||||
logger.error("Received 404 from external service")
|
logger.error("Received 404 from external service")
|
||||||
return ResponseHelper.error("Verify Service url not found (404)", status_code=404)
|
return ResponseHelper.error("Verify Service url not found (404)", status_code=404)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
#check for res 00 and status 200
|
||||||
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', ''),
|
LoanService.set_disburse_verify_result(loan_data['debtId'], result.get('responseCode', ''),
|
||||||
result.get('responseMessage', ''))
|
result.get('responseMessage', ''))
|
||||||
|
|||||||
+28
-3
@@ -47,6 +47,9 @@ class Loan(db.Model):
|
|||||||
verify_description = db.Column(db.String(100), nullable=True)
|
verify_description = db.Column(db.String(100), nullable=True)
|
||||||
reference = db.Column(db.String(50), nullable=True)
|
reference = db.Column(db.String(50), nullable=True)
|
||||||
|
|
||||||
|
total_penal_charge = db.Column(db.Float, default=0.0)
|
||||||
|
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
customer = relationship(
|
customer = relationship(
|
||||||
"Customer",
|
"Customer",
|
||||||
primaryjoin="Customer.id == Loan.customer_id",
|
primaryjoin="Customer.id == Loan.customer_id",
|
||||||
@@ -92,6 +95,8 @@ class Loan(db.Model):
|
|||||||
'reference': self.reference,
|
'reference': self.reference,
|
||||||
'balance': self.balance,
|
'balance': self.balance,
|
||||||
'tenor': self.tenor,
|
'tenor': self.tenor,
|
||||||
|
'totalPenalCharge': self.total_penal_charge,
|
||||||
|
'lastPenalDate': self.last_penal_date
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -236,9 +241,15 @@ class Loan(db.Model):
|
|||||||
"""
|
"""
|
||||||
Get the latest loan without a disbursement date.
|
Get the latest loan without a disbursement date.
|
||||||
"""
|
"""
|
||||||
return cls.query.filter(
|
logger.info("Fetching latest loan without disburse date")
|
||||||
cls.disburse_date.is_(None)
|
|
||||||
).order_by(cls.created_at.desc()).first()
|
try:
|
||||||
|
return cls.query.filter(
|
||||||
|
cls.disburse_date.is_(None)
|
||||||
|
).order_by(cls.created_at.desc()).first()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching latest loan without disburse date: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_latest_loan_with_disburse_date(cls):
|
def get_latest_loan_with_disburse_date(cls):
|
||||||
@@ -388,3 +399,17 @@ class Loan(db.Model):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching overdue loans: {e}")
|
logger.error(f"Error fetching overdue loans: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_penal_to_loan(cls, loan_id, penal_amount):
|
||||||
|
|
||||||
|
loan = cls.query.get(loan_id)
|
||||||
|
|
||||||
|
if not loan:
|
||||||
|
raise ValueError("Loan not found")
|
||||||
|
penal_amount = Decimal(str(penal_amount))
|
||||||
|
|
||||||
|
loan.total_penal_charge = Decimal(str(loan.total_penal_charge or 0)) + penal_amount
|
||||||
|
loan.last_penal_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from os.path import devnull
|
from os.path import devnull
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
@@ -43,7 +43,85 @@ class LoanCharge(db.Model):
|
|||||||
'description': self.description,
|
'description': self.description,
|
||||||
'due': self.due
|
'due': self.due
|
||||||
}
|
}
|
||||||
|
#get last penal
|
||||||
|
@classmethod
|
||||||
|
def get_last_penal_no(cls, loan_id):
|
||||||
|
"""
|
||||||
|
Returns the last penal number created for a loan.
|
||||||
|
Example:
|
||||||
|
PENAL1 -> returns 1
|
||||||
|
PENAL3 -> returns 3
|
||||||
|
If none exists, returns 0.
|
||||||
|
"""
|
||||||
|
last_penal = (
|
||||||
|
cls.query
|
||||||
|
.filter(cls.loan_id == loan_id)
|
||||||
|
.filter(cls.code.like("PENAL%"))
|
||||||
|
.order_by(cls.id.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not last_penal:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(last_penal.code.replace("PENAL", ""))
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
@classmethod
|
||||||
|
def get_penal_charges_by_loan_id(cls, loan_id):
|
||||||
|
"""
|
||||||
|
Returns all penal charges for a specific loan.
|
||||||
|
"""
|
||||||
|
return cls.query.filter(
|
||||||
|
cls.loan_id == loan_id,
|
||||||
|
cls.code.like("PENAL%")
|
||||||
|
).all()
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_loan_charge_by_debt_id(cls, debt_id):
|
def get_loan_charge_by_debt_id(cls, debt_id):
|
||||||
return cls.query.filter_by(loan_id=debt_id)
|
return cls.query.filter_by(loan_id=debt_id)
|
||||||
|
|
||||||
|
#create penal charge
|
||||||
|
@classmethod
|
||||||
|
def create_penal_charges_for_loan(cls, loan_id, transaction_id, percent, penal_no, schedule_number, penal_amount=0.0):
|
||||||
|
"""
|
||||||
|
Create a penal charge for a given loan and schedule.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if loan_id is None:
|
||||||
|
raise ValueError("loan_id cannot be None")
|
||||||
|
|
||||||
|
code = f"PENAL{penal_no:02d}-SCHEDULE{schedule_number:02d}"
|
||||||
|
|
||||||
|
# Check if this penal charge already exists
|
||||||
|
existing = cls.query.filter_by(
|
||||||
|
loan_id=loan_id,
|
||||||
|
code=code
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
penal_charge = cls(
|
||||||
|
loan_id=loan_id,
|
||||||
|
transaction_id=transaction_id,
|
||||||
|
code=code,
|
||||||
|
amount=penal_amount,
|
||||||
|
percent=percent,
|
||||||
|
description=f"Penal Charge {penal_no} for loan {loan_id} schedule {schedule_number}",
|
||||||
|
due=True,
|
||||||
|
due_date=now
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(penal_charge)
|
||||||
|
db.session.commit()
|
||||||
|
except IntegrityError as err:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
|
||||||
|
return penal_charge
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy import or_
|
||||||
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
|
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
# from dateutil.relativedelta import relativedelta
|
# from dateutil.relativedelta import relativedelta
|
||||||
@@ -27,6 +29,9 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
partial_balance = db.Column(db.Float, default=0.0)
|
partial_balance = db.Column(db.Float, default=0.0)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
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))
|
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||||
|
penal_charge = db.Column(db.Float, default=0.0)
|
||||||
|
penal_count = db.Column(db.Integer, default=0)
|
||||||
|
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +53,11 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
'partial_balance': self.partial_balance,
|
'partial_balance': self.partial_balance,
|
||||||
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
||||||
'created_at': self.created_at.isoformat() if self.created_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
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
'penal_charge': self.penal_charge,
|
||||||
|
'penal_count': self.penal_count,
|
||||||
|
'last_penal_date': self.last_penal_date.isoformat() if self.last_penal_date else None
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -89,13 +98,72 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_overdue_repayment_schedule(cls):
|
def get_overdue_repayment_schedule(cls):
|
||||||
"""
|
"""
|
||||||
Get all overdue repayment schedules.
|
Get all overdue repayment schedules that are not repaid.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return cls.query.filter(cls.due_date < datetime.now(timezone.utc), cls.paid == False).all()
|
return cls.query.filter(cls.due_date < datetime.now(timezone.utc), cls.paid == False).order_by(cls.due_date.asc()).all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching overdue repayment schedules: {e}")
|
logger.error(f"Error fetching overdue repayment schedules: {e}")
|
||||||
return []
|
return []
|
||||||
|
@classmethod
|
||||||
|
def get_active_overdue_repayment_schedule(cls):
|
||||||
|
"""
|
||||||
|
Get all overdue repayment schedules that are active.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
cls.query
|
||||||
|
.filter(
|
||||||
|
cls.due_date < datetime.now(timezone.utc),
|
||||||
|
cls.paid_status == RepaymentScheduleStatus.ACTIVE
|
||||||
|
)
|
||||||
|
.order_by(cls.due_date.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching active overdue repayment schedules: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_overdue_repayment_schedule_with_grace_period(cls, grace_period_days, limit=None):
|
||||||
|
try:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
grace_period_date = now - timedelta(days=grace_period_days)
|
||||||
|
penal_interval = timedelta(days=settings.PENAL_CHARGE_INTERVAL_DAYS)
|
||||||
|
|
||||||
|
return cls.query.filter(
|
||||||
|
cls.due_date < grace_period_date,
|
||||||
|
cls.paid == False,
|
||||||
|
or_(
|
||||||
|
cls.last_penal_date == None, # never penalized before
|
||||||
|
cls.last_penal_date < now - penal_interval
|
||||||
|
)
|
||||||
|
).order_by(cls.due_date.asc()).limit(limit).all()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching overdue repayment schedules with grace period: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_partially_paid_overdue_repayment_schedule(cls):
|
||||||
|
"""
|
||||||
|
Get all overdue repayment schedules that are partially paid.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
cls.query
|
||||||
|
.filter(
|
||||||
|
cls.due_date < datetime.now(timezone.utc),
|
||||||
|
cls.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID
|
||||||
|
)
|
||||||
|
.order_by(cls.due_date.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching partially paid overdue repayment schedules: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
|
def get_repayment_schedule_by_transaction_id(cls, transaction_id):
|
||||||
@@ -163,6 +231,24 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
logger.error(f"Error updating repayment schedule {schedule_id} after loan repayment: {e}")
|
logger.error(f"Error updating repayment schedule {schedule_id} after loan repayment: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_repayment_schedule_status_to_active(cls, schedule_id):
|
||||||
|
"""
|
||||||
|
Update repayment schedule status to ACTIVE.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
schedule = cls.query.get(schedule_id)
|
||||||
|
if not schedule:
|
||||||
|
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||||
|
schedule.paid_status = RepaymentScheduleStatus.ACTIVE
|
||||||
|
schedule.updated_at = datetime.now(timezone.utc)
|
||||||
|
db.session.commit()
|
||||||
|
logger.info(f"Updated repayment schedule ID {schedule_id} status to ACTIVE")
|
||||||
|
return schedule.to_dict()
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
logger.error(f"Error updating repayment schedule status for schedule {schedule_id}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
||||||
@@ -222,5 +308,41 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
logger.error(f"Error applying repayment for schedule {schedule_id}: {e}")
|
logger.error(f"Error applying repayment for schedule {schedule_id}: {e}")
|
||||||
raise
|
raise
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_penal_to_schedule(cls, schedule_id, penal_amount):
|
||||||
|
|
||||||
|
schedule = cls.query.get(schedule_id)
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
penal_amount = Decimal(str(penal_amount))
|
||||||
|
|
||||||
|
current_penal = Decimal(str(schedule.penal_charge)) if schedule.penal_charge else Decimal("0")
|
||||||
|
|
||||||
|
schedule.penal_count = (schedule.penal_count or 0) + 1
|
||||||
|
schedule.penal_charge = current_penal + penal_amount
|
||||||
|
schedule.last_penal_date = now
|
||||||
|
schedule.due_process_date = now
|
||||||
|
schedule.updated_at = now
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
# Calculate penal charge
|
||||||
|
@classmethod
|
||||||
|
def calculate_penal_charge(cls, schedule):
|
||||||
|
|
||||||
|
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID:
|
||||||
|
outstanding = Decimal(str(schedule.partial_balance))
|
||||||
|
else:
|
||||||
|
outstanding = Decimal(str(schedule.installment_amount))
|
||||||
|
|
||||||
|
rate = Decimal(str(settings.PENAL_CHARGE_PERCENTAGE)) / 100
|
||||||
|
|
||||||
|
penal_charge = (outstanding * rate).quantize(
|
||||||
|
Decimal("0.01"),
|
||||||
|
rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
|
||||||
|
return penal_charge
|
||||||
|
|
||||||
|
|
||||||
+148
-134
@@ -1,8 +1,10 @@
|
|||||||
|
import time as time_module
|
||||||
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.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.helpers.collect_loan_helper import CollectLoanHelper
|
||||||
from app.utils.auth import get_headers
|
from app.utils.auth import get_headers
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.integrations.simbrella import SimbrellaClient
|
from app.integrations.simbrella import SimbrellaClient
|
||||||
@@ -10,9 +12,11 @@ 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.services.loan_repayment_schedule import LoanRepaymentScheduleService
|
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.loan_status import LoanStatus
|
||||||
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
|
from app.enums.repayment_schedule_status import RepaymentScheduleStatus
|
||||||
from app.utils.mail import send_report_email, get_report_data
|
from app.utils.mail import send_report_email, get_report_data
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
autocall_bp = Blueprint("autocall", __name__)
|
autocall_bp = Blueprint("autocall", __name__)
|
||||||
@@ -98,46 +102,7 @@ def retry_disbursement():
|
|||||||
logger.error(f"Failed to call retry disbursement {data}: {e}")
|
logger.error(f"Failed to call retry disbursement {data}: {e}")
|
||||||
|
|
||||||
|
|
||||||
@autocall_bp.route("/verify-disbursement", methods=["POST"])
|
|
||||||
def retry_verify_disbursement():
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
logger.info(f"Verify Disbursement 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.verify_transaction(data)
|
|
||||||
return ResponseHelper.success(message="Retry Verify Disbursement Request Sent Successfully", status_code=200)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to call retry disbursement {data}: {e}")
|
|
||||||
|
|
||||||
@autocall_bp.route("/start-repayment", methods=["POST"])
|
|
||||||
def start_repayment_office():
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
logger.info(f"Start Repay Transaction ID Data Received for :::: {data}")
|
|
||||||
loan_repayment_function(data, 'OFFICE')
|
|
||||||
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("/direct/loan", methods=["POST"])
|
@autocall_bp.route("/direct/loan", methods=["POST"])
|
||||||
@@ -164,18 +129,12 @@ def direct_loan():
|
|||||||
|
|
||||||
loan = LoanService.get_loan_by_transaction_id(transaction_id=transaction_id)
|
loan = LoanService.get_loan_by_transaction_id(transaction_id=transaction_id)
|
||||||
if not loan:
|
if not loan:
|
||||||
first_error = f"Loan with transaction id {transaction_id} does not exist"
|
logger.warning(f"Loan with transaction id {transaction_id} does not exist")
|
||||||
logger.warning(first_error)
|
return jsonify({
|
||||||
loan = LoanService.get_latest_loan_without_disburse_date()
|
"status": "error",
|
||||||
if not loan:
|
"message": f"Loan with transaction id {transaction_id} does not exist"
|
||||||
logger.info(f"No loan found without disbursement date")
|
}), 400
|
||||||
return jsonify({
|
|
||||||
"status": "error",
|
|
||||||
"first_error": first_error,
|
|
||||||
"message": f"No loan found without disbursement date"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# Tried 2 method to get the loan record at this point
|
|
||||||
loan_data = loan.to_dict()
|
loan_data = loan.to_dict()
|
||||||
|
|
||||||
# Prevent double disbursement
|
# Prevent double disbursement
|
||||||
@@ -281,85 +240,6 @@ def direct_repayment():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def loan_repayment_function(data, initiatedBy):
|
|
||||||
logger.info(f"Data Received Loan Repayment: {data} By {initiatedBy}")
|
|
||||||
|
|
||||||
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": initiatedBy,
|
|
||||||
"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"])
|
@autocall_bp.route("/refresh-verify-collection", methods=["GET"])
|
||||||
@@ -563,25 +443,157 @@ def report():
|
|||||||
logger.error(f"Error generating or sending report: {e}")
|
logger.error(f"Error generating or sending report: {e}")
|
||||||
return ResponseHelper.error("Failed to send report", status_code=500, error=str(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():
|
||||||
|
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"])
|
@autocall_bp.route("/overdue-loans", methods=["GET"])
|
||||||
def overdue_loans():
|
def overdue_loans():
|
||||||
try:
|
try:
|
||||||
# Step 1: Get all overdue loans
|
# Step 1: Get all active overdue loans
|
||||||
overdue_loans = LoanRepaymentScheduleService.get_overdue_repayment_schedule()
|
overdue_loans = LoanRepaymentScheduleService.get_active_overdue_repayment_schedule()
|
||||||
logger.info(f"Found {len(overdue_loans)} overdue loans.")
|
logger.info(f"Found {len(overdue_loans)} overdue loans.")
|
||||||
|
|
||||||
if not overdue_loans:
|
if not overdue_loans:
|
||||||
|
logger.info("No overdue loans found.")
|
||||||
return ResponseHelper.success(message="No overdue loans found", status_code=200)
|
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
|
# Step 2: Process each loan
|
||||||
for loan in overdue_loans:
|
for chunk_index, loan_chunk in enumerate(loan_chunks):
|
||||||
process_overdue_loan(loan)
|
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)
|
return ResponseHelper.success(message="Processed overdue loans successfully", status_code=200)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching overdue loans: {e}")
|
logger.exception(f"Error fetching overdue loans: {e}")
|
||||||
return ResponseHelper.error("Failed to fetch overdue loans", status_code=500, error=str(e))
|
return ResponseHelper.error("Failed to fetch overdue loans", status_code=500, error=str(e))
|
||||||
|
|
||||||
|
|
||||||
@@ -630,6 +642,7 @@ def process_overdue_loan(loan):
|
|||||||
|
|
||||||
# Update loan status
|
# Update loan status
|
||||||
try:
|
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)
|
LoanService.update_status(loan_id=loan.loan_id, status=LoanStatus.START_REPAY)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
@@ -647,6 +660,7 @@ def process_overdue_loan(loan):
|
|||||||
repayment_data["overdueLoanScheduleAmount"] = amount
|
repayment_data["overdueLoanScheduleAmount"] = amount
|
||||||
repayment_data["overdueLoanScheduleId"] = loan.id
|
repayment_data["overdueLoanScheduleId"] = loan.id
|
||||||
repayment_data["Id"] = repayment.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)
|
simbrella_response = SimbrellaClient.collect_loan_user_due_payment(repayment_data)
|
||||||
|
|
||||||
if isinstance(simbrella_response, tuple):
|
if isinstance(simbrella_response, tuple):
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class LoanService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def set_disbursement_loan_description(cls,loan_id,description):
|
def set_disbursement_loan_description(cls,loan_id,description):
|
||||||
|
|
||||||
return Loan.set_disbursement_message(loan_id, result, description)
|
return Loan.set_disbursement_message(loan_id, description)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -117,6 +117,9 @@ class LoanService:
|
|||||||
Get all overdue loans.
|
Get all overdue loans.
|
||||||
"""
|
"""
|
||||||
return Loan.get_overdue_loans()
|
return Loan.get_overdue_loans()
|
||||||
|
@classmethod
|
||||||
|
def apply_penal_to_loan(cls,loan_id,penal_charge):
|
||||||
|
return Loan.apply_penal_to_loan(loan_id,penal_charge)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _update_loan_after_collection(loan, loan_data, updated_loan, amount_collected, data, response_message):
|
def _update_loan_after_collection(loan, loan_data, updated_loan, amount_collected, data, response_message):
|
||||||
if loan.balance is None or loan.balance <= 0:
|
if loan.balance is None or loan.balance <= 0:
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from app.models.loan_charge import LoanCharge
|
||||||
|
|
||||||
|
class LoanChargesService:
|
||||||
|
@classmethod
|
||||||
|
def create_penal_charges_for_loan(cls, loan_id, transaction_id, percent, penal_no, schedule_number, penal_amount=0.0,):
|
||||||
|
return LoanCharge.create_penal_charges_for_loan(loan_id, transaction_id, percent, penal_no,schedule_number, penal_amount)
|
||||||
|
@classmethod
|
||||||
|
def get_last_penal_no(cls,loan_id):
|
||||||
|
return LoanCharge.get_last_penal_no(loan_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_penal_charges_by_loan_id(cls,loan_id):
|
||||||
|
return LoanCharge.get_penal_charges_by_loan_id(loan_id)
|
||||||
@@ -13,6 +13,12 @@ class LoanRepaymentScheduleService:
|
|||||||
def get_overdue_repayment_schedule(cls):
|
def get_overdue_repayment_schedule(cls):
|
||||||
return LoanRepaymentSchedule.get_overdue_repayment_schedule()
|
return LoanRepaymentSchedule.get_overdue_repayment_schedule()
|
||||||
@classmethod
|
@classmethod
|
||||||
|
def get_active_overdue_repayment_schedule(cls):
|
||||||
|
return LoanRepaymentSchedule.get_active_overdue_repayment_schedule()
|
||||||
|
@classmethod
|
||||||
|
def get_partially_paid_overdue_repayment_schedule(cls):
|
||||||
|
return LoanRepaymentSchedule.get_partially_paid_overdue_repayment_schedule()
|
||||||
|
@classmethod
|
||||||
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
|
def get_repayment_schedule_by_id_and_transaction_id(cls, id, transaction_id):
|
||||||
return LoanRepaymentSchedule.get_repayment_schedule_by_id_and_transaction_id(id, transaction_id)
|
return LoanRepaymentSchedule.get_repayment_schedule_by_id_and_transaction_id(id, transaction_id)
|
||||||
|
|
||||||
@@ -26,13 +32,24 @@ class LoanRepaymentScheduleService:
|
|||||||
Update repayment schedule status.
|
Update repayment schedule status.
|
||||||
"""
|
"""
|
||||||
return LoanRepaymentSchedule.update_repayment_schedule_status(schedule_id)
|
return LoanRepaymentSchedule.update_repayment_schedule_status(schedule_id)
|
||||||
|
@classmethod
|
||||||
|
def update_repayment_schedule_status_to_active(cls, schedule_id):
|
||||||
|
"""
|
||||||
|
Update repayment schedule status.
|
||||||
|
"""
|
||||||
|
return LoanRepaymentSchedule.update_repayment_schedule_status_to_active(schedule_id)
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
def update_repayment_schedule_balance(cls, schedule_id, amount_collected):
|
||||||
"""
|
"""
|
||||||
Update repayment schedule balance.
|
Update repayment schedule balance.
|
||||||
"""
|
"""
|
||||||
return LoanRepaymentSchedule.update_repayment_schedule_balance(schedule_id, amount_collected)
|
return LoanRepaymentSchedule.update_repayment_schedule_balance(schedule_id, amount_collected)
|
||||||
|
@classmethod
|
||||||
|
def calculate_penal_charge(cls, schedule):
|
||||||
|
"""
|
||||||
|
Calculate penal charge for a repayment schedule.
|
||||||
|
"""
|
||||||
|
return LoanRepaymentSchedule.calculate_penal_charge(schedule)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_repayment_schedule_description(cls, schedule_id, description):
|
def update_repayment_schedule_description(cls, schedule_id, description):
|
||||||
@@ -41,7 +58,16 @@ class LoanRepaymentScheduleService:
|
|||||||
"""
|
"""
|
||||||
return LoanRepaymentSchedule.update_repayment_schedule_description(schedule_id, description)
|
return LoanRepaymentSchedule.update_repayment_schedule_description(schedule_id, description)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_overdue_repayment_schedule_with_grace_period(cls, grace_period_days, limit=None):
|
||||||
|
return LoanRepaymentSchedule.get_overdue_repayment_schedule_with_grace_period(grace_period_days, limit=limit)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_penal_to_schedule(cls, schedule_id, penal_amount):
|
||||||
|
"""
|
||||||
|
Apply penal charge to a repayment schedule.
|
||||||
|
"""
|
||||||
|
return LoanRepaymentSchedule.apply_penal_to_schedule(schedule_id, penal_amount)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_schedule_updates(updated_loan, data, amount_collected, message, loan_data):
|
def handle_schedule_updates(updated_loan, data, amount_collected, message, loan_data):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: A successful response
|
description: A successful response
|
||||||
|
/autocall/process-penal-charges:
|
||||||
|
get:
|
||||||
|
summary: Get all overdue loans with grace period
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: A successful response
|
||||||
/autocall/direct/loan:
|
/autocall/direct/loan:
|
||||||
post:
|
post:
|
||||||
summary: Direct call for loan disbursement
|
summary: Direct call for loan disbursement
|
||||||
|
|||||||
Reference in New Issue
Block a user