Compare commits

..

122 Commits

Author SHA1 Message Date
VivianDee 3c0e00d9e4 Merge branch 'oracle_migration' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore into oracle_migration 2025-07-09 12:34:54 +01:00
VivianDee b502c128f9 [fix]: migration 2025-07-09 12:33:50 +01:00
VivianDee c81a029e20 Update 48c62b4da905_.py 2025-07-09 12:33:50 +01:00
VivianDee 469d94cea1 [add]: migration 2025-07-09 12:33:50 +01:00
VivianDee d6aabb959e [add]: MIgration to oracle database 2025-07-09 12:33:50 +01:00
VivianDee 3a970816f5 [fix]: migration 2025-07-06 09:41:44 +01:00
VivianDee 65c1608b92 Update 48c62b4da905_.py 2025-07-06 09:33:04 +01:00
VivianDee 2493c075c0 [add]: balance to loan table 2025-07-06 09:26:59 +01:00
VivianDee 4ea06de2bc [add]: migration 2025-07-03 14:56:55 +01:00
VivianDee 1aa6a1710c [add]: MIgration to oracle database 2025-07-03 05:02:08 +01:00
ameye dff000dbb2 Merge branch 'add_balance_to_loan' of DigiFi/digifi-BankToProductCore into master 2025-06-23 10:41:24 +00:00
VivianDee 617738b785 [add]: balance to loan table 2025-06-23 11:13:40 +01:00
CHIEFSOFT\ameye 9db3b68b13 ACTIVE_PARTIAL = "active_partial" 2025-06-22 21:46:44 -04:00
CHIEFSOFT\ameye 1795db35be Revert "mercore starter"
This reverts commit 8bb5ce69e2.
2025-06-22 20:14:37 -04:00
CHIEFSOFT\ameye 8bb5ce69e2 mercore starter 2025-06-22 20:11:32 -04:00
CHIEFSOFT\ameye 5087afcca6 repayment id 2025-06-21 11:27:53 -04:00
CHIEFSOFT\ameye 11b52357ba reapyment is needed 2025-06-21 11:18:36 -04:00
ameye e8361668e5 Merge branch 'salary_table_fix' of DigiFi/digifi-BankToProductCore into master 2025-06-19 09:51:38 +00:00
VivianDee f659fa9cf2 [add]: salary model fix 2025-06-19 04:42:11 +01:00
CHIEFSOFT\ameye 23e340be27 Data adjust 2025-06-18 23:00:15 -04:00
ameye adc2498044 Merge branch 'add_salary_table' of DigiFi/digifi-BankToProductCore into master 2025-06-18 12:32:12 +00:00
VivianDee eb7f783b18 [add]: Salary table 2025-06-18 12:34:28 +01:00
ameye 5040002c54 Merge branch 'update_repayments_data_table' of DigiFi/digifi-BankToProductCore into master 2025-06-17 15:11:06 +00:00
VivianDee dc9415ff79 [fix]: Select offer requested amount 2025-06-16 15:40:50 +01:00
ameye 4822de764a Merge branch 'update_repayments_data_table' of DigiFi/digifi-BankToProductCore into master 2025-06-16 14:34:40 +00:00
VivianDee 51995a3e02 [update]: Repayments table 2025-06-16 12:27:04 +01:00
ameye 265bba2365 Merge branch 'loan_count_fix' of DigiFi/digifi-BankToProductCore into master 2025-06-13 18:53:52 +00:00
VivianDee 9985a58b56 [fix]: Max daily loan count 2025-06-13 19:11:10 +01:00
ameye b41df3fe02 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-13 13:01:45 +00:00
VivianDee 08fe04b7b9 Update offer_analysis.py 2025-06-13 13:57:10 +01:00
vivian.d.simbrellang.com 79317632b6 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-13 12:33:10 +00:00
VivianDee 0d87036b92 Update config.py 2025-06-13 13:32:03 +01:00
ameye 1734007476 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-12 22:46:20 +00:00
VivianDee 6ef2be9625 [add]: Offer analysis update 2025-06-12 15:24:14 +01:00
VivianDee 48020f5284 Update eligibility_check.py 2025-06-12 14:35:32 +01:00
VivianDee 1a6ac6a37f Update offer_analysis.py 2025-06-12 14:00:15 +01:00
ameye bbf6953dc5 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-12 11:44:46 +00:00
VivianDee a2158a768e [update]: Rac Check analysis 2025-06-11 15:19:24 +01:00
CHIEFSOFT\ameye 0af1b7567b Rack Rules 2025-06-10 06:58:53 -04:00
vivian.d.simbrellang.com 4d08983ae3 Merge branch 'repayments_data_model' of DigiFi/digifi-BankToProductCore into master 2025-06-10 07:44:44 +00:00
VivianDee 70e15cd325 [add]: Repayments data Model 2025-06-10 08:42:41 +01:00
CHIEFSOFT\ameye e8d930f9b8 offer analysis 2025-06-05 23:06:09 -04:00
CHIEFSOFT\ameye c400f1d69d addded comments 2025-06-05 18:49:14 -04:00
vivian.d.simbrellang.com f7daa12531 Merge branch 'loan_and_repayment_tables_update' of DigiFi/digifi-BankToProductCore into master 2025-06-04 11:45:24 +00:00
VivianDee 3242a57586 [update]: Loan and repayment table 2025-06-04 12:43:38 +01:00
ameye 463c0a0def Merge branch 'middlewear-integration' of DigiFi/digifi-BankToProductCore into master 2025-06-04 02:36:14 +00:00
VivianDee c061c9b5a4 Update eligibility_check.py 2025-06-02 08:16:12 -04:00
VivianDee 201fa4202e Update eligibility_check.py 2025-06-02 08:07:15 -04:00
CHIEFSOFT\ameye bb4d7ac064 Log added 2025-05-31 21:31:12 -04:00
CHIEFSOFT\ameye 5a2161acaa added acont id 2025-05-31 20:44:42 -04:00
VivianDee 10138f66f3 Update eligibility_check.py 2025-05-30 08:25:40 -04:00
CHIEFSOFT\ameye 9ea0027f71 channel 2025-05-29 15:19:29 -04:00
VivianDee d1b8d15f31 Update eligibility_check.py 2025-05-29 13:00:51 -04:00
ameye f716b47603 Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-28 20:22:53 +00:00
VivianDee ec5db19e20 [add]: loan max amount 2025-05-28 20:19:50 +01:00
VivianDee 729cc26698 [add]: loan ref to repayment 2025-05-28 20:05:38 +01:00
ameye c95e2786b5 Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-27 11:14:42 +00:00
VivianDee 65472d3f07 [update]: loan status response 2025-05-27 02:02:22 +01:00
ameye 29b2697b0e Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-26 12:10:35 +00:00
VivianDee 7a2ff6586f Update provide_loan.py 2025-05-26 13:01:27 +01:00
VivianDee 066ced55b0 Update provide_loan.py 2025-05-26 12:56:49 +01:00
CHIEFSOFT\ameye f6c98d9bfd repayment status 2025-05-25 06:33:46 -04:00
CHIEFSOFT\ameye 9e22e0fcf3 customer with loan listing 2025-05-24 07:41:20 -04:00
CHIEFSOFT\ameye c9aba07e9c Funct log 2025-05-24 07:10:44 -04:00
CHIEFSOFT\ameye 20c9a5c713 Remove logger 2025-05-24 07:08:03 -04:00
CHIEFSOFT\ameye 1cb0d88cc2 No need for new logger 2025-05-24 07:05:18 -04:00
CHIEFSOFT\ameye 3e9d5d4089 Looking for customer 2025-05-24 07:02:50 -04:00
CHIEFSOFT\ameye 2ae49ace86 fix call 2025-05-24 06:04:19 -04:00
CHIEFSOFT\ameye e9de001340 fix string 2025-05-24 05:50:17 -04:00
CHIEFSOFT\ameye 0bdc11423f Rack analysis 2025-05-24 05:37:21 -04:00
CHIEFSOFT\ameye a4ed936392 racResponse 2025-05-24 05:01:20 -04:00
CHIEFSOFT\ameye 1a315b1d80 SIMBRELLA_API_KEY 2025-05-24 04:52:58 -04:00
CHIEFSOFT\ameye 4c30f81bfd Updated racc check payload 2025-05-24 04:42:58 -04:00
CHIEFSOFT\ameye 916261fa94 Fix url 2025-05-24 04:34:11 -04:00
CHIEFSOFT\ameye 081b73a932 Added logger 2025-05-24 04:31:25 -04:00
CHIEFSOFT\ameye 6852986ce5 end poit config 2025-05-24 04:27:07 -04:00
CHIEFSOFT\ameye 0038c22577 SIMBRELLA_ENDPOINT_RAC_CHECKS 2025-05-24 04:22:40 -04:00
ameye 326ee87b13 Merge branch 'loan_min_amount' of DigiFi/digifi-BankToProductCore into master 2025-05-21 14:58:49 +00:00
VivianDee ca22ee86f7 [add]: Loan min amount 2025-05-21 15:49:11 +01:00
ameye aa033a50a3 Merge branch 'Offers_update' of DigiFi/digifi-BankToProductCore into master 2025-05-19 15:15:13 +00:00
VivianDee 31b0367e6a [update]: Offers 2025-05-19 14:37:19 +01:00
VivianDee 89760f81ed [update]: Offers Model 2025-05-19 11:51:44 +01:00
CHIEFSOFT\ameye 701840abd1 Removed product id 2025-05-18 08:33:18 -04:00
ameye df6c42ca2d Merge branch 'Response_fix' of DigiFi/digifi-BankToProductCore into master 2025-05-16 19:21:59 +00:00
VivianDee a81313447b [add]: timestamp fix 2025-05-16 13:40:33 +01:00
ameye a321832d43 Merge branch 'Response_fix' of DigiFi/digifi-BankToProductCore into master 2025-05-14 21:50:14 +00:00
VivianDee 0369323dd6 Update transaction_offers.py 2025-05-14 20:05:20 +01:00
ameye d7b8addeb6 Merge branch 'Response_fix' of DigiFi/digifi-BankToProductCore into master 2025-05-12 18:01:27 +00:00
VivianDee 0db3f44c7b Update offer_analysis.py 2025-05-12 17:19:16 +01:00
VivianDee a7d465bd5c Merge branch 'Response_fix' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore into Response_fix 2025-05-12 17:12:09 +01:00
VivianDee a0ba49f208 [update]: Resposes 2025-05-12 16:31:55 +01:00
VivianDee 1e4f9102c8 Update provide_loan.py 2025-05-12 16:08:42 +01:00
VivianDee dee1edee40 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore 2025-05-12 16:06:24 +01:00
VivianDee 06b5f98f06 [update]: Resposes 2025-05-12 15:36:24 +01:00
CHIEFSOFT\ameye 746ca486da Fix data 2025-05-11 23:44:36 -04:00
CHIEFSOFT\ameye 3d81322515 Fix Intetrest Fee 2025-05-11 20:04:49 -04:00
CHIEFSOFT\ameye eeacffad9a Loan Reference added 2025-05-11 16:05:36 -04:00
CHIEFSOFT\ameye 11a239c67a Linked loan design 2025-05-10 20:08:27 -04:00
CHIEFSOFT\ameye 4ce0142ee0 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore
# Conflicts:
#	app/models/loan.py
2025-05-10 11:18:37 -04:00
ameye c268c4d92b Merge branch 'define_offers' of DigiFi/digifi-BankToProductCore into master 2025-05-10 14:43:43 +00:00
Fluxtra 6d743ea09b Update offer_analysis.py 2025-05-10 15:42:09 +01:00
ameye 89dd4bb191 Merge branch 'define_offers' of DigiFi/digifi-BankToProductCore into master 2025-05-10 14:35:14 +00:00
Fluxtra 4718c9c50b Update loan.py 2025-05-10 15:34:43 +01:00
Fluxtra feb97c3fa8 Update loan.py 2025-05-10 15:32:11 +01:00
Fluxtra 4bcaa3d13d Update loan.py 2025-05-10 15:30:15 +01:00
CHIEFSOFT\ameye b86bd3dece Fix ident 2025-05-10 10:06:14 -04:00
ameye a0a2c01a1c Merge branch 'define_offers' of DigiFi/digifi-BankToProductCore into master 2025-05-10 13:58:44 +00:00
Fluxtra d6faa14b54 Merge branch 'define_offers' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore into define_offers 2025-05-10 14:57:48 +01:00
Fluxtra 332c344efa [fix]: Indentation 2025-05-10 14:47:52 +01:00
CHIEFSOFT\ameye e377858c47 removed comments 2025-05-10 09:43:21 -04:00
ameye ed64d2c97c Merge branch 'define_offers' of DigiFi/digifi-BankToProductCore into master 2025-05-10 13:34:36 +00:00
Fluxtra bbdb7214d1 [add]: define offers update 2025-05-10 14:24:05 +01:00
ameye e9c50f75b1 disburse migration 2025-05-10 08:55:38 -04:00
CHIEFSOFT\ameye c330c3f0e7 disburse_date 2025-05-10 08:54:08 -04:00
CHIEFSOFT\ameye 976fb14614 This ensures the progragation of original transaction id 2025-05-10 07:53:35 -04:00
CHIEFSOFT\ameye 334cb0f2d6 Staered 2025-05-10 06:58:17 -04:00
CHIEFSOFT\ameye 40158b1c54 Analysis steps 2025-05-10 06:20:03 -04:00
ameye b7ae0e6baa New original transaction on offer 2025-05-10 05:55:53 -04:00
CHIEFSOFT\ameye 89b621b9a8 Original Transaction id on offers 2025-05-10 05:53:32 -04:00
CHIEFSOFT\ameye cc3cd5b72b Customer id fix 2025-05-10 05:33:04 -04:00
CHIEFSOFT\ameye f573d5e643 transaction_id 2025-05-10 05:17:28 -04:00
CHIEFSOFT\ameye 09b57d81a2 Moved offer decide 2025-05-10 05:14:53 -04:00
60 changed files with 1241 additions and 1422 deletions
+4
View File
@@ -18,6 +18,10 @@ from flask_jwt_extended import (
def create_app():
"""Factory function to create a Flask app instance"""
# import oracledb
# oracledb.init_oracle_client(lib_dir=None)
app = Flask(__name__)
# Load configuration
+2
View File
@@ -3,4 +3,6 @@ from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
ACTIVE_PARTIAL = "active_partial"
START_REPAY = "start_repay"
REPAID = "repaid"
+56 -195
View File
@@ -1,251 +1,112 @@
from flask import jsonify
from typing import List, Dict, Union, Optional, Any
from typing import Optional, Union, Dict, List, Any
class ResponseHelper:
"""
A helper class for building standardized JSON responses in Flask.
A helper class for building standardized JSON responses using resultCode and resultDescription.
"""
@staticmethod
def build_response(
status: bool,
message: str,
data: Optional[Union[Dict, List, str]] = None,
status_code: int = 200,
error: Optional[Union[Dict, str]] = None,
result_code: str,
result_description: str,
data: Optional[Union[Dict, List, str]] = None
) -> Dict[str, Any]:
"""
Build a standardized JSON response.
Args:
status (bool): Indicates whether the request was successful.
message (str): A message describing the result of the request.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
status_code (int): The HTTP status code for the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
response = {
"status": status,
"statusCode": status_code,
"message": message,
"data": data if data is not None else {},
"error": error if error is not None else {},
"resultCode": result_code,
"resultDescription": result_description
}
return jsonify(response), status_code
if isinstance(data, dict):
response.update(data)
return jsonify(response)
@staticmethod
def success(
data: Optional[Union[Dict, List, str]] = None,
message: str = "Successful",
status_code: int = 200,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Successful",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a success response.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
status_code (int): The HTTP status code for the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, status_code, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def error(
message: str = "An error occurred",
status_code: int = 400,
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "An error occurred",
result_code: str = "01",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return an error response.
Args:
message (str): A message describing the error.
status_code (int): The HTTP status code for the response.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, status_code, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def created(
data: Optional[Union[Dict, List, str]] = None,
message: str = "Resource created successfully",
error: Optional[Union[Dict, str]] = None,
result_description: str = "Resource created successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a created resource.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, 201, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def updated(
data: Optional[Union[Dict, List, str]] = None,
message: str = "Resource updated successfully",
error: Optional[Union[Dict, str]] = None,
result_description: str = "Resource updated successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an updated resource.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, 200, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def internal_server_error(
message: str = "Internal Server Error",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Internal Server Error",
result_code: str = "500",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an internal server error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 500, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unauthorized(
message: str = "Unauthorized",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Unauthorized",
result_code: str = "401",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an unauthorized request.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 401, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def forbidden(
message: str = "Forbidden",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Forbidden",
result_code: str = "403",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a forbidden request.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 403, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def not_found(
message: str = "Resource not found",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Resource not found",
result_code: str = "404",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a not found resource.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 404, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unprocessable_entity(
message: str = "Unprocessable entity",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Unprocessable entity",
result_code: str = "422",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an unprocessable entity.
return ResponseHelper.build_response(result_code, result_description, data)
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 422, error)
@staticmethod
def method_not_allowed(
message: str = "Method Not Allowed",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Method Not Allowed",
result_code: str = "405",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a method not allowed error.
return ResponseHelper.build_response(result_code, result_description, data)
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 405, error)
@staticmethod
def bad_request(
message: str = "Bad Request",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Bad Request",
result_code: str = "400",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a bad request error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 400, error)
return ResponseHelper.build_response(result_code, result_description, data)
+6 -17
View File
@@ -7,36 +7,25 @@ import logging
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
"""
Calls the RACCheck endpoit
"""
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
payload = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
"fbnTransactionId": f"FBN{transaction_id}",
"RAC_Array": [
"SalaryAccount",
"BVN",
"BVNAttachedtoAccount",
"CRC",
"CRMS",
"AccountStatus",
"Lien",
"NoBouncedCheck",
"Whitelist",
"NoPastDueSalaryLoan",
"NoPastDueOtherLoan",
],
"fbnTransactionId": str(transaction_id),
"countryCode": "NG",
"channel": "USSD"
}
logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = {
"Content-Type": "application/json",
"x-api-key": f"{settings.VALID_API_KEY}",
+1
View File
@@ -88,6 +88,7 @@ def loan_status():
@jwt_required()
def repayment():
data = request.get_json()
logger.error(f"HERE 0000a **** ")
# logger.info(f"Repayment request received: {data}")
response = RepaymentService.process_request(data)
return response
+2 -2
View File
@@ -5,8 +5,8 @@ class RepaymentSchema(Schema):
type = fields.Str(required=False)
msisdn = fields.Str(required=False) #optional
debtId = fields.Str(required=True)
productId = fields.Str(required=True)
transactionId = fields.Str(required=True)
accountId = fields.Str(required=True)
customerId = fields.Str(required=True)
channel = fields.Str(required=True)
loanRef = fields.Str(required=True)
initiatedBy = fields.Str(required=False)
+7 -7
View File
@@ -34,7 +34,7 @@ class AuthorizationService(BaseService):
logger.info("Processing Authorization request")
if not data:
return ResponseHelper.bad_request(message="Missing JSON in request")
return ResponseHelper.bad_request(result_description="Missing JSON in request")
# Validate input data using the Authorization schema
schema = AuthorizeRequestSchema()
@@ -44,7 +44,7 @@ class AuthorizationService(BaseService):
validated_data["username"] != USERNAME
or validated_data["password"] != PASSWORD
):
return ResponseHelper.unauthorized(message="Invalid credentials")
return ResponseHelper.unauthorized(result_description="Invalid credentials")
access_token = create_access_token(identity=validated_data["username"])
refresh_token = create_refresh_token(identity=validated_data["username"])
@@ -56,17 +56,17 @@ class AuthorizationService(BaseService):
}
return ResponseHelper.success(
data=response_data, message="Authorization processed successfully"
data={"data": response_data}, result_description="Authorization processed successfully"
)
except ValidationError as e:
logger.error(f"Validation error: {e}")
return ResponseHelper.bad_request(message=f"Validation error: {e}")
return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
except Exception as e:
logger.error(f"Error processing Authorization request: {e}")
return ResponseHelper.internal_server_error(
message=f"Error processing Authorization request: {e}"
result_description=f"Error processing Authorization request: {e}"
)
@staticmethod
@@ -92,11 +92,11 @@ class AuthorizationService(BaseService):
}
return ResponseHelper.success(
data=response_data, message="RefreshToken processed successfully"
data={"data": response_data}, result_description="RefreshToken processed successfully"
)
except Exception as e:
logger.error(f"Error processing RefreshToken request: {e}")
return ResponseHelper.internal_server_error(
message=f"Error processing RefreshToken request: {e}"
result_description=f"Error processing RefreshToken request: {e}"
)
+3 -1
View File
@@ -48,12 +48,14 @@ class BaseService:
"""
Create a new transaction.
"""
channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel")
return Transaction.create_transaction(
transaction_id = validated_data.get("transactionId"),
customer_id = validated_data.get('customerId', None),
account_id = validated_data.get("accountId", None),
type = cls.TRANSACTION_TYPE,
channel = validated_data.get("channel"),
channel = channel,
)
@classmethod
+7 -24
View File
@@ -1,4 +1,5 @@
from flask import request, jsonify
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService
from marshmallow import ValidationError
from app.utils.logger import logger
@@ -34,44 +35,26 @@ class CustomerConsentService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Request is received"
}
db.session.commit()
return response_data
return ResponseHelper.success(result_description="Request is received")
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
return ResponseHelper.internal_server_error()
+110 -60
View File
@@ -1,5 +1,5 @@
from flask import session, jsonify
from app.models.transaction_offers import TransactionOffer
from app.models.loan import Loan
from app.utils.logger import logger
from app.api.services.base_service import BaseService
from app.api.schemas.eligibility_check import EligibilityCheckSchema
@@ -8,6 +8,9 @@ from app.api.enums import TransactionType
from app.api.integrations import SimbrellaIntegration
from app.extensions import db
from app.models import Offer, RACCheck
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
import random
@@ -42,16 +45,17 @@ class EligibilityCheckService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
db.session.flush()
# Determine Loan count
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
if not is_eligible:
return ResponseHelper.error(result_description="Max loan count reached")
# Call RACCheck
response = SimbrellaIntegration.rac_check(
@@ -60,57 +64,81 @@ class EligibilityCheckService(BaseService):
transaction_id = transaction.transaction_id,
)
# this chck for error is not valid
# this chek for error is not valid
if response.status_code != 200:
return jsonify({"message": "RACCheck failed"}), 400
return ResponseHelper.error(result_description="RACCheck failed")
response = response.json()
logger.info(f"This is Response (from Eligibility Check): {str(response)}", exc_info=True)
if not response or response['responseCode'] != '00':
if response:
logger.error(f"{response['responseMessage']}")
return ResponseHelper.error(result_description=f"RACCheck failed")
rack_checks_response = response['data']['racResponse']
rac_check = RACCheck.add_rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.transaction_id,
data = response['RACResponse']
data = rack_checks_response
)
if not rac_check:
logger.error(f"Failed to save RACCheck")
return jsonify({
"message": "Failed to save RACCheck."
}), 400
logger.error(f"Failed to save RACCheck")
return ResponseHelper.error(result_description="Failed to save RACCheck.")
offers = Offer.get_all_offers()
eligible_offers = []
for offer in offers:
# Determine an approved amount
random_float = random.random() # temporary to play data
approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
approved_amount = round(approved_amount, 2)
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id = customer.id,
transaction_id = transaction.transaction_id,
offer_id = offer.id,
min_amount = offer.min_amount,
max_amount = offer.max_amount,
eligible_amount = approved_amount,
product_id = offer.product_id,
tenor = offer.tenor
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
# eligible_offers = []
try:
eligible_offers = OfferAnalysis.decide_offer(
transaction_id=transactionId,
rac_check=rac_check,
validated_data=validated_data,
customer_id=customer_id,
rack_checks_response =rack_checks_response
)
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description= str(ve))
# -----------------------------------------------------------------------
# s = Offer.get_all_offers()
# Visible offer ID: offer_id + padded(transaction_offer.id)
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{offer.id}{padded_id}"
# eligible_offers = []
eligible_offers.append({
"offerId": public_offer_id,
"product_id": offer.product_id,
"min_amount": offer.min_amount,
"max_amount": approved_amount,
"tenor": offer.tenor
})
# for offer in offers:
# # Determine an approved amount
# random_float = random.random() # temporary to play data
# approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
# approved_amount = round(approved_amount, 2)
#
# transaction_offer = TransactionOffer.create_transaction_offer(
# customer_id = customer.id,
# transaction_id = transaction.transaction_id,
# offer_id = offer.id,
# min_amount = offer.min_amount,
# max_amount = offer.max_amount,
# eligible_amount = approved_amount,
# product_id = offer.product_id,
# tenor = offer.tenor
# )
#
# # Visible offer ID: offer_id + padded(transaction_offer.id)
# padded_id = str(transaction_offer.id).zfill(6)
# public_offer_id = f"{offer.id}{padded_id}"
#
# eligible_offers.append({
# "offerId": public_offer_id,
# "product_id": offer.product_id,
# "min_amount": offer.min_amount,
# "max_amount": approved_amount,
# "tenor": offer.tenor
# })
# Simulate processing
response_data = {
@@ -119,30 +147,52 @@ class EligibilityCheckService(BaseService):
"countryCode": "NG",
"msisdn": msisdn,
"eligibleOffers": eligible_offers,
"resultDescription": "Successful",
"resultCode": "00",
"accountId": account_id
}
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
@staticmethod
def check_loan_limits(customer_id):
"""
Checks if a customer has exceeded the loan limits for given offer.
"""
loan = Loan.get_customer_last_loan(customer_id)
if not loan:
return True
offer_id = loan.offer_id[:5]
offer = Offer.get_offer_by_id(offer_id)
if not offer:
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
return False
daily_count = Loan.get_daily_loan_count(customer_id, offer.product_id)
logger.info(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
+14 -41
View File
@@ -6,7 +6,8 @@ from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.extensions import db
from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class LoanStatusService(BaseService):
@@ -28,42 +29,23 @@ class LoanStatusService(BaseService):
# Validate data
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
customer_id = validated_data.get('customerId')
customer = Customer.get_customer(customer_id)
logger.info(f"Looking for customer *** {customer_id}")
customer = Customer.get_customer_with_loan_list(customer_id)
transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId')
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# loans = [
# {
# "debtId": "123456789",
# "loanDate": "2019-10-18 14:26:21.063",
# "dueDate": "2019-11-20 14:26:21.063",
# "currentLoanAmount": 8500,
# "initialLoanAmount": 10000,
# "defaultPenaltyFee": 0,
# "continuousFee": 0,
# "productId": "101"
# }
# ]
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0
@@ -73,36 +55,27 @@ class LoanStatusService(BaseService):
# Simulated processing logic
response_data = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": total_debt_amount,
"resultCode": "00",
"resultDescription": "Successful"
}
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
return ResponseHelper.internal_server_error()
+8 -24
View File
@@ -5,6 +5,7 @@ from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.notification_callback import NotificationCallbackSchema
from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class NotificationCallbackService(BaseService):
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
@@ -27,37 +28,20 @@ class NotificationCallbackService(BaseService):
schema = NotificationCallbackSchema()
validated_data = schema.load(data) # Raises ValidationError if invalid
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success(
# data=response_data,
# message="Notification callback processed successfully"
# )
return response_data
return ResponseHelper.success()
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+218 -4
View File
@@ -1,7 +1,15 @@
from decimal import Decimal
from app.models import Offer, TransactionOffer
from app.models.loan import Loan
import random
import logging
from app.config import Config
RAC_TRUE_CHECK_RULES = Config.rac_true_rules
RAC_FALSE_CHECK_RULES = Config.rac_false_rules
RAC_SALARY_PAYMENTS = Config.rac_salary_payments
logger = logging.getLogger(__name__)
class OfferAnalysis:
@@ -30,8 +38,214 @@ class OfferAnalysis:
if not offer:
raise ValueError("Invalid Offer.")
original_transaction = transaction_id
# we can now find the origin transactions
customer_loan = Loan.get_customer_current_active_loan(customer_id)
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response, offer):
logger.info(f"This is PayLoad for ANALYSYS ***** : {str(rack_response)}", exc_info=True)
logger.info(f"RACk TRUE RULES {str(RAC_TRUE_CHECK_RULES)}", exc_info=True)
logger.info(f"RACk FALSE RULES {str(RAC_FALSE_CHECK_RULES)}", exc_info=True)
logger.info(f"RACk SALARY PAYMENTS {str(RAC_SALARY_PAYMENTS)}", exc_info=True)
if not isinstance(rack_response, dict) or not offer :
raise ValueError("Invalid RAC response format.")
failed_true_rules = []
failed_false_rules = []
salaries = []
# Expects true
for rule in RAC_TRUE_CHECK_RULES:
if not rack_response.get(rule, False):
failed_true_rules.append(rule)
# Expects false
for rule in RAC_FALSE_CHECK_RULES:
if rack_response.get(rule, True):
failed_false_rules.append(rule)
# Salary rules
for key in RAC_SALARY_PAYMENTS:
value = rack_response.get(key)
if isinstance(value, Decimal):
# Only use values greater than 0
if value > 0:
salaries.append(value)
elif isinstance(value, (int, float, str)):
try:
value = Decimal(str(value))
if value > 0:
salaries.append(value)
except:
logger.warning(f"Could not convert value of {key} to Decimal: {value}")
if failed_true_rules or failed_false_rules or not salaries:
logger.warning(f"Failed TRUE rules: {failed_true_rules}")
logger.warning(f"Failed FALSE rules: {failed_false_rules}")
logger.warning("No salary records found in RAC response.")
raise ValueError(f"RAC analysis failed")
logger.info(f"These are the salary amounts ***** : {str(salaries)}", exc_info=True)
#Least salary in the last 6 months
min_salary = min(salaries)
# Check consistency rule
consistent_income = rack_response.get("rule7_consistent_salary_amount", False)
# Determine percentage based on offer tenor
tenor = offer.tenor
if tenor == 30 and consistent_income:
eligible_amount = min_salary * Decimal("0.5")
logger.info("Applying 50% of least salary in 6 months due to 1-month offer tenor with stable income.")
elif tenor == 90 and consistent_income:
eligible_amount = min_salary * Decimal("0.75")
logger.info("Applying 75% of least salary in 6 months due to 3-months offer tenor with stable income.")
else: # Income is not consistent
eligible_amount = 0
logger.info("Applying no percentage on least salary due unstable income.")
logger.info(f"Calculated eligible amount from RAC: {eligible_amount} based on {'stable' if consistent_income else 'unstable'} income.")
return eligible_amount.quantize(Decimal("1.00"))
# "racResponse": {
# "accountStatus": true,
# "bvnValidated": true,
# "creditBureauCheck": false,
# "crmsCheck": true,
# "hasLien": false,
# "hasPastDueLoan": false,
# "hasSalaryAccount": true,
# "isWhitelisted": true,
# "noBouncedCheck": true
# },
#
'''
30 days
Eligibility amount (monthly SOL) - Adoption of 50% of the least salary inflow in the past 6 months
to determine loan eligibility for potential customers.
3 months
Adoption of 75% of the least salary inflow in the past 6 months to determine loan eligibility for
potential customers" for customers that have unstable income. 3 months
'''
# rac_true_rules
return 0
@staticmethod
def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response):
eligible_offers = []
# if we have active offers - we have to feed off it
logger.info(f"**RACK ANALYSIS** {customer_id}")
# Analyze Rack Checks
# new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response) #--> We need detail analysis
# we can now find the origin transactions
# Find the last loan - it will have original_transaction
last_customer_loan = Loan.get_customer_last_loan(customer_id)
# logger.info(f"{last_customer_loan}")
if last_customer_loan:
original_transaction = last_customer_loan.original_transaction or last_customer_loan.transaction_id
logger.info(f"transaction_id |-| original_transaction === > {transaction_id} {original_transaction}")
original_loan = Loan.get_customer_original_loan(customer_id, original_transaction)
if original_loan is not None:
logger.info(f"original_loan === > {original_loan}")
logger.info(f"loan_offer_id === > {original_loan.offer_id}")
original_offer_id = str(original_loan.offer_id[:5]) # The last part is str
transaction_offer_id = int(original_loan.offer_id[5:]) # The last part is int
original_transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, original_loan.product_id)
active_loans = Loan.get_active_loans_by_original_transaction(original_transaction)
sum_active_loans = sum(loan.current_loan_amount for loan in active_loans)
logger.info(f"sum_active_loans === > {sum_active_loans}")
real_eligible_amount = original_loan.eligible_amount - sum_active_loans
if real_eligible_amount < original_transaction_offer.min_amount:
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=original_transaction,
offer_id=original_offer_id,
min_amount=original_transaction_offer.min_amount,
max_amount=original_transaction_offer.max_amount,
eligible_amount=real_eligible_amount,
product_id=original_loan.product_id,
tenor=original_loan.tenor
)
# Visible offer ID: offer_id + padded(transaction_offer.id)
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{original_offer_id}{padded_id}"
eligible_offers.append({
"offerId": public_offer_id,
"product_id": original_transaction_offer.product_id,
"min_amount": original_transaction_offer.min_amount,
"max_amount": round(real_eligible_amount, 2),
"tenor": original_loan.tenor
})
return eligible_offers
offers = Offer.get_all_offers()
for offer in offers:
new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response, offer)
approved_amount = new_eligible_amount
approved_amount = round(approved_amount, 2)
if approved_amount < offer.min_amount:
logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=transaction_id,
offer_id=offer.id,
min_amount=offer.min_amount,
max_amount=offer.max_amount,
eligible_amount=approved_amount,
product_id=offer.product_id,
tenor=offer.tenor
)
# Visible offer ID: offer_id + padded(transaction_offer.id)
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{offer.id}{padded_id}"
eligible_offers.append({
"offerId": public_offer_id,
"product_id": offer.product_id,
"min_amount": offer.min_amount,
"max_amount": approved_amount,
"tenor": offer.tenor
})
return eligible_offers
+31 -38
View File
@@ -15,6 +15,7 @@ from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta
from app.models import LoanRepaymentSchedule
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -42,6 +43,7 @@ class ProvideLoanService(BaseService):
offer_id = validated_data.get('offerId')
amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId")
channel = validated_data.get('channel')
customer = Customer.is_valid_customer(customer_id)
@@ -57,9 +59,14 @@ class ProvideLoanService(BaseService):
)
except ValueError as ve:
logger.error(str(ve))
return jsonify({
"message": str(ve)
}), 400
return ResponseHelper.error(result_description=str(ve))
if(amount < transaction_offer.min_amount):
return ResponseHelper.error(result_description="The amount is less than the minimum allowed transaction amount.")
elif amount > transaction_offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed transaction amount.")
# transaction_offer_id = int(offer_id[5:]) # The last part is int
@@ -86,9 +93,7 @@ class ProvideLoanService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
db.session.flush()
@@ -107,6 +112,8 @@ class ProvideLoanService(BaseService):
insurance = charges["insurance"]
vat = charges["vat"]
padded_id = str(transaction_id).zfill(12)
loan_ref = f"{padded_id}{channel}{offer.product_id}"
# Save the loan details
@@ -117,7 +124,7 @@ class ProvideLoanService(BaseService):
product_id = offer.product_id,
collection_type = collection_type,
transaction_id = validated_data.get('transactionId'),
original_transaction = validated_data.get('transactionId'),
original_transaction = transaction_offer.original_transaction,
initial_loan_amount = validated_data.get('requestedAmount'),
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
@@ -125,29 +132,26 @@ class ProvideLoanService(BaseService):
eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor,
reference = loan_ref
)
if not loan:
logger.error(f"Failed to save loan details")
return jsonify({
"message": "Failed to save loan details."
}), 400
return ResponseHelper.error(result_description="Failed to save loan details.")
db.session.flush()
current_product_id = offer.product_id
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedule:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return jsonify({
"message": "Failed to generate loan repayment schedule."
}), 400
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
# charges = Charge.get_offer_charges(offer.id)
logger.info(f"{charges}")
# logger.info(f"{charges}")
loan_id = loan.id
@@ -156,50 +160,39 @@ class ProvideLoanService(BaseService):
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Invalid Customer or Account")
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"loanRef": loan_ref,
"customerId": customer_id,
"accountId": account_id,
"msisdn": customer.msisdn,
"resultCode": "00",
"resultDescription": "Successful"
"msisdn": customer.msisdn
}
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# Call Kafka in a background thread
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start()
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+32 -47
View File
@@ -1,6 +1,7 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.api.helpers.response_helper import ResponseHelper
from app.models import Repayment
from app.models.customer import Customer
from app.models.loan import Loan
@@ -28,91 +29,75 @@ class RepaymentService(BaseService):
try:
with db.session.begin():
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId')
loan_id = validated_data.get('debtId')
product_id = validated_data.get('productId')
account_id = validated_data.get('accountId')
customer = Customer.get_customer(customer_id)
loan_ref = validated_data.get('loanRef')
# customer = Customer.get_customer_with_loan_list(customer_id)
transaction_id = validated_data.get('transactionId')
initiated_by = validated_data.get('initiatedBy')
logger.error(f"HERE 0002a **** ")
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
logger.error(f"HERE 0001a **** ")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Save the repayment details
repayment = Repayment.create_repayment(
customer_id = customer_id,
loan_id = loan_id,
product_id = product_id,
transaction_id=transaction_id
loan = loan,
transaction_id = transaction_id
)
if not repayment:
logger.error(f"Failed to save repayment details")
return jsonify({
"message": "Failed to save repayment details."
}), 400
return ResponseHelper.error(result_description="Failed to save repayment details.")
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
logger.error(f"Invalid Customer or AccountID {account_id} to CustomerID{customer_id} ")
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Simulated processing logic
# TODO start using repayment_id instead if id or Id
response_data = {
"Id": repayment.id,
"repayment_id": repayment.id,
"initiated_by": repayment.initiated_by,
"transactionId": transaction_id,
"customerId": customer_id,
"productId": product_id,
"debtId": loan_id,
"resultCode": "00",
"resultDescription": "Successful"
"productId": loan.product_id,
"loanRef": loan_ref,
"debtId": loan_id
}
# return ResponseHelper.success(
# data=response_data,
# message="Repayment processed successfully"
# )
# Call Kafka in a background thread
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start()
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+31 -16
View File
@@ -1,7 +1,9 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.models.transaction_offers import TransactionOffer
from app.utils.logger import logger
from app.api.schemas.select_offer import SelectOfferSchema
from app.extensions import db
@@ -35,10 +37,10 @@ class SelectOfferService(BaseService):
transaction_offer_id = validated_data.get("offerId")
transaction_id = validated_data.get("transactionId")
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(
account_id=account_id, customer_id=customer_id
@@ -49,15 +51,30 @@ class SelectOfferService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({"message": "Failed to log transaction."}), 400
else:
return jsonify({"message": "Invalid Customer or Account"}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Get the offer by product ID
offer = Offer.get_offer_by_product_id(product_id)
transaction_offer = TransactionOffer.get_transaction_offer(transaction_offer_id=offer_id)
if not transaction_offer:
logger.error(f"offer {offer_id} not found for customer {customer_id} and transaction {transaction_id}.")
return ResponseHelper.error(result_description="Offer not found.")
db.session.flush()
if amount < transaction_offer.min_amount:
logger.error(f"The amount {amount} is less than the minimum allowed offer amount {transaction_offer.min_amount}.")
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
elif amount > transaction_offer.eligible_amount:
logger.error(f"The amount {amount} is greater than the eligible offer amount {transaction_offer.eligible_amount}.")
return ResponseHelper.error(result_description="The amount is greater than the eligible offer amount.")
charges = SelectOfferService.calculate_charges(offer, amount)
upfront_payment = charges["upfront_payment"]
total_amount = charges["total_amount"]
@@ -91,7 +108,7 @@ class SelectOfferService(BaseService):
"amount": amount,
"upfrontPayment": upfront_payment,
"interestRate": offer.interest_rate,
"interestAmount": interest_amount,
"interestFee": interest_amount,
"managementRate": offer.management_rate,
"managementFee": management["fee"],
"insuranceRate": offer.insurance_rate,
@@ -128,26 +145,24 @@ class SelectOfferService(BaseService):
"customerId": customer_id,
"accountId": account_id,
"loan": offers,
"resultCode": "00",
"resultDescription": "Successful",
}
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": "Validation exception"}), 422
except ValueError as err:
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": str(err)}), 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({"message": "Internal Server Error"}), 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+59 -6
View File
@@ -1,7 +1,6 @@
import os
from datetime import timedelta
class Config:
"""Base configuration for Flask app"""
@@ -9,20 +8,25 @@ class Config:
API_URL = os.getenv("API_URL", "/swagger.json")
DEBUG = True
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345")
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
# Database Configuration
DATABASE_USER = os.environ.get("DATABASE_USER")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME")
DATABASE_NAME = os.environ.get("DATABASE_NAME", "firstadvancedev")
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
# Database Connection
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_DATABASE_URI = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
@@ -34,5 +38,54 @@ class Config:
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "test-api-key-12345")
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
RAC_RESULT_creditBureauCheck = os.environ.get("RAC_RESULT_creditBureauCheck", "false")
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
RAC_RESULT_hasSalaryAccount = os.environ.get("RAC_RESULT_hasSalaryAccount", "true")
RAC_RESULT_isWhitelisted = os.environ.get("RAC_RESULT_isWhitelisted", "true")
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
rac_true_rules = [
"rule1_45day_sal",
"rule2_2m_sal",
"rule3_no_bounced_check",
"rule4_current_loan_payments",
"rule5_no_past_due_fadv_loan",
"rule6_no_past_due_other_loan",
"rule7_consistent_salary_amount",
"rule8_whitelisted",
"rule9_regular_account",
"rule10_bvn_validation",
"rule11_CRC_no_delinquency",
"rule12_CRMS_no_delinquency",
"rule13_BVN_ignore",
"rule14_no_lien",
"rule15_null_ignore"
]
rac_false_rules = [
]
rac_salary_payments = [
"salarypaymenT_1",
"salarypaymenT_2",
"salarypaymenT_3",
"salarypaymenT_4",
"salarypaymenT_5",
"salarypaymenT_6"
]
settings = Config()
+5 -5
View File
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
def register_error_handlers(app):
@app.errorhandler(HTTPException)
def handle_http_exception(e):
return jsonify({'error': e.description}), e.code
return ResponseHelper.error(result_description=e.description, result_code=e.code )
@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({"message": "Method Not Allowed"}), 405
return ResponseHelper.method_not_allowed()
@app.errorhandler(404)
def not_found(error):
return jsonify({"message": "Resource not found"}), 404
return ResponseHelper.not_found()
@app.errorhandler(400)
def bad_request(error):
return jsonify({"message": "Bad Request"}), 400
return ResponseHelper.bad_request()
@app.errorhandler(415)
def unsupported_media_type(error):
return jsonify({"message": "Unsupported Media Type"}), 415
return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
+3 -1
View File
@@ -9,6 +9,8 @@ from .charge import Charge
from .rac_checks import RACCheck
from .loan_repayment_schedule import LoanRepaymentSchedule
from .transaction_offers import TransactionOffer
from .repayments_data import RepaymentsData
from .salary import Salary
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer']
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData', 'Salary']
+6 -3
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
from sqlalchemy.orm import relationship
from app.extensions import db
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Account(db.Model):
__tablename__ = 'accounts'
@@ -11,8 +12,8 @@ class Account(db.Model):
account_type = db.Column(db.String(50))
status = db.Column(db.String(20), default='active')
lien_amount = db.Column(db.Float, default=0.0)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship(
"Customer",
@@ -26,7 +27,9 @@ class Account(db.Model):
account = cls(
id=id,
customer_id=customer_id,
account_type=account_type
account_type=account_type,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+6 -4
View File
@@ -1,6 +1,7 @@
from datetime import datetime, timezone, timedelta
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
class Charge(db.Model):
@@ -12,9 +13,8 @@ class Charge(db.Model):
percent = db.Column(db.Float, default=0.0)
description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
offer = relationship(
"Offer",
primaryjoin="Charge.offer_id == Offer.id",
@@ -57,7 +57,9 @@ class Charge(db.Model):
code = code,
percent = percent,
description = description,
due = due_days
due = due_days,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(charge_obj)
+18 -5
View File
@@ -1,8 +1,12 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
#
# from app.api.services.offer_analysis import logger
from app.extensions import db
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
# from app.utils.logger import logger
class Customer(db.Model):
__tablename__ = 'customers'
@@ -10,9 +14,8 @@ class Customer(db.Model):
id = db.Column(db.String(50), primary_key=True)
msisdn = db.Column(db.String(20), unique=True, nullable=False)
country_code = db.Column(db.String(3), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
accounts = relationship(
"Account",
primaryjoin="Customer.id == Account.customer_id",
@@ -45,9 +48,19 @@ class Customer(db.Model):
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
if cls.query.filter_by(id=id).first():
raise ValueError("Customer already exists")
elif Account.query.filter_by(id=account_id).first():
raise ValueError("Account already exists")
elif cls.query.filter_by(msisdn=msisdn).first():
raise ValueError("msisdn already exists")
# Create the customer
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
customer = cls(
id=id,
msisdn=msisdn,
country_code=country_code,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
db.session.add(customer)
@@ -63,7 +76,7 @@ class Customer(db.Model):
return customer
@classmethod
def get_customer(cls, customer_id):
def get_customer_with_loan_list(cls, customer_id):
"""
Get customer by ID.
"""
+85 -16
View File
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from itertools import product
from app.extensions import db
from app.models.customer import Customer
from app.models.account import Account
@@ -7,6 +8,9 @@ from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from datetime import timedelta
import logging
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
logger = logging.getLogger(__name__)
@@ -31,13 +35,21 @@ class Loan(db.Model):
continuous_fee = db.Column(db.Float, default=0)
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
balance = 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')
tenor = db.Column(db.Integer, nullable=True)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
disburse_date = db.Column(db.DateTime, nullable=True)
disburse_verify = db.Column(db.DateTime, nullable=True)
reference = db.Column(db.String(50), nullable=True)
disburse_result = db.Column(db.String(10), nullable=True)
disburse_description = db.Column(db.String(100), nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
customer = relationship(
"Customer",
@@ -77,6 +89,7 @@ class Loan(db.Model):
installment_amount,
tenor,
eligible_amount,
reference,
status = "pending",
):
# Check if customer exists
@@ -100,11 +113,15 @@ class Loan(db.Model):
current_loan_amount = initial_loan_amount,
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
balance = repayment_amount,
installment_amount = installment_amount,
due_date=due_date,
tenor = tenor,
status = status,
eligible_amount =eligible_amount
eligible_amount =eligible_amount,
reference = reference,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
@@ -136,24 +153,53 @@ class Loan(db.Model):
return loan
@classmethod
def get_customer_current_active_loan(cls, customer_id):
def get_customer_original_loan(cls, customer_id, original_transaction):
"""
Get customer's original loan offer.
"""
original_loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.original_transaction==original_transaction, cls.transaction_id==original_transaction )).first()
if not original_loan:
return None
logger.info(f" get_customer_original_loan ==>>>> {original_loan}")
return original_loan
@classmethod
def get_customer_last_loan(cls, customer_id):
"""
Get customer's active loans.
"""
loan = cls.query.filter_by( customer_id = customer_id).first()
logger.info(f"get_customer_last_loan [customer_id] ==>>>> {customer_id}")
# loan = cls.query.filter_by( cls.customer_id == customer_id).first()
loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.status=='active')).first()
if not loan:
loan = {
"eligible_amount": 0,
"loan_amount": 0,
"customer_id": customer_id,
"transaction_id": "",
"resultDescription": "No Active Loan"
}
logger.info(f" Active Loan ==>>>> {loan}")
return None
# loan = {
# "original_transaction":"",
# "eligible_amount": 0,
# "loan_amount": 0,
# "customer_id": customer_id,
# "transaction_id": "",
# "resultDescription": "No Active Loan"
# }
logger.info(f" get_customer_last_loan ==>>>> {loan}")
return loan
@classmethod
def get_active_loans_by_original_transaction(cls, original_transaction_id):
"""
Get all active loans with the same original_transaction ID.
"""
active_loans = cls.query.filter_by(
original_transaction=original_transaction_id,
# status='active'
).all()
return active_loans
@classmethod
def update_status(cls, loan_id, status):
"""
@@ -171,12 +217,34 @@ class Loan(db.Model):
# Update loan status and the updated_at timestamp
loan.status = status
@classmethod
def get_daily_loan_count(cls, customer_id, product_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,
product_id=product_id,
).filter(
cls.created_at >= start_of_day,
cls.created_at < end_of_day
).count()
def to_dict(self):
"""
Convert the Loan object to a dictionary format for JSON serialization.
"""
return {
'debtId': self.id,
'transactionId': self.transaction_id,
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee,
@@ -186,6 +254,7 @@ class Loan(db.Model):
'repaymentAmount': self.repayment_amount,
'installmentAmount': self.installment_amount,
'status': self.status,
'tenor': self.tenor,
'dueDate': self.due_date.isoformat() if self.due_date else None,
'loanDate': self.created_at.isoformat() if self.created_at else None,
}
+6 -4
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone, timedelta
from app.extensions import db
from sqlalchemy.orm import relationship
from app.utils.logger import logger
from sqlalchemy.sql import func
class LoanCharge(db.Model):
@@ -16,9 +17,8 @@ class LoanCharge(db.Model):
description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanCharge.loan_id == Loan.id",
@@ -63,7 +63,9 @@ class LoanCharge(db.Model):
percent = percent,
description = description,
due = due_days,
due_date = now + timedelta(days=due_days)
due_date = now + timedelta(days=due_days),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(charge_obj)
+6 -4
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from sqlalchemy.sql import func
class LoanRepaymentSchedule(db.Model):
__tablename__ = 'loan_repayment_schedules'
@@ -17,9 +18,8 @@ class LoanRepaymentSchedule(db.Model):
paid = db.Column(db.Boolean, default=False)
paid_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
@@ -45,7 +45,9 @@ class LoanRepaymentSchedule(db.Model):
total_repayment_amount = round(loan.repayment_amount, 2),
installment_amount=round(loan.installment_amount, 2),
product_id = loan.product_id,
transaction_id = transaction_id
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(schedule)
+13 -5
View File
@@ -2,12 +2,13 @@ from datetime import datetime, timezone
from app.extensions import db
from app.models.charge import Charge
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
class Offer(db.Model):
__tablename__ = 'offers'
id = db.Column(db.String, primary_key=True)
product_id = db.Column(db.String, nullable=False)
id = db.Column(db.String(50), primary_key=True)
product_id = db.Column(db.String(50), nullable=False)
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
tenor = db.Column(db.Integer, nullable=False)
@@ -17,8 +18,11 @@ class Offer(db.Model):
insurance_rate = db.Column(db.Float, default=1.0)
vat_rate = db.Column(db.Float, default=7.5)
list_order = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
max_daily_loans = db.Column(db.Integer, nullable=True)
max_active_loans = db.Column(db.Integer, nullable=True)
max_life_loans = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
charges = relationship(
"Charge",
@@ -79,7 +83,11 @@ class Offer(db.Model):
"interest_rate": self.interest_rate,
"management_rate": self.management_rate,
"insurance_rate": self.insurance_rate,
"vat_rate": self.vat_rate
"vat_rate": self.vat_rate,
"maxDailyLoans": self.max_daily_loans,
"maxActiveLoans": self.max_active_loans,
"maxLifeLoans": self.max_life_loans
}
def __repr__(self):
+22 -7
View File
@@ -3,18 +3,30 @@ from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.exc import IntegrityError
from uuid import uuid4
import json
from sqlalchemy.types import JSON
from sqlalchemy.sql import func
class RACCheck(db.Model):
__tablename__ = 'rac_checks'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String, nullable=False)
account_id = db.Column(db.String, nullable=False)
rac_response = db.Column(db.JSON, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=False)
rac_response = db.Column(db.Text, nullable=False)
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())
@property
def rac_response_data(self):
return json.loads(self.rac_response)
@rac_response_data.setter
def rac_response_data(self, value):
self.rac_response = json.dumps(value)
@classmethod
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
@@ -25,8 +37,11 @@ class RACCheck(db.Model):
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)
)
rac_check.rac_response_data = data or {}
try:
db.session.add(rac_check)
@@ -64,7 +79,7 @@ class RACCheck(db.Model):
"transactionId": str(self.transaction_id),
"customerId": self.customer_id,
"accountId": self.account_id,
"racResponse": self.rac_response,
"racResponse": self.rac_response_data,
"createdAt": self.created_at.isoformat(),
"updatedAt": self.updated_at.isoformat() if self.updated_at else None
}
+40 -15
View File
@@ -4,6 +4,7 @@ from app.extensions import db
from app.models.customer import Customer
from app.models.loan import Loan
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Repayment(db.Model):
@@ -17,31 +18,36 @@ class Repayment(db.Model):
loan_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
transaction_id = db.Column(db.String(50), nullable=True)
# repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
repay_date = db.Column(db.DateTime, nullable=True)
repay_result = db.Column(db.String(10), nullable=True)
repay_description = db.Column(db.String(100), nullable=True)
# verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
verify_date = db.Column(db.DateTime, nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
initiated_by = db.Column(db.String(50), nullable=True)
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
@classmethod
def create_repayment(cls, customer_id, loan_id, product_id, transaction_id):
# Check customer exists
if not Customer.is_valid_customer(customer_id):
raise ValueError("Invalid customer")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
def create_repayment(cls, customer_id, loan, transaction_id):
# Check that the loan is active
if loan.status != LoanStatus.ACTIVE:
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
loan_id=loan_id,
product_id=product_id,
transaction_id = transaction_id
loan_id=loan.id,
product_id=loan.product_id,
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
initiated_by='USER_INITIATED'
)
try:
@@ -50,6 +56,25 @@ class Repayment(db.Model):
raise ValueError(f"Database integrity error: {err}")
return repayment
def to_dict(self):
return {
"id": self.id,
"loan_id": self.loan_id,
"customer_id": self.customer_id,
"product_id": self.product_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"transaction_id": self.transaction_id,
"repay_date": self.repay_date.isoformat() if self.repay_date else None,
"repay_result": self.repay_result,
"repay_description": self.repay_description,
"verify_date": self.verify_date.isoformat() if self.verify_date else None,
"verify_result": self.verify_result,
"verify_description": self.verify_description,
"initiated_by": self.initiated_by,
"salary_amount": self.salary_amount
}
def __repr__(self):
return f'<Repayment {self.id}>'
+36
View File
@@ -0,0 +1,36 @@
from datetime import datetime, timezone
from app.extensions import db
class RepaymentsData(db.Model):
__tablename__ = "repayments_data"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
fbn_transaction_id = db.Column(db.String(50), nullable=True)
customer_id = db.Column(db.String(50), nullable=True)
account_id = db.Column(db.String(50), nullable=True)
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
amount_collected = db.Column(db.Float, nullable=True, default=0.0)
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
response_code = db.Column(db.String(10), nullable=True)
response_descr = db.Column(db.String(255), nullable=True)
balance = db.Column(db.Float, nullable=True, default=0.0)
def to_dict(self):
return {
"id": self.id,
"transaction_id": self.transaction_id,
"fbn_transaction_id": self.fbn_transaction_id,
"customer_id": self.customer_id,
"account_id": self.account_id,
"repayment_amount": self.repayment_amount,
"amount_collected": self.amount_collected,
"added_date": self.added_date.isoformat() if self.added_date else None,
"response_code": self.response_code,
"response_descr": self.response_descr,
"balance": self.balance,
}
def __repr__(self):
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.sql import func
class Salary(db.Model):
__tablename__ = 'salaries'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=True)
status = db.Column(db.String(20), default='active')
amount = db.Column(db.Float, nullable=False, default=0.0)
salary_date = db.Column(db.DateTime(timezone=False), server_default=func.now())
created_at = db.Column(db.DateTime(timezone=False), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
def to_dict(self):
return {
"id": self.id,
"customer_id": self.customer_id,
"account_id": self.account_id,
"status": self.status,
"amount": self.amount,
"salary_date": self.salary_date.isoformat() if self.salary_date else None,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<Salary {self.id} - {self.amount}>'
+8 -4
View File
@@ -3,6 +3,7 @@ from app.extensions import db
from app.models import account
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
class Transaction(db.Model):
__tablename__ = 'transactions'
@@ -16,9 +17,10 @@ class Transaction(db.Model):
customer_id = db.Column(db.String(50), nullable=True)
type = db.Column(db.String(50), nullable=False)
channel = db.Column(db.String(50), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
phone_number = db.Column(db.String(50), 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())
def __repr__(self):
return f'<Transaction {self.id}>'
@@ -38,7 +40,9 @@ class Transaction(db.Model):
customer_id = customer_id,
account_id = account_id,
type = type,
channel = channel
channel = channel,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+44 -8
View File
@@ -1,6 +1,12 @@
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import logging
logger = logging.getLogger(__name__)
class TransactionOffer(db.Model):
__tablename__ = 'transaction_offers'
@@ -8,6 +14,7 @@ class TransactionOffer(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
customer_id = db.Column(db.String(50), nullable=False)
transaction_id = db.Column(db.String(50), nullable=False)
original_transaction = db.Column(db.String(50), nullable=True)
offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
min_amount = db.Column(db.Float, nullable=False)
@@ -15,9 +22,8 @@ class TransactionOffer(db.Model):
eligible_amount = db.Column(db.Float, nullable=True)
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship(
"Customer",
primaryjoin="Customer.id == TransactionOffer.customer_id",
@@ -26,9 +32,9 @@ class TransactionOffer(db.Model):
)
@classmethod
def is_valid_transaction_offer(cls, transaction_offer_id, customer_id, product_id):
def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id):
transaction_offer = cls.query.filter_by(
id = transaction_offer_id,
id = transaction_offer,
customer_id = customer_id,
# product_id = product_id
# transaction_id = transaction_id,
@@ -40,19 +46,22 @@ class TransactionOffer(db.Model):
return transaction_offer
@classmethod
def create_transaction_offer(cls, customer_id, transaction_id, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
"""
Class method to create and save a TransactionOffer.
"""
transaction_offer = cls(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=original_transaction,
offer_id=offer_id,
min_amount=min_amount,
max_amount=max_amount,
eligible_amount=eligible_amount,
product_id=product_id,
tenor=tenor
tenor=tenor,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(transaction_offer)
@@ -60,6 +69,33 @@ class TransactionOffer(db.Model):
return transaction_offer
@classmethod
def get_lifetime_loan_count(cls, customer_id):
"""
Returns the total number of loans ever created for a customer.
"""
return cls.query.filter_by(customer_id=customer_id).count()
@classmethod
def get_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()
@classmethod
def get_transaction_offer(cls, transaction_offer_id):
"""
Returns a transaction offer by its ID.
"""
return cls.query.get(transaction_offer_id)
def to_dict(self):
return {
'id': self.id,
@@ -9,6 +9,10 @@
"type": "string",
"example": "Tr201712RK9232P115"
},
"loanRef": {
"type": "string",
"example": "1620029887USSDAMPC"
},
"customerId": {
"type": "string",
"example": "CN621868"
+2 -6
View File
@@ -9,10 +9,6 @@
"type": "string",
"example": "10"
},
"productId": {
"type": "string",
"example": "101"
},
"transactionId": {
"type": "string",
"example": "20171209232115"
@@ -21,9 +17,9 @@
"type": "string",
"example": "CID0000025585"
},
"channel": {
"loanRef": {
"type": "string",
"example": "USSD"
"example": "Trx5847365252USSD3MPC"
},
"accountId": {
"type": "string",
+1 -1
View File
@@ -28,7 +28,7 @@
},
"productId": {
"type": "string",
"example": "2090"
"example": "3MPC"
},
"offerId": {
"type": "string",
+2 -2
View File
@@ -28,7 +28,7 @@
},
"productId": {
"type": "string",
"example": "2030"
"example": "3MPC"
},
"amount": {
"type": "number",
@@ -49,7 +49,7 @@
"format": "float",
"example": 3.0
},
"interestAmount": {
"interestFee": {
"type": "number",
"format": "float",
"example": 3000.00
+31
View File
@@ -0,0 +1,31 @@
"""empty message
Revision ID: 05b5494ad406
Revises: 33e09efd85e3
Create Date: 2025-07-06 09:28:26.264927
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "05b5494ad406"
down_revision = "33e09efd85e3"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("loans", sa.Column("balance", sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("loans", "balance")
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Thu Apr 10 21:50:01 UTC 2025
Revision ID: 1340e7e578b9
Revises: b8f6fd76ead8
Create Date: 2025-04-10 21:50:32.113149
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1340e7e578b9'
down_revision = 'b8f6fd76ead8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.String(length=50), nullable=True))
# ### 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.drop_column('ref_model')
# ### end Alembic commands ###
@@ -1,53 +0,0 @@
"""Migration on Thu Apr 24 17:42:25 UTC 2025
Revision ID: 1b2339f43824
Revises: de9ad96ba34e
Create Date: 2025-04-24 17:43:09.589626
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1b2339f43824'
down_revision = 'de9ad96ba34e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('rac_checks',
sa.Column('id', sa.String(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(), nullable=False),
sa.Column('account_id', sa.String(), nullable=False),
sa.Column('rac_response', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.NUMERIC(precision=10, scale=2),
type_=sa.Float(),
existing_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('installment_amount')
batch_op.drop_column('repayment_amount')
batch_op.drop_column('upfront_fee')
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.Float(),
type_=sa.NUMERIC(precision=10, scale=2),
existing_nullable=True)
op.drop_table('rac_checks')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Wed Apr 16 18:35:18 UTC 2025
Revision ID: 287ecb02d3d7
Revises: a4847b997191
Create Date: 2025-04-16 18:36:04.632791
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '287ecb02d3d7'
down_revision = 'a4847b997191'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
# ### end Alembic commands ###
@@ -1,42 +0,0 @@
"""Migration on Fri Apr 25 15:01:00 UTC 2025
Revision ID: 2a45dd99c9cb
Revises: 2cf0c177ca02
Create Date: 2025-04-25 15:01:51.129681
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a45dd99c9cb'
down_revision = '2cf0c177ca02'
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('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('total_repayment_amount', sa.Float(), nullable=True))
batch_op.drop_column('principal_amount')
batch_op.drop_column('interest_amount')
batch_op.drop_column('total_installment')
# ### 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.add_column(sa.Column('total_installment', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('interest_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('principal_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.drop_column('total_repayment_amount')
batch_op.drop_column('installment_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -1,41 +0,0 @@
"""Migration on Fri Apr 25 14:02:01 UTC 2025
Revision ID: 2cf0c177ca02
Revises: 1b2339f43824
Create Date: 2025-04-25 14:02:42.244146
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2cf0c177ca02'
down_revision = '1b2339f43824'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('principal_amount', sa.Float(), nullable=True),
sa.Column('interest_amount', sa.Float(), nullable=True),
sa.Column('total_installment', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), 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('loan_repayment_schedules')
# ### end Alembic commands ###
-52
View File
@@ -1,52 +0,0 @@
"""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 ###
+280
View File
@@ -0,0 +1,280 @@
"""empty message
Revision ID: 33e09efd85e3
Revises:
Create Date: 2025-07-03 14:07:14.424548
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import literal_column
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = '33e09efd85e3'
down_revision = None
branch_labels = None
depends_on = None
sequences_and_triggers = [
("transactions", "transactions_seq", "trg_transactions_id"),
("transaction_offers", "transaction_offers_seq", "trg_transaction_offers_id"),
("salaries", "salaries_seq", "trg_salaries_id"),
("repayments_data", "repayments_data_seq", "trg_repayments_data_id"),
("repayments", "repayments_seq", "trg_repayments_id"),
("rac_checks", "rac_checks_seq", "trg_rac_checks_id"),
("loans", "loans_seq", "trg_loans_id"),
(
"loan_repayment_schedules",
"loan_repayment_schedules_seq",
"trg_loan_repayment_schedules_id",
),
("loan_charges", "loan_charges_seq", "trg_loan_charges_id"),
("charges", "charges_seq", "trg_charges_id"),
]
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('accounts',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_type', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('lien_amount', sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('offer_id', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('customers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('msisdn', sa.String(length=20), nullable=False),
sa.Column('country_code', sa.String(length=3), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('msisdn')
)
op.create_table('loan_charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('installment_amount', sa.Float(), nullable=True),
sa.Column('total_repayment_amount', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('loans',
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=True),
sa.Column('original_transaction', sa.String(length=50), nullable=True),
sa.Column('account_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('collection_type', sa.String(length=20), nullable=True),
sa.Column('current_loan_amount', sa.Float(), nullable=True),
sa.Column('initial_loan_amount', sa.Float(), nullable=False),
sa.Column('default_penalty_fee', sa.Float(), nullable=True),
sa.Column('continuous_fee', sa.Float(), nullable=True),
sa.Column('upfront_fee', sa.Float(), nullable=True),
sa.Column('repayment_amount', sa.Float(), nullable=True),
sa.Column('installment_amount', sa.Float(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('due_date', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('disburse_date', sa.DateTime(), nullable=True),
sa.Column('disburse_verify', sa.DateTime(), nullable=True),
sa.Column('reference', sa.String(length=50), nullable=True),
sa.Column('disburse_result', sa.String(length=10), nullable=True),
sa.Column('disburse_description', sa.String(length=100), nullable=True),
sa.Column('verify_result', sa.String(length=10), nullable=True),
sa.Column('verify_description', sa.String(length=100), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('offers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=50), nullable=False),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('tenor', sa.Integer(), nullable=False),
sa.Column('schedule', sa.Integer(), nullable=True),
sa.Column('interest_rate', sa.Float(), nullable=True),
sa.Column('management_rate', sa.Float(), nullable=True),
sa.Column('insurance_rate', sa.Float(), nullable=True),
sa.Column('vat_rate', sa.Float(), nullable=True),
sa.Column('list_order', sa.Integer(), nullable=True),
sa.Column('max_daily_loans', sa.Integer(), nullable=True),
sa.Column('max_active_loans', sa.Integer(), nullable=True),
sa.Column('max_life_loans', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('rac_checks',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('rac_response', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('repay_date', sa.DateTime(), nullable=True),
sa.Column('repay_result', sa.String(length=10), nullable=True),
sa.Column('repay_description', sa.String(length=100), nullable=True),
sa.Column('verify_date', sa.DateTime(), nullable=True),
sa.Column('verify_result', sa.String(length=10), nullable=True),
sa.Column('verify_description', sa.String(length=100), nullable=True),
sa.Column('initiated_by', sa.String(length=50), nullable=True),
sa.Column('salary_amount', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('repayments_data',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True),
sa.Column('customer_id', sa.String(length=50), nullable=True),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('repayment_amount', sa.Float(), nullable=True),
sa.Column('amount_collected', sa.Float(), nullable=True),
sa.Column('added_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('response_code', sa.String(length=10), nullable=True),
sa.Column('response_descr', sa.String(length=255), nullable=True),
sa.Column('balance', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('salaries',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('amount', sa.Float(), nullable=False),
sa.Column('salary_date', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
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('original_transaction', sa.String(length=50), nullable=True),
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(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('customer_id', sa.String(length=50), nullable=True),
sa.Column('type', sa.String(length=50), nullable=False),
sa.Column('channel', sa.String(length=50), nullable=False),
sa.Column('phone_number', sa.String(length=50), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
for table, seq, trg in sequences_and_triggers:
op.execute(
text(
f"""
BEGIN
EXECUTE IMMEDIATE 'CREATE SEQUENCE {seq} START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
"""
)
)
op.execute(
text(
f"""
CREATE OR REPLACE TRIGGER {trg}
BEFORE INSERT ON {table}
FOR EACH ROW
BEGIN
IF ||':'||'NEW.id IS NULL THEN
SELECT {seq}.NEXTVAL INTO '||':'||'NEW.id FROM dual;
END IF;
END;
"""
)
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('transactions')
op.drop_table('transaction_offers')
op.drop_table('salaries')
op.drop_table('repayments_data')
op.drop_table('repayments')
op.drop_table('rac_checks')
op.drop_table('offers')
op.drop_table('loans')
op.drop_table('loan_repayment_schedules')
op.drop_table('loan_charges')
op.drop_table('customers')
op.drop_table('charges')
op.drop_table('accounts')
# ### end Alembic commands ###
for table, seq, trg in sequences_and_triggers:
op.execute(text(f"DROP TRIGGER {trg}"))
op.execute(text(f"DROP SEQUENCE {seq}"))
@@ -1,32 +0,0 @@
"""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 ###
@@ -1,32 +0,0 @@
"""Migration on Fri Apr 11 14:15:19 UTC 2025
Revision ID: 610b7e9d15a6
Revises: 9bb0367eb486
Create Date: 2025-04-11 14:16:12.533227
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610b7e9d15a6'
down_revision = '9bb0367eb486'
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('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('loans', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Mon Apr 14 15:15:05 UTC 2025
Revision ID: 783a023a477f
Revises: f6cd1bfc8832
Create Date: 2025-04-14 15:15:36.991148
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '783a023a477f'
down_revision = 'f6cd1bfc8832'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
# ### 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.drop_column('customer_id')
# ### end Alembic commands ###
-41
View File
@@ -1,41 +0,0 @@
"""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 ###
@@ -1,38 +0,0 @@
"""Migration on Sat Apr 26 12:50:46 UTC 2025
Revision ID: 89759cebb9c6
Revises: 2a45dd99c9cb
Create Date: 2025-04-26 12:50:49.771355
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '89759cebb9c6'
down_revision = '2a45dd99c9cb'
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('interest_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('management_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('insurance_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('vat_rate', sa.Float(), 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('vat_rate')
batch_op.drop_column('insurance_rate')
batch_op.drop_column('management_rate')
batch_op.drop_column('interest_rate')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""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 ###
@@ -1,40 +0,0 @@
"""Migration on Fri Apr 11 12:48:01 UTC 2025
Revision ID: 9bb0367eb486
Revises: fd447d78b161
Create Date: 2025-04-11 12:48:36.145311
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9bb0367eb486'
down_revision = 'fd447d78b161'
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('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('current_loan_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('default_penalty_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('continuous_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('due_date', 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('due_date')
batch_op.drop_column('continuous_fee')
batch_op.drop_column('default_penalty_fee')
batch_op.drop_column('current_loan_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -1,57 +0,0 @@
"""Migration on Wed Apr 16 17:42:49 UTC 2025
Revision ID: a4847b997191
Revises: 783a023a477f
Create Date: 2025-04-16 17:43:22.509659
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a4847b997191'
down_revision = '783a023a477f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('offers',
sa.Column('id', sa.String(), nullable=False),
sa.Column('product_id', sa.String(), nullable=False),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('tenor', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), 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('product_id')
op.drop_table('offers')
op.drop_table('loan_charges')
# ### end Alembic commands ###
@@ -1,86 +0,0 @@
"""Migration on Thu Apr 10 16:21:45 UTC 2025
Revision ID: b8f6fd76ead8
Revises:
Create Date: 2025-04-10 16:22:15.946157
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'b8f6fd76ead8'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.VARCHAR(length=50),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('channel',
existing_type=sa.VARCHAR(length=8),
type_=sa.String(length=50),
existing_nullable=False)
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.drop_constraint('transactions_id_key', type_='unique')
# ### 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.create_unique_constraint('transactions_id_key', ['id'])
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('channel',
existing_type=sa.String(length=50),
type_=sa.VARCHAR(length=8),
existing_nullable=False)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(length=50),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
op.drop_table('repayments')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Thu Apr 17 14:15:36 UTC 2025
Revision ID: de9ad96ba34e
Revises: ec8d97f9b584
Create Date: 2025-04-17 14:16:16.537466
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'de9ad96ba34e'
down_revision = 'ec8d97f9b584'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('offer_id', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
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('charges')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""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 ###
@@ -1,34 +0,0 @@
"""Migration on Thu Apr 17 10:40:05 UTC 2025
Revision ID: ec8d97f9b584
Revises: 287ecb02d3d7
Create Date: 2025-04-17 10:40:34.751272
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ec8d97f9b584'
down_revision = '287ecb02d3d7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""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 ###
@@ -1,34 +0,0 @@
"""Migration on Fri Apr 11 14:34:36 UTC 2025
Revision ID: f6cd1bfc8832
Revises: 610b7e9d15a6
Create Date: 2025-04-11 14:35:07.093967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f6cd1bfc8832'
down_revision = '610b7e9d15a6'
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('collection_type', sa.String(length=20), nullable=True))
batch_op.drop_column('product_id')
# ### 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.add_column(sa.Column('product_id', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
batch_op.drop_column('collection_type')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Fri Apr 11 12:02:45 UTC 2025
Revision ID: fd447d78b161
Revises: 1340e7e578b9
Create Date: 2025-04-11 12:03:28.346671
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fd447d78b161'
down_revision = '1340e7e578b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=True)
batch_op.drop_column('ref_model')
# ### 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.add_column(sa.Column('ref_model', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=False)
# ### end Alembic commands ###
+1
View File
@@ -6,6 +6,7 @@ flask-sqlalchemy
flask-migrate
psycopg2-binary
alembic
oracledb
# Schema for validations
Flask-Marshmallow==0.15.0