diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index d46e5d4..13c7922 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -35,7 +35,7 @@ class SimbrellaIntegration: ], } - logger.error(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", @@ -46,7 +46,7 @@ class SimbrellaIntegration: try: response = httpx.post(url, json=payload, headers=headers, timeout=10.0) - logger.error(f"This is Response: {str(response)}", exc_info=True) + logger.info(f"This is Response: {str(response)}", exc_info=True) return response diff --git a/app/api/services/__init__.py b/app/api/services/__init__.py index e11319d..15278db 100644 --- a/app/api/services/__init__.py +++ b/app/api/services/__init__.py @@ -6,3 +6,4 @@ from app.api.services.repayment import RepaymentService from app.api.services.customer_consent import CustomerConsentService from app.api.services.notification_callback import NotificationCallbackService from app.api.services.authorization import AuthorizationService +from app.api.services.offer_analysis import OfferAnalysis diff --git a/app/api/services/base_service.py b/app/api/services/base_service.py index d30619b..53bcfe9 100644 --- a/app/api/services/base_service.py +++ b/app/api/services/base_service.py @@ -123,6 +123,7 @@ class BaseService: return { "interest": interest, + "interest_amount": interest_amount, "management": management, "insurance": insurance, "vat": vat, diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index 1141dd3..6fd8470 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -7,7 +7,7 @@ from marshmallow import ValidationError from app.api.enums import TransactionType from app.api.integrations import SimbrellaIntegration from app.extensions import db -from app.models import Offer +from app.models import Offer, RACCheck import random @@ -57,12 +57,27 @@ class EligibilityCheckService(BaseService): response = SimbrellaIntegration.rac_check( customer_id = customer_id, account_id = account_id, - transaction_id = transaction.id, + transaction_id = transaction.transaction_id, ) # this chck for error is not valid if response.status_code != 200: return jsonify({"message": "RACCheck failed"}), 400 + + response = response.json() + + rac_check = RACCheck.add_rac_check( + customer_id = customer_id, + account_id = account_id, + transaction_id = transaction.transaction_id, + data = response['RACResponse'] + ) + + if not rac_check: + logger.error(f"Failed to save RACCheck") + return jsonify({ + "message": "Failed to save RACCheck." + }), 400 offers = Offer.get_all_offers() diff --git a/app/api/services/offer_analysis.py b/app/api/services/offer_analysis.py new file mode 100644 index 0000000..b1a021a --- /dev/null +++ b/app/api/services/offer_analysis.py @@ -0,0 +1,37 @@ +from app.models import Offer, TransactionOffer +from app.models.loan import Loan + +import logging +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 + # 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 diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 130b817..19bbdea 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -8,13 +8,13 @@ from app.models.loan_charge import LoanCharge from app.utils.logger import logger from app.api.schemas.provide_loan import ProvideLoanSchema from threading import Thread -from app.models import Loan, Offer, Charge , TransactionOffer +from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck from app.api.enums import LoanStatus from app.extensions import db from datetime import datetime, timezone from dateutil.relativedelta import relativedelta from app.models import LoanRepaymentSchedule - +from app.api.services.offer_analysis import OfferAnalysis class ProvideLoanService(BaseService): TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN @@ -45,25 +45,40 @@ class ProvideLoanService(BaseService): customer = Customer.is_valid_customer(customer_id) - if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): - transaction_offer_id = int(offer_id[5:]) # The last part is int + rac_response = RACCheck.get_rac_check(customer_id = customer_id, account_id = account_id) - transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id) - if not transaction_offer: - logger.error(f"Invalid Transaction Offer") + try: + transaction_offer, offer, eligible_amount, original_transaction = OfferAnalysis.get_offer( + transaction_id=transaction_id, + rac_response=rac_response, + validated_data=validated_data + ) + except ValueError as ve: + logger.error(str(ve)) return jsonify({ - "message": "Invalid Transaction Offer." + "message": str(ve) }), 400 + + # transaction_offer_id = int(offer_id[5:]) # The last part is int - eligible_amount = transaction_offer.eligible_amount - offer = Offer.is_valid_offer( transaction_offer.offer_id) + # transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id) + + # if not transaction_offer: + # logger.error(f"Invalid Transaction Offer") + # return jsonify({ + # "message": "Invalid Transaction Offer." + # }), 400 - if not offer: - logger.error(f"Invalid Offer") - return jsonify({ - "message": "Invalid Offer." - }), 400 + # eligible_amount = transaction_offer.eligible_amount + # offer = Offer.is_valid_offer( transaction_offer.offer_id) + + # if not offer: + # logger.error(f"Invalid Offer") + # return jsonify({ + # "message": "Invalid Offer." + # }), 400 # Log Transaction @@ -102,6 +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'), initial_loan_amount = validated_data.get('requestedAmount'), upfront_fee = upfront_fee, repayment_amount = repayment_amount, @@ -131,7 +147,7 @@ class ProvideLoanService(BaseService): # charges = Charge.get_offer_charges(offer.id) - logger.error(f"{charges}") + logger.info(f"{charges}") loan_id = loan.id diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index a039149..11338b3 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -32,10 +32,14 @@ class SelectOfferService(BaseService): customer_id = validated_data.get("customerId") amount = validated_data.get("requestedAmount") product_id = validated_data.get("productId") - offer_id = validated_data.get("offerId") + transaction_offer_id = validated_data.get("offerId") transaction_id = validated_data.get("transactionId") request_id = validated_data.get("requestId") + offer_id = int(transaction_offer_id[5:]) # The last part is int + + #"offerId": "SAL30001129", + if SelectOfferService.validate_account_ownership( account_id=account_id, customer_id=customer_id ): @@ -63,6 +67,7 @@ class SelectOfferService(BaseService): insurance = charges["insurance"] vat = charges["vat"] repayment_amount = charges["repayment_amount"] + interest_amount = charges["interest_amount"] # Calculate the repayment dates @@ -81,11 +86,12 @@ class SelectOfferService(BaseService): offers = [ { - "offerId": offer.id, + "offerId": transaction_offer_id, "productId": product_id, "amount": amount, "upfrontPayment": upfront_payment, "interestRate": offer.interest_rate, + "interestAmount": 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 a6bdacc..9a43d8e 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -5,6 +5,7 @@ from app.models.account import Account from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import relationship from dateutil.relativedelta import relativedelta +from datetime import timedelta class Loan(db.Model): @@ -68,6 +69,7 @@ class Loan(db.Model): initial_loan_amount, collection_type, transaction_id, + original_transaction, upfront_fee, repayment_amount, installment_amount, @@ -81,6 +83,7 @@ class Loan(db.Model): raise ValueError("Customer does not exist") now = datetime.now(timezone.utc) + due_date = now + timedelta(days=tenor) # Create and save the loan loan = cls( @@ -90,13 +93,13 @@ class Loan(db.Model): product_id = product_id, collection_type = collection_type, transaction_id = transaction_id, - original_transaction = transaction_id, + original_transaction = original_transaction, initial_loan_amount = initial_loan_amount, current_loan_amount = initial_loan_amount, upfront_fee = upfront_fee, repayment_amount = repayment_amount, installment_amount = installment_amount, - due_date=now, + due_date=due_date, tenor = tenor, status = status, eligible_amount =eligible_amount @@ -123,13 +126,32 @@ class Loan(db.Model): @classmethod def get_customer_loan(cls, loan_id, customer_id): """ - Get customer's active loans. + Get customer's active loans by loan_id. """ loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first() if not loan: raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.") return loan - + + @classmethod + def get_customer_current_active_loan(cls, customer_id): + """ + Get customer's active loans. + """ + loan = cls.query.filter_by( customer_id = customer_id).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 loan + @classmethod def update_status(cls, loan_id, status): """ diff --git a/app/models/rac_checks.py b/app/models/rac_checks.py index 59d0816..88ec31c 100644 --- a/app/models/rac_checks.py +++ b/app/models/rac_checks.py @@ -1,14 +1,14 @@ from datetime import datetime, timezone from app.extensions import db from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.exc import IntegrityError from uuid import uuid4 from sqlalchemy.types import JSON class RACCheck(db.Model): __tablename__ = 'rac_checks' - id = db.Column(db.String, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) transaction_id = db.Column(db.String(50), nullable=False) customer_id = db.Column(db.String, nullable=False) account_id = db.Column(db.String, nullable=False) @@ -16,6 +16,25 @@ class RACCheck(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)) + @classmethod + def add_rac_check(cls, customer_id, account_id, transaction_id, data = None): + + + # Save the response + rac_check = cls( + customer_id = customer_id, + account_id = account_id, + transaction_id = transaction_id, + rac_response = data + ) + + try: + db.session.add(rac_check) + except IntegrityError as err: + raise ValueError(f"Database integrity error: {err}") + return rac_check + + @classmethod def get_all_rac_checks(cls): """ @@ -24,18 +43,19 @@ class RACCheck(db.Model): rac_checks = cls.query.all() if not rac_checks: - raise ValueError("No available RAC checks") + return None return rac_checks @classmethod - def get_rac_check_by_id(cls, check_id): + def get_rac_check(cls, customer_id, account_id): """ Return a RAC check by its ID. """ - rac_check = cls.query.filter_by(id=check_id).first() + rac_check = cls.query.filter_by( customer_id = customer_id, + account_id = account_id,).first() if not rac_check: - raise ValueError(f"RAC Check with ID {check_id} not found") + raise ValueError(f"RAC Check for customer not found") return rac_check def to_dict(self): diff --git a/app/swagger/schemas/SelectOfferResponse.json b/app/swagger/schemas/SelectOfferResponse.json index a1fe106..41b60d4 100644 --- a/app/swagger/schemas/SelectOfferResponse.json +++ b/app/swagger/schemas/SelectOfferResponse.json @@ -49,6 +49,11 @@ "format": "float", "example": 3.0 }, + "interestAmount": { + "type": "number", + "format": "float", + "example": 3000.00 + }, "ManagementRate": { "type": "number", "format": "float", diff --git a/migrations/versions/3105abd795d4_.py b/migrations/versions/3105abd795d4_.py new file mode 100644 index 0000000..c6ae0da --- /dev/null +++ b/migrations/versions/3105abd795d4_.py @@ -0,0 +1,52 @@ +"""empty message + +Revision ID: 3105abd795d4 +Revises: 95a52be203c4 +Create Date: 2025-05-07 11:44:18.483694 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3105abd795d4' +down_revision = '95a52be203c4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('rac_checks', schema=None) as batch_op: + # Step 1: Drop the default value + batch_op.alter_column('id', + server_default=None, + existing_type=sa.VARCHAR(), + existing_nullable=False + ) + + with op.batch_alter_table('rac_checks', schema=None) as batch_op: + # Step 2: Change the column type + batch_op.alter_column('id', + existing_type=sa.VARCHAR(), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True, + postgresql_using='id::integer' + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('rac_checks', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.Integer(), + type_=sa.VARCHAR(), + existing_nullable=False, + autoincrement=True, + existing_server_default=sa.text("''::character varying")) + + # ### end Alembic commands ###