from decimal import Decimal from app.models import Offer, TransactionOffer from app.models.loan import Loan import random import logging from app.config import Config RAC_TRUE_CHECK_RULES = Config.rac_true_rules RAC_FALSE_CHECK_RULES = Config.rac_false_rules RAC_SALARY_PAYMENTS = Config.rac_salary_payments logger = logging.getLogger(__name__) class OfferAnalysis: @staticmethod def get_offer(transaction_id, rac_response, validated_data): customer_id = validated_data.get("customerId") product_id = validated_data.get("productId") offer_id = validated_data.get("offerId") transaction_offer_id = int(offer_id[5:]) # The last part is int logger.info(f"customer_id == *************** : {customer_id}") logger.info(f"product_id == *************** : {product_id}") logger.info(f"offer_id == *************** : {offer_id}") logger.info(f"transaction_offer_id == *************** : {transaction_offer_id}") transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, product_id) if not transaction_offer: raise ValueError("Invalid Transaction Offer.") eligible_amount = transaction_offer.eligible_amount offer = Offer.is_valid_offer( transaction_offer.offer_id) if not offer: raise ValueError("Invalid Offer.") original_transaction = transaction_id return transaction_offer, offer, eligible_amount, original_transaction @staticmethod def _analyze_rack_checks(rack_response, offer): logger.info(f"This is PayLoad for ANALYSYS ***** : {str(rack_response)}", exc_info=True) logger.info(f"RACk TRUE RULES {str(RAC_TRUE_CHECK_RULES)}", exc_info=True) logger.info(f"RACk FALSE RULES {str(RAC_FALSE_CHECK_RULES)}", exc_info=True) logger.info(f"RACk SALARY PAYMENTS {str(RAC_SALARY_PAYMENTS)}", exc_info=True) if not isinstance(rack_response, dict) or not offer : raise ValueError("Invalid RAC response format.") failed_true_rules = [] failed_false_rules = [] salaries = [] # Expects true for rule in RAC_TRUE_CHECK_RULES: if not rack_response.get(rule, False): failed_true_rules.append(rule) # Expects false for rule in RAC_FALSE_CHECK_RULES: if rack_response.get(rule, True): failed_false_rules.append(rule) # Salary rules for key in RAC_SALARY_PAYMENTS: value = rack_response.get(key) if isinstance(value, Decimal): # Only use values greater than 0 if value > 0: salaries.append(value) elif isinstance(value, (int, float, str)): try: value = Decimal(str(value)) if value > 0: salaries.append(value) except: logger.warning(f"Could not convert value of {key} to Decimal: {value}") if failed_true_rules or failed_false_rules or not salaries: logger.warning(f"Failed TRUE rules: {failed_true_rules}") logger.warning(f"Failed FALSE rules: {failed_false_rules}") logger.warning("No salary records found in RAC response.") raise ValueError(f"RAC analysis failed") logger.info(f"These are the salary amounts ***** : {str(salaries)}", exc_info=True) #Least salary in the last 6 months min_salary = min(salaries) # Check consistency rule consistent_income = rack_response.get("rule7_consistent_salary_amount", False) # Determine percentage based on offer tenor tenor = offer.tenor if tenor == 30 and consistent_income: eligible_amount = min_salary * Decimal("0.5") logger.info("Applying 50% of least salary in 6 months due to 1-month offer tenor with stable income.") elif tenor == 90 and consistent_income: eligible_amount = min_salary * Decimal("0.75") logger.info("Applying 75% of least salary in 6 months due to 3-months offer tenor with stable income.") else: # Income is not consistent eligible_amount = 0 logger.info("Applying no percentage on least salary due unstable income.") logger.info(f"Calculated eligible amount from RAC: {eligible_amount} based on {'stable' if consistent_income else 'unstable'} income.") return eligible_amount.quantize(Decimal("1.00")) # "racResponse": { # "accountStatus": true, # "bvnValidated": true, # "creditBureauCheck": false, # "crmsCheck": true, # "hasLien": false, # "hasPastDueLoan": false, # "hasSalaryAccount": true, # "isWhitelisted": true, # "noBouncedCheck": true # }, # ''' 30 days Eligibility amount (monthly SOL) - Adoption of 50% of the least salary inflow in the past 6 months to determine loan eligibility for potential customers. 3 months Adoption of 75% of the least salary inflow in the past 6 months to determine loan eligibility for potential customers" for customers that have unstable income. 3 months ''' # rac_true_rules return 0 @staticmethod def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response): eligible_offers = [] # if we have active offers - we have to feed off it logger.info(f"**RACK ANALYSIS** {customer_id}") # Analyze Rack Checks # new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response) #--> We need detail analysis # we can now find the origin transactions # Find the last loan - it will have original_transaction last_customer_loan = Loan.get_customer_last_loan(customer_id) # logger.info(f"{last_customer_loan}") if last_customer_loan: original_transaction = last_customer_loan.original_transaction or last_customer_loan.transaction_id logger.info(f"transaction_id |-| original_transaction === > {transaction_id} {original_transaction}") original_loan = Loan.get_customer_original_loan(customer_id, original_transaction) if original_loan is not None: logger.info(f"original_loan === > {original_loan}") logger.info(f"loan_offer_id === > {original_loan.offer_id}") original_offer_id = str(original_loan.offer_id[:5]) # The last part is str transaction_offer_id = int(original_loan.offer_id[5:]) # The last part is int original_transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, original_loan.product_id) active_loans = Loan.get_active_loans_by_original_transaction(original_transaction) sum_active_loans = sum(loan.current_loan_amount for loan in active_loans) logger.info(f"sum_active_loans === > {sum_active_loans}") real_eligible_amount = original_loan.eligible_amount - sum_active_loans if real_eligible_amount < original_transaction_offer.min_amount: logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).") raise ValueError("You are not eligible for a loan at this time.") transaction_offer = TransactionOffer.create_transaction_offer( customer_id=customer_id, transaction_id=transaction_id, original_transaction=original_transaction, offer_id=original_offer_id, min_amount=original_transaction_offer.min_amount, max_amount=original_transaction_offer.max_amount, eligible_amount=real_eligible_amount, product_id=original_loan.product_id, tenor=original_loan.tenor ) # Visible offer ID: offer_id + padded(transaction_offer.id) padded_id = str(transaction_offer.id).zfill(6) public_offer_id = f"{original_offer_id}{padded_id}" eligible_offers.append({ "offerId": public_offer_id, "product_id": original_transaction_offer.product_id, "min_amount": original_transaction_offer.min_amount, "max_amount": round(real_eligible_amount, 2), "tenor": original_loan.tenor }) return eligible_offers offers = Offer.get_all_offers() for offer in offers: new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response, offer) approved_amount = new_eligible_amount approved_amount = round(approved_amount, 2) if approved_amount < offer.min_amount: logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).") raise ValueError("You are not eligible for a loan at this time.") transaction_offer = TransactionOffer.create_transaction_offer( customer_id=customer_id, transaction_id=transaction_id, original_transaction=transaction_id, offer_id=offer.id, min_amount=offer.min_amount, max_amount=offer.max_amount, eligible_amount=approved_amount, product_id=offer.product_id, tenor=offer.tenor ) # Visible offer ID: offer_id + padded(transaction_offer.id) padded_id = str(transaction_offer.id).zfill(6) public_offer_id = f"{offer.id}{padded_id}" eligible_offers.append({ "offerId": public_offer_id, "product_id": offer.product_id, "min_amount": offer.min_amount, "max_amount": approved_amount, "tenor": offer.tenor }) return eligible_offers