Compare commits

...

13 Commits

7 changed files with 149 additions and 49 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ class SimbrellaIntegration:
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
"fbnTransactionId": f"FBN{transaction_id}",
"fbnTransactionId": str(transaction_id),
"countryCode": "NG",
"channel": "USSD"
}
+3 -4
View File
@@ -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
@@ -81,7 +80,7 @@ class EligibilityCheckService(BaseService):
return ResponseHelper.error(result_description=f"RACCheck failed")
rack_checks_response = response['racResponse']
rack_checks_response = response['data']['racResponse']
rac_check = RACCheck.add_rac_check(
customer_id = customer_id,
@@ -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
+94 -9
View File
@@ -1,3 +1,4 @@
from decimal import Decimal
from app.models import Offer, TransactionOffer
from app.models.loan import Loan
import random
@@ -5,7 +6,10 @@ import logging
from app.config import Config
RAC_CHECK_RULES = Config.rac_true_rules
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:
@@ -37,9 +41,86 @@ class OfferAnalysis:
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response):
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 RUKES {str(RAC_CHECK_RULES)}", 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 salarie 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 np 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,
@@ -72,15 +153,13 @@ class OfferAnalysis:
# if we have active offers - we have to feed off it
logger.info(f"**RACK ANALYSIS** {customer_id}")
# Analyze Rack Checks
OfferAnalysis._analyze_rack_checks(rack_checks_response) #--> We need detail analysis
# 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}")
new_eligible_amount = 0
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}")
@@ -132,12 +211,18 @@ class OfferAnalysis:
for offer in offers:
# Get approved amount
random_float = random.random() # temporary to play data
new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response, offer)
approved_amount = new_eligible_amount if new_eligible_amount > 0 else min(offer.max_amount, offer.max_amount * random_float)
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,
+2
View File
@@ -60,8 +60,10 @@ class SelectOfferService(BaseService):
db.session.flush()
if amount < offer.min_amount:
logger.error(f"The amount {amount} is less than the minimum allowed offer amount {offer.min_amount}.")
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
elif amount > offer.max_amount:
logger.error(f"The amount {amount} is greater than the minimum allowed offer amount {offer.min_amount}.")
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed offer amount.")
+29 -17
View File
@@ -34,7 +34,7 @@ class Config:
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "testtest-api-key-12345")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "test-api-key-12345")
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
@@ -49,25 +49,37 @@ class Config:
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
rac_true_rules = [
"rule1-45day-sal",
"rule2-2m-sal",
"rule3-no-bounced-check",
"rule4-current-loan-payments",
"rule5-no-past-due-fadv-loan",
"rule6--no-past-due-other-loan",
"rule7-consistent-salary-amount",
"rule8-whitelisted",
"rule9-regular-account",
"rule10-bvn-validation",
"rule11-CRC-no-delinquency",
"rule12-CRMS-no-delinquency",
"rule13-BVN-ignore",
"rule14-no-lien",
"rule15-null-ignore"
"rule1_45day_sal",
"rule2_2m_sal",
"rule3_no_bounced_check",
"rule4_current_loan_payments",
"rule5_no_past_due_fadv_loan",
"rule6_no_past_due_other_loan",
"rule7_consistent_salary_amount",
"rule8_whitelisted",
"rule9_regular_account",
"rule10_bvn_validation",
"rule11_CRC_no_delinquency",
"rule12_CRMS_no_delinquency",
"rule13_BVN_ignore",
"rule14_no_lien",
"rule15_null_ignore"
]
rac_false_rules = [
]
rac_salary_payments = [
"salarypaymenT_1",
"salarypaymenT_2",
"salarypaymenT_3",
"salarypaymenT_4",
"salarypaymenT_5",
"salarypaymenT_6"
]
settings = Config()
+20 -1
View File
@@ -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.
-17
View File
@@ -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):