Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c061c9b5a4 | |||
| 201fa4202e | |||
| 10138f66f3 | |||
| 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 | |||
| 17db2cf8f9 | |||
| f07866a884 | |||
| 6f8e269a50 | |||
| 4435ca2776 | |||
| d851222024 | |||
| 52ab33f260 | |||
| af7e0f8624 | |||
| c8ab2cd6ba | |||
| 8ac22fa95f | |||
| 57207faf6f | |||
| 9a90609d33 | |||
| 50ca27abfe | |||
| 74066bae56 | |||
| 4c4ef909c2 | |||
| fdd7c58fab | |||
| 4bd163fb31 | |||
| d77181f627 | |||
| 4a236fdd2f | |||
| cae7ffd772 | |||
| 4f92f2a1a0 | |||
| 03adb266bb | |||
| d28bf95c97 | |||
| bd6edf52e1 | |||
| b1260895e0 | |||
| 2addf25a67 | |||
| 9dc431e66d | |||
| 9dae2d951c | |||
| a1d44e0e23 | |||
| d9f972a425 | |||
| 8aa2c86ea2 | |||
| 9c42332a83 | |||
| 92eadbfa16 | |||
| 0fbdebceb3 | |||
| 488a1b4bdd | |||
| cdc74d05c4 | |||
| 1b92ede296 | |||
| 7de4e3651f |
@@ -0,0 +1,35 @@
|
|||||||
|
# Environment Variables
|
||||||
|
BASIC_AUTH_USERNAME=user
|
||||||
|
BASIC_AUTH_PASSWORD=password
|
||||||
|
|
||||||
|
#swagger Configuration
|
||||||
|
SWAGGER_URL="/documentation"
|
||||||
|
API_URL="/swagger.json"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_USER=firstadvance
|
||||||
|
DATABASE_PASSWORD=FirstAdvance!
|
||||||
|
DATABASE_HOST=dev-data.simbrellang.net
|
||||||
|
DATABASE_PORT=10532
|
||||||
|
DATABASE_NAME=firstadvancedev
|
||||||
|
|
||||||
|
# DATABASE_HOST=10.20.30.60
|
||||||
|
# DATABASE_USER=firstadvance
|
||||||
|
# DATABASE_PASSWORD=firstadvance
|
||||||
|
# DATABASE_NAME=firstadvancedev
|
||||||
|
# DATABASE_PORT=5432
|
||||||
|
|
||||||
|
# Flask Configuration
|
||||||
|
FLASK_APP=wsgi.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
APP_PORT=4500
|
||||||
|
|
||||||
|
|
||||||
|
# Bank Call Service Connection
|
||||||
|
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
|
||||||
|
VALID_APP_ID=app1
|
||||||
|
VALID_API_KEY=test-api-key-12345
|
||||||
|
|
||||||
|
|
||||||
|
# Event Bus Broker Configuration
|
||||||
|
KAFKA_BROKER="10.0.0.246:9092"
|
||||||
@@ -3,4 +3,5 @@ from enum import Enum
|
|||||||
class LoanStatus(str, Enum):
|
class LoanStatus(str, Enum):
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
|
START_REPAY = "start_repay"
|
||||||
REPAID = "repaid"
|
REPAID = "repaid"
|
||||||
@@ -1,251 +1,112 @@
|
|||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from typing import List, Dict, Union, Optional, Any
|
from typing import Optional, Union, Dict, List, Any
|
||||||
|
|
||||||
|
|
||||||
class ResponseHelper:
|
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
|
@staticmethod
|
||||||
def build_response(
|
def build_response(
|
||||||
status: bool,
|
result_code: str,
|
||||||
message: str,
|
result_description: str,
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
data: Optional[Union[Dict, List, str]] = None
|
||||||
status_code: int = 200,
|
|
||||||
error: Optional[Union[Dict, str]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> 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 = {
|
response = {
|
||||||
"status": status,
|
"resultCode": result_code,
|
||||||
"statusCode": status_code,
|
"resultDescription": result_description
|
||||||
"message": message,
|
|
||||||
"data": data if data is not None else {},
|
|
||||||
"error": error if error is not None else {},
|
|
||||||
}
|
}
|
||||||
return jsonify(response), status_code
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
response.update(data)
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def success(
|
def success(
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_description: str = "Successful",
|
||||||
message: str = "Successful",
|
result_code: str = "00",
|
||||||
status_code: int = 200,
|
data: Optional[Dict[str, Any]] = None
|
||||||
error: Optional[Union[Dict, str]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def error(
|
def error(
|
||||||
message: str = "An error occurred",
|
result_description: str = "An error occurred",
|
||||||
status_code: int = 400,
|
result_code: str = "01",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
error: Optional[Union[Dict, str]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def created(
|
def created(
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_description: str = "Resource created successfully",
|
||||||
message: str = "Resource created successfully",
|
result_code: str = "00",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def updated(
|
def updated(
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_description: str = "Resource updated successfully",
|
||||||
message: str = "Resource updated successfully",
|
result_code: str = "00",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def internal_server_error(
|
def internal_server_error(
|
||||||
message: str = "Internal Server Error",
|
result_description: str = "Internal Server Error",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "500",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unauthorized(
|
def unauthorized(
|
||||||
message: str = "Unauthorized",
|
result_description: str = "Unauthorized",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "401",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def forbidden(
|
def forbidden(
|
||||||
message: str = "Forbidden",
|
result_description: str = "Forbidden",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "403",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def not_found(
|
def not_found(
|
||||||
message: str = "Resource not found",
|
result_description: str = "Resource not found",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "404",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unprocessable_entity(
|
def unprocessable_entity(
|
||||||
message: str = "Unprocessable entity",
|
result_description: str = "Unprocessable entity",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "422",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for an unprocessable entity.
|
|
||||||
|
|
||||||
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
|
@staticmethod
|
||||||
def method_not_allowed(
|
def method_not_allowed(
|
||||||
message: str = "Method Not Allowed",
|
result_description: str = "Method Not Allowed",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "405",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
Return a response for a method not allowed 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, 405, error)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bad_request(
|
def bad_request(
|
||||||
message: str = "Bad Request",
|
result_description: str = "Bad Request",
|
||||||
data: Optional[Union[Dict, List, str]] = None,
|
result_code: str = "400",
|
||||||
error: Optional[Union[Dict, str]] = None,
|
data: Optional[Dict[str, Any]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
return ResponseHelper.build_response(result_code, result_description, data)
|
||||||
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)
|
|
||||||
@@ -7,15 +7,26 @@ import logging
|
|||||||
|
|
||||||
class SimbrellaIntegration:
|
class SimbrellaIntegration:
|
||||||
BASE_URL = settings.SIMBRELLA_BASE_URL
|
BASE_URL = settings.SIMBRELLA_BASE_URL
|
||||||
|
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rac_check(customer_id, account_id, transaction_id):
|
def rac_check(customer_id, account_id, transaction_id):
|
||||||
"""
|
"""
|
||||||
Calls the RACCheck endpoit
|
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 = {
|
# {
|
||||||
|
# "transactionId": "T001",
|
||||||
|
# "fbnTransactionId": "Tr201712RK9232P115",
|
||||||
|
# "customerId": "CN621868",
|
||||||
|
# "accountId": "2017821799",
|
||||||
|
# "channel": "USSD",
|
||||||
|
# "countryCode": "NG"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
payload_old = {
|
||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"accountId": account_id,
|
"accountId": account_id,
|
||||||
"transactionId": str(transaction_id),
|
"transactionId": str(transaction_id),
|
||||||
@@ -35,7 +46,15 @@ class SimbrellaIntegration:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(f"This is PayLoad: {str(payload)}", exc_info=True)
|
payload = {
|
||||||
|
"customerId": customer_id,
|
||||||
|
"accountId": account_id,
|
||||||
|
"transactionId": str(transaction_id),
|
||||||
|
"fbnTransactionId": f"FBN{transaction_id}",
|
||||||
|
"countryCode": "NG",
|
||||||
|
"channel": "USSD"
|
||||||
|
}
|
||||||
|
# logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -46,7 +65,7 @@ class SimbrellaIntegration:
|
|||||||
try:
|
try:
|
||||||
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
|
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
|
||||||
|
|
||||||
logger.error(f"This is Response: {str(response)}", exc_info=True)
|
logger.info(f"This is Response: {str(response)}", exc_info=True)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ class RepaymentSchema(Schema):
|
|||||||
type = fields.Str(required=False)
|
type = fields.Str(required=False)
|
||||||
msisdn = fields.Str(required=False) #optional
|
msisdn = fields.Str(required=False) #optional
|
||||||
debtId = fields.Str(required=True)
|
debtId = fields.Str(required=True)
|
||||||
productId = fields.Str(required=True)
|
|
||||||
transactionId = fields.Str(required=True)
|
transactionId = fields.Str(required=True)
|
||||||
accountId = fields.Str(required=True)
|
accountId = fields.Str(required=True)
|
||||||
customerId = fields.Str(required=True)
|
customerId = fields.Str(required=True)
|
||||||
channel = fields.Str(required=True)
|
loanRef = fields.Str(required=True)
|
||||||
|
initiatedBy = fields.Str(required=False)
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ class SelectOfferSchema(Schema):
|
|||||||
msisdn = fields.Str(required=True)
|
msisdn = fields.Str(required=True)
|
||||||
requestedAmount = fields.Float(required=True)
|
requestedAmount = fields.Float(required=True)
|
||||||
productId = fields.Str(required=True)
|
productId = fields.Str(required=True)
|
||||||
|
offerId = fields.Str(required=True)
|
||||||
channel = fields.Str(required=True)
|
channel = fields.Str(required=True)
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ from app.api.services.repayment import RepaymentService
|
|||||||
from app.api.services.customer_consent import CustomerConsentService
|
from app.api.services.customer_consent import CustomerConsentService
|
||||||
from app.api.services.notification_callback import NotificationCallbackService
|
from app.api.services.notification_callback import NotificationCallbackService
|
||||||
from app.api.services.authorization import AuthorizationService
|
from app.api.services.authorization import AuthorizationService
|
||||||
|
from app.api.services.offer_analysis import OfferAnalysis
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class AuthorizationService(BaseService):
|
|||||||
logger.info("Processing Authorization request")
|
logger.info("Processing Authorization request")
|
||||||
|
|
||||||
if not data:
|
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
|
# Validate input data using the Authorization schema
|
||||||
schema = AuthorizeRequestSchema()
|
schema = AuthorizeRequestSchema()
|
||||||
@@ -44,7 +44,7 @@ class AuthorizationService(BaseService):
|
|||||||
validated_data["username"] != USERNAME
|
validated_data["username"] != USERNAME
|
||||||
or validated_data["password"] != PASSWORD
|
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"])
|
access_token = create_access_token(identity=validated_data["username"])
|
||||||
refresh_token = create_refresh_token(identity=validated_data["username"])
|
refresh_token = create_refresh_token(identity=validated_data["username"])
|
||||||
@@ -56,17 +56,17 @@ class AuthorizationService(BaseService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponseHelper.success(
|
return ResponseHelper.success(
|
||||||
data=response_data, message="Authorization processed successfully"
|
data={"data": response_data}, result_description="Authorization processed successfully"
|
||||||
)
|
)
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Validation error: {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:
|
except Exception as e:
|
||||||
logger.error(f"Error processing Authorization request: {e}")
|
logger.error(f"Error processing Authorization request: {e}")
|
||||||
return ResponseHelper.internal_server_error(
|
return ResponseHelper.internal_server_error(
|
||||||
message=f"Error processing Authorization request: {e}"
|
result_description=f"Error processing Authorization request: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -92,11 +92,11 @@ class AuthorizationService(BaseService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponseHelper.success(
|
return ResponseHelper.success(
|
||||||
data=response_data, message="RefreshToken processed successfully"
|
data={"data": response_data}, result_description="RefreshToken processed successfully"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing RefreshToken request: {e}")
|
logger.error(f"Error processing RefreshToken request: {e}")
|
||||||
return ResponseHelper.internal_server_error(
|
return ResponseHelper.internal_server_error(
|
||||||
message=f"Error processing RefreshToken request: {e}"
|
result_description=f"Error processing RefreshToken request: {e}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ class BaseService:
|
|||||||
return {"error": "No charges found for the offer"}
|
return {"error": "No charges found for the offer"}
|
||||||
|
|
||||||
loan_charges = offer.charges
|
loan_charges = offer.charges
|
||||||
tenor = offer.tenor // 30 # Convert to months
|
tenor = offer.schedule # offer.tenor // 30 # Convert to months
|
||||||
interest = cls.get_charge_detail(charges = loan_charges, code = "INTEREST", amount = amount)
|
interest = cls.get_charge_detail(rates = offer.interest_rate, charges = loan_charges, code = "INTEREST", amount = amount)
|
||||||
management = cls.get_charge_detail(charges = loan_charges, code = "MGTFEE", amount = amount)
|
management = cls.get_charge_detail(rates = offer.management_rate, charges = loan_charges, code = "MGTFEE", amount = amount)
|
||||||
insurance = cls.get_charge_detail(charges = loan_charges, code = "INSURANCE", amount = amount)
|
insurance = cls.get_charge_detail(rates = offer.insurance_rate, charges = loan_charges, code = "INSURANCE", amount = amount)
|
||||||
vat = cls.get_charge_detail(charges = loan_charges, code = "VAT", amount = amount, management_fee = management["fee"])
|
vat = cls.get_charge_detail(rates = offer.vat_rate, charges = loan_charges, code = "VAT", amount = amount, management_fee = management["fee"])
|
||||||
|
|
||||||
# Separate fees into upfront and postpaid
|
# Separate fees into upfront and postpaid
|
||||||
upfront_fees = [
|
upfront_fees = [
|
||||||
@@ -97,21 +97,33 @@ class BaseService:
|
|||||||
for fee in [interest, management, insurance, vat]
|
for fee in [interest, management, insurance, vat]
|
||||||
if fee["due_days"] != 0
|
if fee["due_days"] != 0
|
||||||
]
|
]
|
||||||
|
vat_test = vat["fee"]
|
||||||
|
logger.info(f"VAT fee == *************** : {vat_test}")
|
||||||
|
|
||||||
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
||||||
upfront_payment = sum(upfront_fees)
|
# upfront_payment = sum(upfront_fees)
|
||||||
|
if offer.schedule == 1:
|
||||||
|
upfront_payment = vat["fee"] + management["fee"] + insurance["fee"] + interest["fee"]
|
||||||
|
interest_amount = interest["fee"]
|
||||||
|
repayment_amount = amount
|
||||||
|
else:
|
||||||
|
upfront_payment = vat["fee"] + insurance["fee"]+management["fee"]
|
||||||
|
interest_amount = interest["fee"]*offer.schedule
|
||||||
|
repayment_amount = amount + interest_amount
|
||||||
|
|
||||||
|
|
||||||
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
|
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
|
||||||
repayment_amount = amount + (sum(postpaid_fees) * tenor)
|
# repayment_amount = amount + (sum(postpaid_fees) * tenor)
|
||||||
|
|
||||||
# Total amount: (upfront_payment + repayment_amount)
|
# Total amount: (upfront_payment + repayment_amount)
|
||||||
total_amount = upfront_payment + repayment_amount
|
total_amount = upfront_payment + repayment_amount
|
||||||
|
|
||||||
# Calculate the installment amount
|
# Calculate the installment amount
|
||||||
installment_amount = repayment_amount / tenor
|
installment_amount = repayment_amount / offer.schedule
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"interest": interest,
|
"interest": interest,
|
||||||
|
"interest_amount": interest_amount,
|
||||||
"management": management,
|
"management": management,
|
||||||
"insurance": insurance,
|
"insurance": insurance,
|
||||||
"vat": vat,
|
"vat": vat,
|
||||||
@@ -123,28 +135,39 @@ class BaseService:
|
|||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_charge_detail(cls, charges, code, amount, management_fee=None):
|
def get_charge_detail(cls, rates, charges, code, amount, management_fee= 0.0):
|
||||||
"""
|
"""
|
||||||
Get details for a specific charge code from a list of loan charges.
|
Get details for a specific charge code from a list of loan charges.
|
||||||
|
|
||||||
Returns default values if not found.
|
Returns default values if not found.
|
||||||
"""
|
"""
|
||||||
|
fee = 0.0
|
||||||
|
|
||||||
|
if code == "VAT" and management_fee > 0:
|
||||||
for charge in charges:
|
fee = management_fee * rates / 100
|
||||||
if charge.code == code:
|
else:
|
||||||
fee = (
|
fee = amount * rates / 100
|
||||||
management_fee * charge.percent / 100
|
|
||||||
if code == "VAT" and management_fee is not None
|
return {
|
||||||
else amount * charge.percent / 100
|
"rate": rates,
|
||||||
)
|
"fee": round(fee, 2),
|
||||||
|
"due_days": 30,
|
||||||
|
"code": code,
|
||||||
|
"description" : "have no idea how to get this yet"
|
||||||
|
}
|
||||||
|
|
||||||
|
# if charge.code == code:
|
||||||
|
# if code == "VAT" and management_fee > 0:
|
||||||
|
# fee = management_fee * rates / 100
|
||||||
|
# else:
|
||||||
|
# fee = amount * rates / 100
|
||||||
|
#
|
||||||
|
# return {
|
||||||
|
# "rate": rates,
|
||||||
|
# "fee": round(fee, 2),
|
||||||
|
# "due_days": charge.due
|
||||||
|
# }
|
||||||
|
|
||||||
return {
|
# return {"rate": 0, "fee": 0, "due_days": 0}
|
||||||
"rate": charge.percent,
|
|
||||||
"fee": round(fee, 2),
|
|
||||||
"due_days": charge.due
|
|
||||||
}
|
|
||||||
|
|
||||||
return {"rate": 0, "fee": 0, "due_days": 0}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
@@ -34,44 +35,26 @@ class CustomerConsentService(BaseService):
|
|||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"Failed to log transaction")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
"message": "Failed to log transaction."
|
|
||||||
}), 400
|
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
# Simulated processing logic
|
|
||||||
response_data = {
|
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Request is received"
|
|
||||||
}
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return response_data
|
return ResponseHelper.success(result_description="Request is received")
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
return jsonify({
|
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.error(result_description=str(err))
|
||||||
return jsonify({
|
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.internal_server_error()
|
||||||
"message": "Internal Server Error"
|
|
||||||
}) , 500
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from flask import session, jsonify
|
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.utils.logger import logger
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from app.api.schemas.eligibility_check import EligibilityCheckSchema
|
from app.api.schemas.eligibility_check import EligibilityCheckSchema
|
||||||
@@ -6,7 +8,12 @@ from marshmallow import ValidationError
|
|||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
from app.api.integrations import SimbrellaIntegration
|
from app.api.integrations import SimbrellaIntegration
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import Offer
|
from app.models import Offer, RACCheck
|
||||||
|
from app.api.services.offer_analysis import OfferAnalysis
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
class EligibilityCheckService(BaseService):
|
class EligibilityCheckService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
||||||
@@ -39,29 +46,100 @@ class EligibilityCheckService(BaseService):
|
|||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"Failed to log transaction")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
"message": "Failed to log transaction."
|
else:
|
||||||
}), 400
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
db.session.flush()
|
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
|
# Call RACCheck
|
||||||
response = SimbrellaIntegration.rac_check(
|
response = SimbrellaIntegration.rac_check(
|
||||||
customer_id = customer_id,
|
customer_id = customer_id,
|
||||||
account_id = account_id,
|
account_id = account_id,
|
||||||
transaction_id = transaction.id,
|
transaction_id = transaction.transaction_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# this chck for error is not valid
|
# this chek for error is not valid
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return jsonify({"message": "RACCheck failed"}), 400
|
return ResponseHelper.error(result_description="RACCheck failed")
|
||||||
|
|
||||||
|
response = response.json()
|
||||||
|
|
||||||
offers = [offer.to_dict() for offer in Offer.get_all_offers()]
|
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 = rack_checks_response
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rac_check:
|
||||||
|
logger.error(f"Failed to save RACCheck")
|
||||||
|
return ResponseHelper.error(result_description="Failed to save RACCheck.")
|
||||||
|
|
||||||
|
# -----------------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()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # 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
|
# Simulate processing
|
||||||
response_data = {
|
response_data = {
|
||||||
@@ -69,31 +147,53 @@ class EligibilityCheckService(BaseService):
|
|||||||
"transactionId": transactionId,
|
"transactionId": transactionId,
|
||||||
"countryCode": "NG",
|
"countryCode": "NG",
|
||||||
"msisdn": msisdn,
|
"msisdn": msisdn,
|
||||||
"eligibleOffers": offers,
|
"eligibleOffers": eligible_offers,
|
||||||
"resultDescription": "Successful",
|
|
||||||
"resultCode": "00",
|
|
||||||
"accountId": account_id
|
"accountId": account_id
|
||||||
}
|
}
|
||||||
|
|
||||||
return response_data
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
|
||||||
|
@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.schemas.loan_status import LoanStatusSchema
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
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):
|
class LoanStatusService(BaseService):
|
||||||
@@ -28,42 +29,23 @@ class LoanStatusService(BaseService):
|
|||||||
# Validate data
|
# Validate data
|
||||||
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
|
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
|
||||||
|
|
||||||
|
|
||||||
customer_id = validated_data.get('customerId')
|
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')
|
transactionId = validated_data.get('transactionId')
|
||||||
account_id = validated_data.get('accountId')
|
account_id = validated_data.get('accountId')
|
||||||
|
|
||||||
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
|
|
||||||
# Get loans
|
# Get loans
|
||||||
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
|
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
|
||||||
|
|
||||||
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
|
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
|
||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"Failed to log transaction")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
"message": "Failed to log transaction."
|
else:
|
||||||
}), 400
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
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"
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
|
|
||||||
total_debt_amount = sum(
|
total_debt_amount = sum(
|
||||||
loan.get("currentLoanAmount") or 0
|
loan.get("currentLoanAmount") or 0
|
||||||
@@ -76,33 +58,23 @@ class LoanStatusService(BaseService):
|
|||||||
"transactionId": transactionId,
|
"transactionId": transactionId,
|
||||||
"loans": loans,
|
"loans": loans,
|
||||||
"totalDebtAmount": total_debt_amount,
|
"totalDebtAmount": total_debt_amount,
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return response_data
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
return jsonify({
|
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.error(result_description=str(err))
|
||||||
return jsonify({
|
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.internal_server_error()
|
||||||
"message": "Internal Server Error"
|
|
||||||
}) , 500
|
|
||||||
@@ -5,6 +5,7 @@ from app.api.enums import TransactionType
|
|||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api.schemas.notification_callback import NotificationCallbackSchema
|
from app.api.schemas.notification_callback import NotificationCallbackSchema
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
class NotificationCallbackService(BaseService):
|
class NotificationCallbackService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
|
||||||
@@ -27,37 +28,20 @@ class NotificationCallbackService(BaseService):
|
|||||||
schema = NotificationCallbackSchema()
|
schema = NotificationCallbackSchema()
|
||||||
validated_data = schema.load(data) # Raises ValidationError if invalid
|
validated_data = schema.load(data) # Raises ValidationError if invalid
|
||||||
|
|
||||||
# Simulated processing logic
|
return ResponseHelper.success()
|
||||||
response_data = {
|
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# return ResponseHelper.success(
|
|
||||||
# data=response_data,
|
|
||||||
# message="Notification callback processed successfully"
|
|
||||||
# )
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description=str(err))
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
return jsonify({
|
db.session.rollback()
|
||||||
"message": "Internal Server Error"
|
return ResponseHelper.internal_server_error()
|
||||||
}) , 500
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
from app.models import Offer, TransactionOffer
|
||||||
|
from app.models.loan import Loan
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OfferAnalysis:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_offer(transaction_id, rac_response, validated_data):
|
||||||
|
customer_id = validated_data.get("customerId")
|
||||||
|
product_id = validated_data.get("productId")
|
||||||
|
offer_id = validated_data.get("offerId")
|
||||||
|
|
||||||
|
transaction_offer_id = int(offer_id[5:]) # The last part is int
|
||||||
|
|
||||||
|
logger.info(f"customer_id == *************** : {customer_id}")
|
||||||
|
logger.info(f"product_id == *************** : {product_id}")
|
||||||
|
logger.info(f"offer_id == *************** : {offer_id}")
|
||||||
|
logger.info(f"transaction_offer_id == *************** : {transaction_offer_id}")
|
||||||
|
|
||||||
|
transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, product_id)
|
||||||
|
|
||||||
|
if not transaction_offer:
|
||||||
|
raise ValueError("Invalid Transaction Offer.")
|
||||||
|
|
||||||
|
eligible_amount = transaction_offer.eligible_amount
|
||||||
|
offer = Offer.is_valid_offer( transaction_offer.offer_id)
|
||||||
|
|
||||||
|
if not offer:
|
||||||
|
raise ValueError("Invalid Offer.")
|
||||||
|
original_transaction = transaction_id
|
||||||
|
|
||||||
|
return transaction_offer, offer, eligible_amount, original_transaction
|
||||||
|
@staticmethod
|
||||||
|
def _analyze_rack_checks(rack_response):
|
||||||
|
|
||||||
|
# "racResponse": {
|
||||||
|
# "accountStatus": true,
|
||||||
|
# "bvnValidated": true,
|
||||||
|
# "creditBureauCheck": false,
|
||||||
|
# "crmsCheck": true,
|
||||||
|
# "hasLien": false,
|
||||||
|
# "hasPastDueLoan": false,
|
||||||
|
# "hasSalaryAccount": true,
|
||||||
|
# "isWhitelisted": true,
|
||||||
|
# "noBouncedCheck": true
|
||||||
|
# },
|
||||||
|
#
|
||||||
|
|
||||||
|
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
|
||||||
|
# _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}")
|
||||||
|
|
||||||
|
new_eligible_amount = 0
|
||||||
|
|
||||||
|
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:
|
||||||
|
# Get approved amount
|
||||||
|
random_float = random.random() # temporary to play data
|
||||||
|
|
||||||
|
approved_amount = new_eligible_amount if new_eligible_amount > 0 else min(offer.max_amount, offer.max_amount * random_float)
|
||||||
|
approved_amount = round(approved_amount, 2)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -8,13 +8,14 @@ from app.models.loan_charge import LoanCharge
|
|||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api.schemas.provide_loan import ProvideLoanSchema
|
from app.api.schemas.provide_loan import ProvideLoanSchema
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from app.models import Loan, Offer, Charge
|
from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck
|
||||||
from app.api.enums import LoanStatus
|
from app.api.enums import LoanStatus
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from app.models import LoanRepaymentSchedule
|
from app.models import LoanRepaymentSchedule
|
||||||
|
from app.api.services.offer_analysis import OfferAnalysis
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
|
|
||||||
class ProvideLoanService(BaseService):
|
class ProvideLoanService(BaseService):
|
||||||
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
||||||
@@ -42,27 +43,57 @@ class ProvideLoanService(BaseService):
|
|||||||
offer_id = validated_data.get('offerId')
|
offer_id = validated_data.get('offerId')
|
||||||
amount = validated_data.get("requestedAmount")
|
amount = validated_data.get("requestedAmount")
|
||||||
product_id = validated_data.get("productId")
|
product_id = validated_data.get("productId")
|
||||||
|
channel = validated_data.get('channel')
|
||||||
|
|
||||||
customer = Customer.is_valid_customer(customer_id)
|
customer = Customer.is_valid_customer(customer_id)
|
||||||
|
|
||||||
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
|
|
||||||
offer = Offer.is_valid_offer(offer_id)
|
rac_response = RACCheck.get_rac_check(customer_id = customer_id, account_id = account_id)
|
||||||
|
|
||||||
if not offer:
|
try:
|
||||||
logger.error(f"Invalid Offer")
|
transaction_offer, offer, eligible_amount, original_transaction = OfferAnalysis.get_offer(
|
||||||
return jsonify({
|
transaction_id=transaction_id,
|
||||||
"message": "Invalid Offer."
|
rac_response=rac_response,
|
||||||
}), 400
|
validated_data=validated_data
|
||||||
|
)
|
||||||
|
except ValueError as ve:
|
||||||
|
logger.error(str(ve))
|
||||||
|
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
|
||||||
|
|
||||||
|
# transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id)
|
||||||
|
|
||||||
|
# if not transaction_offer:
|
||||||
|
# logger.error(f"Invalid Transaction Offer")
|
||||||
|
# return jsonify({
|
||||||
|
# "message": "Invalid Transaction Offer."
|
||||||
|
# }), 400
|
||||||
|
|
||||||
|
# eligible_amount = transaction_offer.eligible_amount
|
||||||
|
# offer = Offer.is_valid_offer( transaction_offer.offer_id)
|
||||||
|
|
||||||
|
# if not offer:
|
||||||
|
# logger.error(f"Invalid Offer")
|
||||||
|
# return jsonify({
|
||||||
|
# "message": "Invalid Offer."
|
||||||
|
# }), 400
|
||||||
|
|
||||||
|
|
||||||
# Log Transaction
|
# Log Transaction
|
||||||
transaction = ProvideLoanService.log_transaction(validated_data=validated_data)
|
transaction = ProvideLoanService.log_transaction(validated_data=validated_data)
|
||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"Failed to log transaction")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
"message": "Failed to log transaction."
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
@@ -70,9 +101,19 @@ class ProvideLoanService(BaseService):
|
|||||||
charges = ProvideLoanService.calculate_charges(offer, amount)
|
charges = ProvideLoanService.calculate_charges(offer, amount)
|
||||||
upfront_fee = charges["upfront_payment"]
|
upfront_fee = charges["upfront_payment"]
|
||||||
repayment_amount = charges["repayment_amount"]
|
repayment_amount = charges["repayment_amount"]
|
||||||
|
#installment_amount = charges["installment_amount"]
|
||||||
|
num_schedules = offer.schedule
|
||||||
|
|
||||||
|
upfront_payment = charges["upfront_payment"]
|
||||||
|
total_amount = charges["total_amount"]
|
||||||
installment_amount = charges["installment_amount"]
|
installment_amount = charges["installment_amount"]
|
||||||
tenor = offer.tenor // 30 # Convert to months
|
interest = charges["interest"]
|
||||||
|
management = charges["management"]
|
||||||
|
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
|
# Save the loan details
|
||||||
@@ -83,33 +124,34 @@ class ProvideLoanService(BaseService):
|
|||||||
product_id = offer.product_id,
|
product_id = offer.product_id,
|
||||||
collection_type = collection_type,
|
collection_type = collection_type,
|
||||||
transaction_id = validated_data.get('transactionId'),
|
transaction_id = validated_data.get('transactionId'),
|
||||||
|
original_transaction = transaction_offer.original_transaction,
|
||||||
initial_loan_amount = validated_data.get('requestedAmount'),
|
initial_loan_amount = validated_data.get('requestedAmount'),
|
||||||
upfront_fee = upfront_fee,
|
upfront_fee = upfront_fee,
|
||||||
repayment_amount = repayment_amount,
|
repayment_amount = repayment_amount,
|
||||||
installment_amount = installment_amount,
|
installment_amount = installment_amount,
|
||||||
status= LoanStatus.ACTIVE
|
eligible_amount=eligible_amount,
|
||||||
|
status = LoanStatus.ACTIVE,
|
||||||
|
tenor = offer.tenor,
|
||||||
|
reference = loan_ref
|
||||||
)
|
)
|
||||||
|
|
||||||
if not loan:
|
if not loan:
|
||||||
logger.error(f"Failed to save loan details")
|
logger.error(f"Failed to save loan details")
|
||||||
return jsonify({
|
|
||||||
"message": "Failed to save loan details."
|
return ResponseHelper.error(result_description="Failed to save loan details.")
|
||||||
}), 400
|
|
||||||
|
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
current_product_id = offer.product_id
|
||||||
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, tenor = tenor)
|
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
|
||||||
|
|
||||||
|
|
||||||
if not schedule:
|
if not schedule:
|
||||||
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
|
||||||
"message": "Failed to generate loan repayment schedule."
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
charges = Charge.get_offer_charges(offer.id)
|
# charges = Charge.get_offer_charges(offer.id)
|
||||||
|
|
||||||
# logger.error(f"{charges}")
|
# logger.info(f"{charges}")
|
||||||
|
|
||||||
loan_id = loan.id
|
loan_id = loan.id
|
||||||
|
|
||||||
@@ -118,50 +160,39 @@ class ProvideLoanService(BaseService):
|
|||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"requestId": request_id,
|
"requestId": request_id,
|
||||||
"transactionId": transaction_id,
|
"transactionId": transaction_id,
|
||||||
|
"loanRef": loan_ref,
|
||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"accountId": account_id,
|
"accountId": account_id,
|
||||||
"msisdn": customer.msisdn,
|
"msisdn": customer.msisdn
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
|
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
|
||||||
# Call Kafka in a background thread
|
# Call Kafka in a background thread
|
||||||
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
|
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return response_data
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
return jsonify({
|
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.error(result_description=str(err))
|
||||||
return jsonify({
|
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.internal_server_error()
|
||||||
"message": "Internal Server Error"
|
|
||||||
}) , 500
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from app.api.enums.loan_status import LoanStatus
|
from app.api.enums.loan_status import LoanStatus
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
from app.models import Repayment
|
from app.models import Repayment
|
||||||
from app.models.customer import Customer
|
from app.models.customer import Customer
|
||||||
from app.models.loan import Loan
|
from app.models.loan import Loan
|
||||||
@@ -31,88 +32,66 @@ class RepaymentService(BaseService):
|
|||||||
customer_id = validated_data.get('customerId')
|
customer_id = validated_data.get('customerId')
|
||||||
request_id = validated_data.get('requestId')
|
request_id = validated_data.get('requestId')
|
||||||
loan_id = validated_data.get('debtId')
|
loan_id = validated_data.get('debtId')
|
||||||
product_id = validated_data.get('productId')
|
|
||||||
account_id = validated_data.get('accountId')
|
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')
|
transaction_id = validated_data.get('transactionId')
|
||||||
|
initiated_by = validated_data.get('initiatedBy')
|
||||||
|
|
||||||
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||||
|
|
||||||
|
# Check loan exists
|
||||||
|
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
|
||||||
|
|
||||||
# Save the repayment details
|
# Save the repayment details
|
||||||
repayment = Repayment.create_repayment(
|
repayment = Repayment.create_repayment(
|
||||||
customer_id = customer_id,
|
customer_id = customer_id,
|
||||||
loan_id = loan_id,
|
loan = loan,
|
||||||
product_id = product_id,
|
transaction_id = transaction_id
|
||||||
transaction_id=transaction_id
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not repayment:
|
if not repayment:
|
||||||
logger.error(f"Failed to save repayment details")
|
logger.error(f"Failed to save repayment details")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to save repayment details.")
|
||||||
"message": "Failed to save repayment details."
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
#Update Loan status
|
#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)
|
transaction = RepaymentService.log_transaction(validated_data = validated_data)
|
||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log transaction")
|
logger.error(f"Failed to log transaction")
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Failed to log transaction.")
|
||||||
"message": "Failed to log transaction."
|
|
||||||
}), 400
|
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
"message": "Invalid Customer or Account"
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Simulated processing logic
|
# Simulated processing logic
|
||||||
response_data = {
|
response_data = {
|
||||||
"transactionId": transaction_id,
|
"transactionId": transaction_id,
|
||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"productId": product_id,
|
"productId": loan.product_id,
|
||||||
"debtId": loan_id,
|
"loanRef": loan_ref,
|
||||||
"resultCode": "00",
|
"debtId": loan_id
|
||||||
"resultDescription": "Successful"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# return ResponseHelper.success(
|
|
||||||
# data=response_data,
|
|
||||||
# message="Repayment processed successfully"
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Call Kafka in a background thread
|
# Call Kafka in a background thread
|
||||||
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
|
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return response_data
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
return jsonify({
|
|
||||||
"message": "Validation exception"
|
|
||||||
}) , 422
|
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
return ResponseHelper.error(result_description=str(err))
|
||||||
return jsonify({
|
|
||||||
"message": str(err)
|
|
||||||
}) , 400
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({
|
return ResponseHelper.internal_server_error()
|
||||||
"message": "Internal Server Error"
|
|
||||||
}) , 500
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
from app.api.helpers.response_helper import ResponseHelper
|
||||||
from app.api.services.base_service import BaseService
|
from app.api.services.base_service import BaseService
|
||||||
from app.api.enums import TransactionType
|
from app.api.enums import TransactionType
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
@@ -32,9 +33,14 @@ class SelectOfferService(BaseService):
|
|||||||
customer_id = validated_data.get("customerId")
|
customer_id = validated_data.get("customerId")
|
||||||
amount = validated_data.get("requestedAmount")
|
amount = validated_data.get("requestedAmount")
|
||||||
product_id = validated_data.get("productId")
|
product_id = validated_data.get("productId")
|
||||||
|
transaction_offer_id = validated_data.get("offerId")
|
||||||
transaction_id = validated_data.get("transactionId")
|
transaction_id = validated_data.get("transactionId")
|
||||||
request_id = validated_data.get("requestId")
|
request_id = validated_data.get("requestId")
|
||||||
|
|
||||||
|
offer_id = int(transaction_offer_id[5:]) # The last part is int
|
||||||
|
|
||||||
|
#"offerId": "SAL30001129",
|
||||||
|
|
||||||
if SelectOfferService.validate_account_ownership(
|
if SelectOfferService.validate_account_ownership(
|
||||||
account_id=account_id, customer_id=customer_id
|
account_id=account_id, customer_id=customer_id
|
||||||
):
|
):
|
||||||
@@ -44,15 +50,22 @@ class SelectOfferService(BaseService):
|
|||||||
|
|
||||||
if not transaction:
|
if not transaction:
|
||||||
logger.error(f"Failed to log 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:
|
else:
|
||||||
return jsonify({"message": "Invalid Customer or Account"}), 400
|
return ResponseHelper.error(result_description="Invalid Customer or Account")
|
||||||
|
|
||||||
# Get the offer by product ID
|
# Get the offer by product ID
|
||||||
offer = Offer.get_offer_by_product_id(product_id)
|
offer = Offer.get_offer_by_product_id(product_id)
|
||||||
|
|
||||||
db.session.flush()
|
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)
|
charges = SelectOfferService.calculate_charges(offer, amount)
|
||||||
upfront_payment = charges["upfront_payment"]
|
upfront_payment = charges["upfront_payment"]
|
||||||
total_amount = charges["total_amount"]
|
total_amount = charges["total_amount"]
|
||||||
@@ -61,6 +74,8 @@ class SelectOfferService(BaseService):
|
|||||||
management = charges["management"]
|
management = charges["management"]
|
||||||
insurance = charges["insurance"]
|
insurance = charges["insurance"]
|
||||||
vat = charges["vat"]
|
vat = charges["vat"]
|
||||||
|
repayment_amount = charges["repayment_amount"]
|
||||||
|
interest_amount = charges["interest_amount"]
|
||||||
|
|
||||||
|
|
||||||
# Calculate the repayment dates
|
# Calculate the repayment dates
|
||||||
@@ -68,7 +83,7 @@ class SelectOfferService(BaseService):
|
|||||||
start_date = date.today()
|
start_date = date.today()
|
||||||
|
|
||||||
# Convert tenor to months
|
# Convert tenor to months
|
||||||
months = tenor // 30
|
months = offer.schedule # tenor // 30
|
||||||
|
|
||||||
recommended_repayment_dates = [
|
recommended_repayment_dates = [
|
||||||
(start_date + relativedelta(months=i + 1)).isoformat()
|
(start_date + relativedelta(months=i + 1)).isoformat()
|
||||||
@@ -79,23 +94,40 @@ class SelectOfferService(BaseService):
|
|||||||
|
|
||||||
offers = [
|
offers = [
|
||||||
{
|
{
|
||||||
"offerId": offer.id,
|
"offerId": transaction_offer_id,
|
||||||
"productId": product_id,
|
"productId": product_id,
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"upfrontPayment": upfront_payment,
|
"upfrontPayment": upfront_payment,
|
||||||
"interestRate": interest["rate"],
|
"interestRate": offer.interest_rate,
|
||||||
"managementRate": management["rate"],
|
"interestFee": interest_amount,
|
||||||
|
"managementRate": offer.management_rate,
|
||||||
"managementFee": management["fee"],
|
"managementFee": management["fee"],
|
||||||
"insuranceRate": insurance["rate"],
|
"insuranceRate": offer.insurance_rate,
|
||||||
"insuranceFee": insurance["fee"],
|
"insuranceFee": insurance["fee"],
|
||||||
"VATRate": vat["rate"],
|
"VATRate": offer.vat_rate,
|
||||||
"VATAmount": vat["fee"],
|
"VATAmount": vat["fee"],
|
||||||
"recommendedRepaymentDates": recommended_repayment_dates,
|
"recommendedRepaymentDates": recommended_repayment_dates,
|
||||||
|
"repaymentAmount": repayment_amount,
|
||||||
"installmentAmount": installment_amount,
|
"installmentAmount": installment_amount,
|
||||||
"totalRepaymentAmount": total_amount,
|
"totalRepaymentAmount": total_amount,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# "offerId": offer.id,
|
||||||
|
# "productId": product_id,
|
||||||
|
# "amount": amount,
|
||||||
|
# "upfrontPayment": upfront_payment,
|
||||||
|
# "interestRate": interest["rate"],
|
||||||
|
# "managementRate": management["rate"],
|
||||||
|
# "managementFee": management["fee"],
|
||||||
|
# "insuranceRate": insurance["rate"],
|
||||||
|
# "insuranceFee": insurance["fee"],
|
||||||
|
# "VATRate": vat["rate"],
|
||||||
|
# "VATAmount": vat["fee"],
|
||||||
|
# "recommendedRepaymentDates": recommended_repayment_dates,
|
||||||
|
# "installmentAmount": installment_amount,
|
||||||
|
# "totalRepaymentAmount": total_amount,
|
||||||
|
#
|
||||||
# Business logic - selecting an offer
|
# Business logic - selecting an offer
|
||||||
response_data = {
|
response_data = {
|
||||||
"outstandingDebtAmount": 0,
|
"outstandingDebtAmount": 0,
|
||||||
@@ -104,26 +136,24 @@ class SelectOfferService(BaseService):
|
|||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"accountId": account_id,
|
"accountId": account_id,
|
||||||
"loan": offers,
|
"loan": offers,
|
||||||
"resultCode": "00",
|
|
||||||
"resultDescription": "Successful",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return response_data
|
return ResponseHelper.success(data=response_data)
|
||||||
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
|
||||||
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({"message": "Validation exception"}), 422
|
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"{getattr(err, 'messages', str(err))}")
|
logger.error(f"{getattr(err, 'messages', str(err))}")
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({"message": str(err)}), 400
|
return ResponseHelper.error(result_description=str(err))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({"message": "Internal Server Error"}), 500
|
return ResponseHelper.internal_server_error()
|
||||||
|
|
||||||
+19
-5
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Base configuration for Flask app"""
|
"""Base configuration for Flask app"""
|
||||||
|
|
||||||
@@ -9,20 +8,19 @@ class Config:
|
|||||||
API_URL = os.getenv("API_URL", "/swagger.json")
|
API_URL = os.getenv("API_URL", "/swagger.json")
|
||||||
|
|
||||||
DEBUG = True
|
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_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
|
||||||
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
|
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
DATABASE_USER = os.environ.get("DATABASE_USER")
|
DATABASE_USER = os.environ.get("DATABASE_USER")
|
||||||
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
|
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
|
||||||
DATABASE_HOST = os.environ.get("DATABASE_HOST")
|
DATABASE_HOST = os.environ.get("DATABASE_HOST")
|
||||||
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
|
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
|
||||||
DATABASE_NAME = os.environ.get("DATABASE_NAME")
|
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_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
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")
|
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
|
||||||
@@ -31,7 +29,23 @@ class Config:
|
|||||||
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
||||||
)
|
)
|
||||||
|
|
||||||
KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
|
# 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", "testtest-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")
|
||||||
|
|
||||||
settings = Config()
|
settings = Config()
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
|
|||||||
def register_error_handlers(app):
|
def register_error_handlers(app):
|
||||||
@app.errorhandler(HTTPException)
|
@app.errorhandler(HTTPException)
|
||||||
def handle_http_exception(e):
|
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)
|
@app.errorhandler(405)
|
||||||
def method_not_allowed(error):
|
def method_not_allowed(error):
|
||||||
return jsonify({"message": "Method Not Allowed"}), 405
|
return ResponseHelper.method_not_allowed()
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
return jsonify({"message": "Resource not found"}), 404
|
return ResponseHelper.not_found()
|
||||||
|
|
||||||
@app.errorhandler(400)
|
@app.errorhandler(400)
|
||||||
def bad_request(error):
|
def bad_request(error):
|
||||||
return jsonify({"message": "Bad Request"}), 400
|
return ResponseHelper.bad_request()
|
||||||
|
|
||||||
@app.errorhandler(415)
|
@app.errorhandler(415)
|
||||||
def unsupported_media_type(error):
|
def unsupported_media_type(error):
|
||||||
return jsonify({"message": "Unsupported Media Type"}), 415
|
return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from .offer import Offer
|
|||||||
from .charge import Charge
|
from .charge import Charge
|
||||||
from .rac_checks import RACCheck
|
from .rac_checks import RACCheck
|
||||||
from .loan_repayment_schedule import LoanRepaymentSchedule
|
from .loan_repayment_schedule import LoanRepaymentSchedule
|
||||||
|
from .transaction_offers import TransactionOffer
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule']
|
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer']
|
||||||
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class Account(db.Model):
|
class Account(db.Model):
|
||||||
__tablename__ = 'accounts'
|
__tablename__ = 'accounts'
|
||||||
@@ -11,8 +12,8 @@ class Account(db.Model):
|
|||||||
account_type = db.Column(db.String(50))
|
account_type = db.Column(db.String(50))
|
||||||
status = db.Column(db.String(20), default='active')
|
status = db.Column(db.String(20), default='active')
|
||||||
lien_amount = db.Column(db.Float, default=0.0)
|
lien_amount = db.Column(db.Float, default=0.0)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
customer = relationship(
|
customer = relationship(
|
||||||
"Customer",
|
"Customer",
|
||||||
@@ -26,7 +27,9 @@ class Account(db.Model):
|
|||||||
account = cls(
|
account = cls(
|
||||||
id=id,
|
id=id,
|
||||||
customer_id=customer_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:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
||||||
class Charge(db.Model):
|
class Charge(db.Model):
|
||||||
@@ -12,9 +13,8 @@ class Charge(db.Model):
|
|||||||
percent = db.Column(db.Float, default=0.0)
|
percent = db.Column(db.Float, default=0.0)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
due = db.Column(db.Integer, nullable=False)
|
due = db.Column(db.Integer, nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
offer = relationship(
|
offer = relationship(
|
||||||
"Offer",
|
"Offer",
|
||||||
primaryjoin="Charge.offer_id == Offer.id",
|
primaryjoin="Charge.offer_id == Offer.id",
|
||||||
@@ -57,7 +57,9 @@ class Charge(db.Model):
|
|||||||
code = code,
|
code = code,
|
||||||
percent = percent,
|
percent = percent,
|
||||||
description = description,
|
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)
|
db.session.add(charge_obj)
|
||||||
|
|||||||
+25
-5
@@ -1,8 +1,12 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
#
|
||||||
|
# from app.api.services.offer_analysis import logger
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.account import Account
|
from app.models.account import Account
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
# from app.utils.logger import logger
|
||||||
|
|
||||||
class Customer(db.Model):
|
class Customer(db.Model):
|
||||||
__tablename__ = 'customers'
|
__tablename__ = 'customers'
|
||||||
@@ -10,9 +14,8 @@ class Customer(db.Model):
|
|||||||
id = db.Column(db.String(50), primary_key=True)
|
id = db.Column(db.String(50), primary_key=True)
|
||||||
msisdn = db.Column(db.String(20), unique=True, nullable=False)
|
msisdn = db.Column(db.String(20), unique=True, nullable=False)
|
||||||
country_code = db.Column(db.String(3), nullable=False)
|
country_code = db.Column(db.String(3), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
accounts = relationship(
|
accounts = relationship(
|
||||||
"Account",
|
"Account",
|
||||||
primaryjoin="Customer.id == Account.customer_id",
|
primaryjoin="Customer.id == Account.customer_id",
|
||||||
@@ -27,6 +30,13 @@ class Customer(db.Model):
|
|||||||
back_populates="customer",
|
back_populates="customer",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transaction_offers = relationship(
|
||||||
|
"TransactionOffer",
|
||||||
|
primaryjoin="Customer.id == TransactionOffer.customer_id",
|
||||||
|
foreign_keys="TransactionOffer.customer_id",
|
||||||
|
back_populates="customer",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_valid_customer(cls, customer_id):
|
def is_valid_customer(cls, customer_id):
|
||||||
customer = cls.query.filter_by(id=customer_id).first()
|
customer = cls.query.filter_by(id=customer_id).first()
|
||||||
@@ -38,9 +48,19 @@ class Customer(db.Model):
|
|||||||
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
|
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
|
||||||
if cls.query.filter_by(id=id).first():
|
if cls.query.filter_by(id=id).first():
|
||||||
raise ValueError("Customer already exists")
|
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
|
# 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:
|
try:
|
||||||
db.session.add(customer)
|
db.session.add(customer)
|
||||||
|
|
||||||
@@ -56,7 +76,7 @@ class Customer(db.Model):
|
|||||||
return customer
|
return customer
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_customer(cls, customer_id):
|
def get_customer_with_loan_list(cls, customer_id):
|
||||||
"""
|
"""
|
||||||
Get customer by ID.
|
Get customer by ID.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+83
-7
@@ -1,10 +1,17 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from itertools import product
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.customer import Customer
|
from app.models.customer import Customer
|
||||||
from app.models.account import Account
|
from app.models.account import Account
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from dateutil.relativedelta import relativedelta
|
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):
|
class Loan(db.Model):
|
||||||
@@ -17,6 +24,7 @@ class Loan(db.Model):
|
|||||||
)
|
)
|
||||||
customer_id = db.Column(db.String(50), nullable=False)
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
transaction_id = db.Column(db.String(50), nullable=True)
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
original_transaction = db.Column(db.String(50), nullable=True)
|
||||||
account_id = db.Column(db.String(50), nullable=False)
|
account_id = db.Column(db.String(50), nullable=False)
|
||||||
offer_id = db.Column(db.String(20), nullable=False)
|
offer_id = db.Column(db.String(20), nullable=False)
|
||||||
product_id = db.Column(db.String(20), nullable=True)
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
@@ -29,9 +37,14 @@ class Loan(db.Model):
|
|||||||
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||||
status = db.Column(db.String(20), default='pending')
|
status = db.Column(db.String(20), default='pending')
|
||||||
|
tenor = db.Column(db.Integer, nullable=True)
|
||||||
due_date = db.Column(db.DateTime, nullable=True)
|
due_date = db.Column(db.DateTime, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
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)
|
||||||
|
|
||||||
customer = relationship(
|
customer = relationship(
|
||||||
"Customer",
|
"Customer",
|
||||||
@@ -65,10 +78,14 @@ class Loan(db.Model):
|
|||||||
initial_loan_amount,
|
initial_loan_amount,
|
||||||
collection_type,
|
collection_type,
|
||||||
transaction_id,
|
transaction_id,
|
||||||
|
original_transaction,
|
||||||
upfront_fee,
|
upfront_fee,
|
||||||
repayment_amount,
|
repayment_amount,
|
||||||
installment_amount,
|
installment_amount,
|
||||||
status="pending",
|
tenor,
|
||||||
|
eligible_amount,
|
||||||
|
reference,
|
||||||
|
status = "pending",
|
||||||
):
|
):
|
||||||
# Check if customer exists
|
# Check if customer exists
|
||||||
customer = Customer.is_valid_customer(customer_id)
|
customer = Customer.is_valid_customer(customer_id)
|
||||||
@@ -76,6 +93,7 @@ class Loan(db.Model):
|
|||||||
raise ValueError("Customer does not exist")
|
raise ValueError("Customer does not exist")
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
due_date = now + timedelta(days=tenor)
|
||||||
|
|
||||||
# Create and save the loan
|
# Create and save the loan
|
||||||
loan = cls(
|
loan = cls(
|
||||||
@@ -85,13 +103,19 @@ class Loan(db.Model):
|
|||||||
product_id = product_id,
|
product_id = product_id,
|
||||||
collection_type = collection_type,
|
collection_type = collection_type,
|
||||||
transaction_id = transaction_id,
|
transaction_id = transaction_id,
|
||||||
|
original_transaction = original_transaction,
|
||||||
initial_loan_amount = initial_loan_amount,
|
initial_loan_amount = initial_loan_amount,
|
||||||
current_loan_amount = initial_loan_amount,
|
current_loan_amount = initial_loan_amount,
|
||||||
upfront_fee = upfront_fee,
|
upfront_fee = upfront_fee,
|
||||||
repayment_amount = repayment_amount,
|
repayment_amount = repayment_amount,
|
||||||
installment_amount = installment_amount,
|
installment_amount = installment_amount,
|
||||||
due_date=now,
|
due_date=due_date,
|
||||||
status = status
|
tenor = tenor,
|
||||||
|
status = status,
|
||||||
|
eligible_amount =eligible_amount,
|
||||||
|
reference = reference,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -115,13 +139,61 @@ class Loan(db.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_customer_loan(cls, loan_id, customer_id):
|
def get_customer_loan(cls, loan_id, customer_id):
|
||||||
"""
|
"""
|
||||||
Get customer's active loans.
|
Get customer's active loans by loan_id.
|
||||||
"""
|
"""
|
||||||
loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first()
|
loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first()
|
||||||
if not loan:
|
if not loan:
|
||||||
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
|
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
|
||||||
return loan
|
return loan
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
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
|
@classmethod
|
||||||
def update_status(cls, loan_id, status):
|
def update_status(cls, loan_id, status):
|
||||||
"""
|
"""
|
||||||
@@ -145,6 +217,9 @@ class Loan(db.Model):
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'debtId': self.id,
|
'debtId': self.id,
|
||||||
|
'transactionId': self.transaction_id,
|
||||||
|
'loanRef': self.reference,
|
||||||
|
'productId': self.product_id,
|
||||||
'initialLoanAmount': self.initial_loan_amount,
|
'initialLoanAmount': self.initial_loan_amount,
|
||||||
'currentLoanAmount': self.current_loan_amount,
|
'currentLoanAmount': self.current_loan_amount,
|
||||||
'defaultPenaltyFee': self.default_penalty_fee,
|
'defaultPenaltyFee': self.default_penalty_fee,
|
||||||
@@ -154,6 +229,7 @@ class Loan(db.Model):
|
|||||||
'repaymentAmount': self.repayment_amount,
|
'repaymentAmount': self.repayment_amount,
|
||||||
'installmentAmount': self.installment_amount,
|
'installmentAmount': self.installment_amount,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
|
'tenor': self.tenor,
|
||||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||||
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-14
@@ -1,6 +1,8 @@
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from app.utils.logger import logger
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
||||||
class LoanCharge(db.Model):
|
class LoanCharge(db.Model):
|
||||||
@@ -15,9 +17,8 @@ class LoanCharge(db.Model):
|
|||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
due = db.Column(db.Integer, nullable=False)
|
due = db.Column(db.Integer, nullable=False)
|
||||||
due_date = db.Column(db.DateTime, nullable=True)
|
due_date = db.Column(db.DateTime, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
loan = relationship(
|
loan = relationship(
|
||||||
"Loan",
|
"Loan",
|
||||||
primaryjoin="LoanCharge.loan_id == Loan.id",
|
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||||
@@ -35,8 +36,8 @@ class LoanCharge(db.Model):
|
|||||||
charges (list): A list of dictionaries with keys:
|
charges (list): A list of dictionaries with keys:
|
||||||
code (str), amount (float), percent (float), description (str), due (int)
|
code (str), amount (float), percent (float), description (str), due (int)
|
||||||
"""
|
"""
|
||||||
if not charges or not isinstance(charges, list):
|
# if not charges or not isinstance(charges, list):
|
||||||
raise ValueError("Charges must be a non-empty list of dictionaries")
|
# raise ValueError("Charges must be a non-empty list of dictionaries")
|
||||||
|
|
||||||
if loan_id is None:
|
if loan_id is None:
|
||||||
raise ValueError("loan_id cannot be None")
|
raise ValueError("loan_id cannot be None")
|
||||||
@@ -44,23 +45,27 @@ class LoanCharge(db.Model):
|
|||||||
loan_charges = []
|
loan_charges = []
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
for charge in charges:
|
|
||||||
due_days = getattr(charge, "due", 0)
|
|
||||||
amount = getattr(charge, "amount", 0.0)
|
|
||||||
percent = getattr(charge, "percent", 0.0)
|
|
||||||
|
|
||||||
if amount == 0.0:
|
subset_keys = ['interest', 'management', 'insurance', 'vat']
|
||||||
amount = (percent / 100.0) * referenced_amount
|
for item in subset_keys:
|
||||||
|
charge = charges[item]
|
||||||
|
due_days = charge['due_days'] # getattr(charge, "due_days", 0)
|
||||||
|
amount = charge['fee'] # getattr(charge, "fee", 0.0)
|
||||||
|
percent = charge['rate'] # getattr(charge, "rate", 0.0)
|
||||||
|
code = charge['code'] # getattr(charge, "code","")
|
||||||
|
description = charge['description'] # getattr(charge, "description", "")
|
||||||
|
|
||||||
charge_obj = cls(
|
charge_obj = cls(
|
||||||
loan_id = loan_id,
|
loan_id = loan_id,
|
||||||
transaction_id = transaction_id,
|
transaction_id = transaction_id,
|
||||||
code = getattr(charge, "code"),
|
code = code,
|
||||||
amount = round(amount, 2),
|
amount = round(amount, 2),
|
||||||
percent = percent,
|
percent = percent,
|
||||||
description = getattr(charge, "description", ""),
|
description = description,
|
||||||
due = due_days,
|
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)
|
db.session.add(charge_obj)
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ from datetime import datetime, timezone
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class LoanRepaymentSchedule(db.Model):
|
class LoanRepaymentSchedule(db.Model):
|
||||||
__tablename__ = 'loan_repayment_schedules'
|
__tablename__ = 'loan_repayment_schedules'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
loan_id = db.Column(db.Integer, nullable=False)
|
loan_id = db.Column(db.Integer, nullable=False)
|
||||||
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
product_id = db.Column(db.String(20), nullable=True)
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
installment_number = db.Column(db.Integer, nullable=False)
|
installment_number = db.Column(db.Integer, nullable=False)
|
||||||
due_date = db.Column(db.DateTime, nullable=False)
|
due_date = db.Column(db.DateTime, nullable=False)
|
||||||
@@ -16,9 +18,8 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
paid = db.Column(db.Boolean, default=False)
|
paid = db.Column(db.Boolean, default=False)
|
||||||
paid_at = db.Column(db.DateTime, nullable=True)
|
paid_at = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
loan = relationship(
|
loan = relationship(
|
||||||
"Loan",
|
"Loan",
|
||||||
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||||
@@ -28,14 +29,14 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_repayment_schedule(cls, loan, tenor):
|
def add_repayment_schedule(cls, loan, num_schedules, transaction_id):
|
||||||
"""
|
"""
|
||||||
Add repayment schedules for a given loan.
|
Add repayment schedules for a given loan.
|
||||||
"""
|
"""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
schedules = []
|
schedules = []
|
||||||
|
|
||||||
for i in range(tenor):
|
for i in range(num_schedules):
|
||||||
due_date = now + relativedelta(months=i + 1)
|
due_date = now + relativedelta(months=i + 1)
|
||||||
schedule = LoanRepaymentSchedule(
|
schedule = LoanRepaymentSchedule(
|
||||||
loan_id=loan.id,
|
loan_id=loan.id,
|
||||||
@@ -43,7 +44,10 @@ class LoanRepaymentSchedule(db.Model):
|
|||||||
due_date=due_date,
|
due_date=due_date,
|
||||||
total_repayment_amount = round(loan.repayment_amount, 2),
|
total_repayment_amount = round(loan.repayment_amount, 2),
|
||||||
installment_amount=round(loan.installment_amount, 2),
|
installment_amount=round(loan.installment_amount, 2),
|
||||||
product_id = loan.product_id
|
product_id = loan.product_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(schedule)
|
db.session.add(schedule)
|
||||||
|
|||||||
+15
-3
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.charge import Charge
|
from app.models.charge import Charge
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class Offer(db.Model):
|
class Offer(db.Model):
|
||||||
__tablename__ = 'offers'
|
__tablename__ = 'offers'
|
||||||
@@ -17,8 +18,11 @@ class Offer(db.Model):
|
|||||||
insurance_rate = db.Column(db.Float, default=1.0)
|
insurance_rate = db.Column(db.Float, default=1.0)
|
||||||
vat_rate = db.Column(db.Float, default=7.5)
|
vat_rate = db.Column(db.Float, default=7.5)
|
||||||
list_order = db.Column(db.Integer, nullable=True)
|
list_order = db.Column(db.Integer, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
max_daily_loans = db.Column(db.Integer, nullable=True)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
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(
|
charges = relationship(
|
||||||
"Charge",
|
"Charge",
|
||||||
@@ -75,7 +79,15 @@ class Offer(db.Model):
|
|||||||
"productId": self.product_id,
|
"productId": self.product_id,
|
||||||
"minAmount": self.min_amount,
|
"minAmount": self.min_amount,
|
||||||
"maxAmount": self.max_amount,
|
"maxAmount": self.max_amount,
|
||||||
"tenor": self.tenor
|
"tenor": self.tenor,
|
||||||
|
"interest_rate": self.interest_rate,
|
||||||
|
"management_rate": self.management_rate,
|
||||||
|
"insurance_rate": self.insurance_rate,
|
||||||
|
"vat_rate": self.vat_rate,
|
||||||
|
"maxDailyLoans": self.max_daily_loans,
|
||||||
|
"maxActiveLoans": self.max_active_loans,
|
||||||
|
"maxLifeLoans": self.max_life_loans
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.exc import IntegrityError
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from sqlalchemy.types import JSON
|
from sqlalchemy.types import JSON
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class RACCheck(db.Model):
|
class RACCheck(db.Model):
|
||||||
__tablename__ = 'rac_checks'
|
__tablename__ = 'rac_checks'
|
||||||
|
|
||||||
id = db.Column(db.String, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
transaction_id = db.Column(db.String(50), nullable=False)
|
transaction_id = db.Column(db.String(50), nullable=False)
|
||||||
customer_id = db.Column(db.String, nullable=False)
|
customer_id = db.Column(db.String, nullable=False)
|
||||||
account_id = db.Column(db.String, nullable=False)
|
account_id = db.Column(db.String, nullable=False)
|
||||||
rac_response = db.Column(db.JSON, nullable=False)
|
rac_response = db.Column(db.JSON, nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
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):
|
||||||
|
|
||||||
|
|
||||||
|
# Save the response
|
||||||
|
rac_check = cls(
|
||||||
|
customer_id = customer_id,
|
||||||
|
account_id = account_id,
|
||||||
|
transaction_id = transaction_id,
|
||||||
|
rac_response = data,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(rac_check)
|
||||||
|
except IntegrityError as err:
|
||||||
|
raise ValueError(f"Database integrity error: {err}")
|
||||||
|
return rac_check
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_rac_checks(cls):
|
def get_all_rac_checks(cls):
|
||||||
@@ -24,18 +45,19 @@ class RACCheck(db.Model):
|
|||||||
rac_checks = cls.query.all()
|
rac_checks = cls.query.all()
|
||||||
|
|
||||||
if not rac_checks:
|
if not rac_checks:
|
||||||
raise ValueError("No available RAC checks")
|
return None
|
||||||
return rac_checks
|
return rac_checks
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_rac_check_by_id(cls, check_id):
|
def get_rac_check(cls, customer_id, account_id):
|
||||||
"""
|
"""
|
||||||
Return a RAC check by its ID.
|
Return a RAC check by its ID.
|
||||||
"""
|
"""
|
||||||
rac_check = cls.query.filter_by(id=check_id).first()
|
rac_check = cls.query.filter_by( customer_id = customer_id,
|
||||||
|
account_id = account_id,).first()
|
||||||
|
|
||||||
if not rac_check:
|
if not rac_check:
|
||||||
raise ValueError(f"RAC Check with ID {check_id} not found")
|
raise ValueError(f"RAC Check for customer not found")
|
||||||
return rac_check
|
return rac_check
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|||||||
+10
-15
@@ -4,6 +4,7 @@ from app.extensions import db
|
|||||||
from app.models.customer import Customer
|
from app.models.customer import Customer
|
||||||
from app.models.loan import Loan
|
from app.models.loan import Loan
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
||||||
class Repayment(db.Model):
|
class Repayment(db.Model):
|
||||||
@@ -17,31 +18,25 @@ class Repayment(db.Model):
|
|||||||
loan_id = db.Column(db.String(50), nullable=False)
|
loan_id = db.Column(db.String(50), nullable=False)
|
||||||
customer_id = db.Column(db.String(50), nullable=False)
|
customer_id = db.Column(db.String(50), nullable=False)
|
||||||
product_id = db.Column(db.String(20), nullable=True)
|
product_id = db.Column(db.String(20), nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
transaction_id = db.Column(db.String(50), nullable=True)
|
transaction_id = db.Column(db.String(50), nullable=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_repayment(cls, customer_id, loan_id, product_id, transaction_id):
|
def create_repayment(cls, customer_id, loan, 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)
|
|
||||||
|
|
||||||
# Check that the loan is active
|
# 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})")
|
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
|
||||||
|
|
||||||
|
|
||||||
repayment = cls(
|
repayment = cls(
|
||||||
customer_id=customer_id,
|
customer_id=customer_id,
|
||||||
loan_id=loan_id,
|
loan_id=loan.id,
|
||||||
product_id=product_id,
|
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from app.extensions import db
|
|||||||
from app.models import account
|
from app.models import account
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy import and_, or_, not_
|
from sqlalchemy import and_, or_, not_
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
class Transaction(db.Model):
|
class Transaction(db.Model):
|
||||||
__tablename__ = 'transactions'
|
__tablename__ = 'transactions'
|
||||||
@@ -16,9 +17,8 @@ class Transaction(db.Model):
|
|||||||
customer_id = db.Column(db.String(50), nullable=True)
|
customer_id = db.Column(db.String(50), nullable=True)
|
||||||
type = db.Column(db.String(50), nullable=False)
|
type = db.Column(db.String(50), nullable=False)
|
||||||
channel = 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))
|
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Transaction {self.id}>'
|
return f'<Transaction {self.id}>'
|
||||||
|
|
||||||
@@ -38,7 +38,9 @@ class Transaction(db.Model):
|
|||||||
customer_id = customer_id,
|
customer_id = customer_id,
|
||||||
account_id = account_id,
|
account_id = account_id,
|
||||||
type = type,
|
type = type,
|
||||||
channel = channel
|
channel = channel,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
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)
|
||||||
|
max_amount = db.Column(db.Float, nullable=False)
|
||||||
|
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(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",
|
||||||
|
foreign_keys=[customer_id],
|
||||||
|
back_populates="transaction_offers",
|
||||||
|
)
|
||||||
|
|
||||||
|
@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
|
||||||
|
# transaction_id = transaction_id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
if not transaction_offer:
|
||||||
|
return False
|
||||||
|
return transaction_offer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(transaction_offer)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
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,
|
||||||
|
'customerId': self.customer_id,
|
||||||
|
'transactionId': self.transaction_id,
|
||||||
|
'offerId': self.offer_id,
|
||||||
|
'productId': self.product_id,
|
||||||
|
'minAmount': self.min_amount,
|
||||||
|
'maxAmount': self.max_amount,
|
||||||
|
'eligibleAmount': self.eligible_amount,
|
||||||
|
'tenor': self.tenor,
|
||||||
|
'createdAt': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<TransactionOffer {self.id}>'
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tr201712RK9232P115"
|
"example": "Tr201712RK9232P115"
|
||||||
},
|
},
|
||||||
|
"loanRef": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1620029887USSDAMPC"
|
||||||
|
},
|
||||||
"customerId": {
|
"customerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CN621868"
|
"example": "CN621868"
|
||||||
|
|||||||
@@ -9,10 +9,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "10"
|
"example": "10"
|
||||||
},
|
},
|
||||||
"productId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "101"
|
|
||||||
},
|
|
||||||
"transactionId": {
|
"transactionId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "20171209232115"
|
"example": "20171209232115"
|
||||||
@@ -21,9 +17,9 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "CID0000025585"
|
"example": "CID0000025585"
|
||||||
},
|
},
|
||||||
"channel": {
|
"loanRef": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "USSD"
|
"example": "Trx5847365252USSD3MPC"
|
||||||
},
|
},
|
||||||
"accountId": {
|
"accountId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
"example": "ACN8263457"
|
"example": "ACN8263457"
|
||||||
},
|
},
|
||||||
"productId": {
|
"productId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "3MPC"
|
||||||
|
},
|
||||||
|
"offerId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "101"
|
"example": "101"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"productId": {
|
"productId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2030"
|
"example": "3MPC"
|
||||||
},
|
},
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@@ -49,6 +49,11 @@
|
|||||||
"format": "float",
|
"format": "float",
|
||||||
"example": 3.0
|
"example": 3.0
|
||||||
},
|
},
|
||||||
|
"interestFee": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 3000.00
|
||||||
|
},
|
||||||
"ManagementRate": {
|
"ManagementRate": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "float",
|
"format": "float",
|
||||||
|
|||||||
@@ -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,52 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 3105abd795d4
|
||||||
|
Revises: 95a52be203c4
|
||||||
|
Create Date: 2025-05-07 11:44:18.483694
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3105abd795d4'
|
||||||
|
down_revision = '95a52be203c4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
|
||||||
|
# Step 1: Drop the default value
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
server_default=None,
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
existing_nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
|
||||||
|
# Step 2: Change the column type
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
postgresql_using='id::integer'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.VARCHAR(),
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
existing_server_default=sa.text("''::character varying"))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration for mloan table
|
||||||
|
|
||||||
|
Revision ID: 38acee611d55
|
||||||
|
Revises: f1e83a993034
|
||||||
|
Create Date: 2025-04-30 09:55:30.552838
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '38acee611d55'
|
||||||
|
down_revision = 'f1e83a993034'
|
||||||
|
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('tenor', sa.Integer(), 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('tenor')
|
||||||
|
|
||||||
|
# ### 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,41 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 86e701febdda
|
||||||
|
Revises: eb99c7fb9e09
|
||||||
|
Create Date: 2025-04-29 07:59:33.305967
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '86e701febdda'
|
||||||
|
down_revision = 'eb99c7fb9e09'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('transaction_offers',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('customer_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('transaction_id', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('offer_id', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('min_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('max_amount', sa.Float(), nullable=False),
|
||||||
|
sa.Column('eligible_amount', sa.Float(), nullable=True),
|
||||||
|
sa.Column('tenor', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('transaction_offers')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Sat May 3 21:53:29 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: 95a52be203c4
|
||||||
|
Revises: 38acee611d55
|
||||||
|
Create Date: 2025-05-03 21:53:32.154029
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '95a52be203c4'
|
||||||
|
down_revision = '38acee611d55'
|
||||||
|
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('eligible_amount', sa.Float(), 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('eligible_amount')
|
||||||
|
|
||||||
|
# ### 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 ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Sat Apr 26 19:02:17 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: eb99c7fb9e09
|
||||||
|
Revises: 89759cebb9c6
|
||||||
|
Create Date: 2025-04-26 19:02:20.443678
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'eb99c7fb9e09'
|
||||||
|
down_revision = '89759cebb9c6'
|
||||||
|
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('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('loans', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('original_transaction')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Migration on Tue Apr 29 20:43:35 UTC 2025
|
||||||
|
|
||||||
|
Revision ID: f1e83a993034
|
||||||
|
Revises: 86e701febdda
|
||||||
|
Create Date: 2025-04-29 20:43:38.595543
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f1e83a993034'
|
||||||
|
down_revision = '86e701febdda'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('transaction_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user