Merge branch 'loan_repayment_schedules' of DigiFi/digifi-BankToProductCore into master
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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']
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule']
|
||||
+13
-3
@@ -4,7 +4,7 @@ from app.models.customer import Customer
|
||||
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
|
||||
|
||||
|
||||
class Loan(db.Model):
|
||||
@@ -42,11 +42,19 @@ class Loan(db.Model):
|
||||
|
||||
loan_charges = relationship(
|
||||
"LoanCharge",
|
||||
primaryjoin="Loan.id == LoanCharge.loan_id",
|
||||
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||
foreign_keys="LoanCharge.loan_id",
|
||||
back_populates="loan",
|
||||
)
|
||||
|
||||
loan_repayment_schedules = relationship(
|
||||
"LoanRepaymentSchedule",
|
||||
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||
foreign_keys="LoanRepaymentSchedule.loan_id",
|
||||
back_populates="loan",
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def create_loan(
|
||||
cls,
|
||||
@@ -92,7 +100,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(
|
||||
@@ -143,6 +150,9 @@ class Loan(db.Model):
|
||||
'defaultPenaltyFee': self.default_penalty_fee,
|
||||
'continuousFee': self.continuous_fee,
|
||||
'collectionType': self.collection_type,
|
||||
'upfrontFee': self.upfront_fee,
|
||||
'repaymentAmount': self.repayment_amount,
|
||||
'installmentAmount': self.installment_amount,
|
||||
'status': self.status,
|
||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
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="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||
foreign_keys=[loan_id],
|
||||
back_populates="loan_repayment_schedules",
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_repayment_schedule(cls, loan, Offer, charges):
|
||||
"""
|
||||
Add repayment schedules for a given loan.
|
||||
"""
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
schedules = []
|
||||
interest_fee = charges["interest"]
|
||||
|
||||
tenor = Offer.tenor // 30
|
||||
|
||||
principal = loan.initial_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}>'
|
||||
+1
-3
@@ -71,9 +71,7 @@ class Offer(db.Model):
|
||||
"productId": self.product_id,
|
||||
"minAmount": self.min_amount,
|
||||
"maxAmount": self.max_amount,
|
||||
"tenor": self.tenor,
|
||||
"schedule": self.schedule,
|
||||
"list_order": self.list_order
|
||||
"tenor": self.tenor
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -33,12 +33,6 @@ def upgrade():
|
||||
existing_type=sa.NUMERIC(precision=10, scale=2),
|
||||
type_=sa.Float(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('upfront_fee', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('repayment_amount', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Migration on Fri Apr 25 14:02:01 UTC 2025
|
||||
|
||||
Revision ID: 2cf0c177ca02
|
||||
Revises: 1b2339f43824
|
||||
Create Date: 2025-04-25 14:02:42.244146
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2cf0c177ca02'
|
||||
down_revision = '1b2339f43824'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('loan_repayment_schedules',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('loan_id', sa.Integer(), nullable=False),
|
||||
sa.Column('installment_number', sa.Integer(), nullable=False),
|
||||
sa.Column('due_date', sa.DateTime(), nullable=False),
|
||||
sa.Column('principal_amount', sa.Float(), nullable=True),
|
||||
sa.Column('interest_amount', sa.Float(), nullable=True),
|
||||
sa.Column('total_installment', sa.Float(), nullable=True),
|
||||
sa.Column('paid', sa.Boolean(), nullable=True),
|
||||
sa.Column('paid_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('loan_repayment_schedules')
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user