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)
|
# 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
|
||||||
|
|
||||||
@@ -153,7 +164,3 @@ class ProvideLoanService(BaseService):
|
|||||||
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']
|
||||||
+13
-3
@@ -4,7 +4,7 @@ from app.models.customer import Customer
|
|||||||
from app.models.account import Account
|
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 dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
class Loan(db.Model):
|
class Loan(db.Model):
|
||||||
@@ -42,11 +42,19 @@ class Loan(db.Model):
|
|||||||
|
|
||||||
loan_charges = relationship(
|
loan_charges = relationship(
|
||||||
"LoanCharge",
|
"LoanCharge",
|
||||||
primaryjoin="Loan.id == LoanCharge.loan_id",
|
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||||
foreign_keys="LoanCharge.loan_id",
|
foreign_keys="LoanCharge.loan_id",
|
||||||
back_populates="loan",
|
back_populates="loan",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loan_repayment_schedules = relationship(
|
||||||
|
"LoanRepaymentSchedule",
|
||||||
|
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||||
|
foreign_keys="LoanRepaymentSchedule.loan_id",
|
||||||
|
back_populates="loan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_loan(
|
def create_loan(
|
||||||
cls,
|
cls,
|
||||||
@@ -92,7 +100,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(
|
||||||
@@ -143,6 +150,9 @@ class Loan(db.Model):
|
|||||||
'defaultPenaltyFee': self.default_penalty_fee,
|
'defaultPenaltyFee': self.default_penalty_fee,
|
||||||
'continuousFee': self.continuous_fee,
|
'continuousFee': self.continuous_fee,
|
||||||
'collectionType': self.collection_type,
|
'collectionType': self.collection_type,
|
||||||
|
'upfrontFee': self.upfront_fee,
|
||||||
|
'repaymentAmount': self.repayment_amount,
|
||||||
|
'installmentAmount': self.installment_amount,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||||
'loanDate': self.created_at.isoformat() if self.created_at 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,
|
"productId": self.product_id,
|
||||||
"minAmount": self.min_amount,
|
"minAmount": self.min_amount,
|
||||||
"maxAmount": self.max_amount,
|
"maxAmount": self.max_amount,
|
||||||
"tenor": self.tenor,
|
"tenor": self.tenor
|
||||||
"schedule": self.schedule,
|
|
||||||
"list_order": self.list_order
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|||||||
@@ -33,12 +33,6 @@ def upgrade():
|
|||||||
existing_type=sa.NUMERIC(precision=10, scale=2),
|
existing_type=sa.NUMERIC(precision=10, scale=2),
|
||||||
type_=sa.Float(),
|
type_=sa.Float(),
|
||||||
existing_nullable=True)
|
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 ###
|
# ### 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