Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6973630845 | |||
| 5d37ba30fb | |||
| cf0502459b | |||
| 851422c335 | |||
| ddbabcaca9 | |||
| c216c55928 | |||
| 0995f08aea | |||
| e034c0ff9d | |||
| 67c6d909f8 | |||
| e08dfe9894 |
@@ -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
|
||||
@@ -67,6 +71,7 @@ class ProvideLoanService(BaseService):
|
||||
upfront_fee = charges["upfront_payment"]
|
||||
repayment_amount = charges["repayment_amount"]
|
||||
installment_amount = charges["installment_amount"]
|
||||
tenor = offer.tenor // 30 # Convert to months
|
||||
|
||||
|
||||
|
||||
@@ -85,19 +90,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, tenor = tenor)
|
||||
|
||||
|
||||
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 +164,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,68 @@
|
||||
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)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
installment_number = db.Column(db.Integer, nullable=False)
|
||||
due_date = db.Column(db.DateTime, nullable=False)
|
||||
installment_amount= db.Column(db.Float, default=0.0)
|
||||
total_repayment_amount = 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, tenor):
|
||||
"""
|
||||
Add repayment schedules for a given loan.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
schedules = []
|
||||
|
||||
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,
|
||||
total_repayment_amount = round(loan.repayment_amount, 2),
|
||||
installment_amount=round(loan.installment_amount, 2),
|
||||
product_id = loan.product_id
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
@@ -47,13 +47,26 @@
|
||||
"productId": {
|
||||
"type": "string",
|
||||
"example": "101"
|
||||
},
|
||||
"installment": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 10000.0
|
||||
},
|
||||
"repaymentDate": {
|
||||
"type": "string",
|
||||
"example": "2025-04-24 10:31:"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"totalDebtAmount": {
|
||||
"type": "integer",
|
||||
"example": 8500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resultCode": {
|
||||
"type": "string",
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
"format": "float",
|
||||
"example": 10000.0
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "string",
|
||||
"example": "2025-04-24 10:31:"
|
||||
},
|
||||
"upfrontPayment": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
@@ -75,7 +79,7 @@
|
||||
"format": "float",
|
||||
"example": 100.0
|
||||
},
|
||||
"recommendedRepaymentDates": {
|
||||
"installmentRepaymentDates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -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,42 @@
|
||||
"""Migration on Fri Apr 25 15:01:00 UTC 2025
|
||||
|
||||
Revision ID: 2a45dd99c9cb
|
||||
Revises: 2cf0c177ca02
|
||||
Create Date: 2025-04-25 15:01:51.129681
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2a45dd99c9cb'
|
||||
down_revision = '2cf0c177ca02'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
|
||||
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('total_repayment_amount', sa.Float(), nullable=True))
|
||||
batch_op.drop_column('principal_amount')
|
||||
batch_op.drop_column('interest_amount')
|
||||
batch_op.drop_column('total_installment')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('total_installment', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||
batch_op.add_column(sa.Column('interest_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||
batch_op.add_column(sa.Column('principal_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||
batch_op.drop_column('total_repayment_amount')
|
||||
batch_op.drop_column('installment_amount')
|
||||
batch_op.drop_column('product_id')
|
||||
|
||||
# ### 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