Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f659fa9cf2 | |||
| 23e340be27 | |||
| adc2498044 | |||
| eb7f783b18 | |||
| 5040002c54 | |||
| dc9415ff79 | |||
| 4822de764a | |||
| 51995a3e02 | |||
| 265bba2365 | |||
| 9985a58b56 | |||
| b41df3fe02 | |||
| 79317632b6 | |||
| 1734007476 | |||
| bbf6953dc5 |
@@ -1,6 +1,5 @@
|
||||
from flask import session, jsonify
|
||||
from app.models.loan import Loan
|
||||
from app.models.transaction_offers import TransactionOffer
|
||||
from app.utils.logger import logger
|
||||
from app.api.services.base_service import BaseService
|
||||
from app.api.schemas.eligibility_check import EligibilityCheckSchema
|
||||
@@ -187,10 +186,10 @@ class EligibilityCheckService(BaseService):
|
||||
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
|
||||
return False
|
||||
|
||||
daily_count = TransactionOffer.get_daily_loan_count(customer_id, offer_id)
|
||||
daily_count = Loan.get_daily_loan_count(customer_id, offer.product_id)
|
||||
|
||||
|
||||
logger.error(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
|
||||
logger.info(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
|
||||
|
||||
if offer.max_daily_loans is not None and daily_count >= offer.max_daily_loans:
|
||||
return False
|
||||
|
||||
@@ -93,7 +93,7 @@ class OfferAnalysis:
|
||||
|
||||
|
||||
|
||||
logger.info(f"These are the salarie amounts ***** : {str(salaries)}", exc_info=True)
|
||||
logger.info(f"These are the salary amounts ***** : {str(salaries)}", exc_info=True)
|
||||
|
||||
#Least salary in the last 6 months
|
||||
min_salary = min(salaries)
|
||||
@@ -113,7 +113,7 @@ class OfferAnalysis:
|
||||
|
||||
else: # Income is not consistent
|
||||
eligible_amount = 0
|
||||
logger.info("Applying np percentage on least salary due unstable income.")
|
||||
logger.info("Applying no percentage on least salary due unstable income.")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from marshmallow import ValidationError
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
from app.api.services.base_service import BaseService
|
||||
from app.api.enums import TransactionType
|
||||
from app.models.transaction_offers import TransactionOffer
|
||||
from app.utils.logger import logger
|
||||
from app.api.schemas.select_offer import SelectOfferSchema
|
||||
from app.extensions import db
|
||||
@@ -57,12 +58,20 @@ class SelectOfferService(BaseService):
|
||||
# Get the offer by product ID
|
||||
offer = Offer.get_offer_by_product_id(product_id)
|
||||
|
||||
transaction_offer = TransactionOffer.get_transaction_offer(transaction_offer_id=offer_id)
|
||||
|
||||
if not transaction_offer:
|
||||
logger.error(f"offer {offer_id} not found for customer {customer_id} and transaction {transaction_id}.")
|
||||
return ResponseHelper.error(result_description="Offer not found.")
|
||||
|
||||
db.session.flush()
|
||||
|
||||
if amount < offer.min_amount:
|
||||
if amount < transaction_offer.min_amount:
|
||||
logger.error(f"The amount {amount} is less than the minimum allowed offer amount {transaction_offer.min_amount}.")
|
||||
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
|
||||
elif amount > offer.max_amount:
|
||||
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed offer amount.")
|
||||
elif amount > transaction_offer.eligible_amount:
|
||||
logger.error(f"The amount {amount} is greater than the eligible offer amount {transaction_offer.eligible_amount}.")
|
||||
return ResponseHelper.error(result_description="The amount is greater than the eligible offer amount.")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from .rac_checks import RACCheck
|
||||
from .loan_repayment_schedule import LoanRepaymentSchedule
|
||||
from .transaction_offers import TransactionOffer
|
||||
from .repayments_data import RepaymentsData
|
||||
from .salary import Salary
|
||||
|
||||
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData']
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData', 'Salary']
|
||||
+20
-1
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from itertools import product
|
||||
from app.extensions import db
|
||||
from app.models.customer import Customer
|
||||
@@ -215,6 +215,25 @@ class Loan(db.Model):
|
||||
# Update loan status and the updated_at timestamp
|
||||
loan.status = status
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_daily_loan_count(cls, customer_id, product_id):
|
||||
"""
|
||||
Returns the count of loans created today for a customer.
|
||||
"""
|
||||
|
||||
start_of_day = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_of_day = start_of_day + timedelta(days=1)
|
||||
|
||||
return cls.query.filter_by(
|
||||
customer_id=customer_id,
|
||||
product_id=product_id,
|
||||
).filter(
|
||||
cls.created_at >= start_of_day,
|
||||
cls.created_at < end_of_day
|
||||
).count()
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Convert the Loan object to a dictionary format for JSON serialization.
|
||||
|
||||
+27
-3
@@ -21,12 +21,16 @@ class Repayment(db.Model):
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
# repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
repay_date = db.Column(db.DateTime, nullable=True)
|
||||
repay_result = db.Column(db.String(10), nullable=True)
|
||||
repay_description = db.Column(db.String(100), nullable=True)
|
||||
verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
# verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
verify_date = db.Column(db.DateTime, nullable=True)
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
initiated_by = db.Column(db.String(50), nullable=True)
|
||||
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
|
||||
@classmethod
|
||||
def create_repayment(cls, customer_id, loan, transaction_id):
|
||||
@@ -42,7 +46,8 @@ class Repayment(db.Model):
|
||||
product_id=loan.product_id,
|
||||
transaction_id = transaction_id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
initiated_by='USER_INITIATED'
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -51,6 +56,25 @@ class Repayment(db.Model):
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
|
||||
return repayment
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"loan_id": self.loan_id,
|
||||
"customer_id": self.customer_id,
|
||||
"product_id": self.product_id,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
"transaction_id": self.transaction_id,
|
||||
"repay_date": self.repay_date.isoformat() if self.repay_date else None,
|
||||
"repay_result": self.repay_result,
|
||||
"repay_description": self.repay_description,
|
||||
"verify_date": self.verify_date.isoformat() if self.verify_date else None,
|
||||
"verify_result": self.verify_result,
|
||||
"verify_description": self.verify_description,
|
||||
"initiated_by": self.initiated_by,
|
||||
"salary_amount": self.salary_amount
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Repayment {self.id}>'
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class RepaymentsData(db.Model):
|
||||
__tablename__ = 'repayments_data'
|
||||
__tablename__ = "repayments_data"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
fbn_transaction_id = db.Column(db.String(50), nullable=True)
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
account_id = db.Column(db.String(50), nullable=True)
|
||||
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
amount_collected = db.Column(db.Float, nullable=True, default=0.0)
|
||||
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
|
||||
response_code = db.Column(db.String(10), nullable=True)
|
||||
response_descr = db.Column(db.String(255), nullable=True)
|
||||
balance = db.Column(db.Float, nullable=True, default=0.0)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"transaction_id": self.transaction_id,
|
||||
"fbn_transaction_id": self.fbn_transaction_id,
|
||||
"customer_id": self.customer_id,
|
||||
"account_id": self.account_id,
|
||||
"repayment_amount": self.repayment_amount,
|
||||
"amount_collected": self.amount_collected,
|
||||
"added_date": self.added_date.isoformat() if self.added_date else None,
|
||||
"response_code": self.response_code,
|
||||
"response_descr": self.response_descr,
|
||||
"balance": self.balance,
|
||||
}
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
class Salary(db.Model):
|
||||
__tablename__ = 'salaries'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
customer_id = db.Column(db.String(50), nullable=False)
|
||||
account_id = db.Column(db.String(50), nullable=True)
|
||||
status = db.Column(db.String(20), default='active')
|
||||
amount = db.Column(db.Float, nullable=False, default=0.0)
|
||||
salary_date = db.Column(db.DateTime(timezone=False), server_default=func.now())
|
||||
created_at = db.Column(db.DateTime(timezone=False), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"customer_id": self.customer_id,
|
||||
"account_id": self.account_id,
|
||||
"status": self.status,
|
||||
"amount": self.amount,
|
||||
"salary_date": self.salary_date.isoformat() if self.salary_date else None,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Salary {self.id} - {self.amount}>'
|
||||
|
||||
@@ -17,8 +17,10 @@ class Transaction(db.Model):
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
type = db.Column(db.String(50), nullable=False)
|
||||
channel = db.Column(db.String(50), nullable=False)
|
||||
phone_number = db.Column(db.String(50), nullable=True)
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Transaction {self.id}>'
|
||||
|
||||
|
||||
@@ -76,23 +76,6 @@ class TransactionOffer(db.Model):
|
||||
"""
|
||||
return cls.query.filter_by(customer_id=customer_id).count()
|
||||
|
||||
@classmethod
|
||||
def get_daily_loan_count(cls, customer_id, offer_id):
|
||||
"""
|
||||
Returns the count of loans created today for a customer.
|
||||
"""
|
||||
|
||||
start_of_day = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_of_day = start_of_day + timedelta(days=1)
|
||||
|
||||
return cls.query.filter_by(
|
||||
customer_id=customer_id,
|
||||
offer_id=offer_id
|
||||
).filter(
|
||||
cls.created_at >= start_of_day,
|
||||
cls.created_at < end_of_day
|
||||
).count()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_latest_transaction_offer(cls, customer_id):
|
||||
@@ -102,6 +85,13 @@ class TransactionOffer(db.Model):
|
||||
return cls.query.filter_by(customer_id=customer_id) \
|
||||
.order_by(cls.created_at.desc()) \
|
||||
.first()
|
||||
|
||||
@classmethod
|
||||
def get_transaction_offer(cls, transaction_offer_id):
|
||||
"""
|
||||
Returns a transaction offer by its ID.
|
||||
"""
|
||||
return cls.query.get(transaction_offer_id)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 1696ee63c28a
|
||||
Revises: b54422fb31e0
|
||||
Create Date: 2025-06-18 12:28:23.942143
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1696ee63c28a'
|
||||
down_revision = 'b54422fb31e0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('salaries',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||
sa.Column('account_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('amount', sa.Float(), nullable=False),
|
||||
sa.Column('salary_date', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('repayments', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('initiated_by', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('salary_amount', sa.Float(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('balance', sa.Float(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
|
||||
batch_op.drop_column('balance')
|
||||
|
||||
with op.batch_alter_table('repayments', schema=None) as batch_op:
|
||||
batch_op.drop_column('salary_amount')
|
||||
batch_op.drop_column('initiated_by')
|
||||
|
||||
op.drop_table('salaries')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 7a0caf83d5be
|
||||
Revises: 1696ee63c28a
|
||||
Create Date: 2025-06-19 04:35:23.660261
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7a0caf83d5be'
|
||||
down_revision = '1696ee63c28a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('salaries', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('account_id', sa.String(length=50), nullable=True))
|
||||
batch_op.drop_column('account_type')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('salaries', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('account_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
|
||||
batch_op.drop_column('account_id')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,46 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: b54422fb31e0
|
||||
Revises: 0acd553309a1
|
||||
Create Date: 2025-06-16 12:24:09.159498
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b54422fb31e0'
|
||||
down_revision = '0acd553309a1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('account_id', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('repayment_amount', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('amount_collected', sa.Float(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('phone_number', sa.String(length=50), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||
batch_op.drop_column('phone_number')
|
||||
|
||||
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
|
||||
batch_op.drop_column('amount_collected')
|
||||
batch_op.drop_column('repayment_amount')
|
||||
batch_op.drop_column('account_id')
|
||||
batch_op.drop_column('customer_id')
|
||||
batch_op.drop_column('fbn_transaction_id')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user