From 0268bbd557e107bbb3c61fb5f7ca8f737b3c0ee5 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Thu, 17 Apr 2025 14:40:46 +0100 Subject: [PATCH 1/5] add loan and loan_charges model --- app/models/loan.py | 59 ++++++++++++++++++++++++++++++++++++++ app/models/loan_charge.py | 43 +++++++++++++++++++++++++++ app/models/transactions.py | 13 +++++++++ 3 files changed, 115 insertions(+) create mode 100644 app/models/loan.py create mode 100644 app/models/loan_charge.py diff --git a/app/models/loan.py b/app/models/loan.py new file mode 100644 index 0000000..656ac3c --- /dev/null +++ b/app/models/loan.py @@ -0,0 +1,59 @@ +from datetime import datetime, timezone +from app.extensions import db +from sqlalchemy.orm import relationship + +class Loan(db.Model): + __tablename__ = "loans" + + 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=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=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) + default_penalty_fee = db.Column(db.Float, default=0) + continuous_fee = db.Column(db.Float, default=0) + status = db.Column(db.String(20), default='pending') + due_date = db.Column(db.DateTime, nullable=True) + 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)) + + customer = relationship( + "Customer", + primaryjoin="Customer.id == Loan.customer_id", + foreign_keys=[customer_id], + back_populates="loans", + ) + + loan_charges = relationship( + "LoanCharge", + primaryjoin="Loan.id == LoanCharge.loan_id", + foreign_keys="LoanCharge.loan_id", + back_populates="loan", + ) + + def __repr__(self): + return f"" + + def to_dict(self): + """ + Convert the Loan object to a dictionary format for JSON serialization. + """ + return { + 'debtId': self.id, + 'initialLoanAmount': self.initial_loan_amount, + 'currentLoanAmount': self.current_loan_amount, + 'defaultPenaltyFee': self.default_penalty_fee, + 'continuousFee': self.continuous_fee, + 'collectionType': self.collection_type, + 'status': self.status, + 'dueDate': self.due_date.isoformat() if self.due_date else None, + 'loanDate': self.created_at.isoformat if self.created_at else None + } \ No newline at end of file diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py new file mode 100644 index 0000000..da12479 --- /dev/null +++ b/app/models/loan_charge.py @@ -0,0 +1,43 @@ +from datetime import datetime, timezone, timedelta +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 = db.Column(db.Integer, nullable=False) + due_date = db.Column(db.DateTime, nullable=True) + 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 __repr__(self): + return f"" + + def to_dict(self): + """ + Convert the Loan charge object to a dictionary format for JSON serialization. + """ + return { + 'id': self.id, + 'loanId': self.loan_id, + 'transactionId': self.transaction_id, + 'code': self.code, + 'amount': self.amount, + 'percent': self.percent, + 'description': self.description, + 'due': self.due + } \ No newline at end of file diff --git a/app/models/transactions.py b/app/models/transactions.py index 17cb957..6edc91c 100644 --- a/app/models/transactions.py +++ b/app/models/transactions.py @@ -20,6 +20,19 @@ class Transaction(db.Model): def __repr__(self): return f'' + def to_dict(self): + """ + Convert the Transaction object to a dictionary format for JSON serialization. + """ + return { + 'id': self.id, + 'transaction_id': self.transaction_id, + 'account_id': self.account_id, + 'customer_id': self.customer_id, + 'type': self.type, + 'channel': self.channel, + } + @classmethod def get_transaction_by_transaction_id(cls, transaction_id): return cls.query.filter_by(transaction_id=transaction_id).first() \ No newline at end of file From d2e272e44b4ab6a390269f46ae81acf5c1b3b957 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Thu, 17 Apr 2025 17:16:01 +0100 Subject: [PATCH 2/5] eliminated some hardcoded values --- app/integrations/simbrella.py | 34 ++++++++++++++++++++++++----- app/models/__init__.py | 6 ++++- app/models/account.py | 25 +++++++++++++++++++++ app/models/customer.py | 41 +++++++++++++++++++++++++++++++++++ app/models/loan.py | 7 +++++- app/models/loan_charge.py | 8 ++++++- app/services/loan.py | 17 +++++++++++++++ app/utils/extras.py | 16 ++++++++++++++ 8 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 app/models/account.py create mode 100644 app/models/customer.py create mode 100644 app/services/loan.py create mode 100644 app/utils/extras.py diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index b40f21c..3f74720 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -1,6 +1,8 @@ import requests from app.config import settings +from app.services.loan import LoanService from app.utils.auth import get_headers +from app.utils.extras import preprocess_loan_charges_data from app.utils.logger import logger from flask import jsonify, current_app from app.services.transactions import TransactionService @@ -26,21 +28,42 @@ class SimbrellaClient: logger.info(f"Transaction id: {data['transactionId']}, was not found") return 0 + # Fetch the loan based on the transaction_id + logger.info(f"Fetching the loan with transaction ID: {data['transactionId']}") + loan = LoanService.get_loan_by_transaction_id(transaction_id=data['transactionId']) + logger.info(f"Response from database: {loan}") + + # If loan is not found + if not loan: + logger.info(f"Could not find loan with transaction id: {data['transactionId']}") + return 0 + + loan_data = loan.to_dict() + logger.info(f"Here is your loan data: {loan_data}") + + loan_charges = preprocess_loan_charges_data([loan_charge.to_dict() for loan_charge in loan.loan_charges]) + logger.info(f"Here are your loan_charges: {loan_charges}") + + mgt_fee = loan_charges.get("MGTFEE")['amount'] + + vat_fee = loan_charges.get("VAT")['amount'] + disbursement_data ={ "requestId": data['requestId'], "transactionId": data['transactionId'], - "debtId": "273194670", + "debtId": loan_data['debtId'], "customerId": data['customerId'], "accountId": data['accountId'], - "productId": "101", - "provideAmount": 100000, + "productId": loan_data['productId'], + "provideAmount": loan_data['currentLoanAmount'], "collectAmountInterest": 5000, - "collectAmountMgtFee": 1000, + "collectAmountMgtFee": mgt_fee, "collectAmountInsurance": 1000, - "collectAmountVAT": 75, + "collectAmountVAT": vat_fee, "countryId": "01", "comment": "Loan Disbursement", } + try: logger.info(f"Here is your Disbursement Request data ****** : {disbursement_data}") response = requests.post(api_url, json=disbursement_data, timeout=10, headers=get_headers()) @@ -51,7 +74,6 @@ class SimbrellaClient: #raise return 0 - # return jsonify(response.json()), response.status_code return 1 @staticmethod diff --git a/app/models/__init__.py b/app/models/__init__.py index 7621c0f..705f676 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,8 @@ from .transactions import Transaction from .repayment import Repayment +from .loan import Loan +from .loan_charge import LoanCharge +from .customer import Customer +from .account import Account -__all__ = ['Transaction', 'Repayment'] \ No newline at end of file +__all__ = ['Transaction', 'Repayment', 'Loan', 'LoanCharge', 'Customer', 'Account'] \ No newline at end of file diff --git a/app/models/account.py b/app/models/account.py new file mode 100644 index 0000000..7394b3a --- /dev/null +++ b/app/models/account.py @@ -0,0 +1,25 @@ +from datetime import datetime, timezone +from sqlalchemy.orm import relationship +from app.extensions import db + + +class Account(db.Model): + __tablename__ = 'accounts' + + id = db.Column(db.String(50), primary_key=True) + customer_id = db.Column(db.String(50), nullable=False) + account_type = db.Column(db.String(50)) + status = db.Column(db.String(20), default='active') + lien_amount = db.Column(db.Float, default=0.0) + 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)) + + customer = relationship( + "Customer", + primaryjoin="Customer.id == Account.customer_id", + foreign_keys=[customer_id], + back_populates="accounts", + ) + + def __repr__(self): + return f'' diff --git a/app/models/customer.py b/app/models/customer.py new file mode 100644 index 0000000..d5f2650 --- /dev/null +++ b/app/models/customer.py @@ -0,0 +1,41 @@ +from datetime import datetime, timezone +from sqlalchemy.orm import relationship +from app.extensions import db + + +class Customer(db.Model): + __tablename__ = 'customers' + + id = db.Column(db.String(50), primary_key=True) + msisdn = db.Column(db.String(20), unique=True, nullable=False) + country_code = db.Column(db.String(3), 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)) + + accounts = relationship( + "Account", + primaryjoin="Customer.id == Account.customer_id", + foreign_keys="Account.customer_id", + back_populates="customer", + ) + + loans = relationship( + "Loan", + primaryjoin="Customer.id == Loan.customer_id", + foreign_keys="Loan.customer_id", + back_populates="customer", + ) + + @classmethod + def get_customer(cls, customer_id): + """ + Get customer by ID. + """ + customer = cls.query.filter_by(id=customer_id).first() + + if not customer: + raise ValueError(f"Customer does not exist") + return customer + + def __repr__(self): + return f'' diff --git a/app/models/loan.py b/app/models/loan.py index 656ac3c..277f383 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -54,6 +54,11 @@ class Loan(db.Model): 'continuousFee': self.continuous_fee, 'collectionType': self.collection_type, 'status': self.status, + 'productId': self.product_id, 'dueDate': self.due_date.isoformat() if self.due_date else None, 'loanDate': self.created_at.isoformat if self.created_at else None - } \ No newline at end of file + } + + @classmethod + def get_loan_by_transaction_id(cls, transaction_id): + return cls.query.filter_by(transaction_id=transaction_id).first() \ No newline at end of file diff --git a/app/models/loan_charge.py b/app/models/loan_charge.py index da12479..0a04789 100644 --- a/app/models/loan_charge.py +++ b/app/models/loan_charge.py @@ -1,4 +1,6 @@ from datetime import datetime, timezone, timedelta +from os.path import devnull + from app.extensions import db from sqlalchemy.orm import relationship @@ -40,4 +42,8 @@ class LoanCharge(db.Model): 'percent': self.percent, 'description': self.description, 'due': self.due - } \ No newline at end of file + } + + @classmethod + def get_loan_charge_by_debt_id(cls, debt_id): + return cls.query.filter_by(loan_id=debt_id) \ No newline at end of file diff --git a/app/services/loan.py b/app/services/loan.py new file mode 100644 index 0000000..b86067b --- /dev/null +++ b/app/services/loan.py @@ -0,0 +1,17 @@ +from app.models import Loan, LoanCharge + +class LoanService: + + @classmethod + def get_loan_by_transaction_id(cls, transaction_id): + """ + Get the loan by transaction ID + """ + return Loan.get_loan_by_transaction_id(transaction_id) + + @classmethod + def get_loan_charge_by_debt_id(cls, debt_id): + """ + Get the loan charge by debt ID + """ + return LoanCharge.get_loan_charge_by_debt_id(debt_id) \ No newline at end of file diff --git a/app/utils/extras.py b/app/utils/extras.py new file mode 100644 index 0000000..effc889 --- /dev/null +++ b/app/utils/extras.py @@ -0,0 +1,16 @@ +def preprocess_loan_charges_data(data): + """ + Preprocesses the data into a dictionary for efficient lookups by 'code'. + + Args: + data: A list of dictionaries. + + Returns: + A dictionary where keys are 'code' values and values are the corresponding dictionaries from the input data. + If multiple items have the same code, the last one encountered will be stored. + """ + preprocessed = {} + for item in data: + if 'code' in item: + preprocessed[item['code']] = item + return preprocessed \ No newline at end of file From 0bc71f81084e4487825fbafb4a5391ca6d84530c Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Thu, 17 Apr 2025 17:26:59 +0100 Subject: [PATCH 3/5] add endpoint for penal charge --- app/integrations/simbrella.py | 13 ++++++++++- app/routes/authentication.py | 8 +++---- app/routes/autocall.py | 11 ++++++++- openapi.yml | 42 +++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index 3f74720..18b443e 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -71,7 +71,6 @@ class SimbrellaClient: except Exception as e: logger.info(f"Failed to call Disbursement endpoint: {e}") - #raise return 0 return 1 @@ -150,6 +149,18 @@ class SimbrellaClient: return data + except Exception as e: + logger.info(f"Failed to call Payment Callback endpoint: {e}") + raise + + @staticmethod + def penal_charge(data): + + try: + logger.info(f"Here is your Penal Charge Request data ***** : {data}") + + return data + except Exception as e: logger.info(f"Failed to call Payment Callback endpoint: {e}") raise \ No newline at end of file diff --git a/app/routes/authentication.py b/app/routes/authentication.py index 20cf323..cce54b5 100644 --- a/app/routes/authentication.py +++ b/app/routes/authentication.py @@ -15,7 +15,7 @@ def health(): @auth_bp.route("/login", methods=["POST"]) def login(): - data = request.json + data = request.get_json() api_url = f"{BASE_URL}/login" response = requests.post(api_url, json=data) @@ -26,7 +26,7 @@ def login(): @auth_bp.route("/status-call", methods=["POST"]) def status_call(): - data = request.json + data = request.get_json() api_url = f"{BASE_URL}/StatusCall" # response = requests.post(api_url, json=data, headers=get_headers()) @@ -49,7 +49,7 @@ def status_call(): @auth_bp.route("/sms", methods=["POST"]) def sms(): - data = request.json + data = request.get_json() api_url = f"{BASE_URL}/SMS" # response = requests.post(api_url, json=data, headers=get_headers()) @@ -66,7 +66,7 @@ def sms(): @auth_bp.route("/bulk-sms", methods=["POST"]) def bulk_sms(): - data = request.json + data = request.get_json() api_url = f"{BASE_URL}/BulkSMS" # response = requests.post(api_url, json=data, headers=get_headers()) diff --git a/app/routes/autocall.py b/app/routes/autocall.py index cc3036e..42335f3 100644 --- a/app/routes/autocall.py +++ b/app/routes/autocall.py @@ -28,9 +28,18 @@ def disbursement(): @autocall_bp.route("/payment-callback", methods=["POST"]) def payment_callback(): - data = request.json() + data = request.get_json() logger.info(f"Calling Callback Components") response = SimbrellaClient.payment_callback(data) + return jsonify(response), 200 + +@autocall_bp.route("/penal-charge", methods=["POST"]) +def penal_charge(): + data = request.get_json() + logger.info(f"Calling Penal Charge Endpoints") + + response = SimbrellaClient.penal_charge(data) + return jsonify(response), 200 \ No newline at end of file diff --git a/openapi.yml b/openapi.yml index c4bc462..761fbe2 100644 --- a/openapi.yml +++ b/openapi.yml @@ -113,6 +113,48 @@ paths: /autocall/payment-callback: get: summary: The Payment callback + responses: + 200: + description: A successful response + /autocall/penal-charge: + post: + summary: Penal Charge Request + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + transactionId: + type: string + example: "T004" + fbnTransactionId: + type: string + example: "Tr201712RK9232P115" + debtId: + type: string + example: "273194670" + customerId: + type: string + example: "CN621868" + accountId: + type: string + example: "2017821799" + penalCharge: + type: number + example: "1.2" + lienAmount: + type: number + example: "101.2" + countryId: + type: string + example: "01" + comment: + type: string + example: "Testing PenalCharge" responses: 200: description: A successful response \ No newline at end of file From 1abc259a67580e8d7512b67310cf05b24eab10bf Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Thu, 17 Apr 2025 17:31:08 +0100 Subject: [PATCH 4/5] update on penal charge endpoint --- app/integrations/simbrella.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index 18b443e..e49c3df 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -1,5 +1,6 @@ import requests from app.config import settings +from app.helpers.response_helper import ResponseHelper from app.services.loan import LoanService from app.utils.auth import get_headers from app.utils.extras import preprocess_loan_charges_data @@ -156,11 +157,26 @@ class SimbrellaClient: @staticmethod def penal_charge(data): + api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/PenalCharge" + logger.info(f"Calling Penal Charge endpoint with data: {data}") + try: logger.info(f"Here is your Penal Charge Request data ***** : {data}") - return data + try: + logger.info(f"Here is your Penal Charge Request data ****** : {data}") + response = requests.post(api_url, json=data, timeout=10, headers=get_headers()) + logger.info(f"Penal Charge response: {response.json()}") + + except Exception as e: + logger.info(f"Failed to call Penal Charge endpoint: {e}") + return ResponseHelper.error("An error occurred", 500) + + return ResponseHelper.success({ + "resultCode": "00", + "resultDescription": "Penal charge debited successfully" + }, "Successful") except Exception as e: - logger.info(f"Failed to call Payment Callback endpoint: {e}") + logger.info(f"Failed to call Penal Charge endpoint: {e}") raise \ No newline at end of file From 4e9ff06cedd7632e23e71098daf475d6e59ada69 Mon Sep 17 00:00:00 2001 From: "oluyemi.a.simbrellang.com" Date: Thu, 17 Apr 2025 17:41:09 +0100 Subject: [PATCH 5/5] some improvements on endpoints --- app/integrations/simbrella.py | 13 +++++-------- app/routes/autocall.py | 11 ++++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index e49c3df..920ea9f 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -121,10 +121,11 @@ class SimbrellaClient: def verify_transaction(): try: - return { + data = { "status": "00", "message": "Transaction verified" } + return ResponseHelper.success(data, "Successful") except Exception as e: logger.info(f"Failed to call TransactionVerify endpoint: {e}") @@ -136,7 +137,7 @@ class SimbrellaClient: try: logger.info(f"Here is your Disbursement Request data ***** : {data}") - return data + return ResponseHelper.success(data, "Successful") except Exception as e: logger.info(f"Failed to call Disbursement endpoint: {e}") @@ -148,7 +149,7 @@ class SimbrellaClient: try: logger.info(f"Here is your Payment Callback Request data ***** : {data}") - return data + return ResponseHelper.success(data, "Successful") except Exception as e: logger.info(f"Failed to call Payment Callback endpoint: {e}") @@ -167,16 +168,12 @@ class SimbrellaClient: logger.info(f"Here is your Penal Charge Request data ****** : {data}") response = requests.post(api_url, json=data, timeout=10, headers=get_headers()) logger.info(f"Penal Charge response: {response.json()}") + return ResponseHelper.success(response.json(), "Successful") except Exception as e: logger.info(f"Failed to call Penal Charge endpoint: {e}") return ResponseHelper.error("An error occurred", 500) - return ResponseHelper.success({ - "resultCode": "00", - "resultDescription": "Penal charge debited successfully" - }, "Successful") - except Exception as e: logger.info(f"Failed to call Penal Charge endpoint: {e}") raise \ No newline at end of file diff --git a/app/routes/autocall.py b/app/routes/autocall.py index 42335f3..230cd4e 100644 --- a/app/routes/autocall.py +++ b/app/routes/autocall.py @@ -1,6 +1,7 @@ from flask import Blueprint, request, jsonify, current_app import requests from app.config import settings +from app.helpers.response_helper import ResponseHelper from app.utils.auth import get_headers from app.utils.logger import logger from app.integrations.simbrella import SimbrellaClient @@ -14,7 +15,7 @@ def verify_transaction(): response = SimbrellaClient.verify_transaction() - return jsonify(response), 200 + return response @autocall_bp.route("/refresh-disbursement", methods=["GET"]) def disbursement(): @@ -23,7 +24,7 @@ def disbursement(): response = SimbrellaClient.verify_transaction() - return jsonify(response), 200 + return response @autocall_bp.route("/payment-callback", methods=["POST"]) @@ -33,13 +34,13 @@ def payment_callback(): response = SimbrellaClient.payment_callback(data) - return jsonify(response), 200 + return response @autocall_bp.route("/penal-charge", methods=["POST"]) def penal_charge(): data = request.get_json() logger.info(f"Calling Penal Charge Endpoints") - response = SimbrellaClient.penal_charge(data) + response = SimbrellaClient.penal_charge(data[0]) - return jsonify(response), 200 \ No newline at end of file + return response \ No newline at end of file