[add]: Lona repayment schedule
This commit is contained in:
@@ -101,7 +101,7 @@ class BaseService:
|
|||||||
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
||||||
upfront_payment = sum(upfront_fees)
|
upfront_payment = sum(upfront_fees)
|
||||||
|
|
||||||
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
|
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
|
||||||
repayment_amount = amount + (sum(postpaid_fees) * tenor)
|
repayment_amount = amount + (sum(postpaid_fees) * tenor)
|
||||||
|
|
||||||
# Total amount: (upfront_payment + repayment_amount)
|
# Total amount: (upfront_payment + repayment_amount)
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ from threading import Thread
|
|||||||
from app.models import Loan, Offer, Charge
|
from app.models import Loan, Offer, Charge
|
||||||
from app.api.enums import LoanStatus
|
from app.api.enums import LoanStatus
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from app.models import LoanRepaymentSchedule
|
||||||
|
|
||||||
|
|
||||||
class ProvideLoanService(BaseService):
|
class ProvideLoanService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
||||||
@@ -85,19 +89,26 @@ class ProvideLoanService(BaseService):
|
|||||||
status= LoanStatus.ACTIVE
|
status= LoanStatus.ACTIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
|
|
||||||
if not loan:
|
if not loan:
|
||||||
logger.error(f"Failed to save loan details")
|
logger.error(f"Failed to save loan details")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Failed to save loan details."
|
"message": "Failed to save loan details."
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, Offer = offer, charges = charges)
|
||||||
|
|
||||||
|
|
||||||
|
if not schedule:
|
||||||
|
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
||||||
|
return jsonify({
|
||||||
|
"message": "Failed to generate loan repayment schedule."
|
||||||
|
}), 400
|
||||||
|
|
||||||
charges = Charge.get_offer_charges(offer.id)
|
charges = Charge.get_offer_charges(offer.id)
|
||||||
|
|
||||||
logger.error(f"{charges}")
|
# logger.error(f"{charges}")
|
||||||
|
|
||||||
loan_id = loan.id
|
loan_id = loan.id
|
||||||
|
|
||||||
@@ -152,8 +163,4 @@ class ProvideLoanService(BaseService):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Internal Server Error"
|
"message": "Internal Server Error"
|
||||||
}) , 500
|
}) , 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ from .loan_charge import LoanCharge
|
|||||||
from .offer import Offer
|
from .offer import Offer
|
||||||
from .charge import Charge
|
from .charge import Charge
|
||||||
from .rac_checks import RACCheck
|
from .rac_checks import RACCheck
|
||||||
|
from .loan_repayment_schedule import LoanRepaymentSchedule
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck']
|
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule']
|
||||||
+11
-1
@@ -5,6 +5,9 @@ from app.models.account import Account
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from app.models.loan_charge import LoanCharge
|
from app.models.loan_charge import LoanCharge
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from app.models.loan_repayment_schedule import LoanRepaymentSchedule # Make sure this import exists
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Loan(db.Model):
|
class Loan(db.Model):
|
||||||
@@ -47,6 +50,13 @@ class Loan(db.Model):
|
|||||||
back_populates="loan",
|
back_populates="loan",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loan_repayent_schedules = relationship(
|
||||||
|
"LoanRepaymentSchedule",
|
||||||
|
primaryjoin="Loan.id == LoanRepaymentSchedule.loan_id",
|
||||||
|
foreign_keys="LoanRepaymentSchedule.loan_id",
|
||||||
|
back_populates="loan",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_loan(
|
def create_loan(
|
||||||
cls,
|
cls,
|
||||||
@@ -60,6 +70,7 @@ class Loan(db.Model):
|
|||||||
upfront_fee,
|
upfront_fee,
|
||||||
repayment_amount,
|
repayment_amount,
|
||||||
installment_amount,
|
installment_amount,
|
||||||
|
tenor,
|
||||||
status="pending",
|
status="pending",
|
||||||
):
|
):
|
||||||
# Check if customer exists
|
# Check if customer exists
|
||||||
@@ -92,7 +103,6 @@ class Loan(db.Model):
|
|||||||
raise ValueError(f"Database integrity error: {err}")
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
return loan
|
return loan
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_active_loans(cls, customer_id):
|
def has_active_loans(cls, customer_id):
|
||||||
active_loans = cls.query.filter_by(
|
active_loans = cls.query.filter_by(
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
class LoanRepaymentSchedule(db.Model):
|
||||||
|
__tablename__ = 'loan_repayment_schedules'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
loan_id = db.Column(db.Integer, nullable=False)
|
||||||
|
installment_number = db.Column(db.Integer, nullable=False)
|
||||||
|
due_date = db.Column(db.DateTime, nullable=False)
|
||||||
|
principal_amount = db.Column(db.Float, default=0.0)
|
||||||
|
interest_amount = db.Column(db.Float, default=0.0)
|
||||||
|
total_installment = db.Column(db.Float, default=0.0)
|
||||||
|
paid = db.Column(db.Boolean, default=False)
|
||||||
|
paid_at = db.Column(db.DateTime, nullable=True)
|
||||||
|
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))
|
||||||
|
|
||||||
|
loan = relationship(
|
||||||
|
"Loan",
|
||||||
|
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||||
|
foreign_keys=[loan_id],
|
||||||
|
back_populates="loan_charges",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_repayment_schedule(cls, loan, Offer, charges):
|
||||||
|
"""
|
||||||
|
Add repayment schedules for a given loan.
|
||||||
|
"""
|
||||||
|
if not loan.amount or not loan.installment_amount:
|
||||||
|
raise ValueError("Loan must have amount and installment_amount set.")
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
schedules = []
|
||||||
|
interest_fee = charges["interest"]
|
||||||
|
|
||||||
|
tenor = Offer.tenor // 30
|
||||||
|
|
||||||
|
principal = loan.amount / tenor
|
||||||
|
interest = interest_fee["fee"] / tenor
|
||||||
|
|
||||||
|
for i in range(tenor):
|
||||||
|
due_date = now + relativedelta(months=i + 1)
|
||||||
|
schedule = LoanRepaymentSchedule(
|
||||||
|
loan_id=loan.id,
|
||||||
|
installment_number=i + 1,
|
||||||
|
due_date=due_date,
|
||||||
|
principal_amount=round(principal, 2),
|
||||||
|
interest_amount=round(interest, 2),
|
||||||
|
total_installment=round(loan.installment_amount, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(schedule)
|
||||||
|
schedules.append(schedule)
|
||||||
|
|
||||||
|
return schedules
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'loanId': self.loan_id,
|
||||||
|
'installmentNumber': self.installment_number,
|
||||||
|
'dueDate': self.due_date.isoformat(),
|
||||||
|
'principalAmount': self.principal_amount,
|
||||||
|
'interestAmount': self.interest_amount,
|
||||||
|
'totalInstallment': self.total_installment,
|
||||||
|
'paid': self.paid,
|
||||||
|
'paidAt': self.paid_at.isoformat() if self.paid_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
|
||||||
Reference in New Issue
Block a user