Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08fe04b7b9 | |||
| 0d87036b92 | |||
| 6ef2be9625 | |||
| 48020f5284 | |||
| 1a6ac6a37f | |||
| a2158a768e | |||
| 0af1b7567b | |||
| 4d08983ae3 | |||
| 70e15cd325 | |||
| e8d930f9b8 | |||
| c400f1d69d | |||
| f7daa12531 | |||
| 3242a57586 | |||
| 463c0a0def | |||
| c061c9b5a4 | |||
| 201fa4202e | |||
| bb4d7ac064 | |||
| 5a2161acaa | |||
| 10138f66f3 | |||
| 9ea0027f71 | |||
| d1b8d15f31 | |||
| f716b47603 | |||
| ec5db19e20 | |||
| 729cc26698 | |||
| c95e2786b5 | |||
| 65472d3f07 | |||
| 29b2697b0e | |||
| 7a2ff6586f | |||
| 066ced55b0 | |||
| f6c98d9bfd | |||
| 9e22e0fcf3 | |||
| c9aba07e9c | |||
| 20c9a5c713 | |||
| 1cb0d88cc2 | |||
| 3e9d5d4089 | |||
| 2ae49ace86 | |||
| e9de001340 | |||
| 0bdc11423f | |||
| a4ed936392 | |||
| 1a315b1d80 | |||
| 4c30f81bfd | |||
| 916261fa94 | |||
| 081b73a932 | |||
| 6852986ce5 | |||
| 0038c22577 | |||
| 326ee87b13 | |||
| ca22ee86f7 | |||
| aa033a50a3 | |||
| 31b0367e6a | |||
| 89760f81ed | |||
| 701840abd1 | |||
| df6c42ca2d | |||
| a81313447b | |||
| a321832d43 | |||
| 0369323dd6 | |||
| d7b8addeb6 | |||
| 0db3f44c7b | |||
| a7d465bd5c | |||
| a0ba49f208 | |||
| 1e4f9102c8 | |||
| dee1edee40 | |||
| 06b5f98f06 | |||
| 746ca486da | |||
| 3d81322515 | |||
| eeacffad9a | |||
| 11a239c67a | |||
| 4ce0142ee0 | |||
| c268c4d92b | |||
| 6d743ea09b | |||
| 89dd4bb191 | |||
| 4718c9c50b | |||
| feb97c3fa8 | |||
| 4bcaa3d13d | |||
| b86bd3dece | |||
| a0a2c01a1c | |||
| d6faa14b54 | |||
| 332c344efa | |||
| e377858c47 | |||
| ed64d2c97c | |||
| bbdb7214d1 | |||
| e9c50f75b1 | |||
| c330c3f0e7 | |||
| 976fb14614 | |||
| 334cb0f2d6 | |||
| 40158b1c54 | |||
| b7ae0e6baa | |||
| 89b621b9a8 | |||
| cc3cd5b72b | |||
| f573d5e643 | |||
| 09b57d81a2 |
@@ -3,4 +3,5 @@ from enum import Enum
|
||||
class LoanStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
ACTIVE = "active"
|
||||
START_REPAY = "start_repay"
|
||||
REPAID = "repaid"
|
||||
@@ -1,251 +1,112 @@
|
||||
from flask import jsonify
|
||||
from typing import List, Dict, Union, Optional, Any
|
||||
from typing import Optional, Union, Dict, List, Any
|
||||
|
||||
|
||||
class ResponseHelper:
|
||||
"""
|
||||
A helper class for building standardized JSON responses in Flask.
|
||||
A helper class for building standardized JSON responses using resultCode and resultDescription.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_response(
|
||||
status: bool,
|
||||
message: str,
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
status_code: int = 200,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_code: str,
|
||||
result_description: str,
|
||||
data: Optional[Union[Dict, List, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build a standardized JSON response.
|
||||
|
||||
Args:
|
||||
status (bool): Indicates whether the request was successful.
|
||||
message (str): A message describing the result of the request.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
status_code (int): The HTTP status code for the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
response = {
|
||||
"status": status,
|
||||
"statusCode": status_code,
|
||||
"message": message,
|
||||
"data": data if data is not None else {},
|
||||
"error": error if error is not None else {},
|
||||
"resultCode": result_code,
|
||||
"resultDescription": result_description
|
||||
}
|
||||
return jsonify(response), status_code
|
||||
|
||||
if isinstance(data, dict):
|
||||
response.update(data)
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
@staticmethod
|
||||
def success(
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
message: str = "Successful",
|
||||
status_code: int = 200,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Successful",
|
||||
result_code: str = "00",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a success response.
|
||||
|
||||
Args:
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
message (str): A message describing the result of the request.
|
||||
status_code (int): The HTTP status code for the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(True, message, data, status_code, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def error(
|
||||
message: str = "An error occurred",
|
||||
status_code: int = 400,
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "An error occurred",
|
||||
result_code: str = "01",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return an error response.
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
status_code (int): The HTTP status code for the response.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, status_code, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def created(
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
message: str = "Resource created successfully",
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Resource created successfully",
|
||||
result_code: str = "00",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for a created resource.
|
||||
|
||||
Args:
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
message (str): A message describing the result of the request.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(True, message, data, 201, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def updated(
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
message: str = "Resource updated successfully",
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Resource updated successfully",
|
||||
result_code: str = "00",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for an updated resource.
|
||||
|
||||
Args:
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
message (str): A message describing the result of the request.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(True, message, data, 200, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def internal_server_error(
|
||||
message: str = "Internal Server Error",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Internal Server Error",
|
||||
result_code: str = "500",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for an internal server error.
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 500, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def unauthorized(
|
||||
message: str = "Unauthorized",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Unauthorized",
|
||||
result_code: str = "401",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for an unauthorized request.
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 401, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def forbidden(
|
||||
message: str = "Forbidden",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Forbidden",
|
||||
result_code: str = "403",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for a forbidden request.
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 403, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def not_found(
|
||||
message: str = "Resource not found",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Resource not found",
|
||||
result_code: str = "404",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for a not found resource.
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 404, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
@staticmethod
|
||||
def unprocessable_entity(
|
||||
message: str = "Unprocessable entity",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Unprocessable entity",
|
||||
result_code: str = "422",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for an unprocessable entity.
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 422, error)
|
||||
|
||||
@staticmethod
|
||||
def method_not_allowed(
|
||||
message: str = "Method Not Allowed",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Method Not Allowed",
|
||||
result_code: str = "405",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for a method not allowed error.
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 405, error)
|
||||
|
||||
@staticmethod
|
||||
def bad_request(
|
||||
message: str = "Bad Request",
|
||||
data: Optional[Union[Dict, List, str]] = None,
|
||||
error: Optional[Union[Dict, str]] = None,
|
||||
result_description: str = "Bad Request",
|
||||
result_code: str = "400",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a response for a bad request error.
|
||||
|
||||
Args:
|
||||
message (str): A message describing the error.
|
||||
data (Optional[Union[Dict, List, str]]): The data to return in the response.
|
||||
error (Optional[Union[Dict, str]]): Any error details to include in the response.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary representing the JSON response.
|
||||
"""
|
||||
return ResponseHelper.build_response(False, message, data, 400, error)
|
||||
return ResponseHelper.build_response(result_code, result_description, data)
|
||||
@@ -7,36 +7,25 @@ import logging
|
||||
|
||||
class SimbrellaIntegration:
|
||||
BASE_URL = settings.SIMBRELLA_BASE_URL
|
||||
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
|
||||
|
||||
@staticmethod
|
||||
def rac_check(customer_id, account_id, transaction_id):
|
||||
"""
|
||||
Calls the RACCheck endpoit
|
||||
"""
|
||||
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
|
||||
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
|
||||
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
|
||||
|
||||
payload = {
|
||||
"customerId": customer_id,
|
||||
"accountId": account_id,
|
||||
"transactionId": str(transaction_id),
|
||||
"fbnTransactionId": f"FBN{transaction_id}",
|
||||
"RAC_Array": [
|
||||
"SalaryAccount",
|
||||
"BVN",
|
||||
"BVNAttachedtoAccount",
|
||||
"CRC",
|
||||
"CRMS",
|
||||
"AccountStatus",
|
||||
"Lien",
|
||||
"NoBouncedCheck",
|
||||
"Whitelist",
|
||||
"NoPastDueSalaryLoan",
|
||||
"NoPastDueOtherLoan",
|
||||
],
|
||||
"fbnTransactionId": str(transaction_id),
|
||||
"countryCode": "NG",
|
||||
"channel": "USSD"
|
||||
}
|
||||
|
||||
logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": f"{settings.VALID_API_KEY}",
|
||||
|
||||
@@ -88,6 +88,7 @@ def loan_status():
|
||||
@jwt_required()
|
||||
def repayment():
|
||||
data = request.get_json()
|
||||
logger.error(f"HERE 0000a **** ")
|
||||
# logger.info(f"Repayment request received: {data}")
|
||||
response = RepaymentService.process_request(data)
|
||||
return response
|
||||
|
||||
@@ -5,8 +5,8 @@ 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)
|
||||
loanRef = fields.Str(required=True)
|
||||
initiatedBy = fields.Str(required=False)
|
||||
|
||||
@@ -34,7 +34,7 @@ class AuthorizationService(BaseService):
|
||||
logger.info("Processing Authorization request")
|
||||
|
||||
if not data:
|
||||
return ResponseHelper.bad_request(message="Missing JSON in request")
|
||||
return ResponseHelper.bad_request(result_description="Missing JSON in request")
|
||||
|
||||
# Validate input data using the Authorization schema
|
||||
schema = AuthorizeRequestSchema()
|
||||
@@ -44,7 +44,7 @@ class AuthorizationService(BaseService):
|
||||
validated_data["username"] != USERNAME
|
||||
or validated_data["password"] != PASSWORD
|
||||
):
|
||||
return ResponseHelper.unauthorized(message="Invalid credentials")
|
||||
return ResponseHelper.unauthorized(result_description="Invalid credentials")
|
||||
|
||||
access_token = create_access_token(identity=validated_data["username"])
|
||||
refresh_token = create_refresh_token(identity=validated_data["username"])
|
||||
@@ -56,17 +56,17 @@ class AuthorizationService(BaseService):
|
||||
}
|
||||
|
||||
return ResponseHelper.success(
|
||||
data=response_data, message="Authorization processed successfully"
|
||||
data={"data": response_data}, result_description="Authorization processed successfully"
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
return ResponseHelper.bad_request(message=f"Validation error: {e}")
|
||||
return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Authorization request: {e}")
|
||||
return ResponseHelper.internal_server_error(
|
||||
message=f"Error processing Authorization request: {e}"
|
||||
result_description=f"Error processing Authorization request: {e}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -92,11 +92,11 @@ class AuthorizationService(BaseService):
|
||||
}
|
||||
|
||||
return ResponseHelper.success(
|
||||
data=response_data, message="RefreshToken processed successfully"
|
||||
data={"data": response_data}, result_description="RefreshToken processed successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing RefreshToken request: {e}")
|
||||
return ResponseHelper.internal_server_error(
|
||||
message=f"Error processing RefreshToken request: {e}"
|
||||
result_description=f"Error processing RefreshToken request: {e}"
|
||||
)
|
||||
|
||||
@@ -48,12 +48,14 @@ class BaseService:
|
||||
"""
|
||||
Create a new transaction.
|
||||
"""
|
||||
channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel")
|
||||
|
||||
return Transaction.create_transaction(
|
||||
transaction_id = validated_data.get("transactionId"),
|
||||
customer_id = validated_data.get('customerId', None),
|
||||
account_id = validated_data.get("accountId", None),
|
||||
type = cls.TRANSACTION_TYPE,
|
||||
channel = validated_data.get("channel"),
|
||||
channel = channel,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import request, jsonify
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
from app.api.services.base_service import BaseService
|
||||
from marshmallow import ValidationError
|
||||
from app.utils.logger import logger
|
||||
@@ -34,44 +35,26 @@ class CustomerConsentService(BaseService):
|
||||
|
||||
if not transaction:
|
||||
logger.error(f"Failed to log transaction")
|
||||
return jsonify({
|
||||
"message": "Failed to log transaction."
|
||||
}), 400
|
||||
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||
else:
|
||||
return jsonify({
|
||||
"message": "Invalid Customer or Account"
|
||||
}), 400
|
||||
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||
|
||||
|
||||
# Simulated processing logic
|
||||
response_data = {
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Request is received"
|
||||
}
|
||||
|
||||
db.session.commit()
|
||||
return response_data
|
||||
return ResponseHelper.success(result_description="Request is received")
|
||||
|
||||
except ValidationError as err:
|
||||
|
||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||
db.session.rollback()
|
||||
|
||||
return jsonify({
|
||||
"message": "Validation exception"
|
||||
}) , 422
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
db.session.rollback()
|
||||
|
||||
return jsonify({
|
||||
"message": str(err)
|
||||
}) , 400
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
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
|
||||
return ResponseHelper.internal_server_error()
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import session, jsonify
|
||||
from app.models.loan import Loan
|
||||
from app.models.transaction_offers import TransactionOffer
|
||||
from app.utils.logger import logger
|
||||
from app.api.services.base_service import BaseService
|
||||
@@ -8,6 +9,9 @@ from app.api.enums import TransactionType
|
||||
from app.api.integrations import SimbrellaIntegration
|
||||
from app.extensions import db
|
||||
from app.models import Offer, RACCheck
|
||||
from app.api.services.offer_analysis import OfferAnalysis
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
|
||||
import random
|
||||
|
||||
|
||||
@@ -42,16 +46,17 @@ class EligibilityCheckService(BaseService):
|
||||
|
||||
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
|
||||
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||
else:
|
||||
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||
|
||||
db.session.flush()
|
||||
|
||||
# Determine Loan count
|
||||
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
|
||||
|
||||
if not is_eligible:
|
||||
return ResponseHelper.error(result_description="Max loan count reached")
|
||||
|
||||
# Call RACCheck
|
||||
response = SimbrellaIntegration.rac_check(
|
||||
@@ -60,57 +65,81 @@ class EligibilityCheckService(BaseService):
|
||||
transaction_id = transaction.transaction_id,
|
||||
)
|
||||
|
||||
# this chck for error is not valid
|
||||
# this chek for error is not valid
|
||||
if response.status_code != 200:
|
||||
return jsonify({"message": "RACCheck failed"}), 400
|
||||
return ResponseHelper.error(result_description="RACCheck failed")
|
||||
|
||||
response = response.json()
|
||||
|
||||
logger.info(f"This is Response (from Eligibility Check): {str(response)}", exc_info=True)
|
||||
|
||||
|
||||
if not response or response['responseCode'] != '00':
|
||||
|
||||
if response:
|
||||
logger.error(f"{response['responseMessage']}")
|
||||
|
||||
return ResponseHelper.error(result_description=f"RACCheck failed")
|
||||
|
||||
rack_checks_response = response['data']['racResponse']
|
||||
|
||||
rac_check = RACCheck.add_rac_check(
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
transaction_id = transaction.transaction_id,
|
||||
data = response['RACResponse']
|
||||
data = rack_checks_response
|
||||
)
|
||||
|
||||
|
||||
if not rac_check:
|
||||
logger.error(f"Failed to save RACCheck")
|
||||
return jsonify({
|
||||
"message": "Failed to save RACCheck."
|
||||
}), 400
|
||||
logger.error(f"Failed to save RACCheck")
|
||||
return ResponseHelper.error(result_description="Failed to save RACCheck.")
|
||||
|
||||
offers = Offer.get_all_offers()
|
||||
|
||||
eligible_offers = []
|
||||
|
||||
for offer in offers:
|
||||
# Determine an approved amount
|
||||
random_float = random.random() # temporary to play data
|
||||
approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
|
||||
approved_amount = round(approved_amount, 2)
|
||||
|
||||
transaction_offer = TransactionOffer.create_transaction_offer(
|
||||
customer_id = customer.id,
|
||||
transaction_id = transaction.transaction_id,
|
||||
offer_id = offer.id,
|
||||
min_amount = offer.min_amount,
|
||||
max_amount = offer.max_amount,
|
||||
eligible_amount = approved_amount,
|
||||
product_id = offer.product_id,
|
||||
tenor = offer.tenor
|
||||
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
|
||||
# eligible_offers = []
|
||||
try:
|
||||
eligible_offers = OfferAnalysis.decide_offer(
|
||||
transaction_id=transactionId,
|
||||
rac_check=rac_check,
|
||||
validated_data=validated_data,
|
||||
customer_id=customer_id,
|
||||
rack_checks_response =rack_checks_response
|
||||
)
|
||||
except ValueError as ve:
|
||||
logger.error(str(ve))
|
||||
return ResponseHelper.error(result_description= str(ve))
|
||||
# -----------------------------------------------------------------------
|
||||
# s = Offer.get_all_offers()
|
||||
|
||||
# Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||
padded_id = str(transaction_offer.id).zfill(6)
|
||||
public_offer_id = f"{offer.id}{padded_id}"
|
||||
# eligible_offers = []
|
||||
|
||||
eligible_offers.append({
|
||||
"offerId": public_offer_id,
|
||||
"product_id": offer.product_id,
|
||||
"min_amount": offer.min_amount,
|
||||
"max_amount": approved_amount,
|
||||
"tenor": offer.tenor
|
||||
})
|
||||
# for offer in offers:
|
||||
# # Determine an approved amount
|
||||
# random_float = random.random() # temporary to play data
|
||||
# approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
|
||||
# approved_amount = round(approved_amount, 2)
|
||||
#
|
||||
# transaction_offer = TransactionOffer.create_transaction_offer(
|
||||
# customer_id = customer.id,
|
||||
# transaction_id = transaction.transaction_id,
|
||||
# offer_id = offer.id,
|
||||
# min_amount = offer.min_amount,
|
||||
# max_amount = offer.max_amount,
|
||||
# eligible_amount = approved_amount,
|
||||
# product_id = offer.product_id,
|
||||
# tenor = offer.tenor
|
||||
# )
|
||||
#
|
||||
# # Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||
# padded_id = str(transaction_offer.id).zfill(6)
|
||||
# public_offer_id = f"{offer.id}{padded_id}"
|
||||
#
|
||||
# eligible_offers.append({
|
||||
# "offerId": public_offer_id,
|
||||
# "product_id": offer.product_id,
|
||||
# "min_amount": offer.min_amount,
|
||||
# "max_amount": approved_amount,
|
||||
# "tenor": offer.tenor
|
||||
# })
|
||||
|
||||
# Simulate processing
|
||||
response_data = {
|
||||
@@ -119,30 +148,52 @@ class EligibilityCheckService(BaseService):
|
||||
"countryCode": "NG",
|
||||
"msisdn": msisdn,
|
||||
"eligibleOffers": eligible_offers,
|
||||
"resultDescription": "Successful",
|
||||
"resultCode": "00",
|
||||
"accountId": account_id
|
||||
}
|
||||
|
||||
return response_data
|
||||
return ResponseHelper.success(data=response_data)
|
||||
|
||||
except ValidationError as err:
|
||||
|
||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||
|
||||
return jsonify({
|
||||
"message": "Validation exception"
|
||||
}) , 422
|
||||
db.session.rollback()
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
|
||||
return jsonify({
|
||||
"message": str(err)
|
||||
}) , 400
|
||||
db.session.rollback()
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
"message": "Internal Server Error"
|
||||
}) , 500
|
||||
db.session.rollback()
|
||||
return ResponseHelper.internal_server_error()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def check_loan_limits(customer_id):
|
||||
"""
|
||||
Checks if a customer has exceeded the loan limits for given offer.
|
||||
"""
|
||||
loan = Loan.get_customer_last_loan(customer_id)
|
||||
|
||||
if not loan:
|
||||
return True
|
||||
|
||||
offer_id = loan.offer_id[:5]
|
||||
|
||||
offer = Offer.get_offer_by_id(offer_id)
|
||||
if not offer:
|
||||
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
|
||||
return False
|
||||
|
||||
daily_count = TransactionOffer.get_daily_loan_count(customer_id, offer_id)
|
||||
|
||||
|
||||
logger.error(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
|
||||
|
||||
if offer.max_daily_loans is not None and daily_count >= offer.max_daily_loans:
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,7 +6,8 @@ from app.utils.logger import logger
|
||||
from app.api.schemas.loan_status import LoanStatusSchema
|
||||
from app.api.services.base_service import BaseService
|
||||
from app.api.enums import TransactionType
|
||||
from app.extensions import db
|
||||
from app.extensions import db
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
|
||||
|
||||
class LoanStatusService(BaseService):
|
||||
@@ -28,42 +29,23 @@ class LoanStatusService(BaseService):
|
||||
# Validate data
|
||||
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
|
||||
|
||||
|
||||
customer_id = validated_data.get('customerId')
|
||||
customer = Customer.get_customer(customer_id)
|
||||
|
||||
logger.info(f"Looking for customer *** {customer_id}")
|
||||
customer = Customer.get_customer_with_loan_list(customer_id)
|
||||
|
||||
transactionId = validated_data.get('transactionId')
|
||||
account_id = validated_data.get('accountId')
|
||||
|
||||
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||
|
||||
# Get loans
|
||||
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
|
||||
|
||||
transaction = LoanStatusService.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
|
||||
|
||||
|
||||
# loans = [
|
||||
# {
|
||||
# "debtId": "123456789",
|
||||
# "loanDate": "2019-10-18 14:26:21.063",
|
||||
# "dueDate": "2019-11-20 14:26:21.063",
|
||||
# "currentLoanAmount": 8500,
|
||||
# "initialLoanAmount": 10000,
|
||||
# "defaultPenaltyFee": 0,
|
||||
# "continuousFee": 0,
|
||||
# "productId": "101"
|
||||
# }
|
||||
# ]
|
||||
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||
else:
|
||||
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||
|
||||
total_debt_amount = sum(
|
||||
loan.get("currentLoanAmount") or 0
|
||||
@@ -73,36 +55,27 @@ class LoanStatusService(BaseService):
|
||||
# Simulated processing logic
|
||||
response_data = {
|
||||
"customerId": customer_id,
|
||||
"accountId": account_id,
|
||||
"transactionId": transactionId,
|
||||
"loans": loans,
|
||||
"totalDebtAmount": total_debt_amount,
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Successful"
|
||||
}
|
||||
|
||||
db.session.commit()
|
||||
return response_data
|
||||
return ResponseHelper.success(data=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
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
db.session.rollback()
|
||||
|
||||
return jsonify({
|
||||
"message": str(err)
|
||||
}) , 400
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
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
|
||||
return ResponseHelper.internal_server_error()
|
||||
@@ -5,6 +5,7 @@ from app.api.enums import TransactionType
|
||||
from app.utils.logger import logger
|
||||
from app.api.schemas.notification_callback import NotificationCallbackSchema
|
||||
from app.extensions import db
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
|
||||
class NotificationCallbackService(BaseService):
|
||||
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
||||
@@ -27,37 +28,20 @@ class NotificationCallbackService(BaseService):
|
||||
schema = NotificationCallbackSchema()
|
||||
validated_data = schema.load(data) # Raises ValidationError if invalid
|
||||
|
||||
# Simulated processing logic
|
||||
response_data = {
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Successful"
|
||||
}
|
||||
|
||||
|
||||
# return ResponseHelper.success(
|
||||
# data=response_data,
|
||||
# message="Notification callback processed successfully"
|
||||
# )
|
||||
|
||||
return response_data
|
||||
return ResponseHelper.success()
|
||||
|
||||
except ValidationError as err:
|
||||
|
||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||
|
||||
return jsonify({
|
||||
"message": "Validation exception"
|
||||
}) , 422
|
||||
db.session.rollback()
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
|
||||
return jsonify({
|
||||
"message": str(err)
|
||||
}) , 400
|
||||
db.session.rollback()
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
"message": "Internal Server Error"
|
||||
}) , 500
|
||||
db.session.rollback()
|
||||
return ResponseHelper.internal_server_error()
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
from decimal import Decimal
|
||||
from app.models import Offer, TransactionOffer
|
||||
from app.models.loan import Loan
|
||||
|
||||
import random
|
||||
import logging
|
||||
|
||||
from app.config import Config
|
||||
|
||||
RAC_TRUE_CHECK_RULES = Config.rac_true_rules
|
||||
RAC_FALSE_CHECK_RULES = Config.rac_false_rules
|
||||
RAC_SALARY_PAYMENTS = Config.rac_salary_payments
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OfferAnalysis:
|
||||
@@ -30,8 +38,213 @@ class OfferAnalysis:
|
||||
if not offer:
|
||||
raise ValueError("Invalid Offer.")
|
||||
original_transaction = transaction_id
|
||||
# we can now find the origin transactions
|
||||
customer_loan = Loan.get_customer_current_active_loan(customer_id)
|
||||
|
||||
|
||||
return transaction_offer, offer, eligible_amount, original_transaction
|
||||
@staticmethod
|
||||
def _analyze_rack_checks(rack_response, offer):
|
||||
logger.info(f"This is PayLoad for ANALYSYS ***** : {str(rack_response)}", exc_info=True)
|
||||
logger.info(f"RACk TRUE RULES {str(RAC_TRUE_CHECK_RULES)}", exc_info=True)
|
||||
logger.info(f"RACk FALSE RULES {str(RAC_FALSE_CHECK_RULES)}", exc_info=True)
|
||||
logger.info(f"RACk SALARY PAYMENTS {str(RAC_SALARY_PAYMENTS)}", exc_info=True)
|
||||
|
||||
if not isinstance(rack_response, dict) or not offer :
|
||||
raise ValueError("Invalid RAC response format.")
|
||||
|
||||
|
||||
|
||||
failed_true_rules = []
|
||||
failed_false_rules = []
|
||||
salaries = []
|
||||
|
||||
# Expects true
|
||||
for rule in RAC_TRUE_CHECK_RULES:
|
||||
if not rack_response.get(rule, False):
|
||||
failed_true_rules.append(rule)
|
||||
|
||||
# Expects false
|
||||
for rule in RAC_FALSE_CHECK_RULES:
|
||||
if rack_response.get(rule, True):
|
||||
failed_false_rules.append(rule)
|
||||
|
||||
|
||||
# Salary rules
|
||||
for key in RAC_SALARY_PAYMENTS:
|
||||
value = rack_response.get(key)
|
||||
|
||||
|
||||
if isinstance(value, Decimal):
|
||||
# Only use values greater than 0
|
||||
if value > 0:
|
||||
salaries.append(value)
|
||||
elif isinstance(value, (int, float, str)):
|
||||
try:
|
||||
value = Decimal(str(value))
|
||||
if value > 0:
|
||||
salaries.append(value)
|
||||
except:
|
||||
logger.warning(f"Could not convert value of {key} to Decimal: {value}")
|
||||
|
||||
|
||||
if failed_true_rules or failed_false_rules or not salaries:
|
||||
logger.warning(f"Failed TRUE rules: {failed_true_rules}")
|
||||
logger.warning(f"Failed FALSE rules: {failed_false_rules}")
|
||||
logger.warning("No salary records found in RAC response.")
|
||||
raise ValueError(f"RAC analysis failed")
|
||||
|
||||
|
||||
|
||||
logger.info(f"These are the salarie amounts ***** : {str(salaries)}", exc_info=True)
|
||||
|
||||
#Least salary in the last 6 months
|
||||
min_salary = min(salaries)
|
||||
|
||||
# Check consistency rule
|
||||
consistent_income = rack_response.get("rule7_consistent_salary_amount", False)
|
||||
|
||||
# Determine percentage based on offer tenor
|
||||
tenor = offer.tenor
|
||||
|
||||
if tenor == 30 and consistent_income:
|
||||
eligible_amount = min_salary * Decimal("0.5")
|
||||
logger.info("Applying 50% of least salary in 6 months due to 1-month offer tenor with stable income.")
|
||||
elif tenor == 90 and consistent_income:
|
||||
eligible_amount = min_salary * Decimal("0.75")
|
||||
logger.info("Applying 75% of least salary in 6 months due to 3-months offer tenor with stable income.")
|
||||
|
||||
else: # Income is not consistent
|
||||
eligible_amount = 0
|
||||
logger.info("Applying np percentage on least salary due unstable income.")
|
||||
|
||||
|
||||
|
||||
logger.info(f"Calculated eligible amount from RAC: {eligible_amount} based on {'stable' if consistent_income else 'unstable'} income.")
|
||||
|
||||
return eligible_amount.quantize(Decimal("1.00"))
|
||||
|
||||
# "racResponse": {
|
||||
# "accountStatus": true,
|
||||
# "bvnValidated": true,
|
||||
# "creditBureauCheck": false,
|
||||
# "crmsCheck": true,
|
||||
# "hasLien": false,
|
||||
# "hasPastDueLoan": false,
|
||||
# "hasSalaryAccount": true,
|
||||
# "isWhitelisted": true,
|
||||
# "noBouncedCheck": true
|
||||
# },
|
||||
#
|
||||
|
||||
'''
|
||||
30 days
|
||||
Eligibility amount (monthly SOL) - Adoption of 50% of the least salary inflow in the past 6 months
|
||||
to determine loan eligibility for potential customers.
|
||||
|
||||
3 months
|
||||
Adoption of 75% of the least salary inflow in the past 6 months to determine loan eligibility for
|
||||
potential customers" for customers that have unstable income. 3 months
|
||||
'''
|
||||
# rac_true_rules
|
||||
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response):
|
||||
eligible_offers = []
|
||||
# if we have active offers - we have to feed off it
|
||||
logger.info(f"**RACK ANALYSIS** {customer_id}")
|
||||
# Analyze Rack Checks
|
||||
# new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response) #--> We need detail analysis
|
||||
|
||||
# we can now find the origin transactions
|
||||
# Find the last loan - it will have original_transaction
|
||||
last_customer_loan = Loan.get_customer_last_loan(customer_id)
|
||||
# logger.info(f"{last_customer_loan}")
|
||||
|
||||
if last_customer_loan:
|
||||
original_transaction = last_customer_loan.original_transaction or last_customer_loan.transaction_id
|
||||
logger.info(f"transaction_id |-| original_transaction === > {transaction_id} {original_transaction}")
|
||||
original_loan = Loan.get_customer_original_loan(customer_id, original_transaction)
|
||||
if original_loan is not None:
|
||||
logger.info(f"original_loan === > {original_loan}")
|
||||
logger.info(f"loan_offer_id === > {original_loan.offer_id}")
|
||||
|
||||
original_offer_id = str(original_loan.offer_id[:5]) # The last part is str
|
||||
transaction_offer_id = int(original_loan.offer_id[5:]) # The last part is int
|
||||
original_transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, original_loan.product_id)
|
||||
|
||||
active_loans = Loan.get_active_loans_by_original_transaction(original_transaction)
|
||||
sum_active_loans = sum(loan.current_loan_amount for loan in active_loans)
|
||||
logger.info(f"sum_active_loans === > {sum_active_loans}")
|
||||
real_eligible_amount = original_loan.eligible_amount - sum_active_loans
|
||||
|
||||
if real_eligible_amount < original_transaction_offer.min_amount:
|
||||
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
|
||||
raise ValueError("You are not eligible for a loan at this time.")
|
||||
|
||||
transaction_offer = TransactionOffer.create_transaction_offer(
|
||||
customer_id=customer_id,
|
||||
transaction_id=transaction_id,
|
||||
original_transaction=original_transaction,
|
||||
offer_id=original_offer_id,
|
||||
min_amount=original_transaction_offer.min_amount,
|
||||
max_amount=original_transaction_offer.max_amount,
|
||||
eligible_amount=real_eligible_amount,
|
||||
product_id=original_loan.product_id,
|
||||
tenor=original_loan.tenor
|
||||
)
|
||||
|
||||
# Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||
padded_id = str(transaction_offer.id).zfill(6)
|
||||
public_offer_id = f"{original_offer_id}{padded_id}"
|
||||
|
||||
eligible_offers.append({
|
||||
"offerId": public_offer_id,
|
||||
"product_id": original_transaction_offer.product_id,
|
||||
"min_amount": original_transaction_offer.min_amount,
|
||||
"max_amount": round(real_eligible_amount, 2),
|
||||
"tenor": original_loan.tenor
|
||||
})
|
||||
return eligible_offers
|
||||
|
||||
|
||||
offers = Offer.get_all_offers()
|
||||
|
||||
|
||||
for offer in offers:
|
||||
|
||||
new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response, offer)
|
||||
|
||||
|
||||
approved_amount = new_eligible_amount
|
||||
approved_amount = round(approved_amount, 2)
|
||||
|
||||
if approved_amount < offer.min_amount:
|
||||
logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
|
||||
raise ValueError("You are not eligible for a loan at this time.")
|
||||
|
||||
|
||||
transaction_offer = TransactionOffer.create_transaction_offer(
|
||||
customer_id=customer_id,
|
||||
transaction_id=transaction_id,
|
||||
original_transaction=transaction_id,
|
||||
offer_id=offer.id,
|
||||
min_amount=offer.min_amount,
|
||||
max_amount=offer.max_amount,
|
||||
eligible_amount=approved_amount,
|
||||
product_id=offer.product_id,
|
||||
tenor=offer.tenor
|
||||
)
|
||||
|
||||
# Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||
padded_id = str(transaction_offer.id).zfill(6)
|
||||
public_offer_id = f"{offer.id}{padded_id}"
|
||||
|
||||
eligible_offers.append({
|
||||
"offerId": public_offer_id,
|
||||
"product_id": offer.product_id,
|
||||
"min_amount": offer.min_amount,
|
||||
"max_amount": approved_amount,
|
||||
"tenor": offer.tenor
|
||||
})
|
||||
|
||||
return eligible_offers
|
||||
@@ -15,6 +15,7 @@ from datetime import datetime, timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from app.models import LoanRepaymentSchedule
|
||||
from app.api.services.offer_analysis import OfferAnalysis
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
|
||||
class ProvideLoanService(BaseService):
|
||||
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
||||
@@ -42,6 +43,7 @@ class ProvideLoanService(BaseService):
|
||||
offer_id = validated_data.get('offerId')
|
||||
amount = validated_data.get("requestedAmount")
|
||||
product_id = validated_data.get("productId")
|
||||
channel = validated_data.get('channel')
|
||||
|
||||
customer = Customer.is_valid_customer(customer_id)
|
||||
|
||||
@@ -57,9 +59,14 @@ class ProvideLoanService(BaseService):
|
||||
)
|
||||
except ValueError as ve:
|
||||
logger.error(str(ve))
|
||||
return jsonify({
|
||||
"message": str(ve)
|
||||
}), 400
|
||||
return ResponseHelper.error(result_description=str(ve))
|
||||
|
||||
|
||||
if(amount < transaction_offer.min_amount):
|
||||
return ResponseHelper.error(result_description="The amount is less than the minimum allowed transaction amount.")
|
||||
elif amount > transaction_offer.max_amount:
|
||||
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed transaction amount.")
|
||||
|
||||
|
||||
# transaction_offer_id = int(offer_id[5:]) # The last part is int
|
||||
|
||||
@@ -86,9 +93,7 @@ class ProvideLoanService(BaseService):
|
||||
|
||||
if not transaction:
|
||||
logger.error(f"Failed to log transaction")
|
||||
return jsonify({
|
||||
"message": "Failed to log transaction."
|
||||
}), 400
|
||||
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||
|
||||
|
||||
db.session.flush()
|
||||
@@ -107,6 +112,8 @@ class ProvideLoanService(BaseService):
|
||||
insurance = charges["insurance"]
|
||||
vat = charges["vat"]
|
||||
|
||||
padded_id = str(transaction_id).zfill(12)
|
||||
loan_ref = f"{padded_id}{channel}{offer.product_id}"
|
||||
|
||||
|
||||
# Save the loan details
|
||||
@@ -117,7 +124,7 @@ class ProvideLoanService(BaseService):
|
||||
product_id = offer.product_id,
|
||||
collection_type = collection_type,
|
||||
transaction_id = validated_data.get('transactionId'),
|
||||
original_transaction = validated_data.get('transactionId'),
|
||||
original_transaction = transaction_offer.original_transaction,
|
||||
initial_loan_amount = validated_data.get('requestedAmount'),
|
||||
upfront_fee = upfront_fee,
|
||||
repayment_amount = repayment_amount,
|
||||
@@ -125,29 +132,26 @@ class ProvideLoanService(BaseService):
|
||||
eligible_amount=eligible_amount,
|
||||
status = LoanStatus.ACTIVE,
|
||||
tenor = offer.tenor,
|
||||
|
||||
reference = loan_ref
|
||||
)
|
||||
|
||||
if not loan:
|
||||
logger.error(f"Failed to save loan details")
|
||||
return jsonify({
|
||||
"message": "Failed to save loan details."
|
||||
}), 400
|
||||
|
||||
return ResponseHelper.error(result_description="Failed to save loan details.")
|
||||
|
||||
db.session.flush()
|
||||
|
||||
current_product_id = offer.product_id
|
||||
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
|
||||
|
||||
|
||||
if not schedule:
|
||||
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
||||
return jsonify({
|
||||
"message": "Failed to generate loan repayment schedule."
|
||||
}), 400
|
||||
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
|
||||
|
||||
# charges = Charge.get_offer_charges(offer.id)
|
||||
|
||||
logger.info(f"{charges}")
|
||||
# logger.info(f"{charges}")
|
||||
|
||||
loan_id = loan.id
|
||||
|
||||
@@ -156,50 +160,39 @@ class ProvideLoanService(BaseService):
|
||||
|
||||
|
||||
else:
|
||||
return jsonify({
|
||||
"message": "Invalid Customer or Account"
|
||||
}), 400
|
||||
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||
|
||||
|
||||
|
||||
response_data = {
|
||||
"requestId": request_id,
|
||||
"transactionId": transaction_id,
|
||||
"loanRef": loan_ref,
|
||||
"customerId": customer_id,
|
||||
"accountId": account_id,
|
||||
"msisdn": customer.msisdn,
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Successful"
|
||||
"msisdn": customer.msisdn
|
||||
}
|
||||
|
||||
|
||||
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
|
||||
# Call Kafka in a background thread
|
||||
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
|
||||
thread.start()
|
||||
|
||||
db.session.commit()
|
||||
return response_data
|
||||
return ResponseHelper.success(data=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
|
||||
db.session.rollback()
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
db.session.rollback()
|
||||
|
||||
return jsonify({
|
||||
"message": str(err)
|
||||
}) , 400
|
||||
db.session.rollback()
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
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
|
||||
db.session.rollback()
|
||||
return ResponseHelper.internal_server_error()
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import request, jsonify
|
||||
from marshmallow import ValidationError
|
||||
from app.api.enums.loan_status import LoanStatus
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
from app.models import Repayment
|
||||
from app.models.customer import Customer
|
||||
from app.models.loan import Loan
|
||||
@@ -28,91 +29,71 @@ class RepaymentService(BaseService):
|
||||
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)
|
||||
loan_ref = validated_data.get('loanRef')
|
||||
# customer = Customer.get_customer_with_loan_list(customer_id)
|
||||
transaction_id = validated_data.get('transactionId')
|
||||
|
||||
initiated_by = validated_data.get('initiatedBy')
|
||||
logger.error(f"HERE 0002a **** ")
|
||||
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||
logger.error(f"HERE 0001a **** ")
|
||||
# Check loan exists
|
||||
loan = Loan.get_customer_loan(loan_id = loan_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,
|
||||
transaction_id=transaction_id
|
||||
|
||||
loan = loan,
|
||||
transaction_id = transaction_id
|
||||
)
|
||||
|
||||
if not repayment:
|
||||
logger.error(f"Failed to save repayment details")
|
||||
return jsonify({
|
||||
"message": "Failed to save repayment details."
|
||||
}), 400
|
||||
|
||||
|
||||
return ResponseHelper.error(result_description="Failed to save repayment details.")
|
||||
|
||||
#Update Loan status
|
||||
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
|
||||
|
||||
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
|
||||
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
|
||||
|
||||
|
||||
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||
else:
|
||||
logger.error(f"Invalid Customer or AccountID {account_id} to CustomerID{customer_id} ")
|
||||
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||
|
||||
# Simulated processing logic
|
||||
response_data = {
|
||||
"transactionId": transaction_id,
|
||||
"customerId": customer_id,
|
||||
"productId": product_id,
|
||||
"debtId": loan_id,
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Successful"
|
||||
"productId": loan.product_id,
|
||||
"loanRef": loan_ref,
|
||||
"debtId": loan_id
|
||||
}
|
||||
|
||||
# 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
|
||||
return ResponseHelper.success(data=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
|
||||
db.session.rollback()
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
db.session.rollback()
|
||||
|
||||
return jsonify({
|
||||
"message": str(err)
|
||||
}) , 400
|
||||
db.session.rollback()
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
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
|
||||
db.session.rollback()
|
||||
return ResponseHelper.internal_server_error()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from flask import request, jsonify
|
||||
from marshmallow import ValidationError
|
||||
from app.api.helpers.response_helper import ResponseHelper
|
||||
from app.api.services.base_service import BaseService
|
||||
from app.api.enums import TransactionType
|
||||
from app.utils.logger import logger
|
||||
@@ -49,15 +50,22 @@ class SelectOfferService(BaseService):
|
||||
|
||||
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
|
||||
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||
else:
|
||||
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||
|
||||
# Get the offer by product ID
|
||||
offer = Offer.get_offer_by_product_id(product_id)
|
||||
|
||||
db.session.flush()
|
||||
|
||||
if amount < offer.min_amount:
|
||||
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
|
||||
elif amount > offer.max_amount:
|
||||
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed offer amount.")
|
||||
|
||||
|
||||
|
||||
charges = SelectOfferService.calculate_charges(offer, amount)
|
||||
upfront_payment = charges["upfront_payment"]
|
||||
total_amount = charges["total_amount"]
|
||||
@@ -91,7 +99,7 @@ class SelectOfferService(BaseService):
|
||||
"amount": amount,
|
||||
"upfrontPayment": upfront_payment,
|
||||
"interestRate": offer.interest_rate,
|
||||
"interestAmount": interest_amount,
|
||||
"interestFee": interest_amount,
|
||||
"managementRate": offer.management_rate,
|
||||
"managementFee": management["fee"],
|
||||
"insuranceRate": offer.insurance_rate,
|
||||
@@ -128,26 +136,24 @@ class SelectOfferService(BaseService):
|
||||
"customerId": customer_id,
|
||||
"accountId": account_id,
|
||||
"loan": offers,
|
||||
"resultCode": "00",
|
||||
"resultDescription": "Successful",
|
||||
}
|
||||
|
||||
db.session.commit()
|
||||
return response_data
|
||||
return ResponseHelper.success(data=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:
|
||||
db.session.rollback()
|
||||
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||
|
||||
except ValueError as err:
|
||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||
db.session.rollback()
|
||||
return jsonify({"message": str(err)}), 400
|
||||
db.session.rollback()
|
||||
return ResponseHelper.error(result_description=str(err))
|
||||
|
||||
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
|
||||
db.session.rollback()
|
||||
return ResponseHelper.internal_server_error()
|
||||
|
||||
+51
-4
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Config:
|
||||
"""Base configuration for Flask app"""
|
||||
|
||||
@@ -9,20 +8,19 @@ class Config:
|
||||
API_URL = os.getenv("API_URL", "/swagger.json")
|
||||
|
||||
DEBUG = True
|
||||
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
|
||||
VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345")
|
||||
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
|
||||
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
|
||||
|
||||
# Database Configuration
|
||||
DATABASE_USER = os.environ.get("DATABASE_USER")
|
||||
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
|
||||
DATABASE_HOST = os.environ.get("DATABASE_HOST")
|
||||
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
|
||||
DATABASE_NAME = os.environ.get("DATABASE_NAME")
|
||||
|
||||
# Database Connection
|
||||
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
|
||||
|
||||
|
||||
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
|
||||
@@ -34,5 +32,54 @@ class Config:
|
||||
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
|
||||
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
|
||||
|
||||
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
|
||||
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
|
||||
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "test-api-key-12345")
|
||||
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
|
||||
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
|
||||
|
||||
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
|
||||
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
|
||||
RAC_RESULT_creditBureauCheck = os.environ.get("RAC_RESULT_creditBureauCheck", "false")
|
||||
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
|
||||
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
|
||||
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
|
||||
RAC_RESULT_hasSalaryAccount = os.environ.get("RAC_RESULT_hasSalaryAccount", "true")
|
||||
RAC_RESULT_isWhitelisted = os.environ.get("RAC_RESULT_isWhitelisted", "true")
|
||||
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
|
||||
|
||||
rac_true_rules = [
|
||||
"rule1_45day_sal",
|
||||
"rule2_2m_sal",
|
||||
"rule3_no_bounced_check",
|
||||
"rule4_current_loan_payments",
|
||||
"rule5_no_past_due_fadv_loan",
|
||||
"rule6_no_past_due_other_loan",
|
||||
"rule7_consistent_salary_amount",
|
||||
"rule8_whitelisted",
|
||||
"rule9_regular_account",
|
||||
"rule10_bvn_validation",
|
||||
"rule11_CRC_no_delinquency",
|
||||
"rule12_CRMS_no_delinquency",
|
||||
"rule13_BVN_ignore",
|
||||
"rule14_no_lien",
|
||||
"rule15_null_ignore"
|
||||
]
|
||||
|
||||
rac_false_rules = [
|
||||
|
||||
]
|
||||
|
||||
rac_salary_payments = [
|
||||
"salarypaymenT_1",
|
||||
"salarypaymenT_2",
|
||||
"salarypaymenT_3",
|
||||
"salarypaymenT_4",
|
||||
"salarypaymenT_5",
|
||||
"salarypaymenT_6"
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
settings = Config()
|
||||
|
||||
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
|
||||
def register_error_handlers(app):
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_http_exception(e):
|
||||
return jsonify({'error': e.description}), e.code
|
||||
return ResponseHelper.error(result_description=e.description, result_code=e.code )
|
||||
|
||||
@app.errorhandler(405)
|
||||
def method_not_allowed(error):
|
||||
return jsonify({"message": "Method Not Allowed"}), 405
|
||||
return ResponseHelper.method_not_allowed()
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return jsonify({"message": "Resource not found"}), 404
|
||||
return ResponseHelper.not_found()
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(error):
|
||||
return jsonify({"message": "Bad Request"}), 400
|
||||
return ResponseHelper.bad_request()
|
||||
|
||||
@app.errorhandler(415)
|
||||
def unsupported_media_type(error):
|
||||
return jsonify({"message": "Unsupported Media Type"}), 415
|
||||
return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
|
||||
|
||||
@@ -9,6 +9,7 @@ from .charge import Charge
|
||||
from .rac_checks import RACCheck
|
||||
from .loan_repayment_schedule import LoanRepaymentSchedule
|
||||
from .transaction_offers import TransactionOffer
|
||||
from .repayments_data import RepaymentsData
|
||||
|
||||
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer']
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData']
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.extensions import db
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
class Account(db.Model):
|
||||
__tablename__ = 'accounts'
|
||||
@@ -11,8 +12,8 @@ class Account(db.Model):
|
||||
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))
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
@@ -26,7 +27,9 @@ class Account(db.Model):
|
||||
account = cls(
|
||||
id=id,
|
||||
customer_id=customer_id,
|
||||
account_type=account_type
|
||||
account_type=account_type,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
|
||||
class Charge(db.Model):
|
||||
@@ -12,9 +13,8 @@ class Charge(db.Model):
|
||||
percent = db.Column(db.Float, default=0.0)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
due = db.Column(db.Integer, 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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
offer = relationship(
|
||||
"Offer",
|
||||
primaryjoin="Charge.offer_id == Offer.id",
|
||||
@@ -57,7 +57,9 @@ class Charge(db.Model):
|
||||
code = code,
|
||||
percent = percent,
|
||||
description = description,
|
||||
due = due_days
|
||||
due = due_days,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.session.add(charge_obj)
|
||||
|
||||
+18
-5
@@ -1,8 +1,12 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import relationship
|
||||
#
|
||||
# from app.api.services.offer_analysis import logger
|
||||
from app.extensions import db
|
||||
from app.models.account import Account
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.sql import func
|
||||
# from app.utils.logger import logger
|
||||
|
||||
class Customer(db.Model):
|
||||
__tablename__ = 'customers'
|
||||
@@ -10,9 +14,8 @@ class Customer(db.Model):
|
||||
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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
accounts = relationship(
|
||||
"Account",
|
||||
primaryjoin="Customer.id == Account.customer_id",
|
||||
@@ -45,9 +48,19 @@ class Customer(db.Model):
|
||||
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
|
||||
if cls.query.filter_by(id=id).first():
|
||||
raise ValueError("Customer already exists")
|
||||
elif Account.query.filter_by(id=account_id).first():
|
||||
raise ValueError("Account already exists")
|
||||
elif cls.query.filter_by(msisdn=msisdn).first():
|
||||
raise ValueError("msisdn already exists")
|
||||
|
||||
# Create the customer
|
||||
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
|
||||
customer = cls(
|
||||
id=id,
|
||||
msisdn=msisdn,
|
||||
country_code=country_code,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
try:
|
||||
db.session.add(customer)
|
||||
|
||||
@@ -63,7 +76,7 @@ class Customer(db.Model):
|
||||
return customer
|
||||
|
||||
@classmethod
|
||||
def get_customer(cls, customer_id):
|
||||
def get_customer_with_loan_list(cls, customer_id):
|
||||
"""
|
||||
Get customer by ID.
|
||||
"""
|
||||
|
||||
+65
-15
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime, timezone
|
||||
from itertools import product
|
||||
from app.extensions import db
|
||||
from app.models.customer import Customer
|
||||
from app.models.account import Account
|
||||
@@ -6,6 +7,11 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from sqlalchemy import and_, or_, not_
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Loan(db.Model):
|
||||
@@ -33,9 +39,16 @@ class Loan(db.Model):
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
tenor = db.Column(db.Integer, nullable=True)
|
||||
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))
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
disburse_date = db.Column(db.DateTime, nullable=True)
|
||||
disburse_verify = db.Column(db.DateTime, nullable=True)
|
||||
reference = db.Column(db.String(50), nullable=True)
|
||||
disburse_result = db.Column(db.String(10), nullable=True)
|
||||
disburse_description = db.Column(db.String(100), nullable=True)
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
@@ -75,6 +88,7 @@ class Loan(db.Model):
|
||||
installment_amount,
|
||||
tenor,
|
||||
eligible_amount,
|
||||
reference,
|
||||
status = "pending",
|
||||
):
|
||||
# Check if customer exists
|
||||
@@ -102,7 +116,10 @@ class Loan(db.Model):
|
||||
due_date=due_date,
|
||||
tenor = tenor,
|
||||
status = status,
|
||||
eligible_amount =eligible_amount
|
||||
eligible_amount =eligible_amount,
|
||||
reference = reference,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -134,24 +151,53 @@ class Loan(db.Model):
|
||||
return loan
|
||||
|
||||
@classmethod
|
||||
def get_customer_current_active_loan(cls, customer_id):
|
||||
def get_customer_original_loan(cls, customer_id, original_transaction):
|
||||
"""
|
||||
Get customer's original loan offer.
|
||||
"""
|
||||
original_loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.original_transaction==original_transaction, cls.transaction_id==original_transaction )).first()
|
||||
if not original_loan:
|
||||
return None
|
||||
|
||||
logger.info(f" get_customer_original_loan ==>>>> {original_loan}")
|
||||
return original_loan
|
||||
|
||||
@classmethod
|
||||
def get_customer_last_loan(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans.
|
||||
"""
|
||||
loan = cls.query.filter_by( customer_id = customer_id).first()
|
||||
logger.info(f"get_customer_last_loan [customer_id] ==>>>> {customer_id}")
|
||||
# loan = cls.query.filter_by( cls.customer_id == customer_id).first()
|
||||
loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.status=='active')).first()
|
||||
|
||||
if not loan:
|
||||
loan = {
|
||||
"eligible_amount": 0,
|
||||
"loan_amount": 0,
|
||||
"customer_id": customer_id,
|
||||
"transaction_id": "",
|
||||
"resultDescription": "No Active Loan"
|
||||
}
|
||||
|
||||
logger.info(f" Active Loan ==>>>> {loan}")
|
||||
|
||||
return None
|
||||
# loan = {
|
||||
# "original_transaction":"",
|
||||
# "eligible_amount": 0,
|
||||
# "loan_amount": 0,
|
||||
# "customer_id": customer_id,
|
||||
# "transaction_id": "",
|
||||
# "resultDescription": "No Active Loan"
|
||||
# }
|
||||
logger.info(f" get_customer_last_loan ==>>>> {loan}")
|
||||
return loan
|
||||
|
||||
@classmethod
|
||||
def get_active_loans_by_original_transaction(cls, original_transaction_id):
|
||||
"""
|
||||
Get all active loans with the same original_transaction ID.
|
||||
"""
|
||||
|
||||
active_loans = cls.query.filter_by(
|
||||
original_transaction=original_transaction_id,
|
||||
# status='active'
|
||||
).all()
|
||||
|
||||
return active_loans
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
@@ -175,6 +221,9 @@ class Loan(db.Model):
|
||||
"""
|
||||
return {
|
||||
'debtId': self.id,
|
||||
'transactionId': self.transaction_id,
|
||||
'loanRef': self.reference,
|
||||
'productId': self.product_id,
|
||||
'initialLoanAmount': self.initial_loan_amount,
|
||||
'currentLoanAmount': self.current_loan_amount,
|
||||
'defaultPenaltyFee': self.default_penalty_fee,
|
||||
@@ -184,6 +233,7 @@ class Loan(db.Model):
|
||||
'repaymentAmount': self.repayment_amount,
|
||||
'installmentAmount': self.installment_amount,
|
||||
'status': self.status,
|
||||
'tenor': self.tenor,
|
||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime, timezone, timedelta
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.utils.logger import logger
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
|
||||
class LoanCharge(db.Model):
|
||||
@@ -16,9 +17,8 @@ class LoanCharge(db.Model):
|
||||
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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
loan = relationship(
|
||||
"Loan",
|
||||
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||
@@ -63,7 +63,9 @@ class LoanCharge(db.Model):
|
||||
percent = percent,
|
||||
description = description,
|
||||
due = due_days,
|
||||
due_date = now + timedelta(days=due_days)
|
||||
due_date = now + timedelta(days=due_days),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.session.add(charge_obj)
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
class LoanRepaymentSchedule(db.Model):
|
||||
__tablename__ = 'loan_repayment_schedules'
|
||||
@@ -17,9 +18,8 @@ class LoanRepaymentSchedule(db.Model):
|
||||
paid = db.Column(db.Boolean, default=False)
|
||||
paid_at = 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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
loan = relationship(
|
||||
"Loan",
|
||||
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||
@@ -45,7 +45,9 @@ class LoanRepaymentSchedule(db.Model):
|
||||
total_repayment_amount = round(loan.repayment_amount, 2),
|
||||
installment_amount=round(loan.installment_amount, 2),
|
||||
product_id = loan.product_id,
|
||||
transaction_id = transaction_id
|
||||
transaction_id = transaction_id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.session.add(schedule)
|
||||
|
||||
+11
-3
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from app.models.charge import Charge
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
class Offer(db.Model):
|
||||
__tablename__ = 'offers'
|
||||
@@ -17,8 +18,11 @@ class Offer(db.Model):
|
||||
insurance_rate = db.Column(db.Float, default=1.0)
|
||||
vat_rate = db.Column(db.Float, default=7.5)
|
||||
list_order = db.Column(db.Integer, 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))
|
||||
max_daily_loans = db.Column(db.Integer, nullable=True)
|
||||
max_active_loans = db.Column(db.Integer, nullable=True)
|
||||
max_life_loans = db.Column(db.Integer, nullable=True)
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
charges = relationship(
|
||||
"Charge",
|
||||
@@ -79,7 +83,11 @@ class Offer(db.Model):
|
||||
"interest_rate": self.interest_rate,
|
||||
"management_rate": self.management_rate,
|
||||
"insurance_rate": self.insurance_rate,
|
||||
"vat_rate": self.vat_rate
|
||||
"vat_rate": self.vat_rate,
|
||||
"maxDailyLoans": self.max_daily_loans,
|
||||
"maxActiveLoans": self.max_active_loans,
|
||||
"maxLifeLoans": self.max_life_loans
|
||||
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from uuid import uuid4
|
||||
from sqlalchemy.types import JSON
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
class RACCheck(db.Model):
|
||||
__tablename__ = 'rac_checks'
|
||||
@@ -13,9 +14,8 @@ class RACCheck(db.Model):
|
||||
customer_id = db.Column(db.String, nullable=False)
|
||||
account_id = db.Column(db.String, nullable=False)
|
||||
rac_response = db.Column(db.JSON, 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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
@classmethod
|
||||
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
|
||||
|
||||
@@ -25,7 +25,9 @@ class RACCheck(db.Model):
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
transaction_id = transaction_id,
|
||||
rac_response = data
|
||||
rac_response = data,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
+16
-15
@@ -4,6 +4,7 @@ from app.extensions import db
|
||||
from app.models.customer import Customer
|
||||
from app.models.loan import Loan
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
|
||||
class Repayment(db.Model):
|
||||
@@ -17,31 +18,31 @@ class Repayment(db.Model):
|
||||
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))
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
repay_result = db.Column(db.String(10), nullable=True)
|
||||
repay_description = db.Column(db.String(100), nullable=True)
|
||||
verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
|
||||
@classmethod
|
||||
def create_repayment(cls, customer_id, loan_id, product_id, transaction_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)
|
||||
def create_repayment(cls, customer_id, loan, transaction_id):
|
||||
|
||||
# Check that the loan is active
|
||||
if loan.status != LoanStatus.ACTIVE:
|
||||
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
|
||||
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,
|
||||
transaction_id = transaction_id
|
||||
loan_id=loan.id,
|
||||
product_id=loan.product_id,
|
||||
transaction_id = transaction_id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
|
||||
class RepaymentsData(db.Model):
|
||||
__tablename__ = 'repayments_data'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
|
||||
response_code = db.Column(db.String(10), nullable=True)
|
||||
response_descr = db.Column(db.String(255), nullable=True)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"transaction_id": self.transaction_id,
|
||||
"added_date": self.added_date.isoformat() if self.added_date else None,
|
||||
"response_code": self.response_code,
|
||||
"response_descr": self.response_descr,
|
||||
}
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
|
||||
@@ -3,6 +3,7 @@ from app.extensions import db
|
||||
from app.models import account
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy import and_, or_, not_
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
@@ -16,9 +17,8 @@ class Transaction(db.Model):
|
||||
customer_id = db.Column(db.String(50), nullable=True)
|
||||
type = db.Column(db.String(50), nullable=False)
|
||||
channel = 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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
def __repr__(self):
|
||||
return f'<Transaction {self.id}>'
|
||||
|
||||
@@ -38,7 +38,9 @@ class Transaction(db.Model):
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
type = type,
|
||||
channel = channel
|
||||
channel = channel,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from app.api.enums.loan_status import LoanStatus
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class TransactionOffer(db.Model):
|
||||
__tablename__ = 'transaction_offers'
|
||||
@@ -8,6 +14,7 @@ class TransactionOffer(db.Model):
|
||||
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=False)
|
||||
original_transaction = db.Column(db.String(50), nullable=True)
|
||||
offer_id = db.Column(db.String(20), nullable=False)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
min_amount = db.Column(db.Float, nullable=False)
|
||||
@@ -15,9 +22,8 @@ class TransactionOffer(db.Model):
|
||||
eligible_amount = db.Column(db.Float, nullable=True)
|
||||
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
|
||||
|
||||
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))
|
||||
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
primaryjoin="Customer.id == TransactionOffer.customer_id",
|
||||
@@ -26,12 +32,11 @@ class TransactionOffer(db.Model):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id):
|
||||
transaction_offer = cls.query.filter_by(
|
||||
id = transaction_offer,
|
||||
customer_id = customer_id,
|
||||
product_id = product_id
|
||||
# product_id = product_id
|
||||
# transaction_id = transaction_id,
|
||||
).first()
|
||||
|
||||
@@ -41,19 +46,22 @@ class TransactionOffer(db.Model):
|
||||
return transaction_offer
|
||||
|
||||
@classmethod
|
||||
def create_transaction_offer(cls, customer_id, transaction_id, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
|
||||
def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
|
||||
"""
|
||||
Class method to create and save a TransactionOffer.
|
||||
"""
|
||||
transaction_offer = cls(
|
||||
customer_id=customer_id,
|
||||
transaction_id=transaction_id,
|
||||
original_transaction=original_transaction,
|
||||
offer_id=offer_id,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
eligible_amount=eligible_amount,
|
||||
product_id=product_id,
|
||||
tenor=tenor
|
||||
tenor=tenor,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.session.add(transaction_offer)
|
||||
@@ -61,6 +69,43 @@ class TransactionOffer(db.Model):
|
||||
|
||||
return transaction_offer
|
||||
|
||||
@classmethod
|
||||
def get_lifetime_loan_count(cls, customer_id):
|
||||
"""
|
||||
Returns the total number of loans ever created for a customer.
|
||||
"""
|
||||
return cls.query.filter_by(customer_id=customer_id).count()
|
||||
|
||||
@classmethod
|
||||
def get_daily_loan_count(cls, customer_id, offer_id):
|
||||
"""
|
||||
Returns the count of loans created today for a customer.
|
||||
"""
|
||||
|
||||
start_of_day = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_of_day = start_of_day + timedelta(days=1)
|
||||
|
||||
return cls.query.filter_by(
|
||||
customer_id=customer_id,
|
||||
offer_id=offer_id
|
||||
).filter(
|
||||
cls.created_at >= start_of_day,
|
||||
cls.created_at < end_of_day
|
||||
).count()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_latest_transaction_offer(cls, customer_id):
|
||||
"""
|
||||
Returns the most recent transaction offer for the given customer based on creation time.
|
||||
"""
|
||||
return cls.query.filter_by(customer_id=customer_id) \
|
||||
.order_by(cls.created_at.desc()) \
|
||||
.first()
|
||||
|
||||
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"type": "string",
|
||||
"example": "Tr201712RK9232P115"
|
||||
},
|
||||
"loanRef": {
|
||||
"type": "string",
|
||||
"example": "1620029887USSDAMPC"
|
||||
},
|
||||
"customerId": {
|
||||
"type": "string",
|
||||
"example": "CN621868"
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
"type": "string",
|
||||
"example": "10"
|
||||
},
|
||||
"productId": {
|
||||
"type": "string",
|
||||
"example": "101"
|
||||
},
|
||||
"transactionId": {
|
||||
"type": "string",
|
||||
"example": "20171209232115"
|
||||
@@ -21,9 +17,9 @@
|
||||
"type": "string",
|
||||
"example": "CID0000025585"
|
||||
},
|
||||
"channel": {
|
||||
"loanRef": {
|
||||
"type": "string",
|
||||
"example": "USSD"
|
||||
"example": "Trx5847365252USSD3MPC"
|
||||
},
|
||||
"accountId": {
|
||||
"type": "string",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"productId": {
|
||||
"type": "string",
|
||||
"example": "2090"
|
||||
"example": "3MPC"
|
||||
},
|
||||
"offerId": {
|
||||
"type": "string",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"productId": {
|
||||
"type": "string",
|
||||
"example": "2030"
|
||||
"example": "3MPC"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
@@ -49,7 +49,7 @@
|
||||
"format": "float",
|
||||
"example": 3.0
|
||||
},
|
||||
"interestAmount": {
|
||||
"interestFee": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 3000.00
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Migration on Tue Jun 10 08:41:00 WAT 2025
|
||||
|
||||
Revision ID: 0acd553309a1
|
||||
Revises: 45790fd659fb
|
||||
Create Date: 2025-06-10 08:41:45.222513
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0acd553309a1'
|
||||
down_revision = '45790fd659fb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('repayments_data',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('transaction_id', sa.String(length=50), nullable=False),
|
||||
sa.Column('added_date', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('response_code', sa.String(length=10), nullable=True),
|
||||
sa.Column('response_descr', sa.String(length=255), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('repayments_data')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Migration on Sat May 10 09:54:34 UTC 2025
|
||||
|
||||
Revision ID: 173ea45db189
|
||||
Revises: 3105abd795d4
|
||||
Create Date: 2025-05-10 09:54:39.380499
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '173ea45db189'
|
||||
down_revision = '3105abd795d4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
|
||||
batch_op.drop_column('original_transaction')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,250 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 2eee4157505f
|
||||
Revises: 565bc3d0ba6e
|
||||
Create Date: 2025-05-16 13:24:41.914400
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2eee4157505f'
|
||||
down_revision = '565bc3d0ba6e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('accounts', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('charges', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('customers', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('offers', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('repayments', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('transactions', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
|
||||
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('repayments', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('offers', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('customers', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('charges', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('accounts', schema=None) as batch_op:
|
||||
batch_op.alter_column('updated_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,54 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 45790fd659fb
|
||||
Revises: b3a5e10bc77e
|
||||
Create Date: 2025-06-04 12:37:48.180736
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '45790fd659fb'
|
||||
down_revision = 'b3a5e10bc77e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('disburse_result', sa.String(length=10), nullable=True))
|
||||
batch_op.add_column(sa.Column('disburse_description', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('verify_result', sa.String(length=10), nullable=True))
|
||||
batch_op.add_column(sa.Column('verify_description', sa.String(length=100), nullable=True))
|
||||
|
||||
with op.batch_alter_table('repayments', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('repay_date', sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column('repay_result', sa.String(length=10), nullable=True))
|
||||
batch_op.add_column(sa.Column('repay_description', sa.String(length=100), nullable=True))
|
||||
batch_op.add_column(sa.Column('verify_date', sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column('verify_result', sa.String(length=10), nullable=True))
|
||||
batch_op.add_column(sa.Column('verify_description', sa.String(length=100), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('repayments', schema=None) as batch_op:
|
||||
batch_op.drop_column('verify_description')
|
||||
batch_op.drop_column('verify_result')
|
||||
batch_op.drop_column('verify_date')
|
||||
batch_op.drop_column('repay_description')
|
||||
batch_op.drop_column('repay_result')
|
||||
batch_op.drop_column('repay_date')
|
||||
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.drop_column('verify_description')
|
||||
batch_op.drop_column('verify_result')
|
||||
batch_op.drop_column('disburse_description')
|
||||
batch_op.drop_column('disburse_result')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Migration on Sat May 10 12:54:52 UTC 2025
|
||||
|
||||
Revision ID: 565bc3d0ba6e
|
||||
Revises: 173ea45db189
|
||||
Create Date: 2025-05-10 12:54:56.683215
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '565bc3d0ba6e'
|
||||
down_revision = '173ea45db189'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('disburse_date', sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column('disburse_verify', sa.DateTime(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.drop_column('disburse_verify')
|
||||
batch_op.drop_column('disburse_date')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: b3a5e10bc77e
|
||||
Revises: e8dd9b841ad7
|
||||
Create Date: 2025-05-27 01:52:48.538333
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b3a5e10bc77e'
|
||||
down_revision = 'e8dd9b841ad7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('reference', sa.String(length=50), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('loans', schema=None) as batch_op:
|
||||
batch_op.drop_column('reference')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,36 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: e8dd9b841ad7
|
||||
Revises: 2eee4157505f
|
||||
Create Date: 2025-05-19 11:46:19.204637
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e8dd9b841ad7'
|
||||
down_revision = '2eee4157505f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('offers', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('max_daily_loans', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('max_active_loans', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('max_life_loans', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('offers', schema=None) as batch_op:
|
||||
batch_op.drop_column('max_life_loans')
|
||||
batch_op.drop_column('max_active_loans')
|
||||
batch_op.drop_column('max_daily_loans')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user