From cb1823400878de5bc84e21ca8228da4f70c97ec1 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:10:25 +0100 Subject: [PATCH 1/9] [add]: Loan charges model --- app/models/loan.py | 7 +++++++ app/models/loan_charge.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 app/models/loan_charge.py diff --git a/app/models/loan.py b/app/models/loan.py index ab8e89f..0e4e592 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -36,6 +36,13 @@ class Loan(db.Model): back_populates="loans", ) + loan_charges = relationship( + "LoanCharge", + primaryjoin="Loan.id == LoanCharge.loan_id", + foreign_keys="LoanCharge.loan_id", + back_populates="loan", + ) + @classmethod def create_loan(cls, customer_id, account_id, offer_id, initial_loan_amount, collection_type, transaction_id, status='pending'): diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py new file mode 100644 index 0000000..f01a9f4 --- /dev/null +++ b/app/models/loan_charge.py @@ -0,0 +1,41 @@ +from datetime import datetime, timezone +from app.extensions import db +from sqlalchemy.orm import relationship + + +class LoanCharge(db.Model): + __tablename__ = 'loan_charges' + + 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) + description = db.Column(db.Text, nullable=True) + due_date = db.Column(db.DateTime, nullable=False) + 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)) + + loan = relationship( + "Loan", + primaryjoin="LoanCharge.loan_id == Loan.id", + foreign_keys=[loan_id], + back_populates="loan_charges", + ) + + + def to_dict(self): + return { + 'id': self.id, + 'loanId': self.loan_id, + 'transactionId': self.transaction_id, + 'code': self.code, + 'amount': self.amount, + 'percent': self.percent, + 'description': self.description, + 'dueDate': self.due_date.isoformat() if self.due_date else None, + } + + def __repr__(self): + return f"" From 142a7eb8860183f3d5ba6f456d97dcd3969f5dac Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:23:23 +0100 Subject: [PATCH 2/9] [add]: Loan charges model --- app/models/__init__.py | 3 ++- app/models/loan.py | 2 +- app/models/loan_charge.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index af5353a..c592d80 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,5 +3,6 @@ from .account import Account from .loan import Loan from .transaction import Transaction from .repayment import Repayment +from .loan_charge import LoanCharge -__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment'] \ No newline at end of file +__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge'] \ No newline at end of file diff --git a/app/models/loan.py b/app/models/loan.py index 0e4e592..df46f9a 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -4,7 +4,7 @@ from app.models.customer import Customer from app.models.account import Account from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import relationship -from app.models import Customer +from app.models import Customer, Loa class Loan(db.Model): diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py index f01a9f4..d3f7c2a 100644 --- a/app/models/loan_charge.py +++ b/app/models/loan_charge.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone from app.extensions import db +from app.models import LoanCharge from sqlalchemy.orm import relationship @@ -23,6 +24,7 @@ class LoanCharge(db.Model): foreign_keys=[loan_id], back_populates="loan_charges", ) + def to_dict(self): From aba5a021971064fab33d9925402d0687748e00ae Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:30:20 +0100 Subject: [PATCH 3/9] [update]: Loan Charge model --- app/api/services/provide_loan.py | 8 +++++++- app/models/loan.py | 3 ++- app/models/loan_charge.py | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/api/services/provide_loan.py b/app/api/services/provide_loan.py index 108bbac..74c470c 100644 --- a/app/api/services/provide_loan.py +++ b/app/api/services/provide_loan.py @@ -33,6 +33,11 @@ class ProvideLoanService(BaseService): request_id = validated_data.get('requestId') collection_type = validated_data.get('collectionType') transaction_id = validated_data.get('transactionId') + offer_id = validated_data.get('offerId') + product_id = validated_data.get('productrId') + + + if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): @@ -50,7 +55,8 @@ class ProvideLoanService(BaseService): loan = Loan.create_loan( customer_id = customer_id, account_id = account_id, - offer_id = validated_data.get('offerId'), + offer_id = offer_id, + product_id = product_id, collection_type = collection_type, transaction_id = validated_data.get('transactionId'), initial_loan_amount = validated_data.get('requestedAmount'), diff --git a/app/models/loan.py b/app/models/loan.py index df46f9a..cddec15 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -19,6 +19,7 @@ class Loan(db.Model): transaction_id = db.Column(db.String(50), nullable=True) account_id = db.Column(db.String(50), nullable=False) offer_id = db.Column(db.String(20), nullable=False) + product_id = db.Column(db.String(20), nullable=False) collection_type = db.Column(db.String(20), nullable=True) current_loan_amount = db.Column(db.Float, nullable=True) initial_loan_amount = db.Column(db.Float, nullable=False) @@ -44,7 +45,7 @@ class Loan(db.Model): ) @classmethod - def create_loan(cls, customer_id, account_id, offer_id, initial_loan_amount, collection_type, transaction_id, status='pending'): + 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) diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py index d3f7c2a..0074079 100644 --- a/app/models/loan_charge.py +++ b/app/models/loan_charge.py @@ -14,7 +14,7 @@ class LoanCharge(db.Model): amount = db.Column(db.Float, default=0.0) percent = db.Column(db.Float, default=0.0) description = db.Column(db.Text, nullable=True) - due_date = db.Column(db.DateTime, nullable=False) + due = db.Column(db.Integer, nullable=False) 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)) @@ -36,7 +36,7 @@ class LoanCharge(db.Model): 'amount': self.amount, 'percent': self.percent, 'description': self.description, - 'dueDate': self.due_date.isoformat() if self.due_date else None, + 'due': self.due, } def __repr__(self): From 86801b13fb0ec7e78464e58d6ee3a6ef37bac18d Mon Sep 17 00:00:00 2001 From: Vivian Dee Date: Wed, 16 Apr 2025 15:54:39 +0100 Subject: [PATCH 4/9] [add]: Offers Model --- app/models/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index c592d80..64d16f4 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -4,5 +4,7 @@ from .loan import Loan from .transaction import Transaction from .repayment import Repayment from .loan_charge import LoanCharge +from .offer import Offer, -__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge'] \ No newline at end of file + +__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer'] \ No newline at end of file From f55f179672c6b965a92963c6d2cb0dd242ee1ab5 Mon Sep 17 00:00:00 2001 From: Vivian Dee Date: Wed, 16 Apr 2025 15:57:03 +0100 Subject: [PATCH 5/9] [update]: offers --- app/models/__init__.py | 2 +- app/models/loan.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index 64d16f4..1d7fc97 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -4,7 +4,7 @@ from .loan import Loan from .transaction import Transaction from .repayment import Repayment from .loan_charge import LoanCharge -from .offer import Offer, +from .offer import Offer __all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer'] \ No newline at end of file diff --git a/app/models/loan.py b/app/models/loan.py index cddec15..c924650 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -4,7 +4,7 @@ from app.models.customer import Customer from app.models.account import Account from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import relationship -from app.models import Customer, Loa +from app.models import Customer, LoanCharge class Loan(db.Model): From 9cfa4a67b1a8899615011e4fd1221bf7d668d660 Mon Sep 17 00:00:00 2001 From: Vivian Dee Date: Wed, 16 Apr 2025 15:58:56 +0100 Subject: [PATCH 6/9] [update]: Loan Charge --- app/models/loan.py | 2 +- app/models/loan_charge.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/loan.py b/app/models/loan.py index c924650..c380447 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -4,7 +4,7 @@ from app.models.customer import Customer from app.models.account import Account from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import relationship -from app.models import Customer, LoanCharge +from app.models.loan_charge import LoanCharge class Loan(db.Model): diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py index 0074079..048742c 100644 --- a/app/models/loan_charge.py +++ b/app/models/loan_charge.py @@ -1,6 +1,5 @@ from datetime import datetime, timezone from app.extensions import db -from app.models import LoanCharge from sqlalchemy.orm import relationship From 359621dc9d130d39bd24370eb81c9451825f92ed Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 16 Apr 2025 19:16:03 +0100 Subject: [PATCH 7/9] [add]: Offers --- app/api/services/eligibility_check.py | 2 + app/models/loan.py | 3 +- app/models/offer.py | 11 +++- ...1_migration_on_wed_apr_16_17_42_49_utc_.py | 57 +++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 migrations/versions/a4847b997191_migration_on_wed_apr_16_17_42_49_utc_.py diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index bf0d0aa..cbbb787 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -6,6 +6,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 class EligibilityCheckService(BaseService): TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK @@ -53,6 +54,7 @@ class EligibilityCheckService(BaseService): account_id = account_id, transaction_id = transaction.id, ) + logger.error(f"This is Response Returned ****** : {str(response)}") # this chck for error is not valid diff --git a/app/models/loan.py b/app/models/loan.py index c380447..fdd0f0b 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -19,7 +19,7 @@ class Loan(db.Model): transaction_id = db.Column(db.String(50), nullable=True) account_id = db.Column(db.String(50), nullable=False) offer_id = db.Column(db.String(20), nullable=False) - product_id = db.Column(db.String(20), nullable=False) + product_id = db.Column(db.String(20), nullable=True) collection_type = db.Column(db.String(20), nullable=True) current_loan_amount = db.Column(db.Float, nullable=True) initial_loan_amount = db.Column(db.Float, nullable=False) @@ -59,6 +59,7 @@ class Loan(db.Model): customer_id = customer_id, account_id = account_id, offer_id = offer_id, + product_id = product_id, collection_type = collection_type, transaction_id = transaction_id, initial_loan_amount = initial_loan_amount, diff --git a/app/models/offer.py b/app/models/offer.py index ce62fa2..24c694d 100644 --- a/app/models/offer.py +++ b/app/models/offer.py @@ -4,7 +4,7 @@ from app.extensions import db class Offer(db.Model): __tablename__ = 'offers' - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.String, primary_key=True) product_id = db.Column(db.String, nullable=False) min_amount = db.Column(db.Float, nullable=False) max_amount = db.Column(db.Float, nullable=False) @@ -12,5 +12,14 @@ 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)) + def to_dict(self): + return { + "offerId": self.id, + "productId": self.product_id, + "minAmount": self.min_amount, + "maxAmount": self.max_amount, + "tenor": self.tenor + } + def __repr__(self): return f'' \ No newline at end of file diff --git a/migrations/versions/a4847b997191_migration_on_wed_apr_16_17_42_49_utc_.py b/migrations/versions/a4847b997191_migration_on_wed_apr_16_17_42_49_utc_.py new file mode 100644 index 0000000..a41dc5c --- /dev/null +++ b/migrations/versions/a4847b997191_migration_on_wed_apr_16_17_42_49_utc_.py @@ -0,0 +1,57 @@ +"""Migration on Wed Apr 16 17:42:49 UTC 2025 + +Revision ID: a4847b997191 +Revises: 783a023a477f +Create Date: 2025-04-16 17:43:22.509659 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a4847b997191' +down_revision = '783a023a477f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('loan_charges', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('loan_id', sa.Integer(), nullable=False), + sa.Column('transaction_id', sa.String(length=50), nullable=True), + sa.Column('code', sa.String(length=50), nullable=False), + sa.Column('amount', sa.Float(), nullable=True), + sa.Column('percent', sa.Float(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('due', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('offers', + sa.Column('id', sa.String(), nullable=False), + sa.Column('product_id', sa.String(), nullable=False), + sa.Column('min_amount', sa.Float(), nullable=False), + sa.Column('max_amount', sa.Float(), nullable=False), + sa.Column('tenor', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('loans', schema=None) as batch_op: + batch_op.add_column(sa.Column('product_id', sa.String(length=20), 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('product_id') + + op.drop_table('offers') + op.drop_table('loan_charges') + # ### end Alembic commands ### 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 8/9] [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 From e14e290ff97f93ee7c4517781fb4c0aaa696baf5 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Wed, 16 Apr 2025 22:46:48 +0100 Subject: [PATCH 9/9] [update]: RACChecks --- app/api/integrations/simbrella.py | 48 +++++++++++++-------------- app/api/services/eligibility_check.py | 6 ++-- requirements.txt | 3 +- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/app/api/integrations/simbrella.py b/app/api/integrations/simbrella.py index 9bd7638..5db7012 100644 --- a/app/api/integrations/simbrella.py +++ b/app/api/integrations/simbrella.py @@ -1,8 +1,9 @@ -import requests +import httpx import json -from requests.auth import HTTPBasicAuth from app.utils.logger import logger from app.config import settings +import logging + class SimbrellaIntegration: BASE_URL = settings.SIMBRELLA_BASE_URL @@ -13,42 +14,41 @@ class SimbrellaIntegration: Calls the RACCheck endpoit """ url = f"{SimbrellaIntegration.BASE_URL}/RACCheck" - + payload = { "customerId": customer_id, "accountId": account_id, - "transactionId": transaction_id, + "transactionId": str(transaction_id), + "fbnTransactionId": f"FBN{transaction_id}", "RAC_Array": [ - { - "salaryAccount": True, - "bvn": "12345678901", - "crc": False, - "crms": True, - "accountStatus": "active", - "lien": False, - "noBouncedCheck": True, - "existingLoan": False, - "whitelist": True, - "noPastDueSalaryLoan": True, - "noPastDueOtherLoans": False - } - ] + "SalaryAccount", + "BVN", + "BVNAttachedtoAccount", + "CRC", + "CRMS", + "AccountStatus", + "Lien", + "NoBouncedCheck", + "Whitelist", + "NoPastDueSalaryLoan", + "NoPastDueOtherLoan", + ], } - logger.error(f"This is PayLoad: {str(payload)}",exc_info=True) + logger.error(f"This is PayLoad: {str(payload)}", exc_info=True) headers = { - 'Content-Type': 'application/json', - 'x-api-key': f'{settings.VALID_API_KEY}', - 'App-Id': f'{settings.VALID_APP_ID}' + "Content-Type": "application/json", + "x-api-key": f"{settings.VALID_API_KEY}", + "App-Id": f"{settings.VALID_APP_ID}", } try: - response = requests.post(url, json=payload, timeout=10, headers=headers) + response = httpx.post(url, json=payload, headers=headers, timeout=10.0) logger.error(f"This is Response: {str(response)}", exc_info=True) - return response.json() + 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)}") diff --git a/app/api/services/eligibility_check.py b/app/api/services/eligibility_check.py index ebd94dd..6cdf075 100644 --- a/app/api/services/eligibility_check.py +++ b/app/api/services/eligibility_check.py @@ -48,17 +48,17 @@ class EligibilityCheckService(BaseService): "message": "Invalid Customer or Account" }), 400 + db.session.flush() + # Call RACCheck response = SimbrellaIntegration.rac_check( customer_id = customer_id, account_id = account_id, transaction_id = transaction.id, ) - - logger.error(f"This is Response Returned ****** : {str(response)}") # this chck for error is not valid - if response.get("status") != 200: + if response.status_code != 200: return jsonify({"message": "RACCheck failed"}), 400 offers = [offer.to_dict() for offer in Offer.get_all_offers()] diff --git a/requirements.txt b/requirements.txt index 9d5c945..95d8b87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,8 @@ flask-swagger-ui python-dotenv # Requests -requests +httpx + # JWT flask-jwt-extended