diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index b40f21c..920ea9f 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -1,6 +1,9 @@ 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 from app.utils.logger import logger from flask import jsonify, current_app from app.services.transactions import TransactionService @@ -26,21 +29,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()) @@ -48,10 +72,8 @@ class SimbrellaClient: except Exception as e: logger.info(f"Failed to call Disbursement endpoint: {e}") - #raise return 0 - # return jsonify(response.json()), response.status_code return 1 @staticmethod @@ -99,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}") @@ -114,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}") @@ -126,8 +149,31 @@ 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}") + raise + + @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}") + + 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()}") + 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) + + 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/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 new file mode 100644 index 0000000..277f383 --- /dev/null +++ b/app/models/loan.py @@ -0,0 +1,64 @@ +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, + '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 + } + + @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 new file mode 100644 index 0000000..0a04789 --- /dev/null +++ b/app/models/loan_charge.py @@ -0,0 +1,49 @@ +from datetime import datetime, timezone, timedelta +from os.path import devnull + +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 + } + + @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/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 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..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,14 +24,23 @@ def disbursement(): response = SimbrellaClient.verify_transaction() - return jsonify(response), 200 + return response @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 \ No newline at end of file + 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[0]) + + return response \ 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 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