This commit is contained in:
VivianDee
2025-05-12 16:06:24 +01:00
12 changed files with 272 additions and 58 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ class SimbrellaIntegration:
], ],
} }
logger.info(f"This is PayLoad: {str(payload)}", exc_info=True) # logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
+46 -30
View File
@@ -8,6 +8,8 @@ from app.api.enums import TransactionType
from app.api.integrations import SimbrellaIntegration from app.api.integrations import SimbrellaIntegration
from app.extensions import db from app.extensions import db
from app.models import Offer, RACCheck from app.models import Offer, RACCheck
from app.api.services.offer_analysis import OfferAnalysis
import random import random
@@ -78,39 +80,53 @@ class EligibilityCheckService(BaseService):
return jsonify({ return jsonify({
"message": "Failed to save RACCheck." "message": "Failed to save RACCheck."
}), 400 }), 400
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
offers = Offer.get_all_offers() # eligible_offers = []
try:
eligible_offers = [] eligible_offers = OfferAnalysis.decide_offer(
transaction_id=transactionId,
for offer in offers: rac_check=rac_check,
# Determine an approved amount validated_data=validated_data,
random_float = random.random() # temporary to play data customer_id=customer_id
approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
approved_amount = round(approved_amount, 2)
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id = customer.id,
transaction_id = 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
) )
except ValueError as ve:
logger.error(str(ve))
return jsonify({
"message": str(ve)
}), 400
# -----------------------------------------------------------------------
# s = Offer.get_all_offers()
# Visible offer ID: offer_id + padded(transaction_offer.id) # eligible_offers = []
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{offer.id}{padded_id}"
eligible_offers.append({ # for offer in offers:
"offerId": public_offer_id, # # Determine an approved amount
"product_id": offer.product_id, # random_float = random.random() # temporary to play data
"min_amount": offer.min_amount, # approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
"max_amount": approved_amount, # approved_amount = round(approved_amount, 2)
"tenor": offer.tenor #
}) # transaction_offer = TransactionOffer.create_transaction_offer(
# customer_id = customer.id,
# transaction_id = 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
# })
# Simulate processing # Simulate processing
response_data = { response_data = {
+93 -4
View File
@@ -1,6 +1,6 @@
from app.models import Offer, TransactionOffer from app.models import Offer, TransactionOffer
from app.models.loan import Loan from app.models.loan import Loan
import random
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,8 +30,97 @@ class OfferAnalysis:
if not offer: if not offer:
raise ValueError("Invalid Offer.") raise ValueError("Invalid Offer.")
original_transaction = transaction_id original_transaction = transaction_id
# we can now find the origin transactions
customer_loan = Loan.get_customer_current_active_loan(customer_id)
return transaction_offer, offer, eligible_amount, original_transaction return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def decide_offer(transaction_id, rac_check, validated_data, customer_id):
eligible_offers = []
# if we have active offers - we have to feed off it
logger.info(f"LOOOOOOOOOOOOOOOOOO** {customer_id}")
# 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}")
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
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": real_eligible_amount,
"tenor": original_loan.tenor
})
return eligible_offers
offers = Offer.get_all_offers()
for offer in offers:
# Get approved amount
random_float = random.random() # temporary to play data
approved_amount = new_eligible_amount if new_eligible_amount > 0 else min(offer.max_amount, offer.max_amount * random_float)
approved_amount = round(approved_amount, 2)
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
+7 -5
View File
@@ -42,6 +42,7 @@ class ProvideLoanService(BaseService):
offer_id = validated_data.get('offerId') offer_id = validated_data.get('offerId')
amount = validated_data.get("requestedAmount") amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId") product_id = validated_data.get("productId")
channel = validated_data.get('channel')
customer = Customer.is_valid_customer(customer_id) customer = Customer.is_valid_customer(customer_id)
@@ -108,7 +109,6 @@ class ProvideLoanService(BaseService):
vat = charges["vat"] vat = charges["vat"]
# Save the loan details # Save the loan details
loan = Loan.create_loan( loan = Loan.create_loan(
customer_id = customer_id, customer_id = customer_id,
@@ -117,7 +117,7 @@ class ProvideLoanService(BaseService):
product_id = offer.product_id, product_id = offer.product_id,
collection_type = collection_type, collection_type = collection_type,
transaction_id = validated_data.get('transactionId'), transaction_id = validated_data.get('transactionId'),
original_transaction = validated_data.get('transactionId'), original_transaction = transaction_offer.original_transaction,
initial_loan_amount = validated_data.get('requestedAmount'), initial_loan_amount = validated_data.get('requestedAmount'),
upfront_fee = upfront_fee, upfront_fee = upfront_fee,
repayment_amount = repayment_amount, repayment_amount = repayment_amount,
@@ -125,7 +125,6 @@ class ProvideLoanService(BaseService):
eligible_amount=eligible_amount, eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE, status = LoanStatus.ACTIVE,
tenor = offer.tenor, tenor = offer.tenor,
) )
if not loan: if not loan:
@@ -135,7 +134,7 @@ class ProvideLoanService(BaseService):
}), 400 }), 400
db.session.flush() db.session.flush()
current_product_id = offer.product_id
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id) schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
@@ -147,7 +146,7 @@ class ProvideLoanService(BaseService):
# charges = Charge.get_offer_charges(offer.id) # charges = Charge.get_offer_charges(offer.id)
logger.info(f"{charges}") # logger.info(f"{charges}")
loan_id = loan.id loan_id = loan.id
@@ -160,6 +159,8 @@ class ProvideLoanService(BaseService):
"message": "Invalid Customer or Account" "message": "Invalid Customer or Account"
}), 400 }), 400
padded_loan_id = str(loan_id).zfill(9)
loanRef = f"LID{padded_loan_id}{channel}{current_product_id}"
response_data = { response_data = {
"requestId": request_id, "requestId": request_id,
@@ -167,6 +168,7 @@ class ProvideLoanService(BaseService):
"customerId": customer_id, "customerId": customer_id,
"accountId": account_id, "accountId": account_id,
"msisdn": customer.msisdn, "msisdn": customer.msisdn,
"loanRef": loanRef,
"resultCode": "00", "resultCode": "00",
"resultDescription": "Successful" "resultDescription": "Successful"
} }
+1 -1
View File
@@ -91,7 +91,7 @@ class SelectOfferService(BaseService):
"amount": amount, "amount": amount,
"upfrontPayment": upfront_payment, "upfrontPayment": upfront_payment,
"interestRate": offer.interest_rate, "interestRate": offer.interest_rate,
"interestAmount": interest_amount, "interestFee": interest_amount,
"managementRate": offer.management_rate, "managementRate": offer.management_rate,
"managementFee": management["fee"], "managementFee": management["fee"],
"insuranceRate": offer.insurance_rate, "insuranceRate": offer.insurance_rate,
+47 -12
View File
@@ -6,6 +6,10 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import timedelta from datetime import timedelta
import logging
from sqlalchemy import and_, or_, not_
logger = logging.getLogger(__name__)
class Loan(db.Model): class Loan(db.Model):
@@ -36,6 +40,8 @@ class Loan(db.Model):
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
eligible_amount = db.Column(db.Float, nullable=True, default=0.0) 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)
customer = relationship( customer = relationship(
"Customer", "Customer",
@@ -134,24 +140,53 @@ class Loan(db.Model):
return loan return loan
@classmethod @classmethod
def get_customer_current_active_loan(cls, customer_id): def get_customer_original_loan(cls, customer_id, original_transaction):
"""
Get customer's original loan offer.
"""
original_loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.original_transaction==original_transaction, cls.transaction_id==original_transaction )).first()
if not original_loan:
return None
logger.info(f" get_customer_original_loan ==>>>> {original_loan}")
return original_loan
@classmethod
def get_customer_last_loan(cls, customer_id):
""" """
Get customer's active loans. Get customer's active loans.
""" """
loan = cls.query.filter_by( customer_id = customer_id).first() logger.info(f"get_customer_last_loan [customer_id] ==>>>> {customer_id}")
# loan = cls.query.filter_by( cls.customer_id == customer_id).first()
loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.status=='active')).first()
if not loan: if not loan:
loan = { return None
"eligible_amount": 0, # loan = {
"loan_amount": 0, # "original_transaction":"",
"customer_id": customer_id, # "eligible_amount": 0,
"transaction_id": "", # "loan_amount": 0,
"resultDescription": "No Active Loan" # "customer_id": customer_id,
} # "transaction_id": "",
# "resultDescription": "No Active Loan"
logger.info(f" Active Loan ==>>>> {loan}") # }
logger.info(f" get_customer_last_loan ==>>>> {loan}")
return loan return loan
@classmethod
def get_active_loans_by_original_transaction(cls, original_transaction_id):
"""
Get all active loans with the same original_transaction ID.
"""
active_loans = cls.query.filter_by(
original_transaction=original_transaction_id,
# status='active'
).all()
return active_loans
@classmethod @classmethod
def update_status(cls, loan_id, status): def update_status(cls, loan_id, status):
""" """
+3 -1
View File
@@ -8,6 +8,7 @@ class TransactionOffer(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
customer_id = db.Column(db.String(50), nullable=False) customer_id = db.Column(db.String(50), nullable=False)
transaction_id = db.Column(db.String(50), nullable=False) transaction_id = db.Column(db.String(50), nullable=False)
original_transaction = db.Column(db.String(50), nullable=True)
offer_id = db.Column(db.String(20), nullable=False) offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True) product_id = db.Column(db.String(20), nullable=True)
min_amount = db.Column(db.Float, nullable=False) min_amount = db.Column(db.Float, nullable=False)
@@ -41,13 +42,14 @@ class TransactionOffer(db.Model):
return transaction_offer return transaction_offer
@classmethod @classmethod
def create_transaction_offer(cls, customer_id, transaction_id, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None): def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
""" """
Class method to create and save a TransactionOffer. Class method to create and save a TransactionOffer.
""" """
transaction_offer = cls( transaction_offer = cls(
customer_id=customer_id, customer_id=customer_id,
transaction_id=transaction_id, transaction_id=transaction_id,
original_transaction=original_transaction,
offer_id=offer_id, offer_id=offer_id,
min_amount=min_amount, min_amount=min_amount,
max_amount=max_amount, max_amount=max_amount,
@@ -9,6 +9,10 @@
"type": "string", "type": "string",
"example": "Tr201712RK9232P115" "example": "Tr201712RK9232P115"
}, },
"loanRef": {
"type": "string",
"example": "1620029887USSDAMPC"
},
"customerId": { "customerId": {
"type": "string", "type": "string",
"example": "CN621868" "example": "CN621868"
+1 -1
View File
@@ -28,7 +28,7 @@
}, },
"productId": { "productId": {
"type": "string", "type": "string",
"example": "2090" "example": "3MPC"
}, },
"offerId": { "offerId": {
"type": "string", "type": "string",
+2 -2
View File
@@ -28,7 +28,7 @@
}, },
"productId": { "productId": {
"type": "string", "type": "string",
"example": "2030" "example": "3MPC"
}, },
"amount": { "amount": {
"type": "number", "type": "number",
@@ -49,7 +49,7 @@
"format": "float", "format": "float",
"example": 3.0 "example": 3.0
}, },
"interestAmount": { "interestFee": {
"type": "number", "type": "number",
"format": "float", "format": "float",
"example": 3000.00 "example": 3000.00
@@ -0,0 +1,32 @@
"""Migration on Sat May 10 09:54:34 UTC 2025
Revision ID: 173ea45db189
Revises: 3105abd795d4
Create Date: 2025-05-10 09:54:39.380499
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '173ea45db189'
down_revision = '3105abd795d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -0,0 +1,34 @@
"""Migration on Sat May 10 12:54:52 UTC 2025
Revision ID: 565bc3d0ba6e
Revises: 173ea45db189
Create Date: 2025-05-10 12:54:56.683215
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '565bc3d0ba6e'
down_revision = '173ea45db189'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('disburse_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('disburse_verify', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('disburse_verify')
batch_op.drop_column('disburse_date')
# ### end Alembic commands ###