From c216c55928e5e795fadc83f017fd68d3cf8e2ac7 Mon Sep 17 00:00:00 2001 From: Vivian Dee Date: Fri, 25 Apr 2025 14:29:13 +0100 Subject: [PATCH] [add]: Lona repayment schedule --- app/api/services/base_service.py | 2 +- app/api/services/provide_loan.py | 29 +++++++---- app/models/__init__.py | 3 +- app/models/loan.py | 12 ++++- app/models/loan_repayment_schedule.py | 74 +++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 app/models/loan_repayment_schedule.py diff --git a/app/api/services/base_service.py b/app/api/services/base_service.py index b3a657e..fa1a77e 100644 --- a/app/api/services/base_service.py +++ b/app/api/services/base_service.py @@ -101,7 +101,7 @@ class BaseService: # Up-front payment: (only those fees due immediately i.e due_days == 0) 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) # Total amount: (upfront_payment + repayment_amount) diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 74a521a..232466c 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -11,6 +11,10 @@ from threading import Thread from app.models import Loan, Offer, Charge from app.api.enums import LoanStatus from app.extensions import db +from datetime import datetime, timezone +from dateutil.relativedelta import relativedelta +from app.models import LoanRepaymentSchedule + class ProvideLoanService(BaseService): TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN @@ -85,19 +89,26 @@ class ProvideLoanService(BaseService): status= LoanStatus.ACTIVE ) - db.session.flush() - - if not loan: logger.error(f"Failed to save loan details") return jsonify({ "message": "Failed to save loan details." }), 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) - logger.error(f"{charges}") + # logger.error(f"{charges}") loan_id = loan.id @@ -152,8 +163,4 @@ class ProvideLoanService(BaseService): db.session.rollback() return jsonify({ "message": "Internal Server Error" - }) , 500 - - - - + }) , 500 \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index 9cb9074..9fa2f1e 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -7,6 +7,7 @@ from .loan_charge import LoanCharge from .offer import Offer from .charge import Charge from .rac_checks import RACCheck +from .loan_repayment_schedule import LoanRepaymentSchedule -__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck'] \ No newline at end of file +__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule'] \ No newline at end of file diff --git a/app/models/loan.py b/app/models/loan.py index b8f9c1a..79c86b8 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -5,6 +5,9 @@ from app.models.account import Account from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import relationship 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): @@ -47,6 +50,13 @@ class Loan(db.Model): back_populates="loan", ) + loan_repayent_schedules = relationship( + "LoanRepaymentSchedule", + primaryjoin="Loan.id == LoanRepaymentSchedule.loan_id", + foreign_keys="LoanRepaymentSchedule.loan_id", + back_populates="loan", + ) + @classmethod def create_loan( cls, @@ -60,6 +70,7 @@ class Loan(db.Model): upfront_fee, repayment_amount, installment_amount, + tenor, status="pending", ): # Check if customer exists @@ -92,7 +103,6 @@ class Loan(db.Model): raise ValueError(f"Database integrity error: {err}") return loan - @classmethod def has_active_loans(cls, customer_id): active_loans = cls.query.filter_by( diff --git a/app/models/loan_repayment_schedule.py b/app/models/loan_repayment_schedule.py new file mode 100644 index 0000000..2523964 --- /dev/null +++ b/app/models/loan_repayment_schedule.py @@ -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''