250 lines
10 KiB
Python
250 lines
10 KiB
Python
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 |