From 93ed8b3d17b65dd620bb7c6f59a95bcc86415b99 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:36:26 +0100 Subject: [PATCH] [add]: loan charges and offers. Fix RACCheck --- app/api/integrations/simbrella.py | 11 +-- app/api/schemas/provide_loan.py | 2 +- app/api/services/eligibility_check.py | 22 +---- app/api/services/provide_loan.py | 93 ++++++++++++------- app/api/services/select_offer.py | 2 +- app/models/account.py | 2 +- app/models/customer.py | 2 +- app/models/loan.py | 6 +- app/models/loan_charge.py | 34 ++++++- app/models/offer.py | 20 ++++ ...7_migration_on_wed_apr_16_18_35_18_utc_.py | 32 +++++++ scripts/entrypoint.sh | 6 +- 12 files changed, 161 insertions(+), 71 deletions(-) create mode 100644 migrations/versions/287ecb02d3d7_migration_on_wed_apr_16_18_35_18_utc_.py diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index 1bfae3e..9bd7638 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -36,6 +36,7 @@ class SimbrellaIntegration: } logger.error(f"This is PayLoad: {str(payload)}",exc_info=True) + headers = { 'Content-Type': 'application/json', 'x-api-key': f'{settings.VALID_API_KEY}', @@ -44,12 +45,10 @@ class SimbrellaIntegration: try: response = requests.post(url, json=payload, timeout=10, headers=headers) + logger.error(f"This is Response: {str(response)}", exc_info=True) - # Raise an error for non-200 responses - if response.status_code != 200: - response.raise_for_status() return response.json() - except requests.exceptions.RequestException as err: - logger.error(f"RACCheck API call failed: {str(err)}", exc_info=True) - return {"error": "RACCheck API error"} + 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)}") diff --git a/app/api/schemas/provide_loan.py b/app/api/schemas/provide_loan.py index 5959e29..ad6c8b1 100644 --- a/app/api/schemas/provide_loan.py +++ b/app/api/schemas/provide_loan.py @@ -12,5 +12,5 @@ class ProvideLoanSchema(Schema): # lienAmount = fields.Float(required=True) requestedAmount = fields.Float(required=True) collectionType = fields.Int(required=True) - offerId = fields.Int(required=True) + offerId = fields.Str(required=True) channel = fields.Str(required=True) \ No newline at end of file diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index cbbb787..ebd94dd 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -58,26 +58,10 @@ class EligibilityCheckService(BaseService): logger.error(f"This is Response Returned ****** : {str(response)}") # this chck for error is not valid - logger.error(f"Check for ERROR is not valid ****** FIX THIS !!!!!") - #if "error" in response or response.get("status") != 200: - # return jsonify({"message": "RACCheck failed"}), 400 + if response.get("status") != 200: + return jsonify({"message": "RACCheck failed"}), 400 - offers = [ - { - "offerId": "SAL90", - "productId": "2030", - "minAmount": 5000, - "maxAmount": 100000, - "tenor": 30 - }, - { - "offerId": "SAL30", - "productId": "2090", - "minAmount": 5000, - "maxAmount": 500000, - "tenor": 90 - } - ] + offers = [offer.to_dict() for offer in Offer.get_all_offers()] # Simulate processing response_data = { diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 74c470c..30da2ca 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -3,10 +3,12 @@ from marshmallow import ValidationError from app.api.integrations.kafka import KafkaIntegration from app.api.services.base_service import BaseService from app.api.enums import TransactionType +from app.models.customer import Customer +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.loan import Loan +from app.models import Loan, Offer from app.api.enums import LoanStatus from app.extensions import db @@ -34,13 +36,20 @@ class ProvideLoanService(BaseService): collection_type = validated_data.get('collectionType') transaction_id = validated_data.get('transactionId') offer_id = validated_data.get('offerId') - product_id = validated_data.get('productrId') - - + customer = Customer.is_valid_customer(customer_id) if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + offer = Offer.is_valid_offer(offer_id) + + if not offer: + logger.error(f"Invalid Offer") + return jsonify({ + "message": "Invalid Offer." + }), 400 + + # Log Transaction transaction = ProvideLoanService.log_transaction(validated_data=validated_data) @@ -56,52 +65,68 @@ class ProvideLoanService(BaseService): customer_id = customer_id, account_id = account_id, offer_id = offer_id, - product_id = product_id, + product_id = offer.product_id, collection_type = collection_type, transaction_id = validated_data.get('transactionId'), initial_loan_amount = validated_data.get('requestedAmount'), status= LoanStatus.ACTIVE ) + db.session.flush() + + if not loan: logger.error(f"Failed to save loan details") return jsonify({ "message": "Failed to save loan details." }), 400 + - logger.error(f"********* We need to develop the fee array here") - loan_def = { - "offers": [ - { - "offerId": "SAL90", - "productId": "2030", - "minAmount": 5000, - "maxAmount": 100000, - "tenor": 30 - }, - { - "offerId": "SAL30", - "productId": "2090", - "minAmount": 3000, - "maxAmount": 500000, - "tenor": 90 - } - ], - "loan_fee": { - "SAL30": [ - {"code": "INTEREST", "percent": 1.1, "due": 0, "description": "This is fee 000"}, - {"code": "MGTFEE", "percent": 2.5, "due": 0, "description": "This is fee 001"}, - {"code": "INSURANCE", "percent": 3.5, "due": 0, "description": "This is fee 001"}, - {"code": "VAT", "percent": 1.0, "due": 0, "description": "This is fee 001"}, - ], - "SAL90": [ + charges = [ {"code": "INTEREST", "percent": 1.1, "due": 0, "description": "This is fee 9000"}, {"code": "MGTFEE", "percent": 1.5, "due": 0, "description": "This is fee 90002"}, {"code": "INSURANCE", "percent": 1.5, "due": 30, "description": "This is fee 90003"}, {"code": "VAT", "percent": 1.5, "due": 60, "description": "This is fee 90004"}, ] - } - } + loan_id = loan.id + + loan_charges = LoanCharge.create_charges_for_loan(loan_id = loan_id, charges = charges) + + + # logger.error(f"********* We need to develop the fee array here") + + # loan_def = { + # "offers": [ + # { + # "offerId": "SAL90", + # "productId": "2030", + # "minAmount": 5000, + # "maxAmount": 100000, + # "tenor": 30 + # }, + # { + # "offerId": "SAL30", + # "productId": "2090", + # "minAmount": 3000, + # "maxAmount": 500000, + # "tenor": 90 + # } + # ], + # "loan_fee": { + # "SAL30": [ + # {"code": "INTEREST", "percent": 1.1, "due": 0, "description": "This is fee 000"}, + # {"code": "MGTFEE", "percent": 2.5, "due": 0, "description": "This is fee 001"}, + # {"code": "INSURANCE", "percent": 3.5, "due": 0, "description": "This is fee 001"}, + # {"code": "VAT", "percent": 1.0, "due": 0, "description": "This is fee 001"}, + # ], + # "SAL90": [ + # {"code": "INTEREST", "percent": 1.1, "due": 0, "description": "This is fee 9000"}, + # {"code": "MGTFEE", "percent": 1.5, "due": 0, "description": "This is fee 90002"}, + # {"code": "INSURANCE", "percent": 1.5, "due": 30, "description": "This is fee 90003"}, + # {"code": "VAT", "percent": 1.5, "due": 60, "description": "This is fee 90004"}, + # ] + # } + # } # Log Transaction @@ -124,7 +149,7 @@ class ProvideLoanService(BaseService): "transactionId": transaction_id, "customerId": customer_id, "accountId": account_id, - "msisdn": "3451342", + "msisdn": customer.msisdn, "resultCode": "00", "resultDescription": "Successful" } diff --git a/app/api/services/select_offer.py b/app/api/services/select_offer.py index 6a0a914..6f70aeb 100644 --- a/app/api/services/select_offer.py +++ b/app/api/services/select_offer.py @@ -44,7 +44,7 @@ class SelectOfferService(BaseService): offers = [ { - "offerId": "14451", + "offerId": "SAL90", "productId": "2030", "amount": 10000.0, "upfrontPayment": 1000.0, diff --git a/app/models/account.py b/app/models/account.py index e8a90ce..d136d58 100644 --- a/app/models/account.py +++ b/app/models/account.py @@ -42,7 +42,7 @@ class Account(db.Model): return False if account.lien_amount > 0: return False - return True + return account def __repr__(self): return f'' diff --git a/app/models/customer.py b/app/models/customer.py index 7692f5a..c601efe 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -32,7 +32,7 @@ class Customer(db.Model): customer = cls.query.filter_by(id=customer_id).first() if not customer: return False - return True + return customer @classmethod def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'): diff --git a/app/models/loan.py b/app/models/loan.py index fdd0f0b..6a96b51 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -48,8 +48,8 @@ class Loan(db.Model): def create_loan(cls, customer_id, account_id, offer_id, product_id, initial_loan_amount, collection_type, transaction_id, status='pending'): # Check if customer exists - is_valid = Customer.is_valid_customer(customer_id) - if not is_valid: + customer = Customer.is_valid_customer(customer_id) + if not customer: raise ValueError("Customer does not exist") now = datetime.now(timezone.utc) @@ -67,7 +67,7 @@ class Loan(db.Model): due_date=now, status = status ) - + try: db.session.add(loan) except IntegrityError as err: diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py index 048742c..3e71030 100644 --- a/app/models/loan_charge.py +++ b/app/models/loan_charge.py @@ -8,7 +8,6 @@ class LoanCharge(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) loan_id = db.Column(db.Integer, nullable=False) - transaction_id = db.Column(db.String(50), nullable=True) code = db.Column(db.String(50), nullable=False) amount = db.Column(db.Float, default=0.0) percent = db.Column(db.Float, default=0.0) @@ -24,7 +23,38 @@ class LoanCharge(db.Model): back_populates="loan_charges", ) - + @classmethod + def create_charges_for_loan(cls, loan_id, charges): + """ + Create loan charges for a given loan. + + Args: + loan_id (int): ID of the loan to associate charges with. + charges (list): A list of dictionaries with keys: + code (str), amount (float), percent (float), description (str), due (int) + """ + if not charges or not isinstance(charges, list): + raise ValueError("Charges must be a non-empty list of dictionaries") + + if loan_id is None: + raise ValueError("loan_id cannot be None") + + loan_charges = [] + for charge in charges: + charge_obj = cls( + loan_id=loan_id, + code=charge.get("code"), + amount=charge.get("amount", 0.0), + percent=charge.get("percent", 0.0), + description=charge.get("description", ""), + due=charge.get("due", 0) + ) + db.session.add(charge_obj) + loan_charges.append(charge_obj) + + return loan_charges + + def to_dict(self): return { diff --git a/app/models/offer.py b/app/models/offer.py index 24c694d..2590f46 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -12,6 +12,26 @@ class Offer(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 get_all_offers(cls): + """ + Return all offers in dictionary format. + """ + offers = cls.query.all() + + if not offers: + raise ValueError(f"No available offers") + return offers + + @classmethod + def is_valid_offer(cls, offer_id): + offer = cls.query.filter_by(id=str(offer_id)).first() + + + if not offer: + return False + return offer + def to_dict(self): return { "offerId": self.id, diff --git a/migrations/versions/287ecb02d3d7_migration_on_wed_apr_16_18_35_18_utc_.py b/migrations/versions/287ecb02d3d7_migration_on_wed_apr_16_18_35_18_utc_.py new file mode 100644 index 0000000..583c616 --- /dev/null +++ b/migrations/versions/287ecb02d3d7_migration_on_wed_apr_16_18_35_18_utc_.py @@ -0,0 +1,32 @@ +"""Migration on Wed Apr 16 18:35:18 UTC 2025 + +Revision ID: 287ecb02d3d7 +Revises: a4847b997191 +Create Date: 2025-04-16 18:36:04.632791 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '287ecb02d3d7' +down_revision = 'a4847b997191' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('loan_charges', schema=None) as batch_op: + batch_op.drop_column('transaction_id') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('loan_charges', schema=None) as batch_op: + batch_op.add_column(sa.Column('transaction_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True)) + + # ### end Alembic commands ### diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 3e73752..9a12075 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,8 +1,8 @@ #!/bin/sh -echo "Running DB migrations..." -flask db migrate -m "Migration on $(date)" -flask db upgrade +# echo "Running DB migrations..." +# flask db migrate -m "Migration on $(date)" +# flask db upgrade echo "Starting Gunicorn server..." exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app