From 52072304800d972b88d007ed64ec1cdc0b586c63 Mon Sep 17 00:00:00 2001 From: Azeez Muibi Date: Thu, 24 Apr 2025 10:11:26 +0100 Subject: [PATCH] update --- app/api/routes/routes.py | 11 ++ app/api/schemas/repayment.py | 12 +++ app/api/services/__init__.py | 1 + app/api/services/repayment.py | 115 +++++++++++++++++++++ app/models/repayment.py | 52 ++++++++++ app/swagger/digifi_swagger.json | 34 +++--- app/swagger/paths/Repayment.json | 56 ++++++++++ app/swagger/schemas/RepaymentRequest.json | 36 +++++++ app/swagger/schemas/RepaymentResponse.json | 28 +++++ 9 files changed, 328 insertions(+), 17 deletions(-) create mode 100644 app/api/schemas/repayment.py create mode 100644 app/api/services/repayment.py create mode 100644 app/models/repayment.py create mode 100644 app/swagger/paths/Repayment.json create mode 100644 app/swagger/schemas/RepaymentRequest.json create mode 100644 app/swagger/schemas/RepaymentResponse.json diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index ec7f596..fce6ec7 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -1,5 +1,7 @@ from flask import Blueprint, request, jsonify, send_from_directory from flask import Blueprint, request, jsonify + +from app.api.services import RepaymentService from app.api.services.loan_service import LoanService from app.api.services.transaction_service import TransactionService from app.api.services.auth_service import AuthService @@ -135,3 +137,12 @@ def get_transactions(): # logger.info(f"Get transactions request received with filters: {filters}") response = TransactionService.process_request(filters) return response + +# Repayment Endpoint +@api.route("/Repayment", methods=["POST"]) +# @jwt_required() +def repayment(): + data = request.get_json() + # logger.info(f"Repayment request received: {data}") + response = RepaymentService.process_request(data) + return response \ No newline at end of file diff --git a/app/api/schemas/repayment.py b/app/api/schemas/repayment.py new file mode 100644 index 0000000..0a393b9 --- /dev/null +++ b/app/api/schemas/repayment.py @@ -0,0 +1,12 @@ +from marshmallow import Schema, fields + +# Repayment Schema +class RepaymentSchema(Schema): + type = fields.Str(required=False) + msisdn = fields.Str(required=False) #optional + debtId = fields.Str(required=True) + productId = fields.Str(required=True) + transactionId = fields.Str(required=True) + accountId = fields.Str(required=True) + customerId = fields.Str(required=True) + channel = fields.Str(required=True) diff --git a/app/api/services/__init__.py b/app/api/services/__init__.py index f3ca5d4..104ca8b 100644 --- a/app/api/services/__init__.py +++ b/app/api/services/__init__.py @@ -5,3 +5,4 @@ from app.api.services.transaction_service import TransactionService from app.api.services.loan_service import LoanService from app.api.services.auth_service import AuthService from app.api.services.dashboard_service import DashboardService +from app.api.services.repayment import RepaymentService diff --git a/app/api/services/repayment.py b/app/api/services/repayment.py new file mode 100644 index 0000000..dac4fb0 --- /dev/null +++ b/app/api/services/repayment.py @@ -0,0 +1,115 @@ +from flask import request, jsonify +from marshmallow import ValidationError +from app.api.enums.loan_status import LoanStatus +from app.models import Repayment +from app.models.customer import Customer +from app.models.loan import Loan +from app.utils.logger import logger +from app.api.schemas.repayment import RepaymentSchema +from app.api.services.base_service import BaseService +from app.api.enums import TransactionType +from threading import Thread +from app.extensions import db + +class RepaymentService(BaseService): + TRANSACTION_TYPE = TransactionType.REPAYMENT + + @staticmethod + def process_request(data): + """ + Process the Repayment request. + + Args: + data (dict): The request data. + + Returns: + dict: A standardized response. + """ + try: + with db.session.begin(): + validated_data = RepaymentService.validate_data(data, RepaymentSchema()) + customer_id = validated_data.get('customerId') + request_id = validated_data.get('requestId') + loan_id = validated_data.get('debtId') + product_id = validated_data.get('productId') + account_id = validated_data.get('accountId') + customer = Customer.get_customer(customer_id) + + if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): + + # Save the repayment details + repayment = Repayment.create_repayment( + customer_id = customer_id, + loan_id = loan_id, + product_id = product_id + + ) + + if not repayment: + logger.error(f"Failed to save repayment details") + return jsonify({ + "message": "Failed to save repayment details." + }), 400 + + + #Update Loan status + Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID) + + transaction = RepaymentService.log_transaction(validated_data = validated_data) + + if not transaction: + logger.error(f"Failed to log transaction") + return jsonify({ + "message": "Failed to log transaction." + }), 400 + else: + return jsonify({ + "message": "Invalid Customer or Account" + }), 400 + + + + # Simulated processing logic + response_data = { + "customerId": customer_id, + "productId": product_id, + "debtId": loan_id, + "resultCode": "00", + "resultDescription": "Successful" + } + + # return ResponseHelper.success( + # data=response_data, + # message="Repayment processed successfully" + # ) + + # Call Kafka in a background thread + thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT")) + thread.start() + + db.session.commit() + return response_data + + except ValidationError as err: + + logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") + db.session.rollback() + + return jsonify({ + "message": "Validation exception" + }) , 422 + + except ValueError as err: + logger.error(f"{getattr(err, 'messages', str(err))}") + db.session.rollback() + + return jsonify({ + "message": str(err) + }) , 400 + + except Exception as e: + logger.error(f"An error occurred: {str(e)}", exc_info=True) + db.session.rollback() + return jsonify({ + "message": "Internal Server Error" + }) , 500 diff --git a/app/models/repayment.py b/app/models/repayment.py new file mode 100644 index 0000000..91850c4 --- /dev/null +++ b/app/models/repayment.py @@ -0,0 +1,52 @@ +from datetime import datetime, timezone +from app.api.enums.loan_status import LoanStatus +from app.extensions import db +from app.models.customer import Customer +from app.models.loan import Loan +from sqlalchemy.exc import IntegrityError + + +class Repayment(db.Model): + __tablename__ = 'repayments' + + id = db.Column( + db.Integer, + primary_key=True, + autoincrement=True, + ) + loan_id = db.Column(db.String(50), nullable=False) + customer_id = db.Column(db.String(50), nullable=False) + product_id = db.Column(db.String(20), 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)) + + @classmethod + def create_repayment(cls, customer_id, loan_id, product_id): + + # Check customer exists + if not Customer.is_valid_customer(customer_id): + raise ValueError("Invalid customer") + + # Check loan exists + loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id) + + # Check that the loan is active + if loan.status != LoanStatus.ACTIVE: + raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})") + + + repayment = cls( + customer_id=customer_id, + loan_id=loan_id, + product_id=product_id, + ) + + try: + db.session.add(repayment) + except IntegrityError as err: + raise ValueError(f"Database integrity error: {err}") + + return repayment + + def __repr__(self): + return f'' diff --git a/app/swagger/digifi_swagger.json b/app/swagger/digifi_swagger.json index 56478bf..852be8e 100644 --- a/app/swagger/digifi_swagger.json +++ b/app/swagger/digifi_swagger.json @@ -45,14 +45,6 @@ "url": "https://www.simbrellang.net" } }, - { - "name": "AuthorizeRefresh", - "description": "This feature will be used for refreshing authorized customers.", - "externalDocs": { - "description": "Find out more", - "url": "https://www.simbrellang.net" - } - }, { "name": "Loans", "description": "Get all loans with optional filtering.", @@ -68,6 +60,14 @@ "description": "Find out more", "url": "https://www.simbrellang.net" } + }, + { + "name": "Repayment", + "description": "Repayment Request.", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } } ], "paths": { @@ -80,14 +80,14 @@ "/Authorize": { "$ref": "../swagger/paths/Authorize.json" }, - "/AuthorizeRefresh": { - "$ref": "../swagger/paths/AuthorizeRefresh.json" - }, "/loans": { "$ref": "../swagger/paths/Loans.json" }, "/transactions": { "$ref": "../swagger/paths/Transactions.json" + }, + "/Repayment": { + "$ref": "../swagger/paths/Repayment.json" } }, "components": { @@ -101,12 +101,6 @@ "AuthorizeRequest": { "$ref": "../swagger/schemas/AuthorizeRequest.json" }, - "AuthorizeRefreshResponse": { - "$ref": "../swagger/schemas/AuthorizeRefreshResponse.json" - }, - "AuthorizeRefreshRequest": { - "$ref": "../swagger/schemas/AuthorizeRefreshRequest.json" - }, "LoginRequest": { "$ref": "../swagger/schemas/LoginRequest.json" }, @@ -121,6 +115,12 @@ }, "TransactionsResponse": { "$ref": "../swagger/schemas/TransactionsResponse.json" + }, + "RepaymentRequest": { + "$ref": "../swagger/schemas/RepaymentRequest.json" + }, + "RepaymentResponse": { + "$ref": "../swagger/schemas/RepaymentResponse.json" } }, "securitySchemes": { diff --git a/app/swagger/paths/Repayment.json b/app/swagger/paths/Repayment.json new file mode 100644 index 0000000..e88d18c --- /dev/null +++ b/app/swagger/paths/Repayment.json @@ -0,0 +1,56 @@ +{ + "post": { + "tags": [ + "Repayment" + ], + "summary": "Repayment Request", + "description": "Repayment Request", + "operationId": "Repayment", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/RepaymentRequest.json" + } + }, + "application/xml": { + "schema": { + "$ref": "../schemas/RepaymentRequest.json" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "../schemas/RepaymentRequest.json" + } + } + } + }, + "responses": { + "200": { + "description": "Repayment Successful", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/RepaymentResponse.json" + } + }, + "application/xml": { + "schema": { + "$ref": "../schemas/RepaymentResponse.json" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "422": { + "description": "Validation exception" + }, + "500": { + "description": "Internal server error" + } + } + } +} \ No newline at end of file diff --git a/app/swagger/schemas/RepaymentRequest.json b/app/swagger/schemas/RepaymentRequest.json new file mode 100644 index 0000000..9543f74 --- /dev/null +++ b/app/swagger/schemas/RepaymentRequest.json @@ -0,0 +1,36 @@ +{ + "type": "object", + "properties": { + "msisdn": { + "type": "string", + "example": "3451342" + }, + "debtId": { + "type": "string", + "example": "10" + }, + "productId": { + "type": "string", + "example": "101" + }, + "transactionId": { + "type": "string", + "example": "20171209232115" + }, + "customerId": { + "type": "string", + "example": "CID0000025585" + }, + "channel": { + "type": "string", + "example": "USSD" + }, + "accountId": { + "type": "string", + "example": "ACN8263457" + } + }, + "xml": { + "name": "RepaymentRequest" + } +} \ No newline at end of file diff --git a/app/swagger/schemas/RepaymentResponse.json b/app/swagger/schemas/RepaymentResponse.json new file mode 100644 index 0000000..06e4666 --- /dev/null +++ b/app/swagger/schemas/RepaymentResponse.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "CN621868" + }, + "productId": { + "type": "string", + "example": "101" + }, + "debtId": { + "type": "string", + "example": "273194670" + }, + "resultCode": { + "type": "string", + "example": "00" + }, + "resultDescription": { + "type": "string", + "example": "Successful" + } + }, + "xml": { + "name": "RepaymentResponse" + } +} \ No newline at end of file