diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index fce6ec7..86b3b7c 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -1,7 +1,8 @@ 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.repayment_service import RepaymentService +from app.api.services.loan_charge_service import LoanChargeService from app.api.services.loan_service import LoanService from app.api.services.transaction_service import TransactionService from app.api.services.auth_service import AuthService @@ -138,11 +139,37 @@ def get_transactions(): 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 +@api.route('/repayments', methods=['GET']) +# @token_required +def get_all_repayments(): + # Extract query parameters for filtering + filters = { + 'loan_id': request.args.get('loan_id'), + 'customer_id': request.args.get('customer_id'), + 'product_id': request.args.get('product_id'), + 'start_date': request.args.get('start_date'), + 'end_date': request.args.get('end_date'), + 'page': request.args.get('page', 1), + 'limit': request.args.get('limit', 20) + } + # logger.info(f"Get repayments request received with filters: {filters}") + response = RepaymentService.get_all_repayments(filters) + return jsonify(response) + +@api.route('/loan-charges', methods=['GET']) +# @token_required +def get_all_loan_charges(): + # Extract query parameters for filtering + filters = { + 'loan_id': request.args.get('loan_id'), + 'code': request.args.get('code'), + 'start_date': request.args.get('start_date'), + 'end_date': request.args.get('end_date'), + 'due_before': request.args.get('due_before'), + 'due_after': request.args.get('due_after'), + 'page': request.args.get('page', 1), + 'limit': request.args.get('limit', 20) + } + # logger.info(f"Get loan charges request received with filters: {filters}") + response = LoanChargeService.get_all_loan_charges(filters) + return jsonify(response) \ No newline at end of file diff --git a/app/api/schemas/customer_consent.py b/app/api/schemas/customer_consent_schema.py similarity index 100% rename from app/api/schemas/customer_consent.py rename to app/api/schemas/customer_consent_schema.py diff --git a/app/api/schemas/loan_status.py b/app/api/schemas/loan_status_schema.py similarity index 100% rename from app/api/schemas/loan_status.py rename to app/api/schemas/loan_status_schema.py diff --git a/app/api/schemas/repayment.py b/app/api/schemas/repayment.py deleted file mode 100644 index 0a393b9..0000000 --- a/app/api/schemas/repayment.py +++ /dev/null @@ -1,12 +0,0 @@ -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 c8287dc..230ab4e 100644 --- a/app/api/services/__init__.py +++ b/app/api/services/__init__.py @@ -6,3 +6,4 @@ 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_service import RepaymentService +from app.api.services.loan_charge_service import LoanChargeService diff --git a/app/api/services/customer_consent.py b/app/api/services/customer_consent.py index a6b4f07..df59be5 100644 --- a/app/api/services/customer_consent.py +++ b/app/api/services/customer_consent.py @@ -2,7 +2,7 @@ from flask import request, jsonify from app.api.services.base_service import BaseService from marshmallow import ValidationError from app.utils.logger import logger -from app.api.schemas.customer_consent import CustomerConsentSchema +from app.api.schemas.customer_consent_schema import CustomerConsentSchema from app.api.services.base_service import BaseService from app.api.enums import TransactionType from app.extensions import db diff --git a/app/api/services/loan_charge_service.py b/app/api/services/loan_charge_service.py new file mode 100644 index 0000000..d90221a --- /dev/null +++ b/app/api/services/loan_charge_service.py @@ -0,0 +1,107 @@ +import logging +from datetime import datetime +from flask import jsonify +from app.models.loan_charge import LoanCharge + +# Configure logging +logger = logging.getLogger(__name__) + +class LoanChargeService: + """ + Service class for handling loan charge-related operations. + """ + + @staticmethod + def get_all_loan_charges(filters=None): + """ + Get all loan charges with optional filtering. + + Args: + filters (dict, optional): Filters for the loan charges query. + + Returns: + dict: A standardized response with loan charges data. + """ + try: + if filters is None: + filters = {} + + # Extract filters + loan_id = filters.get('loan_id') + code = filters.get('code') + start_date = filters.get('start_date') + end_date = filters.get('end_date') + due_before = filters.get('due_before') + due_after = filters.get('due_after') + + # Extract pagination parameters + page = int(filters.get('page', 1)) + limit = int(filters.get('limit', 20)) + + # Ensure page and limit are valid + if page < 1: + page = 1 + if limit < 1 or limit > 100: + limit = 20 + + # Convert string dates to datetime objects if provided + if start_date and isinstance(start_date, str): + start_date = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + if end_date and isinstance(end_date, str): + end_date = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + if due_before and isinstance(due_before, str): + due_before = datetime.fromisoformat(due_before.replace('Z', '+00:00')) + if due_after and isinstance(due_after, str): + due_after = datetime.fromisoformat(due_after.replace('Z', '+00:00')) + + # Get loan charges with optional filters and pagination + loan_charges, total_count = LoanCharge.get_all_loan_charges( + loan_id=loan_id, + code=code, + start_date=start_date, + end_date=end_date, + due_before=due_before, + due_after=due_after, + page=page, + limit=limit + ) + + # Convert loan charges to dictionary format + loan_charges_data = [] + for charge in loan_charges: + loan_charges_data.append({ + 'loan_id': charge.loan_id, + 'code': charge.code, + 'amount': charge.amount, + 'percent': charge.percent, + 'description': charge.description, + 'due': charge.due, + 'transaction_id': charge.transaction_id, + 'due_date': charge.due_date.isoformat() if charge.due_date else None, + 'created_at': charge.created_at.isoformat() if charge.created_at else None, + 'updated_at': charge.updated_at.isoformat() if charge.updated_at else None + }) + + # Calculate total pages + total_pages = (total_count + limit - 1) // limit + + response_data = { + 'loan_charges': loan_charges_data, + 'count': len(loan_charges_data), + 'pagination': { + 'total_count': total_count, + 'total_pages': total_pages, + 'current_page': page, + 'limit': limit, + 'has_next': page < total_pages, + 'has_prev': page > 1 + } + } + + return response_data + + except Exception as e: + logger.error(f"An error occurred: {str(e)}", exc_info=True) + return jsonify({ + "message": "Internal Server Error" + }), 500 \ No newline at end of file diff --git a/app/api/services/loan_status.py b/app/api/services/loan_status.py index 55de9b8..7c4598a 100644 --- a/app/api/services/loan_status.py +++ b/app/api/services/loan_status.py @@ -3,7 +3,7 @@ from marshmallow import ValidationError from app.api.enums.loan_status import LoanStatus from app.models import Customer from app.utils.logger import logger -from app.api.schemas.loan_status import LoanStatusSchema +from app.api.schemas.loan_status_schema import LoanStatusSchema from app.api.services.base_service import BaseService from app.api.enums import TransactionType from app.extensions import db diff --git a/app/api/services/repayment_service.py b/app/api/services/repayment_service.py index 040ce61..48b2dcc 100644 --- a/app/api/services/repayment_service.py +++ b/app/api/services/repayment_service.py @@ -1,115 +1,97 @@ -from flask import request, jsonify -from marshmallow import ValidationError -from app.api.enums.loan_status import LoanStatus -from app.models.customer import Customer -from app.models.loan import Loan +import logging +from datetime import datetime +from flask import jsonify from app.models.repayment import Repayment -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 +# Configure logging +logger = logging.getLogger(__name__) + +class RepaymentService: + """ + Service class for handling repayment-related operations. + """ @staticmethod - def process_request(data): + def get_all_repayments(filters=None): """ - Process the Repayment request. + Get all repayments with optional filtering. Args: - data (dict): The request data. + filters (dict, optional): Filters for the repayments query. Returns: - dict: A standardized response. + dict: A standardized response with repayments data. """ 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)): + if filters is None: + filters = {} - # Save the repayment details - repayment = Repayment.create_repayment( - customer_id = customer_id, - loan_id = loan_id, - product_id = product_id + # Extract filters + loan_id = filters.get('loan_id') + customer_id = filters.get('customer_id') + product_id = filters.get('product_id') + start_date = filters.get('start_date') + end_date = filters.get('end_date') - ) + # Extract pagination parameters + page = int(filters.get('page', 1)) + limit = int(filters.get('limit', 20)) - 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) + # Ensure page and limit are valid + if page < 1: + page = 1 + if limit < 1 or limit > 100: + limit = 20 - 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 - + # Convert string dates to datetime objects if provided + if start_date and isinstance(start_date, str): + start_date = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + if end_date and isinstance(end_date, str): + end_date = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + # Get repayments with optional filters and pagination + repayments, total_count = Repayment.get_all_repayments( + loan_id=loan_id, + customer_id=customer_id, + product_id=product_id, + start_date=start_date, + end_date=end_date, + page=page, + limit=limit + ) - # Simulated processing logic - response_data = { - "customerId": customer_id, - "productId": product_id, - "debtId": loan_id, - "resultCode": "00", - "resultDescription": "Successful" + # Convert repayments to dictionary format + repayments_data = [] + for repayment in repayments: + repayments_data.append({ + 'loan_id': repayment.loan_id, + 'customer_id': repayment.customer_id, + 'product_id': repayment.product_id, + 'transaction_id': repayment.transaction_id, + 'created_at': repayment.created_at.isoformat() if repayment.created_at else None, + 'updated_at': repayment.updated_at.isoformat() if repayment.updated_at else None + }) + + # Calculate total pages + total_pages = (total_count + limit - 1) // limit + + response_data = { + 'repayments': repayments_data, + 'count': len(repayments_data), + 'pagination': { + 'total_count': total_count, + 'total_pages': total_pages, + 'current_page': page, + 'limit': limit, + 'has_next': page < total_pages, + 'has_prev': page > 1 } + } - # 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 + return response_data 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 + }), 500 \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index 9fbb27f..e1a7e80 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,5 +3,7 @@ from .account import Account from .loan import Loan from .transaction import Transaction from .user import User +from .repayment import Repayment +from .loan_charge import LoanCharge -__all__ = ['Customer', 'Account', 'Loan', 'Transaction', User] \ No newline at end of file +__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'User', 'Repayment', 'LoanCharge'] \ 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..c5a5804 --- /dev/null +++ b/app/models/loan_charge.py @@ -0,0 +1,90 @@ +from datetime import datetime, timezone +from app.extensions import db +from sqlalchemy.exc import IntegrityError + +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) + code = db.Column(db.String(50), nullable=False) + amount = db.Column(db.Float, default=0.00) + percent = db.Column(db.Float) + description = db.Column(db.String(255)) + due = db.Column(db.Integer) + 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)) + transaction_id = db.Column(db.String(50)) + due_date = db.Column(db.DateTime) + + @classmethod + def get_all_loan_charges(cls, loan_id=None, code=None, start_date=None, end_date=None, + due_before=None, due_after=None, page=1, limit=20): + """ + Get all loan charges with optional filtering + + Args: + loan_id (int, optional): Filter by loan ID + code (str, optional): Filter by charge code + start_date (datetime, optional): Filter by start date (created_at) + end_date (datetime, optional): Filter by end date (created_at) + due_before (datetime, optional): Filter charges due before this date + due_after (datetime, optional): Filter charges due after this date + page (int, optional): Page number for pagination + limit (int, optional): Number of items per page + + Returns: + tuple: (list of LoanCharge objects, total count) + """ + query = cls.query + + # Apply filters if provided + if loan_id: + query = query.filter(cls.loan_id == loan_id) + + if code: + query = query.filter(cls.code == code) + + if start_date: + query = query.filter(cls.created_at >= start_date) + + if end_date: + query = query.filter(cls.created_at <= end_date) + + if due_before: + query = query.filter(cls.due_date <= due_before) + + if due_after: + query = query.filter(cls.due_date >= due_after) + + # Order by created_at descending (newest first) + query = query.order_by(cls.created_at.desc()) + + # Get total count before pagination + total_count = query.count() + + # Apply pagination + offset = (page - 1) * limit + query = query.limit(limit).offset(offset) + + return query.all(), total_count + + def to_dict(self): + """ + Convert the LoanCharge object to a dictionary format for JSON serialization. + """ + return { + 'loan_id': self.loan_id, + 'code': self.code, + 'amount': self.amount, + 'percent': self.percent, + 'description': self.description, + 'due': self.due, + 'transaction_id': self.transaction_id, + 'due_date': self.due_date.isoformat() if self.due_date else None, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/app/models/repayment.py b/app/models/repayment.py index 3e19ad3..6635dc2 100644 --- a/app/models/repayment.py +++ b/app/models/repayment.py @@ -1,51 +1,78 @@ 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, - ) + 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) + product_id = db.Column(db.String(50), 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)) + transaction_id = db.Column(db.String(50)) @classmethod - def create_repayment(cls, customer_id, loan_id, product_id): + def get_all_repayments(cls, loan_id=None, customer_id=None, product_id=None, + start_date=None, end_date=None, page=1, limit=20): + """ + Get all repayments with optional filtering - # Check customer exists - if not Customer.is_valid_customer(customer_id): - raise ValueError("Invalid customer") + Args: + loan_id (str, optional): Filter by loan ID + customer_id (str, optional): Filter by customer ID + product_id (str, optional): Filter by product ID + start_date (datetime, optional): Filter by start date (created_at) + end_date (datetime, optional): Filter by end date (created_at) + page (int, optional): Page number for pagination + limit (int, optional): Number of items per page - # 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})") + Returns: + tuple: (list of Repayment objects, total count) + """ + query = cls.query - repayment = cls( - customer_id=customer_id, - loan_id=loan_id, - product_id=product_id, - ) + # Apply filters if provided + if loan_id: + query = query.filter(cls.loan_id == loan_id) - try: - db.session.add(repayment) - except IntegrityError as err: - raise ValueError(f"Database integrity error: {err}") + if customer_id: + query = query.filter(cls.customer_id == customer_id) - return repayment + if product_id: + query = query.filter(cls.product_id == product_id) + + if start_date: + query = query.filter(cls.created_at >= start_date) + + if end_date: + query = query.filter(cls.created_at <= end_date) + + # Order by created_at descending (newest first) + query = query.order_by(cls.created_at.desc()) + + # Get total count before pagination + total_count = query.count() + + # Apply pagination + offset = (page - 1) * limit + query = query.limit(limit).offset(offset) + + return query.all(), total_count + + def to_dict(self): + """ + Convert the Repayment object to a dictionary format for JSON serialization. + """ + return { + 'loan_id': self.loan_id, + 'customer_id': self.customer_id, + 'product_id': self.product_id, + 'transaction_id': self.transaction_id, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } def __repr__(self): - return f'' + return f'' \ No newline at end of file diff --git a/app/swagger/digifi_swagger.json b/app/swagger/digifi_swagger.json index 6e22e37..4d83cdb 100644 --- a/app/swagger/digifi_swagger.json +++ b/app/swagger/digifi_swagger.json @@ -65,8 +65,16 @@ } }, { - "name": "Repayment", - "description": "Repayment Request.", + "name": "Repayments", + "description": "Get all repayments with optional filtering.", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } + }, + { + "name": "Loan Charges", + "description": "Get all loan charges with optional filtering.", "externalDocs": { "description": "Find out more", "url": "https://www.simbrellang.net" @@ -89,8 +97,11 @@ "/transactions": { "$ref": "../swagger/paths/Transactions.json" }, - "/Repayment": { - "$ref": "../swagger/paths/Repayment.json" + "/repayments": { + "$ref": "../swagger/paths/Repayments.json" + }, + "/loan-charges": { + "$ref": "../swagger/paths/LoanCharges.json" } }, "components": { @@ -119,11 +130,11 @@ "TransactionsResponse": { "$ref": "../swagger/schemas/TransactionsResponse.json" }, - "RepaymentRequest": { - "$ref": "../swagger/schemas/RepaymentRequest.json" + "RepaymentsResponse": { + "$ref": "../swagger/schemas/RepaymentsResponse.json" }, - "RepaymentResponse": { - "$ref": "../swagger/schemas/RepaymentResponse.json" + "LoanChargesResponse": { + "$ref": "../swagger/schemas/LoanChargesResponse.json" } }, "securitySchemes": { diff --git a/app/swagger/paths/LoanCharges.json b/app/swagger/paths/LoanCharges.json new file mode 100644 index 0000000..db6ce8b --- /dev/null +++ b/app/swagger/paths/LoanCharges.json @@ -0,0 +1,117 @@ +{ + "get": { + "tags": ["Loan Charges"], + "summary": "Get all loan charges with optional filtering", + "description": "Retrieve loan charges with various filter options including loan ID, charge code, etc.", + "operationId": "getLoanCharges", + "parameters": [ + { + "name": "loan_id", + "in": "query", + "description": "Filter by loan ID", + "required": false, + "schema": { + "type": "integer" + }, + "example": 7463 + }, + { + "name": "code", + "in": "query", + "description": "Filter by charge code", + "required": false, + "schema": { + "type": "string" + }, + "example": "INTEREST" + }, + { + "name": "start_date", + "in": "query", + "description": "Filter by start date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-01-01T00:00:00Z" + }, + { + "name": "end_date", + "in": "query", + "description": "Filter by end date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-12-31T23:59:59Z" + }, + { + "name": "due_before", + "in": "query", + "description": "Filter charges due before this date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-12-31T23:59:59Z" + }, + { + "name": "due_after", + "in": "query", + "description": "Filter charges due after this date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-01-01T00:00:00Z" + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "minimum": 1 + }, + "example": 1 + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page (max 100)", + "required": false, + "schema": { + "type": "integer", + "default": 20, + "minimum": 1, + "maximum": 100 + }, + "example": 20 + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/LoanChargesResponse.json" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "500": { + "description": "Internal server error" + } + } + } +} \ No newline at end of file diff --git a/app/swagger/paths/Repayment.json b/app/swagger/paths/Repayment.json deleted file mode 100644 index e88d18c..0000000 --- a/app/swagger/paths/Repayment.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "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/paths/Repayments.json b/app/swagger/paths/Repayments.json new file mode 100644 index 0000000..4854422 --- /dev/null +++ b/app/swagger/paths/Repayments.json @@ -0,0 +1,105 @@ +{ + "get": { + "tags": ["Repayments"], + "summary": "Get all repayments with optional filtering", + "description": "Retrieve repayments with various filter options including loan ID, customer ID, product ID, etc.", + "operationId": "getRepayments", + "parameters": [ + { + "name": "loan_id", + "in": "query", + "description": "Filter by loan ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "10" + }, + { + "name": "customer_id", + "in": "query", + "description": "Filter by customer ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "CID0000025585" + }, + { + "name": "product_id", + "in": "query", + "description": "Filter by product ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "101" + }, + { + "name": "start_date", + "in": "query", + "description": "Filter by start date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-01-01T00:00:00Z" + }, + { + "name": "end_date", + "in": "query", + "description": "Filter by end date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-12-31T23:59:59Z" + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "minimum": 1 + }, + "example": 1 + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page (max 100)", + "required": false, + "schema": { + "type": "integer", + "default": 20, + "minimum": 1, + "maximum": 100 + }, + "example": 20 + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/RepaymentsResponse.json" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "500": { + "description": "Internal server error" + } + } + } +} \ No newline at end of file diff --git a/app/swagger/schemas/LoanChargesResponse.json b/app/swagger/schemas/LoanChargesResponse.json new file mode 100644 index 0000000..402cdf1 --- /dev/null +++ b/app/swagger/schemas/LoanChargesResponse.json @@ -0,0 +1,100 @@ +{ + "type": "object", + "properties": { + "loan_charges": { + "type": "array", + "items": { + "type": "object", + "properties": { + "loan_id": { + "type": "integer", + "example": 7463 + }, + "code": { + "type": "string", + "example": "INTEREST" + }, + "amount": { + "type": "number", + "format": "float", + "example": 0.00 + }, + "percent": { + "type": "number", + "format": "float", + "example": 1.1 + }, + "description": { + "type": "string", + "example": "This is fee 9000" + }, + "due": { + "type": "integer", + "example": 0 + }, + "transaction_ { + "type": "integer", + "example": 0 + }, + "transaction_id": { + "type": "string", + "example": "TRX123456", + "nullable": true + }, + "due_date": { + "type": "string", + "format": "date-time", + "example": "2025-04-16T20:03:31.998445Z", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2025-04-16T20:03:31.998085Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2025-04-16T20:03:31.998445Z" + } + } + } + }, + "count": { + "type": "integer", + "example": 1 + }, + "pagination": { + "type": "object", + "properties": { + "total_count": { + "type": "integer", + "example": 100 + }, + "total_pages": { + "type": "integer", + "example": 5 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "limit": { + "type": "integer", + "example": 20 + }, + "has_next": { + "type": "boolean", + "example": true + }, + "has_prev": { + "type": "boolean", + "example": false + } + } + } + }, + "xml": { + "name": "LoanChargesResponse" + } +} \ No newline at end of file diff --git a/app/swagger/schemas/RepaymentRequest.json b/app/swagger/schemas/RepaymentRequest.json deleted file mode 100644 index 9543f74..0000000 --- a/app/swagger/schemas/RepaymentRequest.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "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 deleted file mode 100644 index 06e4666..0000000 --- a/app/swagger/schemas/RepaymentResponse.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "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 diff --git a/app/swagger/schemas/RepaymentsResponse.json b/app/swagger/schemas/RepaymentsResponse.json new file mode 100644 index 0000000..f451483 --- /dev/null +++ b/app/swagger/schemas/RepaymentsResponse.json @@ -0,0 +1,76 @@ +{ + "type": "object", + "properties": { + "repayments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "loan_id": { + "type": "string", + "example": "10" + }, + "customer_id": { + "type": "string", + "example": "CID0000025585" + }, + "product_id": { + "type": "string", + "example": "101" + }, + "transaction_id": { + "type": "string", + "example": "TRX123456", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2025-04-10T16:45:47.879552Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2025-04-10T16:45:47.879642Z" + } + } + } + }, + "count": { + "type": "integer", + "example": 1 + }, + "pagination": { + "type": "object", + "properties": { + "total_count": { + "type": "integer", + "example": 100 + }, + "total_pages": { + "type": "integer", + "example": 5 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "limit": { + "type": "integer", + "example": 20 + }, + "has_next": { + "type": "boolean", + "example": true + }, + "has_prev": { + "type": "boolean", + "example": false + } + } + } + }, + "xml": { + "name": "RepaymentsResponse" + } +} \ No newline at end of file