From 8cfa957cc0fb3f333b76128b7681fed5d0d74685 Mon Sep 17 00:00:00 2001 From: Vivian Dee Date: Wed, 23 Apr 2025 18:35:47 +0100 Subject: [PATCH 1/3] [update]: Select Offer --- app/api/services/select_offer.py | 72 ++++++++++++++++++++++++++------ app/models/offer.py | 22 ++++++++++ 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index 6f70aeb..46ab444 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -5,6 +5,7 @@ from app.api.enums import TransactionType from app.utils.logger import logger from app.api.schemas.select_offer import SelectOfferSchema from app.extensions import db +from app.models import Offer class SelectOfferService(BaseService): @@ -28,6 +29,8 @@ class SelectOfferService(BaseService): ) account_id = validated_data.get("accountId") customer_id = validated_data.get("customerId") + amount = validated_data.get("amount") + product_id = validated_data.get("productId") if SelectOfferService.validate_account_ownership( account_id=account_id, customer_id=customer_id @@ -42,22 +45,67 @@ class SelectOfferService(BaseService): else: return jsonify({"message": "Invalid Customer or Account"}), 400 + # Get the offer by product ID + offer = Offer.get_offer_by_product_id(product_id) + + if not offer: + logger.error(f"Offer with product ID {product_id} not found") + return jsonify({"message": "Offer not found"}), 404 + + # Get the loan charges for the offer + loan_charges = offer.charges + if not loan_charges: + logger.error(f"No charges found for offer ID {offer.id}") + return jsonify({"message": "No charges found for the offer"}), 404 + + # logger.error(f"{loan_charges}") + + fees_and_dues = { + charge.code: { + "rate": charge.percent, + "fee": amount * charge.percent / 100, + "due_days": charge.due, + } + for charge in loan_charges + } + + + # Total amount (principal + all fees) + total_amount = amount + sum(item["fee"] for item in fees_and_dues.values()) + + # Up-front payment: only those fees due immediately (due_days == 0) + upfront_payment = sum( + item["fee"] + for item in fees_and_dues.values() + if item["due_days"] == 0 + ) + + # Calculate the repayment dates + tenor = offer.tenor + start_date = date.today() + + recommended_repayment_dates = [ + (start_date + relativedelta(months=i + 1)).isoformat() + for i in range(tenor) + ] + + offers = [ { - "offerId": "SAL90", - "productId": "2030", - "amount": 10000.0, - "upfrontPayment": 1000.0, + "offerId": Offer.id, + "productId": product_id, + "amount": amount, + "upfrontPayment": upfront_payment, "interestRate": 3.0, - "managementRate": 1.0, - "managementFee": 1.0, - "insuranceRate": 1.0, - "insuranceFee": 100.0, - "VATRate": 7.5, - "VATAmount": 100.0, + "managementRate": fees_and_dues["management"]["rate"], + "managementFee": fees_and_dues["management"]["fee"], + "insuranceRate": fees_and_dues["insurance"]["rate"], + "insuranceFee": fees_and_dues["insurance"]["fee"], + "VATRate": fees_and_dues["VAT"]["rate"], + "VATAmount": fees_and_dues["VAT"]["fee"], "recommendedRepaymentDates": ["2022-11-30"], - "installmentAmount": 11000.0, - "totalRepaymentAmount": 11000.0, + "installmentAmount": recommended_repayment_dates, + "totalRepaymentAmount": total_amount, } ] diff --git a/app/models/offer.py b/app/models/offer.py index b51dc25..a2009e7 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -40,6 +40,28 @@ class Offer(db.Model): if not offer: return False return offer + + @classmethod + def get_offer_by_id(cls, offer_id): + """ + Return an offer by its ID. + """ + offer = cls.query.filter_by(id=str(offer_id)).first() + + if not offer: + raise ValueError(f"Offer with ID {offer_id} not found") + return offer + + @classmethod + def get_offer_by_product_id(cls, product_id): + """ + Return an offer by its product ID. + """ + offer = cls.query.filter_by(product_id=str(product_id)).first() + + if not offer: + raise ValueError(f"Offer with Product ID {product_id} not found") + return offer def to_dict(self): return { From 4b92c33d5a83f10b62fb2bfe16dd68e9bf6903e4 Mon Sep 17 00:00:00 2001 From: Vivian Dee Date: Wed, 23 Apr 2025 18:57:22 +0100 Subject: [PATCH 2/3] [fix]: loan charges and instalment amount --- app/api/integrations/simbrella.py | 4 ++++ app/api/services/select_offer.py | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index 5db7012..e0e2b96 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -52,3 +52,7 @@ class SimbrellaIntegration: except Exception as e: logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True) raise Exception(f"RACCheck API call failed: {str(e)}") + # return httpx.Response( + # status_code=200, + # json={} + # ) diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index 46ab444..8021ca3 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -58,7 +58,7 @@ class SelectOfferService(BaseService): logger.error(f"No charges found for offer ID {offer.id}") return jsonify({"message": "No charges found for the offer"}), 404 - # logger.error(f"{loan_charges}") + logger.error(f"{loan_charges}") fees_and_dues = { charge.code: { @@ -88,6 +88,9 @@ class SelectOfferService(BaseService): (start_date + relativedelta(months=i + 1)).isoformat() for i in range(tenor) ] + + # Calculate the installment amount + installment_amount = total_amount / tenor offers = [ @@ -96,15 +99,15 @@ class SelectOfferService(BaseService): "productId": product_id, "amount": amount, "upfrontPayment": upfront_payment, - "interestRate": 3.0, - "managementRate": fees_and_dues["management"]["rate"], - "managementFee": fees_and_dues["management"]["fee"], - "insuranceRate": fees_and_dues["insurance"]["rate"], - "insuranceFee": fees_and_dues["insurance"]["fee"], + "interestRate": fees_and_dues["INTEREST"]["rate"], + "managementRate": fees_and_dues["MGTFEE"]["rate"], + "managementFee": fees_and_dues["MGTFEE"]["fee"], + "insuranceRate": fees_and_dues["INSURANCE"]["rate"], + "insuranceFee": fees_and_dues["INSURANCE"]["fee"], "VATRate": fees_and_dues["VAT"]["rate"], "VATAmount": fees_and_dues["VAT"]["fee"], - "recommendedRepaymentDates": ["2022-11-30"], - "installmentAmount": recommended_repayment_dates, + "recommendedRepaymentDates": recommended_repayment_dates, + "installmentAmount": installment_amount, "totalRepaymentAmount": total_amount, } ] From 7d691db7a5a3093375708b1c7b93b6f3c5e2a943 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:59:07 +0100 Subject: [PATCH 3/3] [update]: Select Offer --- app/api/integrations/simbrella.py | 6 +-- app/api/services/select_offer.py | 88 +++++++++++++++++++++---------- app/models/rac_checks.py | 12 +++++ requirements.txt | 4 ++ 4 files changed, 77 insertions(+), 33 deletions(-) create mode 100644 app/models/rac_checks.py diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index e0e2b96..d46e5d4 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -49,10 +49,8 @@ class SimbrellaIntegration: logger.error(f"This is Response: {str(response)}", exc_info=True) return response + except Exception as e: logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True) raise Exception(f"RACCheck API call failed: {str(e)}") - # return httpx.Response( - # status_code=200, - # json={} - # ) + diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index 8021ca3..a207f2b 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -6,7 +6,8 @@ from app.utils.logger import logger from app.api.schemas.select_offer import SelectOfferSchema from app.extensions import db from app.models import Offer - +from datetime import date +from dateutil.relativedelta import relativedelta class SelectOfferService(BaseService): TRANSACTION_TYPE = TransactionType.SELECT_OFFER @@ -29,8 +30,10 @@ class SelectOfferService(BaseService): ) account_id = validated_data.get("accountId") customer_id = validated_data.get("customerId") - amount = validated_data.get("amount") + amount = validated_data.get("requestedAmount") product_id = validated_data.get("productId") + transaction_id = validated_data.get("transactionId") + request_id = validated_data.get("requestId") if SelectOfferService.validate_account_ownership( account_id=account_id, customer_id=customer_id @@ -60,52 +63,57 @@ class SelectOfferService(BaseService): logger.error(f"{loan_charges}") - fees_and_dues = { - charge.code: { - "rate": charge.percent, - "fee": amount * charge.percent / 100, - "due_days": charge.due, - } - for charge in loan_charges - } + db.session.flush() + interest = SelectOfferService.get_charge_detail(loan_charges, "INTEREST", amount) + management = SelectOfferService.get_charge_detail(loan_charges, "MGTFEE", amount) + insurance = SelectOfferService.get_charge_detail(loan_charges, "INSURANCE", amount) + vat = SelectOfferService.get_charge_detail(loan_charges, "VAT", amount) + + + # Up-front payment: (principal + only those fees due immediately i.e due_days == 0) + upfront_payment = amount + sum( + amount * charge.percent / 100 + for charge in loan_charges + if charge.due == 0 + ) # Total amount (principal + all fees) - total_amount = amount + sum(item["fee"] for item in fees_and_dues.values()) - - # Up-front payment: only those fees due immediately (due_days == 0) - upfront_payment = sum( - item["fee"] - for item in fees_and_dues.values() - if item["due_days"] == 0 + total_amount = amount + sum( + amount * charge.percent / 100 + for charge in loan_charges ) + # Calculate the repayment dates tenor = offer.tenor start_date = date.today() + # Convert tenor to months + months = tenor // 30 + recommended_repayment_dates = [ (start_date + relativedelta(months=i + 1)).isoformat() - for i in range(tenor) + for i in range(months) ] - # Calculate the installment amount + # Calculate the installment amount installment_amount = total_amount / tenor offers = [ { - "offerId": Offer.id, + "offerId": offer.id, "productId": product_id, "amount": amount, "upfrontPayment": upfront_payment, - "interestRate": fees_and_dues["INTEREST"]["rate"], - "managementRate": fees_and_dues["MGTFEE"]["rate"], - "managementFee": fees_and_dues["MGTFEE"]["fee"], - "insuranceRate": fees_and_dues["INSURANCE"]["rate"], - "insuranceFee": fees_and_dues["INSURANCE"]["fee"], - "VATRate": fees_and_dues["VAT"]["rate"], - "VATAmount": fees_and_dues["VAT"]["fee"], + "interestRate": interest["rate"], + "managementRate": management["rate"], + "managementFee": management["fee"], + "insuranceRate": insurance["rate"], + "insuranceFee": insurance["fee"], + "VATRate": vat["rate"], + "VATAmount": vat["fee"], "recommendedRepaymentDates": recommended_repayment_dates, "installmentAmount": installment_amount, "totalRepaymentAmount": total_amount, @@ -115,8 +123,8 @@ class SelectOfferService(BaseService): # Business logic - selecting an offer response_data = { "outstandingDebtAmount": 0, - "requestId": "202111170001371256908", - "transactionId": transaction.id, + "requestId": request_id, + "transactionId": transaction_id, "customerId": customer_id, "accountId": account_id, "loan": offers, @@ -142,3 +150,25 @@ class SelectOfferService(BaseService): logger.error(f"An error occurred: {str(e)}", exc_info=True) db.session.rollback() return jsonify({"message": "Internal Server Error"}), 500 + + + + @staticmethod + def get_charge_detail(charges, code, amount): + """ + Get details for a specific charge code from a list of loan charges. + + Returns default values if not found. + """ + + + for charge in charges: + if charge.code == code: + return { + "rate": charge.percent, + "fee": amount * charge.percent / 100, + "due_days": charge.due, + } + + return {"rate": 0, "fee": 0, "due_days": 0} + diff --git a/app/models/rac_checks.py b/app/models/rac_checks.py new file mode 100644 index 0000000..9e33a84 --- /dev/null +++ b/app/models/rac_checks.py @@ -0,0 +1,12 @@ +from datetime import datetime, timezone +from app.extensions import db + +class RACCheck(Base): + __tablename__ = "rac_checks" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + transaction_id = Column(UUID, ForeignKey('transactions.id'), nullable=False) + customer_id = Column(String, nullable=False) + account_id = Column(String, nullable=False) + rac_response = Column(JSON, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) diff --git a/requirements.txt b/requirements.txt index 95d8b87..124ae97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,7 @@ flask-jwt-extended # Kafka confluent-kafka==1.9.2 + + +python-dateutil>=2.8.0 +