From 75e9a96ba34bc97b8a67ed2735ff311be0cb567f Mon Sep 17 00:00:00 2001 From: Azeez Muibi Date: Mon, 28 Apr 2025 08:40:14 +0100 Subject: [PATCH] update --- app/api/routes/routes.py | 25 +++- app/api/services/__init__.py | 1 + .../loan_repayment_schedule_service.py | 108 ++++++++++++++++ app/api/services/loan_service.py | 6 + app/models/loan.py | 68 ++++++----- app/models/loan_repayment_schedule.py | 71 +++++++++-- app/swagger/digifi_swagger.json | 14 +++ app/swagger/paths/Loans.json | 32 ++++- app/swagger/paths/RepaymentSchedules.json | 115 ++++++++++++++++++ app/swagger/schemas/LoansResponse.json | 33 ++++- .../schemas/RepaymentSchedulesResponse.json | 100 +++++++++++++++ 11 files changed, 529 insertions(+), 44 deletions(-) create mode 100644 app/api/services/loan_repayment_schedule_service.py create mode 100644 app/swagger/paths/RepaymentSchedules.json create mode 100644 app/swagger/schemas/RepaymentSchedulesResponse.json diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index 86b3b7c..48c6d4a 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -1,6 +1,7 @@ from flask import Blueprint, request, jsonify, send_from_directory from flask import Blueprint, request, jsonify +from app.api.services import LoanRepaymentScheduleService 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 @@ -98,18 +99,19 @@ def get_dashboard(): result = DashboardService.get_dashboard_data() return jsonify(result) - - @api.route('/loans', methods=['GET']) # @token_required def get_loans(): # Extract query parameters for filtering filters = { + 'id': request.args.get('id'), 'customer_id': request.args.get('customer_id'), 'account_id': request.args.get('account_id'), 'status': request.args.get('status'), 'offer_id': request.args.get('offer_id'), 'product_id': request.args.get('product_id'), + 'transaction_id': request.args.get('transaction_id'), + 'original_transaction': request.args.get('original_transaction'), 'start_date': request.args.get('start_date'), 'end_date': request.args.get('end_date'), 'due_before': request.args.get('due_before'), @@ -121,7 +123,6 @@ def get_loans(): response = LoanService.process_request(filters) return response - @api.route('/transactions', methods=['GET']) # @token_required def get_transactions(): @@ -172,4 +173,22 @@ def get_all_loan_charges(): } # logger.info(f"Get loan charges request received with filters: {filters}") response = LoanChargeService.get_all_loan_charges(filters) + return jsonify(response) + +@api.route('/repayment-schedules', methods=['GET']) +# @token_required +def get_all_repayment_schedules(): + # Extract query parameters for filtering + filters = { + 'loan_id': request.args.get('loan_id'), + 'product_id': request.args.get('product_id'), + 'paid': request.args.get('paid'), + 'due_before': request.args.get('due_before'), + 'due_after': request.args.get('due_after'), + 'installment_number': request.args.get('installment_number'), + 'page': request.args.get('page', 1), + 'limit': request.args.get('limit', 20) + } + # logger.info(f"Get repayment schedules request received with filters: {filters}") + response = LoanRepaymentScheduleService.get_all_repayment_schedules(filters) return jsonify(response) \ No newline at end of file diff --git a/app/api/services/__init__.py b/app/api/services/__init__.py index 230ab4e..aed55cc 100644 --- a/app/api/services/__init__.py +++ b/app/api/services/__init__.py @@ -7,3 +7,4 @@ 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 +from app.api.services.loan_repayment_schedule_service import LoanRepaymentScheduleService diff --git a/app/api/services/loan_repayment_schedule_service.py b/app/api/services/loan_repayment_schedule_service.py new file mode 100644 index 0000000..ee995c1 --- /dev/null +++ b/app/api/services/loan_repayment_schedule_service.py @@ -0,0 +1,108 @@ +import logging +from datetime import datetime +from flask import jsonify +from app.models.loan_repayment_schedule import LoanRepaymentSchedule + +# Configure logging +logger = logging.getLogger(__name__) + +class LoanRepaymentScheduleService: + """ + Service class for handling loan repayment schedule-related operations. + """ + + @staticmethod + def get_all_repayment_schedules(filters=None): + """ + Get all loan repayment schedules with optional filtering. + + Args: + filters (dict, optional): Filters for the loan repayment schedules query. + + Returns: + dict: A standardized response with loan repayment schedules data. + """ + try: + if filters is None: + filters = {} + + # Extract filters + loan_id = filters.get('loan_id') + product_id = filters.get('product_id') + paid = filters.get('paid') + due_before = filters.get('due_before') + due_after = filters.get('due_after') + installment_number = filters.get('installment_number') + + # 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 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')) + + # Convert paid string to boolean if provided + if paid is not None and isinstance(paid, str): + paid = paid.lower() == 'true' + + # Get loan repayment schedules with optional filters and pagination + schedules, total_count = LoanRepaymentSchedule.get_all_repayment_schedules( + loan_id=loan_id, + product_id=product_id, + paid=paid, + due_before=due_before, + due_after=due_after, + installment_number=installment_number, + page=page, + limit=limit + ) + + # Convert schedules to dictionary format + schedules_data = [] + for schedule in schedules: + schedules_data.append({ + 'id': schedule.id, + 'loan_id': schedule.loan_id, + 'product_id': schedule.product_id, + 'installment_number': schedule.installment_number, + 'due_date': schedule.due_date.isoformat() if schedule.due_date else None, + 'installment_amount': schedule.installment_amount, + 'total_repayment_amount': schedule.total_repayment_amount, + 'paid': schedule.paid, + 'paid_at': schedule.paid_at.isoformat() if schedule.paid_at else None, + 'created_at': schedule.created_at.isoformat() if schedule.created_at else None, + 'updated_at': schedule.updated_at.isoformat() if schedule.updated_at else None + }) + + # Calculate total pages + total_pages = (total_count + limit - 1) // limit + + response_data = { + 'repayment_schedules': schedules_data, + 'count': len(schedules_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_service.py b/app/api/services/loan_service.py index d77fd34..329657e 100644 --- a/app/api/services/loan_service.py +++ b/app/api/services/loan_service.py @@ -28,11 +28,14 @@ class LoanService: filters = {} # Extract filters + id = filters.get('id') customer_id = filters.get('customer_id') account_id = filters.get('account_id') status = filters.get('status') offer_id = filters.get('offer_id') product_id = filters.get('product_id') + transaction_id = filters.get('transaction_id') + original_transaction = filters.get('original_transaction') start_date = filters.get('start_date') end_date = filters.get('end_date') due_before = filters.get('due_before') @@ -60,11 +63,14 @@ class LoanService: # Get loans with optional filters and pagination loans, total_count = Loan.get_all_loans( + id=id, customer_id=customer_id, account_id=account_id, status=status, offer_id=offer_id, product_id=product_id, + transaction_id=transaction_id, + original_transaction=original_transaction, start_date=start_date, end_date=end_date, due_before=due_before, diff --git a/app/models/loan.py b/app/models/loan.py index 5feb817..2c0c076 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -42,16 +42,15 @@ class Loan(db.Model): back_populates="loans", ) - - @classmethod - def get_all_loans(cls, customer_id=None, account_id=None, status=None, offer_id=None, + def get_all_loans(cls, id=None, customer_id=None, account_id=None, status=None, offer_id=None, product_id=None, start_date=None, end_date=None, due_before=None, due_after=None, - page=1, limit=20): + transaction_id=None, original_transaction=None, page=1, limit=20): """ Get all loans with optional filtering Args: + id (int, optional): Filter by loan ID customer_id (str, optional): Filter by customer ID account_id (str, optional): Filter by account ID status (str, optional): Filter by loan status @@ -61,6 +60,8 @@ class Loan(db.Model): end_date (datetime, optional): Filter by end date (created_at) due_before (datetime, optional): Filter loans due before this date due_after (datetime, optional): Filter loans due after this date + transaction_id (str, optional): Filter by transaction ID + original_transaction (str, optional): Filter by original transaction Returns: list: List of Loan objects @@ -68,6 +69,9 @@ class Loan(db.Model): query = cls.query logger.info(f"Get all loan models from loans model cme back") # Apply filters if provided + if id: + query = query.filter(cls.id == id) + if customer_id: query = query.filter(cls.customer_id == customer_id) @@ -83,6 +87,12 @@ class Loan(db.Model): if product_id: query = query.filter(cls.product_id == product_id) + if transaction_id: + query = query.filter(cls.transaction_id == transaction_id) + + if original_transaction: + query = query.filter(cls.original_transaction == original_transaction) + if start_date: query = query.filter(cls.created_at >= start_date) @@ -106,31 +116,31 @@ class Loan(db.Model): query = query.limit(limit).offset(offset) return query.all(), total_count - # - # def to_dict(self): - # """ - # Convert the Loan object to a dictionary format for JSON serialization. - # """ - # return { - # 'id': self.id, - # 'customer_id': self.customer_id, - # 'account_id': self.account_id, - # 'transaction_id': self.transaction_id, - # 'original_transaction': self.original_transaction, - # 'offer_id': self.offer_id, - # 'initial_loan_amount': self.initial_loan_amount, - # 'current_loan_amount': self.current_loan_amount, - # 'status': self.status, - # 'product_id': self.product_id, - # 'default_penalty_fee': self.default_penalty_fee, - # 'continuous_fee': self.continuous_fee, - # 'upfront_fee': self.upfront_fee, - # 'repayment_amount': self.repayment_amount, - # 'installment_amount': self.installment_amount, - # '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 to_dict(self): + """ + Convert the Loan object to a dictionary format for JSON serialization. + """ + return { + 'id': self.id, + 'customer_id': self.customer_id, + 'account_id': self.account_id, + 'transaction_id': self.transaction_id, + 'original_transaction': self.original_transaction, + 'offer_id': self.offer_id, + 'initial_loan_amount': self.initial_loan_amount, + 'current_loan_amount': self.current_loan_amount, + 'status': self.status, + 'product_id': self.product_id, + 'default_penalty_fee': self.default_penalty_fee, + 'continuous_fee': self.continuous_fee, + 'upfront_fee': self.upfront_fee, + 'repayment_amount': self.repayment_amount, + 'installment_amount': self.installment_amount, + '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/loan_repayment_schedule.py b/app/models/loan_repayment_schedule.py index 64c67cb..f2cad9d 100644 --- a/app/models/loan_repayment_schedule.py +++ b/app/models/loan_repayment_schedule.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from app.extensions import db from sqlalchemy.orm import relationship -from dateutil.relativedelta import relativedelta +# from dateutil.relativedelta import relativedelta class LoanRepaymentSchedule(db.Model): __tablename__ = 'loan_repayment_schedules' @@ -26,20 +26,73 @@ class LoanRepaymentSchedule(db.Model): back_populates="loan_repayment_schedules", ) + @classmethod + def get_all_repayment_schedules(cls, loan_id=None, product_id=None, paid=None, + due_before=None, due_after=None, installment_number=None, + page=1, limit=20): + """ + Get all loan repayment schedules with optional filtering + Args: + loan_id (int, optional): Filter by loan ID + product_id (str, optional): Filter by product ID + paid (bool, optional): Filter by paid status + due_before (datetime, optional): Filter schedules due before this date + due_after (datetime, optional): Filter schedules due after this date + installment_number (int, optional): Filter by installment number + page (int, optional): Page number for pagination + limit (int, optional): Number of items per page + + Returns: + tuple: (list of LoanRepaymentSchedule objects, total count) + """ + query = cls.query + + # Apply filters if provided + if loan_id: + query = query.filter(cls.loan_id == loan_id) + + if product_id: + query = query.filter(cls.product_id == product_id) + + if paid is not None: + query = query.filter(cls.paid == paid) + + if due_before: + query = query.filter(cls.due_date <= due_before) + + if due_after: + query = query.filter(cls.due_date >= due_after) + + if installment_number: + query = query.filter(cls.installment_number == installment_number) + + # Order by due_date and installment_number + query = query.order_by(cls.due_date.asc(), cls.installment_number.asc()) + + # 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): return { 'id': self.id, - 'loanId': self.loan_id, - 'installmentNumber': self.installment_number, - 'dueDate': self.due_date.isoformat(), - 'principalAmount': self.principal_amount, - 'interestAmount': self.interest_amount, - 'totalInstallment': self.total_installment, + 'loan_id': self.loan_id, + 'product_id': self.product_id, + 'installment_number': self.installment_number, + 'due_date': self.due_date.isoformat() if self.due_date else None, + 'installment_amount': self.installment_amount, + 'total_repayment_amount': self.total_repayment_amount, 'paid': self.paid, - 'paidAt': self.paid_at.isoformat() if self.paid_at else None + 'paid_at': self.paid_at.isoformat() if self.paid_at 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'' + return f'' \ No newline at end of file diff --git a/app/swagger/digifi_swagger.json b/app/swagger/digifi_swagger.json index 4d83cdb..4d4081e 100644 --- a/app/swagger/digifi_swagger.json +++ b/app/swagger/digifi_swagger.json @@ -79,6 +79,14 @@ "description": "Find out more", "url": "https://www.simbrellang.net" } + }, + { + "name": "Repayment Schedules", + "description": "Get all loan repayment schedules with optional filtering.", + "externalDocs": { + "description": "Find out more", + "url": "https://www.simbrellang.net" + } } ], "paths": { @@ -102,6 +110,9 @@ }, "/loan-charges": { "$ref": "../swagger/paths/LoanCharges.json" + }, + "/repayment-schedules": { + "$ref": "../swagger/paths/RepaymentSchedules.json" } }, "components": { @@ -135,6 +146,9 @@ }, "LoanChargesResponse": { "$ref": "../swagger/schemas/LoanChargesResponse.json" + }, + "RepaymentSchedulesResponse": { + "$ref": "../swagger/schemas/RepaymentSchedulesResponse.json" } }, "securitySchemes": { diff --git a/app/swagger/paths/Loans.json b/app/swagger/paths/Loans.json index b8f40b6..8491188 100644 --- a/app/swagger/paths/Loans.json +++ b/app/swagger/paths/Loans.json @@ -5,6 +5,16 @@ "description": "Retrieve loans with various filter options including customer ID, account ID, status, etc.", "operationId": "getLoans", "parameters": [ + { + "name": "id", + "in": "query", + "description": "Filter by loan ID", + "required": false, + "schema": { + "type": "integer" + }, + "example": 1234 + }, { "name": "customer_id", "in": "query", @@ -25,6 +35,26 @@ }, "example": "ACC456" }, + { + "name": "transaction_id", + "in": "query", + "description": "Filter by transaction ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "TRX789" + }, + { + "name": "original_transaction", + "in": "query", + "description": "Filter by original transaction", + "required": false, + "schema": { + "type": "string" + }, + "example": "ORIG123" + }, { "name": "status", "in": "query", @@ -144,4 +174,4 @@ } } } -} +} \ No newline at end of file diff --git a/app/swagger/paths/RepaymentSchedules.json b/app/swagger/paths/RepaymentSchedules.json new file mode 100644 index 0000000..1302f2c --- /dev/null +++ b/app/swagger/paths/RepaymentSchedules.json @@ -0,0 +1,115 @@ +{ + "get": { + "tags": ["Repayment Schedules"], + "summary": "Get all loan repayment schedules with optional filtering", + "description": "Retrieve loan repayment schedules with various filter options including loan ID, product ID, paid status, etc.", + "operationId": "getRepaymentSchedules", + "parameters": [ + { + "name": "loan_id", + "in": "query", + "description": "Filter by loan ID", + "required": false, + "schema": { + "type": "integer" + }, + "example": 1234 + }, + { + "name": "product_id", + "in": "query", + "description": "Filter by product ID", + "required": false, + "schema": { + "type": "string" + }, + "example": "101" + }, + { + "name": "paid", + "in": "query", + "description": "Filter by paid status (true/false)", + "required": false, + "schema": { + "type": "boolean" + }, + "example": false + }, + { + "name": "due_before", + "in": "query", + "description": "Filter schedules 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 schedules due after this date (ISO format)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2023-01-01T00:00:00Z" + }, + { + "name": "installment_number", + "in": "query", + "description": "Filter by installment number", + "required": false, + "schema": { + "type": "integer" + }, + "example": 1 + }, + { + "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/RepaymentSchedulesResponse.json" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "500": { + "description": "Internal server error" + } + } + } +} \ No newline at end of file diff --git a/app/swagger/schemas/LoansResponse.json b/app/swagger/schemas/LoansResponse.json index f772828..29e99f1 100644 --- a/app/swagger/schemas/LoansResponse.json +++ b/app/swagger/schemas/LoansResponse.json @@ -18,6 +18,16 @@ "type": "string", "example": "ACC456" }, + "transaction_id": { + "type": "string", + "example": "TRX789", + "nullable": true + }, + "original_transaction": { + "type": "string", + "example": "ORIG123", + "nullable": true + }, "offer_id": { "type": "string", "example": "OFFER789" @@ -50,10 +60,29 @@ "format": "float", "example": 50.0 }, + "upfront_fee": { + "type": "number", + "format": "float", + "example": 100.0, + "nullable": true + }, + "repayment_amount": { + "type": "number", + "format": "float", + "example": 10500.0, + "nullable": true + }, + "installment_amount": { + "type": "number", + "format": "float", + "example": 3500.0, + "nullable": true + }, "due_date": { "type": "string", "format": "date-time", - "example": "2023-12-31T23:59:59Z" + "example": "2023-12-31T23:59:59Z", + "nullable": true }, "created_at": { "type": "string", @@ -105,4 +134,4 @@ "xml": { "name": "LoansResponse" } -} +} \ No newline at end of file diff --git a/app/swagger/schemas/RepaymentSchedulesResponse.json b/app/swagger/schemas/RepaymentSchedulesResponse.json new file mode 100644 index 0000000..6eafcc9 --- /dev/null +++ b/app/swagger/schemas/RepaymentSchedulesResponse.json @@ -0,0 +1,100 @@ +{ + "type": "object", + "properties": { + "repayment_schedules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "loan_id": { + "type": "integer", + "example": 1234 + }, + "product_id": { + "type": "string", + "example": "101" + }, + "installment_number": { + "type": "integer", + "example": 1 + }, + "due_date": { + "type": "string", + "format": "date-time", + "example": "2023-05-15T00:00:00Z" + }, + "installment_amount": { + "type": "number", + "format": "float", + "example": 1000.0 + }, + "total_repayment_amount": { + "type": "number", + "format": "float", + "example": 1050.0 + }, + "paid": { + "type": "boolean", + "example": false + }, + "paid_at": { + "type": "string", + "format": "date-time", + "example": null, + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2023-01-15T10:30:00Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2023-01-15T10:30:00Z" + } + } + } + }, + "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": "RepaymentSchedulesResponse" + } +} \ No newline at end of file