diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index c318d8c..ba9e2b7 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -1,4 +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 @@ -50,6 +51,12 @@ class EligibilityCheckService(BaseService): return ResponseHelper.error(result_description="Invalid Customer or Account") db.session.flush() + + # Determine Loan count + is_eligible = EligibilityCheckService.check_loan_limits(customer_id) + + if not is_eligible: + return ResponseHelper.error(result_description="Max loan count reached") # Call RACCheck response = SimbrellaIntegration.rac_check( @@ -58,7 +65,7 @@ class EligibilityCheckService(BaseService): transaction_id = transaction.transaction_id, ) - # this chck for error is not valid + # this chek for error is not valid if response.status_code != 200: return ResponseHelper.error(result_description="RACCheck failed") @@ -146,4 +153,33 @@ class EligibilityCheckService(BaseService): except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) db.session.rollback() - return ResponseHelper.internal_server_error() \ No newline at end of file + return ResponseHelper.internal_server_error() + + + @staticmethod + def check_loan_limits(customer_id): + """ + Checks if a customer has exceeded the loan limits for given offer. + """ + loan = Loan.get_customer_last_loan(customer_id) + + if not loan: + return True + + offer_id = loan.offer_id[:5] + + offer = Offer.get_offer_by_id(offer_id) + if not offer: + 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) + + + logger.error(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 + + + return True diff --git a/app/models/account.py b/app/models/account.py index 1ee197d..30774e7 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -13,7 +13,7 @@ class Account(db.Model): status = db.Column(db.String(20), default='active') lien_amount = db.Column(db.Float, default=0.0) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) customer = relationship( "Customer", @@ -27,7 +27,9 @@ class Account(db.Model): account = cls( id=id, customer_id=customer_id, - account_type=account_type + account_type=account_type, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) try: diff --git a/app/models/charge.py b/app/models/charge.py index 17986b1..91fa42c 100644 --- a/app/models/charge.py +++ b/app/models/charge.py @@ -14,7 +14,7 @@ class Charge(db.Model): description = db.Column(db.Text, nullable=True) due = db.Column(db.Integer, nullable=False) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) offer = relationship( "Offer", primaryjoin="Charge.offer_id == Offer.id", @@ -57,7 +57,9 @@ class Charge(db.Model): code = code, percent = percent, description = description, - due = due_days + due = due_days, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) db.session.add(charge_obj) diff --git a/app/models/customer.py b/app/models/customer.py index 036712c..0103761 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -12,7 +12,7 @@ class Customer(db.Model): msisdn = db.Column(db.String(20), unique=True, nullable=False) country_code = db.Column(db.String(3), nullable=False) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) accounts = relationship( "Account", primaryjoin="Customer.id == Account.customer_id", @@ -47,7 +47,13 @@ class Customer(db.Model): raise ValueError("Customer already exists") # Create the customer - customer = cls(id=id, msisdn=msisdn, country_code=country_code) + customer = cls( + id=id, + msisdn=msisdn, + country_code=country_code, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) + ) try: db.session.add(customer) diff --git a/app/models/loan.py b/app/models/loan.py index 6a0b9ab..fd0328e 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -39,7 +39,7 @@ class Loan(db.Model): tenor = db.Column(db.Integer, nullable=True) due_date = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) eligible_amount = db.Column(db.Float, nullable=True, default=0.0) disburse_date = db.Column(db.DateTime, nullable=True) disburse_verify = db.Column(db.DateTime, nullable=True) @@ -109,7 +109,9 @@ class Loan(db.Model): due_date=due_date, tenor = tenor, status = status, - eligible_amount =eligible_amount + eligible_amount =eligible_amount, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) try: diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py index 36c451d..ce9ca22 100644 --- a/app/models/loan_charge.py +++ b/app/models/loan_charge.py @@ -18,7 +18,7 @@ class LoanCharge(db.Model): due = db.Column(db.Integer, nullable=False) due_date = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) loan = relationship( "Loan", primaryjoin="LoanCharge.loan_id == Loan.id", @@ -63,7 +63,9 @@ class LoanCharge(db.Model): percent = percent, description = description, due = due_days, - due_date = now + timedelta(days=due_days) + due_date = now + timedelta(days=due_days), + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) db.session.add(charge_obj) diff --git a/app/models/loan_repayment_schedule.py b/app/models/loan_repayment_schedule.py index 62aa952..948b66a 100644 --- a/app/models/loan_repayment_schedule.py +++ b/app/models/loan_repayment_schedule.py @@ -19,7 +19,7 @@ class LoanRepaymentSchedule(db.Model): paid_at = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) loan = relationship( "Loan", primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id", @@ -45,7 +45,9 @@ class LoanRepaymentSchedule(db.Model): total_repayment_amount = round(loan.repayment_amount, 2), installment_amount=round(loan.installment_amount, 2), product_id = loan.product_id, - transaction_id = transaction_id + transaction_id = transaction_id, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) db.session.add(schedule) diff --git a/app/models/offer.py b/app/models/offer.py index b5c0b70..a3250ae 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -18,8 +18,12 @@ class Offer(db.Model): insurance_rate = db.Column(db.Float, default=1.0) vat_rate = db.Column(db.Float, default=7.5) list_order = db.Column(db.Integer, nullable=True) + max_daily_loans = db.Column(db.Integer, nullable=True) + max_active_loans = db.Column(db.Integer, nullable=True) + max_life_loans = db.Column(db.Integer, nullable=True) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + charges = relationship( "Charge", primaryjoin="Offer.id == Charge.offer_id", @@ -79,7 +83,11 @@ class Offer(db.Model): "interest_rate": self.interest_rate, "management_rate": self.management_rate, "insurance_rate": self.insurance_rate, - "vat_rate": self.vat_rate + "vat_rate": self.vat_rate, + "maxDailyLoans": self.max_daily_loans, + "maxActiveLoans": self.max_active_loans, + "maxLifeLoans": self.max_life_loans + } def __repr__(self): diff --git a/app/models/rac_checks.py b/app/models/rac_checks.py index dd6a6f1..a4d56b5 100644 --- a/app/models/rac_checks.py +++ b/app/models/rac_checks.py @@ -15,7 +15,7 @@ class RACCheck(db.Model): account_id = db.Column(db.String, nullable=False) rac_response = db.Column(db.JSON, nullable=False) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) @classmethod def add_rac_check(cls, customer_id, account_id, transaction_id, data = None): @@ -25,7 +25,9 @@ class RACCheck(db.Model): customer_id = customer_id, account_id = account_id, transaction_id = transaction_id, - rac_response = data + rac_response = data, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) try: diff --git a/app/models/repayment.py b/app/models/repayment.py index f1faea4..f9ef530 100644 --- a/app/models/repayment.py +++ b/app/models/repayment.py @@ -19,7 +19,7 @@ class Repayment(db.Model): customer_id = db.Column(db.String(50), nullable=False) product_id = db.Column(db.String(20), nullable=True) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=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) @classmethod @@ -42,7 +42,9 @@ class Repayment(db.Model): customer_id=customer_id, loan_id=loan_id, product_id=product_id, - transaction_id = transaction_id + transaction_id = transaction_id, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) try: diff --git a/app/models/transaction.py b/app/models/transaction.py index a772905..f90c0da 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -18,7 +18,7 @@ class Transaction(db.Model): type = db.Column(db.String(50), nullable=False) channel = db.Column(db.String(50), nullable=False) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) def __repr__(self): return f'' @@ -38,7 +38,9 @@ class Transaction(db.Model): customer_id = customer_id, account_id = account_id, type = type, - channel = channel + channel = channel, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) try: diff --git a/app/models/transaction_offers.py b/app/models/transaction_offers.py index 87b42a9..d40fa4f 100644 --- a/app/models/transaction_offers.py +++ b/app/models/transaction_offers.py @@ -1,4 +1,5 @@ -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta +from app.api.enums.loan_status import LoanStatus from app.extensions import db from sqlalchemy.orm import relationship from sqlalchemy.sql import func @@ -22,7 +23,7 @@ class TransactionOffer(db.Model): tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) - updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) customer = relationship( "Customer", primaryjoin="Customer.id == TransactionOffer.customer_id", @@ -31,7 +32,6 @@ class TransactionOffer(db.Model): ) @classmethod - def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id): transaction_offer = cls.query.filter_by( id = transaction_offer, @@ -59,7 +59,9 @@ class TransactionOffer(db.Model): max_amount=max_amount, eligible_amount=eligible_amount, product_id=product_id, - tenor=tenor + tenor=tenor, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) db.session.add(transaction_offer) @@ -67,6 +69,43 @@ class TransactionOffer(db.Model): return transaction_offer + @classmethod + def get_lifetime_loan_count(cls, customer_id): + """ + Returns the total number of loans ever created for a customer. + """ + 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): + """ + Returns the most recent transaction offer for the given customer based on creation time. + """ + return cls.query.filter_by(customer_id=customer_id) \ + .order_by(cls.created_at.desc()) \ + .first() + + + + def to_dict(self): return { 'id': self.id, diff --git a/migrations/versions/e8dd9b841ad7_.py b/migrations/versions/e8dd9b841ad7_.py new file mode 100644 index 0000000..22a3b68 --- /dev/null +++ b/migrations/versions/e8dd9b841ad7_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: e8dd9b841ad7 +Revises: 2eee4157505f +Create Date: 2025-05-19 11:46:19.204637 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e8dd9b841ad7' +down_revision = '2eee4157505f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('offers', schema=None) as batch_op: + batch_op.add_column(sa.Column('max_daily_loans', sa.Integer(), nullable=True)) + batch_op.add_column(sa.Column('max_active_loans', sa.Integer(), nullable=True)) + batch_op.add_column(sa.Column('max_life_loans', sa.Integer(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('offers', schema=None) as batch_op: + batch_op.drop_column('max_life_loans') + batch_op.drop_column('max_active_loans') + batch_op.drop_column('max_daily_loans') + + # ### end Alembic commands ###