diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index 13c7922..e46210d 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -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 = { "Content-Type": "application/json", diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 6fd8470..263d9cb 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -8,6 +8,8 @@ from app.api.enums import TransactionType from app.api.integrations import SimbrellaIntegration from app.extensions import db from app.models import Offer, RACCheck +from app.api.services.offer_analysis import OfferAnalysis + import random @@ -78,39 +80,53 @@ class EligibilityCheckService(BaseService): return jsonify({ "message": "Failed to save RACCheck." }), 400 - - offers = Offer.get_all_offers() - - eligible_offers = [] - - for offer in offers: - # Determine an approved amount - random_float = random.random() # temporary to play data - 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 +# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ---------------------- + # eligible_offers = [] + try: + eligible_offers = OfferAnalysis.decide_offer( + transaction_id=transactionId, + rac_check=rac_check, + validated_data=validated_data, + customer_id=customer_id ) + 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) - padded_id = str(transaction_offer.id).zfill(6) - public_offer_id = f"{offer.id}{padded_id}" + # eligible_offers = [] - eligible_offers.append({ - "offerId": public_offer_id, - "product_id": offer.product_id, - "min_amount": offer.min_amount, - "max_amount": approved_amount, - "tenor": offer.tenor - }) + # for offer in offers: + # # Determine an approved amount + # random_float = random.random() # temporary to play data + # 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 + # ) + # + # # 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 response_data = { diff --git a/app/api/services/offer_analysis.py b/app/api/services/offer_analysis.py index b1a021a..d97cc7e 100644 --- a/app/api/services/offer_analysis.py +++ b/app/api/services/offer_analysis.py @@ -1,6 +1,6 @@ from app.models import Offer, TransactionOffer from app.models.loan import Loan - +import random import logging logger = logging.getLogger(__name__) @@ -30,8 +30,97 @@ class OfferAnalysis: if not offer: raise ValueError("Invalid Offer.") 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 + + @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 \ No newline at end of file diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 19bbdea..88c348f 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -42,6 +42,7 @@ class ProvideLoanService(BaseService): offer_id = validated_data.get('offerId') amount = validated_data.get("requestedAmount") product_id = validated_data.get("productId") + channel = validated_data.get('channel') customer = Customer.is_valid_customer(customer_id) @@ -108,7 +109,6 @@ class ProvideLoanService(BaseService): vat = charges["vat"] - # Save the loan details loan = Loan.create_loan( customer_id = customer_id, @@ -117,7 +117,7 @@ class ProvideLoanService(BaseService): product_id = offer.product_id, collection_type = collection_type, 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'), upfront_fee = upfront_fee, repayment_amount = repayment_amount, @@ -125,7 +125,6 @@ class ProvideLoanService(BaseService): eligible_amount=eligible_amount, status = LoanStatus.ACTIVE, tenor = offer.tenor, - ) if not loan: @@ -135,7 +134,7 @@ class ProvideLoanService(BaseService): }), 400 db.session.flush() - + current_product_id = offer.product_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) - logger.info(f"{charges}") + # logger.info(f"{charges}") loan_id = loan.id @@ -159,7 +158,9 @@ class ProvideLoanService(BaseService): return jsonify({ "message": "Invalid Customer or Account" }), 400 - + + padded_loan_id = str(loan_id).zfill(9) + loanRef = f"LID{padded_loan_id}{channel}{current_product_id}" response_data = { "requestId": request_id, @@ -167,6 +168,7 @@ class ProvideLoanService(BaseService): "customerId": customer_id, "accountId": account_id, "msisdn": customer.msisdn, + "loanRef": loanRef, "resultCode": "00", "resultDescription": "Successful" } diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index 11338b3..db7a7a6 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -91,7 +91,7 @@ class SelectOfferService(BaseService): "amount": amount, "upfrontPayment": upfront_payment, "interestRate": offer.interest_rate, - "interestAmount": interest_amount, + "interestFee": interest_amount, "managementRate": offer.management_rate, "managementFee": management["fee"], "insuranceRate": offer.insurance_rate, diff --git a/app/models/loan.py b/app/models/loan.py index 9a43d8e..2af5711 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -6,6 +6,10 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import relationship from dateutil.relativedelta import relativedelta from datetime import timedelta +import logging +from sqlalchemy import and_, or_, not_ + +logger = logging.getLogger(__name__) class Loan(db.Model): @@ -36,6 +40,8 @@ class Loan(db.Model): 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)) 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", @@ -134,24 +140,53 @@ class Loan(db.Model): return loan @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. """ - 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: - loan = { - "eligible_amount": 0, - "loan_amount": 0, - "customer_id": customer_id, - "transaction_id": "", - "resultDescription": "No Active Loan" - } - - logger.info(f" Active Loan ==>>>> {loan}") - + return None + # loan = { + # "original_transaction":"", + # "eligible_amount": 0, + # "loan_amount": 0, + # "customer_id": customer_id, + # "transaction_id": "", + # "resultDescription": "No Active Loan" + # } + logger.info(f" get_customer_last_loan ==>>>> {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 def update_status(cls, loan_id, status): """ diff --git a/app/models/transaction_offers.py b/app/models/transaction_offers.py index aca0a8d..847ae6e 100644 --- a/app/models/transaction_offers.py +++ b/app/models/transaction_offers.py @@ -8,6 +8,7 @@ class TransactionOffer(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) customer_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) product_id = db.Column(db.String(20), nullable=True) min_amount = db.Column(db.Float, nullable=False) @@ -41,13 +42,14 @@ class TransactionOffer(db.Model): return transaction_offer @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. """ transaction_offer = cls( customer_id=customer_id, transaction_id=transaction_id, + original_transaction=original_transaction, offer_id=offer_id, min_amount=min_amount, max_amount=max_amount, diff --git a/app/swagger/schemas/ProvideLoanResponse.json b/app/swagger/schemas/ProvideLoanResponse.json index d7ddb3c..1da4cb2 100644 --- a/app/swagger/schemas/ProvideLoanResponse.json +++ b/app/swagger/schemas/ProvideLoanResponse.json @@ -9,6 +9,10 @@ "type": "string", "example": "Tr201712RK9232P115" }, + "loanRef": { + "type": "string", + "example": "1620029887USSDAMPC" + }, "customerId": { "type": "string", "example": "CN621868" diff --git a/app/swagger/schemas/SelectOfferRequest.json b/app/swagger/schemas/SelectOfferRequest.json index e51bb99..0540a8e 100644 --- a/app/swagger/schemas/SelectOfferRequest.json +++ b/app/swagger/schemas/SelectOfferRequest.json @@ -28,7 +28,7 @@ }, "productId": { "type": "string", - "example": "2090" + "example": "3MPC" }, "offerId": { "type": "string", diff --git a/app/swagger/schemas/SelectOfferResponse.json b/app/swagger/schemas/SelectOfferResponse.json index 41b60d4..f32d39a 100644 --- a/app/swagger/schemas/SelectOfferResponse.json +++ b/app/swagger/schemas/SelectOfferResponse.json @@ -28,7 +28,7 @@ }, "productId": { "type": "string", - "example": "2030" + "example": "3MPC" }, "amount": { "type": "number", @@ -49,7 +49,7 @@ "format": "float", "example": 3.0 }, - "interestAmount": { + "interestFee": { "type": "number", "format": "float", "example": 3000.00 diff --git a/migrations/versions/173ea45db189_migration_on_sat_may_10_09_54_34_utc_.py b/migrations/versions/173ea45db189_migration_on_sat_may_10_09_54_34_utc_.py new file mode 100644 index 0000000..0c3686b --- /dev/null +++ b/migrations/versions/173ea45db189_migration_on_sat_may_10_09_54_34_utc_.py @@ -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 ### diff --git a/migrations/versions/565bc3d0ba6e_migration_on_sat_may_10_12_54_52_utc_.py b/migrations/versions/565bc3d0ba6e_migration_on_sat_may_10_12_54_52_utc_.py new file mode 100644 index 0000000..91895f2 --- /dev/null +++ b/migrations/versions/565bc3d0ba6e_migration_on_sat_may_10_12_54_52_utc_.py @@ -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 ###