Compare commits

...

110 Commits

Author SHA1 Message Date
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
VivianDee 10138f66f3 Update eligibility_check.py 2025-05-30 08:25:40 -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
VivianDee 17db2cf8f9 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore 2025-05-10 08:53:09 +01:00
VivianDee f07866a884 [add]: transaction fix 2025-05-10 08:50:10 +01:00
CHIEFSOFT\ameye 6f8e269a50 clean up active loans 2025-05-09 23:20:05 -04:00
CHIEFSOFT\ameye 4435ca2776 original transaction id 2025-05-09 23:14:08 -04:00
CHIEFSOFT\ameye d851222024 Removed product id 2025-05-09 19:13:51 -04:00
CHIEFSOFT\ameye 52ab33f260 Logger 2025-05-09 19:10:32 -04:00
CHIEFSOFT\ameye af7e0f8624 Loggers 2025-05-09 19:07:43 -04:00
CHIEFSOFT\ameye c8ab2cd6ba transaction_offer_id 2025-05-09 18:58:27 -04:00
CHIEFSOFT\ameye 8ac22fa95f Offer id and interest amount 2025-05-09 18:52:25 -04:00
CHIEFSOFT\ameye 57207faf6f Intereat amount on sawagger 2025-05-09 18:42:19 -04:00
ameye 9a90609d33 Merge branch 'fix_loan_due_date' of DigiFi/digifi-BankToProductCore into master 2025-05-07 21:33:05 +00:00
VivianDee 50ca27abfe [add]: Offer analysis 2025-05-07 12:07:57 +01:00
VivianDee 74066bae56 [add]: offer analysis 2025-05-06 07:09:36 +01:00
VivianDee 4c4ef909c2 [add]: Offer analysis 2025-05-05 17:03:39 +01:00
VivianDee fdd7c58fab [fix]: loan due date 2025-05-05 12:17:26 +01:00
ameye 4bd163fb31 eligible amount migration 2025-05-03 17:59:17 -04:00
ameye d77181f627 Fix loan id 2025-05-03 17:44:38 -04:00
CHIEFSOFT\ameye 4a236fdd2f eligible_amount 2025-05-03 17:40:38 -04:00
CHIEFSOFT\ameye cae7ffd772 transaction offer 2025-05-03 17:08:55 -04:00
ameye 4f92f2a1a0 Select offer update 2025-05-03 16:48:55 -04:00
CHIEFSOFT\ameye 03adb266bb approved_amount 2025-05-03 09:04:42 -04:00
CHIEFSOFT\ameye d28bf95c97 random play on the data 2025-05-03 08:52:25 -04:00
ameye bd6edf52e1 Merge branch 'loan_schedule_fix' of DigiFi/digifi-BankToProductCore into master 2025-04-30 12:22:14 +00:00
VivianDee b1260895e0 Update provide_loan.py 2025-04-30 12:24:26 +01:00
VivianDee 2addf25a67 [fix]: Offer schedules 2025-04-30 12:11:32 +01:00
ameye 9dc431e66d Merge branch 'loan_schedule_fix' of DigiFi/digifi-BankToProductCore into master 2025-04-30 09:03:57 +00:00
VivianDee 9dae2d951c [add]: transaction id to loan schedules, [add]: tenor to loans 2025-04-30 09:57:49 +01:00
ameye a1d44e0e23 Transaction ID on payment Schedule table 2025-04-30 03:28:59 -04:00
ameye d9f972a425 Merge branch 'advanced_eligibility' of DigiFi/digifi-BankToProductCore into master 2025-04-29 19:51:23 +00:00
VivianDee 8aa2c86ea2 [fix]: transaction id 2025-04-29 16:52:51 +01:00
vivian.d.simbrellang.com 9c42332a83 Merge branch 'advanced_eligibility' of DigiFi/digifi-BankToProductCore into master 2025-04-29 07:27:41 +00:00
VivianDee 92eadbfa16 [update]: Eligibility check 2025-04-29 08:26:41 +01:00
ameye 0fbdebceb3 Merge branch 'DIG-loan-linking-001' of DigiFi/digifi-BankToProductCore into master 2025-04-26 19:40:29 +00:00
ameye 488a1b4bdd First step to linked loans 2025-04-26 15:39:37 -04:00
ameye cdc74d05c4 Merge branch 'DIG-rework-rates-fee-calulations-001' of DigiFi/digifi-BankToProductCore into master 2025-04-26 18:34:27 +00:00
ameye 1b92ede296 Reworked the fee calculations structure 2025-04-26 14:31:06 -04:00
ameye 7de4e3651f Merge branch 'DIG-move-data-new-columns-001' of DigiFi/digifi-BankToProductCore into master 2025-04-26 12:56:10 +00:00
46 changed files with 1612 additions and 547 deletions
+35
View File
@@ -0,0 +1,35 @@
# Environment Variables
BASIC_AUTH_USERNAME=user
BASIC_AUTH_PASSWORD=password
#swagger Configuration
SWAGGER_URL="/documentation"
API_URL="/swagger.json"
# Database Configuration
DATABASE_USER=firstadvance
DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=dev-data.simbrellang.net
DATABASE_PORT=10532
DATABASE_NAME=firstadvancedev
# DATABASE_HOST=10.20.30.60
# DATABASE_USER=firstadvance
# DATABASE_PASSWORD=firstadvance
# DATABASE_NAME=firstadvancedev
# DATABASE_PORT=5432
# Flask Configuration
FLASK_APP=wsgi.py
FLASK_ENV=development
APP_PORT=4500
# Bank Call Service Connection
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
VALID_APP_ID=app1
VALID_API_KEY=test-api-key-12345
# Event Bus Broker Configuration
KAFKA_BROKER="10.0.0.246:9092"
+1
View File
@@ -3,4 +3,5 @@ from enum import Enum
class LoanStatus(str, Enum): class LoanStatus(str, Enum):
PENDING = "pending" PENDING = "pending"
ACTIVE = "active" ACTIVE = "active"
START_REPAY = "start_repay"
REPAID = "repaid" REPAID = "repaid"
+56 -195
View File
@@ -1,251 +1,112 @@
from flask import jsonify from flask import jsonify
from typing import List, Dict, Union, Optional, Any from typing import Optional, Union, Dict, List, Any
class ResponseHelper: class ResponseHelper:
""" """
A helper class for building standardized JSON responses in Flask. A helper class for building standardized JSON responses using resultCode and resultDescription.
""" """
@staticmethod @staticmethod
def build_response( def build_response(
status: bool, result_code: str,
message: str, result_description: str,
data: Optional[Union[Dict, List, str]] = None, data: Optional[Union[Dict, List, str]] = None
status_code: int = 200,
error: Optional[Union[Dict, str]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""
Build a standardized JSON response.
Args:
status (bool): Indicates whether the request was successful.
message (str): A message describing the result of the request.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
status_code (int): The HTTP status code for the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
response = { response = {
"status": status, "resultCode": result_code,
"statusCode": status_code, "resultDescription": result_description
"message": message,
"data": data if data is not None else {},
"error": error if error is not None else {},
} }
return jsonify(response), status_code
if isinstance(data, dict):
response.update(data)
return jsonify(response)
@staticmethod @staticmethod
def success( def success(
data: Optional[Union[Dict, List, str]] = None, result_description: str = "Successful",
message: str = "Successful", result_code: str = "00",
status_code: int = 200, data: Optional[Dict[str, Any]] = None
error: Optional[Union[Dict, str]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a success response.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
status_code (int): The HTTP status code for the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, status_code, error)
@staticmethod @staticmethod
def error( def error(
message: str = "An error occurred", result_description: str = "An error occurred",
status_code: int = 400, result_code: str = "01",
data: Optional[Union[Dict, List, str]] = None, data: Optional[Dict[str, Any]] = None
error: Optional[Union[Dict, str]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return an error response.
Args:
message (str): A message describing the error.
status_code (int): The HTTP status code for the response.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, status_code, error)
@staticmethod @staticmethod
def created( def created(
data: Optional[Union[Dict, List, str]] = None, result_description: str = "Resource created successfully",
message: str = "Resource created successfully", result_code: str = "00",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for a created resource.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, 201, error)
@staticmethod @staticmethod
def updated( def updated(
data: Optional[Union[Dict, List, str]] = None, result_description: str = "Resource updated successfully",
message: str = "Resource updated successfully", result_code: str = "00",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for an updated resource.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, 200, error)
@staticmethod @staticmethod
def internal_server_error( def internal_server_error(
message: str = "Internal Server Error", result_description: str = "Internal Server Error",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "500",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for an internal server error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 500, error)
@staticmethod @staticmethod
def unauthorized( def unauthorized(
message: str = "Unauthorized", result_description: str = "Unauthorized",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "401",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for an unauthorized request.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 401, error)
@staticmethod @staticmethod
def forbidden( def forbidden(
message: str = "Forbidden", result_description: str = "Forbidden",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "403",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for a forbidden request.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 403, error)
@staticmethod @staticmethod
def not_found( def not_found(
message: str = "Resource not found", result_description: str = "Resource not found",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "404",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for a not found resource.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 404, error)
@staticmethod @staticmethod
def unprocessable_entity( def unprocessable_entity(
message: str = "Unprocessable entity", result_description: str = "Unprocessable entity",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "422",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for an unprocessable entity.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 422, error)
@staticmethod @staticmethod
def method_not_allowed( def method_not_allowed(
message: str = "Method Not Allowed", result_description: str = "Method Not Allowed",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "405",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for a method not allowed error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 405, error)
@staticmethod @staticmethod
def bad_request( def bad_request(
message: str = "Bad Request", result_description: str = "Bad Request",
data: Optional[Union[Dict, List, str]] = None, result_code: str = "400",
error: Optional[Union[Dict, str]] = None, data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" return ResponseHelper.build_response(result_code, result_description, data)
Return a response for a bad request error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 400, error)
+23 -4
View File
@@ -7,15 +7,26 @@ import logging
class SimbrellaIntegration: class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
@staticmethod @staticmethod
def rac_check(customer_id, account_id, transaction_id): def rac_check(customer_id, account_id, transaction_id):
""" """
Calls the RACCheck endpoit Calls the RACCheck endpoit
""" """
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck" url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
payload = { # {
# "transactionId": "T001",
# "fbnTransactionId": "Tr201712RK9232P115",
# "customerId": "CN621868",
# "accountId": "2017821799",
# "channel": "USSD",
# "countryCode": "NG"
# }
#
payload_old = {
"customerId": customer_id, "customerId": customer_id,
"accountId": account_id, "accountId": account_id,
"transactionId": str(transaction_id), "transactionId": str(transaction_id),
@@ -35,7 +46,15 @@ class SimbrellaIntegration:
], ],
} }
logger.error(f"This is PayLoad: {str(payload)}", exc_info=True) payload = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
"fbnTransactionId": f"FBN{transaction_id}",
"countryCode": "NG",
"channel": "USSD"
}
# logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -46,7 +65,7 @@ class SimbrellaIntegration:
try: try:
response = httpx.post(url, json=payload, headers=headers, timeout=10.0) response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.error(f"This is Response: {str(response)}", exc_info=True) logger.info(f"This is Response: {str(response)}", exc_info=True)
return response return response
+2 -2
View File
@@ -5,8 +5,8 @@ class RepaymentSchema(Schema):
type = fields.Str(required=False) type = fields.Str(required=False)
msisdn = fields.Str(required=False) #optional msisdn = fields.Str(required=False) #optional
debtId = fields.Str(required=True) debtId = fields.Str(required=True)
productId = fields.Str(required=True)
transactionId = fields.Str(required=True) transactionId = fields.Str(required=True)
accountId = fields.Str(required=True) accountId = fields.Str(required=True)
customerId = fields.Str(required=True) customerId = fields.Str(required=True)
channel = fields.Str(required=True) loanRef = fields.Str(required=True)
initiatedBy = fields.Str(required=False)
+1
View File
@@ -9,5 +9,6 @@ class SelectOfferSchema(Schema):
msisdn = fields.Str(required=True) msisdn = fields.Str(required=True)
requestedAmount = fields.Float(required=True) requestedAmount = fields.Float(required=True)
productId = fields.Str(required=True) productId = fields.Str(required=True)
offerId = fields.Str(required=True)
channel = fields.Str(required=True) channel = fields.Str(required=True)
+1
View File
@@ -6,3 +6,4 @@ from app.api.services.repayment import RepaymentService
from app.api.services.customer_consent import CustomerConsentService from app.api.services.customer_consent import CustomerConsentService
from app.api.services.notification_callback import NotificationCallbackService from app.api.services.notification_callback import NotificationCallbackService
from app.api.services.authorization import AuthorizationService from app.api.services.authorization import AuthorizationService
from app.api.services.offer_analysis import OfferAnalysis
+7 -7
View File
@@ -34,7 +34,7 @@ class AuthorizationService(BaseService):
logger.info("Processing Authorization request") logger.info("Processing Authorization request")
if not data: if not data:
return ResponseHelper.bad_request(message="Missing JSON in request") return ResponseHelper.bad_request(result_description="Missing JSON in request")
# Validate input data using the Authorization schema # Validate input data using the Authorization schema
schema = AuthorizeRequestSchema() schema = AuthorizeRequestSchema()
@@ -44,7 +44,7 @@ class AuthorizationService(BaseService):
validated_data["username"] != USERNAME validated_data["username"] != USERNAME
or validated_data["password"] != PASSWORD or validated_data["password"] != PASSWORD
): ):
return ResponseHelper.unauthorized(message="Invalid credentials") return ResponseHelper.unauthorized(result_description="Invalid credentials")
access_token = create_access_token(identity=validated_data["username"]) access_token = create_access_token(identity=validated_data["username"])
refresh_token = create_refresh_token(identity=validated_data["username"]) refresh_token = create_refresh_token(identity=validated_data["username"])
@@ -56,17 +56,17 @@ class AuthorizationService(BaseService):
} }
return ResponseHelper.success( return ResponseHelper.success(
data=response_data, message="Authorization processed successfully" data={"data": response_data}, result_description="Authorization processed successfully"
) )
except ValidationError as e: except ValidationError as e:
logger.error(f"Validation error: {e}") logger.error(f"Validation error: {e}")
return ResponseHelper.bad_request(message=f"Validation error: {e}") return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
except Exception as e: except Exception as e:
logger.error(f"Error processing Authorization request: {e}") logger.error(f"Error processing Authorization request: {e}")
return ResponseHelper.internal_server_error( return ResponseHelper.internal_server_error(
message=f"Error processing Authorization request: {e}" result_description=f"Error processing Authorization request: {e}"
) )
@staticmethod @staticmethod
@@ -92,11 +92,11 @@ class AuthorizationService(BaseService):
} }
return ResponseHelper.success( return ResponseHelper.success(
data=response_data, message="RefreshToken processed successfully" data={"data": response_data}, result_description="RefreshToken processed successfully"
) )
except Exception as e: except Exception as e:
logger.error(f"Error processing RefreshToken request: {e}") logger.error(f"Error processing RefreshToken request: {e}")
return ResponseHelper.internal_server_error( return ResponseHelper.internal_server_error(
message=f"Error processing RefreshToken request: {e}" result_description=f"Error processing RefreshToken request: {e}"
) )
+47 -24
View File
@@ -79,11 +79,11 @@ class BaseService:
return {"error": "No charges found for the offer"} return {"error": "No charges found for the offer"}
loan_charges = offer.charges loan_charges = offer.charges
tenor = offer.tenor // 30 # Convert to months tenor = offer.schedule # offer.tenor // 30 # Convert to months
interest = cls.get_charge_detail(charges = loan_charges, code = "INTEREST", amount = amount) interest = cls.get_charge_detail(rates = offer.interest_rate, charges = loan_charges, code = "INTEREST", amount = amount)
management = cls.get_charge_detail(charges = loan_charges, code = "MGTFEE", amount = amount) management = cls.get_charge_detail(rates = offer.management_rate, charges = loan_charges, code = "MGTFEE", amount = amount)
insurance = cls.get_charge_detail(charges = loan_charges, code = "INSURANCE", amount = amount) insurance = cls.get_charge_detail(rates = offer.insurance_rate, charges = loan_charges, code = "INSURANCE", amount = amount)
vat = cls.get_charge_detail(charges = loan_charges, code = "VAT", amount = amount, management_fee = management["fee"]) vat = cls.get_charge_detail(rates = offer.vat_rate, charges = loan_charges, code = "VAT", amount = amount, management_fee = management["fee"])
# Separate fees into upfront and postpaid # Separate fees into upfront and postpaid
upfront_fees = [ upfront_fees = [
@@ -97,21 +97,33 @@ class BaseService:
for fee in [interest, management, insurance, vat] for fee in [interest, management, insurance, vat]
if fee["due_days"] != 0 if fee["due_days"] != 0
] ]
vat_test = vat["fee"]
logger.info(f"VAT fee == *************** : {vat_test}")
# Up-front payment: (only those fees due immediately i.e due_days == 0) # Up-front payment: (only those fees due immediately i.e due_days == 0)
upfront_payment = sum(upfront_fees) # upfront_payment = sum(upfront_fees)
if offer.schedule == 1:
upfront_payment = vat["fee"] + management["fee"] + insurance["fee"] + interest["fee"]
interest_amount = interest["fee"]
repayment_amount = amount
else:
upfront_payment = vat["fee"] + insurance["fee"]+management["fee"]
interest_amount = interest["fee"]*offer.schedule
repayment_amount = amount + interest_amount
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0) # Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
repayment_amount = amount + (sum(postpaid_fees) * tenor) # repayment_amount = amount + (sum(postpaid_fees) * tenor)
# Total amount: (upfront_payment + repayment_amount) # Total amount: (upfront_payment + repayment_amount)
total_amount = upfront_payment + repayment_amount total_amount = upfront_payment + repayment_amount
# Calculate the installment amount # Calculate the installment amount
installment_amount = repayment_amount / tenor installment_amount = repayment_amount / offer.schedule
return { return {
"interest": interest, "interest": interest,
"interest_amount": interest_amount,
"management": management, "management": management,
"insurance": insurance, "insurance": insurance,
"vat": vat, "vat": vat,
@@ -123,28 +135,39 @@ class BaseService:
@classmethod @classmethod
def get_charge_detail(cls, charges, code, amount, management_fee=None): def get_charge_detail(cls, rates, charges, code, amount, management_fee= 0.0):
""" """
Get details for a specific charge code from a list of loan charges. Get details for a specific charge code from a list of loan charges.
Returns default values if not found. Returns default values if not found.
""" """
fee = 0.0
if code == "VAT" and management_fee > 0:
for charge in charges: fee = management_fee * rates / 100
if charge.code == code: else:
fee = ( fee = amount * rates / 100
management_fee * charge.percent / 100
if code == "VAT" and management_fee is not None return {
else amount * charge.percent / 100 "rate": rates,
) "fee": round(fee, 2),
"due_days": 30,
"code": code,
"description" : "have no idea how to get this yet"
}
# if charge.code == code:
# if code == "VAT" and management_fee > 0:
# fee = management_fee * rates / 100
# else:
# fee = amount * rates / 100
#
# return {
# "rate": rates,
# "fee": round(fee, 2),
# "due_days": charge.due
# }
return { # return {"rate": 0, "fee": 0, "due_days": 0}
"rate": charge.percent,
"fee": round(fee, 2),
"due_days": charge.due
}
return {"rate": 0, "fee": 0, "due_days": 0}
+7 -24
View File
@@ -1,4 +1,5 @@
from flask import request, jsonify from flask import request, jsonify
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from marshmallow import ValidationError from marshmallow import ValidationError
from app.utils.logger import logger from app.utils.logger import logger
@@ -34,44 +35,26 @@ class CustomerConsentService(BaseService):
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({ return ResponseHelper.error(result_description="Failed to log transaction.")
"message": "Failed to log transaction."
}), 400
else: else:
return jsonify({ return ResponseHelper.error(result_description="Invalid Customer or Account")
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Request is received"
}
db.session.commit() db.session.commit()
return response_data return ResponseHelper.success(result_description="Request is received")
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.error(result_description=str(err))
return jsonify({
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback() db.session.rollback()
return jsonify({ return ResponseHelper.internal_server_error()
"message": "Internal Server Error"
}) , 500
+128 -28
View File
@@ -1,4 +1,6 @@
from flask import session, jsonify from flask import session, jsonify
from app.models.loan import Loan
from app.models.transaction_offers import TransactionOffer
from app.utils.logger import logger from app.utils.logger import logger
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from app.api.schemas.eligibility_check import EligibilityCheckSchema from app.api.schemas.eligibility_check import EligibilityCheckSchema
@@ -6,7 +8,12 @@ from marshmallow import ValidationError
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.api.integrations import SimbrellaIntegration from app.api.integrations import SimbrellaIntegration
from app.extensions import db from app.extensions import db
from app.models import Offer from app.models import Offer, RACCheck
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
import random
class EligibilityCheckService(BaseService): class EligibilityCheckService(BaseService):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@@ -39,29 +46,100 @@ class EligibilityCheckService(BaseService):
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({ return ResponseHelper.error(result_description="Failed to log transaction.")
"message": "Failed to log transaction." else:
}), 400 return ResponseHelper.error(result_description="Invalid Customer or Account")
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
db.session.flush() db.session.flush()
# Determine Loan count
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
if not is_eligible:
return ResponseHelper.error(result_description="Max loan count reached")
# Call RACCheck # Call RACCheck
response = SimbrellaIntegration.rac_check( response = SimbrellaIntegration.rac_check(
customer_id = customer_id, customer_id = customer_id,
account_id = account_id, account_id = account_id,
transaction_id = transaction.id, transaction_id = transaction.transaction_id,
) )
# this chck for error is not valid # this chek for error is not valid
if response.status_code != 200: if response.status_code != 200:
return jsonify({"message": "RACCheck failed"}), 400 return ResponseHelper.error(result_description="RACCheck failed")
response = response.json()
offers = [offer.to_dict() for offer in Offer.get_all_offers()] logger.info(f"This is Response (from Eligibility Check): {str(response)}", exc_info=True)
if not response or response['responseCode'] != '00':
if response:
logger.error(f"{response['responseMessage']}")
return ResponseHelper.error(result_description=f"RACCheck failed")
rack_checks_response = response['data']['racResponse']
rac_check = RACCheck.add_rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.transaction_id,
data = rack_checks_response
)
if not rac_check:
logger.error(f"Failed to save RACCheck")
return ResponseHelper.error(result_description="Failed to save RACCheck.")
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
# eligible_offers = []
try:
eligible_offers = OfferAnalysis.decide_offer(
transaction_id=transactionId,
rac_check=rac_check,
validated_data=validated_data,
customer_id=customer_id,
rack_checks_response =rack_checks_response
)
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description= str(ve))
# -----------------------------------------------------------------------
# s = Offer.get_all_offers()
# eligible_offers = []
# for offer in offers:
# # Determine an approved amount
# random_float = random.random() # temporary to play data
# approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
# approved_amount = round(approved_amount, 2)
#
# transaction_offer = TransactionOffer.create_transaction_offer(
# customer_id = customer.id,
# transaction_id = transaction.transaction_id,
# offer_id = offer.id,
# min_amount = offer.min_amount,
# max_amount = offer.max_amount,
# eligible_amount = approved_amount,
# product_id = offer.product_id,
# tenor = offer.tenor
# )
#
# # Visible offer ID: offer_id + padded(transaction_offer.id)
# padded_id = str(transaction_offer.id).zfill(6)
# public_offer_id = f"{offer.id}{padded_id}"
#
# eligible_offers.append({
# "offerId": public_offer_id,
# "product_id": offer.product_id,
# "min_amount": offer.min_amount,
# "max_amount": approved_amount,
# "tenor": offer.tenor
# })
# Simulate processing # Simulate processing
response_data = { response_data = {
@@ -69,31 +147,53 @@ class EligibilityCheckService(BaseService):
"transactionId": transactionId, "transactionId": transactionId,
"countryCode": "NG", "countryCode": "NG",
"msisdn": msisdn, "msisdn": msisdn,
"eligibleOffers": offers, "eligibleOffers": eligible_offers,
"resultDescription": "Successful",
"resultCode": "00",
"accountId": account_id "accountId": account_id
} }
return response_data return ResponseHelper.success(data=response_data)
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return ResponseHelper.unprocessable_entity(result_description="Validation exception")
"message": "Validation exception"
}) , 422
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return ResponseHelper.error(result_description=str(err))
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({ db.session.rollback()
"message": "Internal Server Error" return ResponseHelper.internal_server_error()
}) , 500
@staticmethod
def check_loan_limits(customer_id):
"""
Checks if a customer has exceeded the loan limits for given offer.
"""
loan = Loan.get_customer_last_loan(customer_id)
if not loan:
return True
offer_id = loan.offer_id[:5]
offer = Offer.get_offer_by_id(offer_id)
if not offer:
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
return False
daily_count = TransactionOffer.get_daily_loan_count(customer_id, offer_id)
logger.error(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
if offer.max_daily_loans is not None and daily_count >= offer.max_daily_loans:
return False
return True
+13 -41
View File
@@ -6,7 +6,8 @@ from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.extensions import db from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class LoanStatusService(BaseService): class LoanStatusService(BaseService):
@@ -28,42 +29,23 @@ class LoanStatusService(BaseService):
# Validate data # Validate data
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema()) validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
customer_id = validated_data.get('customerId') customer_id = validated_data.get('customerId')
customer = Customer.get_customer(customer_id)
logger.info(f"Looking for customer *** {customer_id}")
customer = Customer.get_customer_with_loan_list(customer_id)
transactionId = validated_data.get('transactionId') transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId') account_id = validated_data.get('accountId')
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans # Get loans
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE] loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data) transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({ return ResponseHelper.error(result_description="Failed to log transaction.")
"message": "Failed to log transaction." else:
}), 400 return ResponseHelper.error(result_description="Invalid Customer or Account")
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# loans = [
# {
# "debtId": "123456789",
# "loanDate": "2019-10-18 14:26:21.063",
# "dueDate": "2019-11-20 14:26:21.063",
# "currentLoanAmount": 8500,
# "initialLoanAmount": 10000,
# "defaultPenaltyFee": 0,
# "continuousFee": 0,
# "productId": "101"
# }
# ]
total_debt_amount = sum( total_debt_amount = sum(
loan.get("currentLoanAmount") or 0 loan.get("currentLoanAmount") or 0
@@ -76,33 +58,23 @@ class LoanStatusService(BaseService):
"transactionId": transactionId, "transactionId": transactionId,
"loans": loans, "loans": loans,
"totalDebtAmount": total_debt_amount, "totalDebtAmount": total_debt_amount,
"resultCode": "00",
"resultDescription": "Successful"
} }
db.session.commit() db.session.commit()
return response_data return ResponseHelper.success(data=response_data)
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.error(result_description=str(err))
return jsonify({
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback() db.session.rollback()
return jsonify({ return ResponseHelper.internal_server_error()
"message": "Internal Server Error"
}) , 500
+8 -24
View File
@@ -5,6 +5,7 @@ from app.api.enums import TransactionType
from app.utils.logger import logger from app.utils.logger import logger
from app.api.schemas.notification_callback import NotificationCallbackSchema from app.api.schemas.notification_callback import NotificationCallbackSchema
from app.extensions import db from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class NotificationCallbackService(BaseService): class NotificationCallbackService(BaseService):
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
@@ -27,37 +28,20 @@ class NotificationCallbackService(BaseService):
schema = NotificationCallbackSchema() schema = NotificationCallbackSchema()
validated_data = schema.load(data) # Raises ValidationError if invalid validated_data = schema.load(data) # Raises ValidationError if invalid
# Simulated processing logic return ResponseHelper.success()
response_data = {
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success(
# data=response_data,
# message="Notification callback processed successfully"
# )
return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return ResponseHelper.unprocessable_entity(result_description="Validation exception")
"message": "Validation exception"
}) , 422
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return ResponseHelper.error(result_description=str(err))
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({ db.session.rollback()
"message": "Internal Server Error" return ResponseHelper.internal_server_error()
}) , 500
+150
View File
@@ -0,0 +1,150 @@
from app.models import Offer, TransactionOffer
from app.models.loan import Loan
import random
import logging
logger = logging.getLogger(__name__)
class OfferAnalysis:
@staticmethod
def get_offer(transaction_id, rac_response, validated_data):
customer_id = validated_data.get("customerId")
product_id = validated_data.get("productId")
offer_id = validated_data.get("offerId")
transaction_offer_id = int(offer_id[5:]) # The last part is int
logger.info(f"customer_id == *************** : {customer_id}")
logger.info(f"product_id == *************** : {product_id}")
logger.info(f"offer_id == *************** : {offer_id}")
logger.info(f"transaction_offer_id == *************** : {transaction_offer_id}")
transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, product_id)
if not transaction_offer:
raise ValueError("Invalid Transaction Offer.")
eligible_amount = transaction_offer.eligible_amount
offer = Offer.is_valid_offer( transaction_offer.offer_id)
if not offer:
raise ValueError("Invalid Offer.")
original_transaction = transaction_id
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response):
# "racResponse": {
# "accountStatus": true,
# "bvnValidated": true,
# "creditBureauCheck": false,
# "crmsCheck": true,
# "hasLien": false,
# "hasPastDueLoan": false,
# "hasSalaryAccount": true,
# "isWhitelisted": true,
# "noBouncedCheck": true
# },
#
return 0
@staticmethod
def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response):
eligible_offers = []
# if we have active offers - we have to feed off it
logger.info(f"**RACK ANALYSIS** {customer_id}")
# Analyze Rack Checks
# _analyze_rack_checks(rack_checks_response) --> We need detail analysis
# we can now find the origin transactions
# Find the last loan - it will have original_transaction
last_customer_loan = Loan.get_customer_last_loan(customer_id)
# logger.info(f"{last_customer_loan}")
new_eligible_amount = 0
if last_customer_loan:
original_transaction = last_customer_loan.original_transaction or last_customer_loan.transaction_id
logger.info(f"transaction_id |-| original_transaction === > {transaction_id} {original_transaction}")
original_loan = Loan.get_customer_original_loan(customer_id, original_transaction)
if original_loan is not None:
logger.info(f"original_loan === > {original_loan}")
logger.info(f"loan_offer_id === > {original_loan.offer_id}")
original_offer_id = str(original_loan.offer_id[:5]) # The last part is str
transaction_offer_id = int(original_loan.offer_id[5:]) # The last part is int
original_transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, original_loan.product_id)
active_loans = Loan.get_active_loans_by_original_transaction(original_transaction)
sum_active_loans = sum(loan.current_loan_amount for loan in active_loans)
logger.info(f"sum_active_loans === > {sum_active_loans}")
real_eligible_amount = original_loan.eligible_amount - sum_active_loans
if real_eligible_amount < original_transaction_offer.min_amount:
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=original_transaction,
offer_id=original_offer_id,
min_amount=original_transaction_offer.min_amount,
max_amount=original_transaction_offer.max_amount,
eligible_amount=real_eligible_amount,
product_id=original_loan.product_id,
tenor=original_loan.tenor
)
# Visible offer ID: offer_id + padded(transaction_offer.id)
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{original_offer_id}{padded_id}"
eligible_offers.append({
"offerId": public_offer_id,
"product_id": original_transaction_offer.product_id,
"min_amount": original_transaction_offer.min_amount,
"max_amount": round(real_eligible_amount, 2),
"tenor": original_loan.tenor
})
return eligible_offers
offers = Offer.get_all_offers()
for offer in offers:
# Get approved amount
random_float = random.random() # temporary to play data
approved_amount = new_eligible_amount if new_eligible_amount > 0 else min(offer.max_amount, offer.max_amount * random_float)
approved_amount = round(approved_amount, 2)
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=transaction_id,
offer_id=offer.id,
min_amount=offer.min_amount,
max_amount=offer.max_amount,
eligible_amount=approved_amount,
product_id=offer.product_id,
tenor=offer.tenor
)
# Visible offer ID: offer_id + padded(transaction_offer.id)
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{offer.id}{padded_id}"
eligible_offers.append({
"offerId": public_offer_id,
"product_id": offer.product_id,
"min_amount": offer.min_amount,
"max_amount": approved_amount,
"tenor": offer.tenor
})
return eligible_offers
+77 -46
View File
@@ -8,13 +8,14 @@ from app.models.loan_charge import LoanCharge
from app.utils.logger import logger from app.utils.logger import logger
from app.api.schemas.provide_loan import ProvideLoanSchema from app.api.schemas.provide_loan import ProvideLoanSchema
from threading import Thread from threading import Thread
from app.models import Loan, Offer, Charge from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck
from app.api.enums import LoanStatus from app.api.enums import LoanStatus
from app.extensions import db from app.extensions import db
from datetime import datetime, timezone from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from app.models import LoanRepaymentSchedule from app.models import LoanRepaymentSchedule
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
class ProvideLoanService(BaseService): class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -42,27 +43,57 @@ class ProvideLoanService(BaseService):
offer_id = validated_data.get('offerId') offer_id = validated_data.get('offerId')
amount = validated_data.get("requestedAmount") amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId") product_id = validated_data.get("productId")
channel = validated_data.get('channel')
customer = Customer.is_valid_customer(customer_id) customer = Customer.is_valid_customer(customer_id)
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
offer = Offer.is_valid_offer(offer_id) rac_response = RACCheck.get_rac_check(customer_id = customer_id, account_id = account_id)
if not offer: try:
logger.error(f"Invalid Offer") transaction_offer, offer, eligible_amount, original_transaction = OfferAnalysis.get_offer(
return jsonify({ transaction_id=transaction_id,
"message": "Invalid Offer." rac_response=rac_response,
}), 400 validated_data=validated_data
)
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description=str(ve))
if(amount < transaction_offer.min_amount):
return ResponseHelper.error(result_description="The amount is less than the minimum allowed transaction amount.")
elif amount > transaction_offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed transaction amount.")
# transaction_offer_id = int(offer_id[5:]) # The last part is int
# transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id)
# if not transaction_offer:
# logger.error(f"Invalid Transaction Offer")
# return jsonify({
# "message": "Invalid Transaction Offer."
# }), 400
# eligible_amount = transaction_offer.eligible_amount
# offer = Offer.is_valid_offer( transaction_offer.offer_id)
# if not offer:
# logger.error(f"Invalid Offer")
# return jsonify({
# "message": "Invalid Offer."
# }), 400
# Log Transaction # Log Transaction
transaction = ProvideLoanService.log_transaction(validated_data=validated_data) transaction = ProvideLoanService.log_transaction(validated_data=validated_data)
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({ return ResponseHelper.error(result_description="Failed to log transaction.")
"message": "Failed to log transaction."
}), 400
db.session.flush() db.session.flush()
@@ -70,9 +101,19 @@ class ProvideLoanService(BaseService):
charges = ProvideLoanService.calculate_charges(offer, amount) charges = ProvideLoanService.calculate_charges(offer, amount)
upfront_fee = charges["upfront_payment"] upfront_fee = charges["upfront_payment"]
repayment_amount = charges["repayment_amount"] repayment_amount = charges["repayment_amount"]
#installment_amount = charges["installment_amount"]
num_schedules = offer.schedule
upfront_payment = charges["upfront_payment"]
total_amount = charges["total_amount"]
installment_amount = charges["installment_amount"] installment_amount = charges["installment_amount"]
tenor = offer.tenor // 30 # Convert to months interest = charges["interest"]
management = charges["management"]
insurance = charges["insurance"]
vat = charges["vat"]
padded_id = str(transaction_id).zfill(12)
loan_ref = f"{padded_id}{channel}{offer.product_id}"
# Save the loan details # Save the loan details
@@ -83,33 +124,34 @@ class ProvideLoanService(BaseService):
product_id = offer.product_id, product_id = offer.product_id,
collection_type = collection_type, collection_type = collection_type,
transaction_id = validated_data.get('transactionId'), transaction_id = validated_data.get('transactionId'),
original_transaction = transaction_offer.original_transaction,
initial_loan_amount = validated_data.get('requestedAmount'), initial_loan_amount = validated_data.get('requestedAmount'),
upfront_fee = upfront_fee, upfront_fee = upfront_fee,
repayment_amount = repayment_amount, repayment_amount = repayment_amount,
installment_amount = installment_amount, installment_amount = installment_amount,
status= LoanStatus.ACTIVE eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor,
reference = loan_ref
) )
if not loan: if not loan:
logger.error(f"Failed to save loan details") logger.error(f"Failed to save loan details")
return jsonify({
"message": "Failed to save loan details." return ResponseHelper.error(result_description="Failed to save loan details.")
}), 400
db.session.flush() db.session.flush()
current_product_id = offer.product_id
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, tenor = tenor) schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedule: if not schedule:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}") logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return jsonify({ return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
"message": "Failed to generate loan repayment schedule."
}), 400
charges = Charge.get_offer_charges(offer.id) # charges = Charge.get_offer_charges(offer.id)
# logger.error(f"{charges}") # logger.info(f"{charges}")
loan_id = loan.id loan_id = loan.id
@@ -118,50 +160,39 @@ class ProvideLoanService(BaseService):
else: else:
return jsonify({ return ResponseHelper.error(result_description="Invalid Customer or Account")
"message": "Invalid Customer or Account"
}), 400
response_data = { response_data = {
"requestId": request_id, "requestId": request_id,
"transactionId": transaction_id, "transactionId": transaction_id,
"loanRef": loan_ref,
"customerId": customer_id, "customerId": customer_id,
"accountId": account_id, "accountId": account_id,
"msisdn": customer.msisdn, "msisdn": customer.msisdn
"resultCode": "00",
"resultDescription": "Successful"
} }
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id) # KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# Call Kafka in a background thread # Call Kafka in a background thread
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT")) thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start() thread.start()
db.session.commit() db.session.commit()
return response_data return ResponseHelper.success(data=response_data)
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.error(result_description=str(err))
return jsonify({
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback() db.session.rollback()
return jsonify({ return ResponseHelper.internal_server_error()
"message": "Internal Server Error"
}) , 500
+24 -45
View File
@@ -1,6 +1,7 @@
from flask import request, jsonify from flask import request, jsonify
from marshmallow import ValidationError from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus from app.api.enums.loan_status import LoanStatus
from app.api.helpers.response_helper import ResponseHelper
from app.models import Repayment from app.models import Repayment
from app.models.customer import Customer from app.models.customer import Customer
from app.models.loan import Loan from app.models.loan import Loan
@@ -31,88 +32,66 @@ class RepaymentService(BaseService):
customer_id = validated_data.get('customerId') customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId') request_id = validated_data.get('requestId')
loan_id = validated_data.get('debtId') loan_id = validated_data.get('debtId')
product_id = validated_data.get('productId')
account_id = validated_data.get('accountId') account_id = validated_data.get('accountId')
customer = Customer.get_customer(customer_id) loan_ref = validated_data.get('loanRef')
# customer = Customer.get_customer_with_loan_list(customer_id)
transaction_id = validated_data.get('transactionId') transaction_id = validated_data.get('transactionId')
initiated_by = validated_data.get('initiatedBy')
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Save the repayment details # Save the repayment details
repayment = Repayment.create_repayment( repayment = Repayment.create_repayment(
customer_id = customer_id, customer_id = customer_id,
loan_id = loan_id, loan = loan,
product_id = product_id, transaction_id = transaction_id
transaction_id=transaction_id
) )
if not repayment: if not repayment:
logger.error(f"Failed to save repayment details") logger.error(f"Failed to save repayment details")
return jsonify({ return ResponseHelper.error(result_description="Failed to save repayment details.")
"message": "Failed to save repayment details."
}), 400
#Update Loan status #Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID) Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
transaction = RepaymentService.log_transaction(validated_data = validated_data) transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({ return ResponseHelper.error(result_description="Failed to log transaction.")
"message": "Failed to log transaction."
}), 400
else: else:
return jsonify({ return ResponseHelper.error(result_description="Invalid Customer or Account")
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic # Simulated processing logic
response_data = { response_data = {
"transactionId": transaction_id, "transactionId": transaction_id,
"customerId": customer_id, "customerId": customer_id,
"productId": product_id, "productId": loan.product_id,
"debtId": loan_id, "loanRef": loan_ref,
"resultCode": "00", "debtId": loan_id
"resultDescription": "Successful"
} }
# return ResponseHelper.success(
# data=response_data,
# message="Repayment processed successfully"
# )
# Call Kafka in a background thread # Call Kafka in a background thread
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT")) thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start() thread.start()
db.session.commit() db.session.commit()
return response_data return ResponseHelper.success(data=response_data)
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return ResponseHelper.error(result_description=str(err))
return jsonify({
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback() db.session.rollback()
return jsonify({ return ResponseHelper.internal_server_error()
"message": "Internal Server Error"
}) , 500
+50 -20
View File
@@ -1,5 +1,6 @@
from flask import request, jsonify from flask import request, jsonify
from marshmallow import ValidationError from marshmallow import ValidationError
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.utils.logger import logger from app.utils.logger import logger
@@ -32,9 +33,14 @@ class SelectOfferService(BaseService):
customer_id = validated_data.get("customerId") customer_id = validated_data.get("customerId")
amount = validated_data.get("requestedAmount") amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId") product_id = validated_data.get("productId")
transaction_offer_id = validated_data.get("offerId")
transaction_id = validated_data.get("transactionId") transaction_id = validated_data.get("transactionId")
request_id = validated_data.get("requestId") request_id = validated_data.get("requestId")
offer_id = int(transaction_offer_id[5:]) # The last part is int
#"offerId": "SAL30001129",
if SelectOfferService.validate_account_ownership( if SelectOfferService.validate_account_ownership(
account_id=account_id, customer_id=customer_id account_id=account_id, customer_id=customer_id
): ):
@@ -44,15 +50,22 @@ class SelectOfferService(BaseService):
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({"message": "Failed to log transaction."}), 400 return ResponseHelper.error(result_description="Failed to log transaction.")
else: else:
return jsonify({"message": "Invalid Customer or Account"}), 400 return ResponseHelper.error(result_description="Invalid Customer or Account")
# Get the offer by product ID # Get the offer by product ID
offer = Offer.get_offer_by_product_id(product_id) offer = Offer.get_offer_by_product_id(product_id)
db.session.flush() db.session.flush()
if amount < offer.min_amount:
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
elif amount > offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed offer amount.")
charges = SelectOfferService.calculate_charges(offer, amount) charges = SelectOfferService.calculate_charges(offer, amount)
upfront_payment = charges["upfront_payment"] upfront_payment = charges["upfront_payment"]
total_amount = charges["total_amount"] total_amount = charges["total_amount"]
@@ -61,6 +74,8 @@ class SelectOfferService(BaseService):
management = charges["management"] management = charges["management"]
insurance = charges["insurance"] insurance = charges["insurance"]
vat = charges["vat"] vat = charges["vat"]
repayment_amount = charges["repayment_amount"]
interest_amount = charges["interest_amount"]
# Calculate the repayment dates # Calculate the repayment dates
@@ -68,7 +83,7 @@ class SelectOfferService(BaseService):
start_date = date.today() start_date = date.today()
# Convert tenor to months # Convert tenor to months
months = tenor // 30 months = offer.schedule # tenor // 30
recommended_repayment_dates = [ recommended_repayment_dates = [
(start_date + relativedelta(months=i + 1)).isoformat() (start_date + relativedelta(months=i + 1)).isoformat()
@@ -79,23 +94,40 @@ class SelectOfferService(BaseService):
offers = [ offers = [
{ {
"offerId": offer.id, "offerId": transaction_offer_id,
"productId": product_id, "productId": product_id,
"amount": amount, "amount": amount,
"upfrontPayment": upfront_payment, "upfrontPayment": upfront_payment,
"interestRate": interest["rate"], "interestRate": offer.interest_rate,
"managementRate": management["rate"], "interestFee": interest_amount,
"managementRate": offer.management_rate,
"managementFee": management["fee"], "managementFee": management["fee"],
"insuranceRate": insurance["rate"], "insuranceRate": offer.insurance_rate,
"insuranceFee": insurance["fee"], "insuranceFee": insurance["fee"],
"VATRate": vat["rate"], "VATRate": offer.vat_rate,
"VATAmount": vat["fee"], "VATAmount": vat["fee"],
"recommendedRepaymentDates": recommended_repayment_dates, "recommendedRepaymentDates": recommended_repayment_dates,
"repaymentAmount": repayment_amount,
"installmentAmount": installment_amount, "installmentAmount": installment_amount,
"totalRepaymentAmount": total_amount, "totalRepaymentAmount": total_amount,
} }
] ]
# "offerId": offer.id,
# "productId": product_id,
# "amount": amount,
# "upfrontPayment": upfront_payment,
# "interestRate": interest["rate"],
# "managementRate": management["rate"],
# "managementFee": management["fee"],
# "insuranceRate": insurance["rate"],
# "insuranceFee": insurance["fee"],
# "VATRate": vat["rate"],
# "VATAmount": vat["fee"],
# "recommendedRepaymentDates": recommended_repayment_dates,
# "installmentAmount": installment_amount,
# "totalRepaymentAmount": total_amount,
#
# Business logic - selecting an offer # Business logic - selecting an offer
response_data = { response_data = {
"outstandingDebtAmount": 0, "outstandingDebtAmount": 0,
@@ -104,26 +136,24 @@ class SelectOfferService(BaseService):
"customerId": customer_id, "customerId": customer_id,
"accountId": account_id, "accountId": account_id,
"loan": offers, "loan": offers,
"resultCode": "00",
"resultDescription": "Successful",
} }
db.session.commit() db.session.commit()
return response_data return ResponseHelper.success(data=response_data)
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return jsonify({"message": "Validation exception"}), 422 return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback() db.session.rollback()
return jsonify({"message": str(err)}), 400 return ResponseHelper.error(result_description=str(err))
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback() db.session.rollback()
return jsonify({"message": "Internal Server Error"}), 500 return ResponseHelper.internal_server_error()
+19 -5
View File
@@ -1,7 +1,6 @@
import os import os
from datetime import timedelta from datetime import timedelta
class Config: class Config:
"""Base configuration for Flask app""" """Base configuration for Flask app"""
@@ -9,20 +8,19 @@ class Config:
API_URL = os.getenv("API_URL", "/swagger.json") API_URL = os.getenv("API_URL", "/swagger.json")
DEBUG = True DEBUG = True
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345")
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user") BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password") BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
# Database Configuration
DATABASE_USER = os.environ.get("DATABASE_USER") DATABASE_USER = os.environ.get("DATABASE_USER")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD") DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_HOST = os.environ.get("DATABASE_HOST") DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532) DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME") DATABASE_NAME = os.environ.get("DATABASE_NAME")
# Database Connection
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
@@ -31,7 +29,23 @@ class Config:
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30) "JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
) )
KAFKA_BROKER = 'dev-events.simbrellang.net:9085' # KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "testtest-api-key-12345")
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
RAC_RESULT_creditBureauCheck = os.environ.get("RAC_RESULT_creditBureauCheck", "false")
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
RAC_RESULT_hasSalaryAccount = os.environ.get("RAC_RESULT_hasSalaryAccount", "true")
RAC_RESULT_isWhitelisted = os.environ.get("RAC_RESULT_isWhitelisted", "true")
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
settings = Config() settings = Config()
+5 -5
View File
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
def register_error_handlers(app): def register_error_handlers(app):
@app.errorhandler(HTTPException) @app.errorhandler(HTTPException)
def handle_http_exception(e): def handle_http_exception(e):
return jsonify({'error': e.description}), e.code return ResponseHelper.error(result_description=e.description, result_code=e.code )
@app.errorhandler(405) @app.errorhandler(405)
def method_not_allowed(error): def method_not_allowed(error):
return jsonify({"message": "Method Not Allowed"}), 405 return ResponseHelper.method_not_allowed()
@app.errorhandler(404) @app.errorhandler(404)
def not_found(error): def not_found(error):
return jsonify({"message": "Resource not found"}), 404 return ResponseHelper.not_found()
@app.errorhandler(400) @app.errorhandler(400)
def bad_request(error): def bad_request(error):
return jsonify({"message": "Bad Request"}), 400 return ResponseHelper.bad_request()
@app.errorhandler(415) @app.errorhandler(415)
def unsupported_media_type(error): def unsupported_media_type(error):
return jsonify({"message": "Unsupported Media Type"}), 415 return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
+2 -1
View File
@@ -8,6 +8,7 @@ from .offer import Offer
from .charge import Charge from .charge import Charge
from .rac_checks import RACCheck from .rac_checks import RACCheck
from .loan_repayment_schedule import LoanRepaymentSchedule from .loan_repayment_schedule import LoanRepaymentSchedule
from .transaction_offers import TransactionOffer
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule'] __all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer']
+6 -3
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.extensions import db from app.extensions import db
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Account(db.Model): class Account(db.Model):
__tablename__ = 'accounts' __tablename__ = 'accounts'
@@ -11,8 +12,8 @@ class Account(db.Model):
account_type = db.Column(db.String(50)) account_type = db.Column(db.String(50))
status = db.Column(db.String(20), default='active') status = db.Column(db.String(20), default='active')
lien_amount = db.Column(db.Float, default=0.0) lien_amount = db.Column(db.Float, default=0.0)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship( customer = relationship(
"Customer", "Customer",
@@ -26,7 +27,9 @@ class Account(db.Model):
account = cls( account = cls(
id=id, id=id,
customer_id=customer_id, customer_id=customer_id,
account_type=account_type account_type=account_type,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
try: try:
+6 -4
View File
@@ -1,6 +1,7 @@
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from app.extensions import db from app.extensions import db
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
class Charge(db.Model): class Charge(db.Model):
@@ -12,9 +13,8 @@ class Charge(db.Model):
percent = db.Column(db.Float, default=0.0) percent = db.Column(db.Float, default=0.0)
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False) due = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
offer = relationship( offer = relationship(
"Offer", "Offer",
primaryjoin="Charge.offer_id == Offer.id", primaryjoin="Charge.offer_id == Offer.id",
@@ -57,7 +57,9 @@ class Charge(db.Model):
code = code, code = code,
percent = percent, percent = percent,
description = description, description = description,
due = due_days due = due_days,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
db.session.add(charge_obj) db.session.add(charge_obj)
+25 -5
View File
@@ -1,8 +1,12 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
#
# from app.api.services.offer_analysis import logger
from app.extensions import db from app.extensions import db
from app.models.account import Account from app.models.account import Account
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
# from app.utils.logger import logger
class Customer(db.Model): class Customer(db.Model):
__tablename__ = 'customers' __tablename__ = 'customers'
@@ -10,9 +14,8 @@ class Customer(db.Model):
id = db.Column(db.String(50), primary_key=True) id = db.Column(db.String(50), primary_key=True)
msisdn = db.Column(db.String(20), unique=True, nullable=False) msisdn = db.Column(db.String(20), unique=True, nullable=False)
country_code = db.Column(db.String(3), nullable=False) country_code = db.Column(db.String(3), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
accounts = relationship( accounts = relationship(
"Account", "Account",
primaryjoin="Customer.id == Account.customer_id", primaryjoin="Customer.id == Account.customer_id",
@@ -27,6 +30,13 @@ class Customer(db.Model):
back_populates="customer", back_populates="customer",
) )
transaction_offers = relationship(
"TransactionOffer",
primaryjoin="Customer.id == TransactionOffer.customer_id",
foreign_keys="TransactionOffer.customer_id",
back_populates="customer",
)
@classmethod @classmethod
def is_valid_customer(cls, customer_id): def is_valid_customer(cls, customer_id):
customer = cls.query.filter_by(id=customer_id).first() customer = cls.query.filter_by(id=customer_id).first()
@@ -38,9 +48,19 @@ class Customer(db.Model):
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'): def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
if cls.query.filter_by(id=id).first(): if cls.query.filter_by(id=id).first():
raise ValueError("Customer already exists") raise ValueError("Customer already exists")
elif Account.query.filter_by(id=account_id).first():
raise ValueError("Account already exists")
elif cls.query.filter_by(msisdn=msisdn).first():
raise ValueError("msisdn already exists")
# Create the customer # Create the customer
customer = cls(id=id, msisdn=msisdn, country_code=country_code) customer = cls(
id=id,
msisdn=msisdn,
country_code=country_code,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try: try:
db.session.add(customer) db.session.add(customer)
@@ -56,7 +76,7 @@ class Customer(db.Model):
return customer return customer
@classmethod @classmethod
def get_customer(cls, customer_id): def get_customer_with_loan_list(cls, customer_id):
""" """
Get customer by ID. Get customer by ID.
""" """
+83 -7
View File
@@ -1,10 +1,17 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from itertools import product
from app.extensions import db from app.extensions import db
from app.models.customer import Customer from app.models.customer import Customer
from app.models.account import Account from app.models.account import Account
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import timedelta
import logging
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
logger = logging.getLogger(__name__)
class Loan(db.Model): class Loan(db.Model):
@@ -17,6 +24,7 @@ class Loan(db.Model):
) )
customer_id = db.Column(db.String(50), nullable=False) customer_id = db.Column(db.String(50), nullable=False)
transaction_id = db.Column(db.String(50), nullable=True) transaction_id = db.Column(db.String(50), nullable=True)
original_transaction = db.Column(db.String(50), nullable=True)
account_id = db.Column(db.String(50), nullable=False) account_id = db.Column(db.String(50), nullable=False)
offer_id = db.Column(db.String(20), nullable=False) offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True) product_id = db.Column(db.String(20), nullable=True)
@@ -29,9 +37,14 @@ class Loan(db.Model):
repayment_amount = db.Column(db.Float, nullable=True, default=0.0) repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
installment_amount = db.Column(db.Float, nullable=True, default=0.0) installment_amount = db.Column(db.Float, nullable=True, default=0.0)
status = db.Column(db.String(20), default='pending') status = db.Column(db.String(20), default='pending')
tenor = db.Column(db.Integer, nullable=True)
due_date = db.Column(db.DateTime, nullable=True) due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
disburse_date = db.Column(db.DateTime, nullable=True)
disburse_verify = db.Column(db.DateTime, nullable=True)
reference = db.Column(db.String(50), nullable=True)
customer = relationship( customer = relationship(
"Customer", "Customer",
@@ -65,10 +78,14 @@ class Loan(db.Model):
initial_loan_amount, initial_loan_amount,
collection_type, collection_type,
transaction_id, transaction_id,
original_transaction,
upfront_fee, upfront_fee,
repayment_amount, repayment_amount,
installment_amount, installment_amount,
status="pending", tenor,
eligible_amount,
reference,
status = "pending",
): ):
# Check if customer exists # Check if customer exists
customer = Customer.is_valid_customer(customer_id) customer = Customer.is_valid_customer(customer_id)
@@ -76,6 +93,7 @@ class Loan(db.Model):
raise ValueError("Customer does not exist") raise ValueError("Customer does not exist")
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
due_date = now + timedelta(days=tenor)
# Create and save the loan # Create and save the loan
loan = cls( loan = cls(
@@ -85,13 +103,19 @@ class Loan(db.Model):
product_id = product_id, product_id = product_id,
collection_type = collection_type, collection_type = collection_type,
transaction_id = transaction_id, transaction_id = transaction_id,
original_transaction = original_transaction,
initial_loan_amount = initial_loan_amount, initial_loan_amount = initial_loan_amount,
current_loan_amount = initial_loan_amount, current_loan_amount = initial_loan_amount,
upfront_fee = upfront_fee, upfront_fee = upfront_fee,
repayment_amount = repayment_amount, repayment_amount = repayment_amount,
installment_amount = installment_amount, installment_amount = installment_amount,
due_date=now, due_date=due_date,
status = status tenor = tenor,
status = status,
eligible_amount =eligible_amount,
reference = reference,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
try: try:
@@ -115,13 +139,61 @@ class Loan(db.Model):
@classmethod @classmethod
def get_customer_loan(cls, loan_id, customer_id): def get_customer_loan(cls, loan_id, customer_id):
""" """
Get customer's active loans. Get customer's active loans by loan_id.
""" """
loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first() loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first()
if not loan: if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.") raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
return loan return loan
@classmethod
def get_customer_original_loan(cls, customer_id, original_transaction):
"""
Get customer's original loan offer.
"""
original_loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.original_transaction==original_transaction, cls.transaction_id==original_transaction )).first()
if not original_loan:
return None
logger.info(f" get_customer_original_loan ==>>>> {original_loan}")
return original_loan
@classmethod
def get_customer_last_loan(cls, customer_id):
"""
Get customer's active loans.
"""
logger.info(f"get_customer_last_loan [customer_id] ==>>>> {customer_id}")
# loan = cls.query.filter_by( cls.customer_id == customer_id).first()
loan = cls.query.filter(and_( cls.customer_id ==customer_id, cls.status=='active')).first()
if not loan:
return None
# loan = {
# "original_transaction":"",
# "eligible_amount": 0,
# "loan_amount": 0,
# "customer_id": customer_id,
# "transaction_id": "",
# "resultDescription": "No Active Loan"
# }
logger.info(f" get_customer_last_loan ==>>>> {loan}")
return loan
@classmethod
def get_active_loans_by_original_transaction(cls, original_transaction_id):
"""
Get all active loans with the same original_transaction ID.
"""
active_loans = cls.query.filter_by(
original_transaction=original_transaction_id,
# status='active'
).all()
return active_loans
@classmethod @classmethod
def update_status(cls, loan_id, status): def update_status(cls, loan_id, status):
""" """
@@ -145,6 +217,9 @@ class Loan(db.Model):
""" """
return { return {
'debtId': self.id, 'debtId': self.id,
'transactionId': self.transaction_id,
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount, 'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount, 'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee, 'defaultPenaltyFee': self.default_penalty_fee,
@@ -154,6 +229,7 @@ class Loan(db.Model):
'repaymentAmount': self.repayment_amount, 'repaymentAmount': self.repayment_amount,
'installmentAmount': self.installment_amount, 'installmentAmount': self.installment_amount,
'status': self.status, 'status': self.status,
'tenor': self.tenor,
'dueDate': self.due_date.isoformat() if self.due_date else None, 'dueDate': self.due_date.isoformat() if self.due_date else None,
'loanDate': self.created_at.isoformat() if self.created_at else None, 'loanDate': self.created_at.isoformat() if self.created_at else None,
} }
+19 -14
View File
@@ -1,6 +1,8 @@
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from app.extensions import db from app.extensions import db
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.utils.logger import logger
from sqlalchemy.sql import func
class LoanCharge(db.Model): class LoanCharge(db.Model):
@@ -15,9 +17,8 @@ class LoanCharge(db.Model):
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False) due = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=True) due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship( loan = relationship(
"Loan", "Loan",
primaryjoin="LoanCharge.loan_id == Loan.id", primaryjoin="LoanCharge.loan_id == Loan.id",
@@ -35,8 +36,8 @@ class LoanCharge(db.Model):
charges (list): A list of dictionaries with keys: charges (list): A list of dictionaries with keys:
code (str), amount (float), percent (float), description (str), due (int) code (str), amount (float), percent (float), description (str), due (int)
""" """
if not charges or not isinstance(charges, list): # if not charges or not isinstance(charges, list):
raise ValueError("Charges must be a non-empty list of dictionaries") # raise ValueError("Charges must be a non-empty list of dictionaries")
if loan_id is None: if loan_id is None:
raise ValueError("loan_id cannot be None") raise ValueError("loan_id cannot be None")
@@ -44,23 +45,27 @@ class LoanCharge(db.Model):
loan_charges = [] loan_charges = []
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
for charge in charges:
due_days = getattr(charge, "due", 0)
amount = getattr(charge, "amount", 0.0)
percent = getattr(charge, "percent", 0.0)
if amount == 0.0: subset_keys = ['interest', 'management', 'insurance', 'vat']
amount = (percent / 100.0) * referenced_amount for item in subset_keys:
charge = charges[item]
due_days = charge['due_days'] # getattr(charge, "due_days", 0)
amount = charge['fee'] # getattr(charge, "fee", 0.0)
percent = charge['rate'] # getattr(charge, "rate", 0.0)
code = charge['code'] # getattr(charge, "code","")
description = charge['description'] # getattr(charge, "description", "")
charge_obj = cls( charge_obj = cls(
loan_id = loan_id, loan_id = loan_id,
transaction_id = transaction_id, transaction_id = transaction_id,
code = getattr(charge, "code"), code = code,
amount = round(amount, 2), amount = round(amount, 2),
percent = percent, percent = percent,
description = getattr(charge, "description", ""), description = description,
due = due_days, due = due_days,
due_date = now + timedelta(days=due_days) due_date = now + timedelta(days=due_days),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
db.session.add(charge_obj) db.session.add(charge_obj)
+10 -6
View File
@@ -2,12 +2,14 @@ from datetime import datetime, timezone
from app.extensions import db from app.extensions import db
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from sqlalchemy.sql import func
class LoanRepaymentSchedule(db.Model): class LoanRepaymentSchedule(db.Model):
__tablename__ = 'loan_repayment_schedules' __tablename__ = 'loan_repayment_schedules'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
loan_id = db.Column(db.Integer, nullable=False) loan_id = db.Column(db.Integer, nullable=False)
transaction_id = db.Column(db.String(50), nullable=True)
product_id = db.Column(db.String(20), nullable=True) product_id = db.Column(db.String(20), nullable=True)
installment_number = db.Column(db.Integer, nullable=False) installment_number = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=False) due_date = db.Column(db.DateTime, nullable=False)
@@ -16,9 +18,8 @@ class LoanRepaymentSchedule(db.Model):
paid = db.Column(db.Boolean, default=False) paid = db.Column(db.Boolean, default=False)
paid_at = db.Column(db.DateTime, nullable=True) paid_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship( loan = relationship(
"Loan", "Loan",
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id", primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
@@ -28,14 +29,14 @@ class LoanRepaymentSchedule(db.Model):
@classmethod @classmethod
def add_repayment_schedule(cls, loan, tenor): def add_repayment_schedule(cls, loan, num_schedules, transaction_id):
""" """
Add repayment schedules for a given loan. Add repayment schedules for a given loan.
""" """
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
schedules = [] schedules = []
for i in range(tenor): for i in range(num_schedules):
due_date = now + relativedelta(months=i + 1) due_date = now + relativedelta(months=i + 1)
schedule = LoanRepaymentSchedule( schedule = LoanRepaymentSchedule(
loan_id=loan.id, loan_id=loan.id,
@@ -43,7 +44,10 @@ class LoanRepaymentSchedule(db.Model):
due_date=due_date, due_date=due_date,
total_repayment_amount = round(loan.repayment_amount, 2), total_repayment_amount = round(loan.repayment_amount, 2),
installment_amount=round(loan.installment_amount, 2), installment_amount=round(loan.installment_amount, 2),
product_id = loan.product_id product_id = loan.product_id,
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
db.session.add(schedule) db.session.add(schedule)
+15 -3
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
from app.extensions import db from app.extensions import db
from app.models.charge import Charge from app.models.charge import Charge
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
class Offer(db.Model): class Offer(db.Model):
__tablename__ = 'offers' __tablename__ = 'offers'
@@ -17,8 +18,11 @@ class Offer(db.Model):
insurance_rate = db.Column(db.Float, default=1.0) insurance_rate = db.Column(db.Float, default=1.0)
vat_rate = db.Column(db.Float, default=7.5) vat_rate = db.Column(db.Float, default=7.5)
list_order = db.Column(db.Integer, nullable=True) list_order = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) max_daily_loans = db.Column(db.Integer, nullable=True)
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) max_active_loans = db.Column(db.Integer, nullable=True)
max_life_loans = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
charges = relationship( charges = relationship(
"Charge", "Charge",
@@ -75,7 +79,15 @@ class Offer(db.Model):
"productId": self.product_id, "productId": self.product_id,
"minAmount": self.min_amount, "minAmount": self.min_amount,
"maxAmount": self.max_amount, "maxAmount": self.max_amount,
"tenor": self.tenor "tenor": self.tenor,
"interest_rate": self.interest_rate,
"management_rate": self.management_rate,
"insurance_rate": self.insurance_rate,
"vat_rate": self.vat_rate,
"maxDailyLoans": self.max_daily_loans,
"maxActiveLoans": self.max_active_loans,
"maxLifeLoans": self.max_life_loans
} }
def __repr__(self): def __repr__(self):
+30 -8
View File
@@ -1,20 +1,41 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from app.extensions import db from app.extensions import db
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.exc import IntegrityError
from uuid import uuid4 from uuid import uuid4
from sqlalchemy.types import JSON from sqlalchemy.types import JSON
from sqlalchemy.sql import func
class RACCheck(db.Model): class RACCheck(db.Model):
__tablename__ = 'rac_checks' __tablename__ = 'rac_checks'
id = db.Column(db.String, primary_key=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False) transaction_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String, nullable=False) customer_id = db.Column(db.String, nullable=False)
account_id = db.Column(db.String, nullable=False) account_id = db.Column(db.String, nullable=False)
rac_response = db.Column(db.JSON, nullable=False) rac_response = db.Column(db.JSON, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@classmethod
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
# Save the response
rac_check = cls(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction_id,
rac_response = data,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
db.session.add(rac_check)
except IntegrityError as err:
raise ValueError(f"Database integrity error: {err}")
return rac_check
@classmethod @classmethod
def get_all_rac_checks(cls): def get_all_rac_checks(cls):
@@ -24,18 +45,19 @@ class RACCheck(db.Model):
rac_checks = cls.query.all() rac_checks = cls.query.all()
if not rac_checks: if not rac_checks:
raise ValueError("No available RAC checks") return None
return rac_checks return rac_checks
@classmethod @classmethod
def get_rac_check_by_id(cls, check_id): def get_rac_check(cls, customer_id, account_id):
""" """
Return a RAC check by its ID. Return a RAC check by its ID.
""" """
rac_check = cls.query.filter_by(id=check_id).first() rac_check = cls.query.filter_by( customer_id = customer_id,
account_id = account_id,).first()
if not rac_check: if not rac_check:
raise ValueError(f"RAC Check with ID {check_id} not found") raise ValueError(f"RAC Check for customer not found")
return rac_check return rac_check
def to_dict(self): def to_dict(self):
+10 -15
View File
@@ -4,6 +4,7 @@ from app.extensions import db
from app.models.customer import Customer from app.models.customer import Customer
from app.models.loan import Loan from app.models.loan import Loan
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Repayment(db.Model): class Repayment(db.Model):
@@ -17,31 +18,25 @@ class Repayment(db.Model):
loan_id = db.Column(db.String(50), nullable=False) loan_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String(50), nullable=False) customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=True) product_id = db.Column(db.String(20), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
transaction_id = db.Column(db.String(50), nullable=True) transaction_id = db.Column(db.String(50), nullable=True)
@classmethod @classmethod
def create_repayment(cls, customer_id, loan_id, product_id, transaction_id): def create_repayment(cls, customer_id, loan, transaction_id):
# Check customer exists
if not Customer.is_valid_customer(customer_id):
raise ValueError("Invalid customer")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Check that the loan is active # Check that the loan is active
if loan.status != LoanStatus.ACTIVE: if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})") raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls( repayment = cls(
customer_id=customer_id, customer_id=customer_id,
loan_id=loan_id, loan_id=loan.id,
product_id=product_id, product_id=loan.product_id,
transaction_id = transaction_id transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
try: try:
+6 -4
View File
@@ -3,6 +3,7 @@ from app.extensions import db
from app.models import account from app.models import account
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_ from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
class Transaction(db.Model): class Transaction(db.Model):
__tablename__ = 'transactions' __tablename__ = 'transactions'
@@ -16,9 +17,8 @@ class Transaction(db.Model):
customer_id = db.Column(db.String(50), nullable=True) customer_id = db.Column(db.String(50), nullable=True)
type = db.Column(db.String(50), nullable=False) type = db.Column(db.String(50), nullable=False)
channel = db.Column(db.String(50), nullable=False) channel = db.Column(db.String(50), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
def __repr__(self): def __repr__(self):
return f'<Transaction {self.id}>' return f'<Transaction {self.id}>'
@@ -38,7 +38,9 @@ class Transaction(db.Model):
customer_id = customer_id, customer_id = customer_id,
account_id = account_id, account_id = account_id,
type = type, type = type,
channel = channel channel = channel,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
) )
try: try:
+125
View File
@@ -0,0 +1,125 @@
from datetime import datetime, timezone, timedelta
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import logging
logger = logging.getLogger(__name__)
class TransactionOffer(db.Model):
__tablename__ = 'transaction_offers'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
customer_id = db.Column(db.String(50), nullable=False)
transaction_id = db.Column(db.String(50), nullable=False)
original_transaction = db.Column(db.String(50), nullable=True)
offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
eligible_amount = db.Column(db.Float, nullable=True)
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship(
"Customer",
primaryjoin="Customer.id == TransactionOffer.customer_id",
foreign_keys=[customer_id],
back_populates="transaction_offers",
)
@classmethod
def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id):
transaction_offer = cls.query.filter_by(
id = transaction_offer,
customer_id = customer_id,
# product_id = product_id
# transaction_id = transaction_id,
).first()
if not transaction_offer:
return False
return transaction_offer
@classmethod
def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
"""
Class method to create and save a TransactionOffer.
"""
transaction_offer = cls(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=original_transaction,
offer_id=offer_id,
min_amount=min_amount,
max_amount=max_amount,
eligible_amount=eligible_amount,
product_id=product_id,
tenor=tenor,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(transaction_offer)
db.session.flush()
return transaction_offer
@classmethod
def get_lifetime_loan_count(cls, customer_id):
"""
Returns the total number of loans ever created for a customer.
"""
return cls.query.filter_by(customer_id=customer_id).count()
@classmethod
def get_daily_loan_count(cls, customer_id, offer_id):
"""
Returns the count of loans created today for a customer.
"""
start_of_day = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = start_of_day + timedelta(days=1)
return cls.query.filter_by(
customer_id=customer_id,
offer_id=offer_id
).filter(
cls.created_at >= start_of_day,
cls.created_at < end_of_day
).count()
@classmethod
def get_latest_transaction_offer(cls, customer_id):
"""
Returns the most recent transaction offer for the given customer based on creation time.
"""
return cls.query.filter_by(customer_id=customer_id) \
.order_by(cls.created_at.desc()) \
.first()
def to_dict(self):
return {
'id': self.id,
'customerId': self.customer_id,
'transactionId': self.transaction_id,
'offerId': self.offer_id,
'productId': self.product_id,
'minAmount': self.min_amount,
'maxAmount': self.max_amount,
'eligibleAmount': self.eligible_amount,
'tenor': self.tenor,
'createdAt': self.created_at.isoformat() if self.created_at else None,
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
}
def __repr__(self):
return f'<TransactionOffer {self.id}>'
@@ -9,6 +9,10 @@
"type": "string", "type": "string",
"example": "Tr201712RK9232P115" "example": "Tr201712RK9232P115"
}, },
"loanRef": {
"type": "string",
"example": "1620029887USSDAMPC"
},
"customerId": { "customerId": {
"type": "string", "type": "string",
"example": "CN621868" "example": "CN621868"
+2 -6
View File
@@ -9,10 +9,6 @@
"type": "string", "type": "string",
"example": "10" "example": "10"
}, },
"productId": {
"type": "string",
"example": "101"
},
"transactionId": { "transactionId": {
"type": "string", "type": "string",
"example": "20171209232115" "example": "20171209232115"
@@ -21,9 +17,9 @@
"type": "string", "type": "string",
"example": "CID0000025585" "example": "CID0000025585"
}, },
"channel": { "loanRef": {
"type": "string", "type": "string",
"example": "USSD" "example": "Trx5847365252USSD3MPC"
}, },
"accountId": { "accountId": {
"type": "string", "type": "string",
@@ -27,6 +27,10 @@
"example": "ACN8263457" "example": "ACN8263457"
}, },
"productId": { "productId": {
"type": "string",
"example": "3MPC"
},
"offerId": {
"type": "string", "type": "string",
"example": "101" "example": "101"
}, },
+6 -1
View File
@@ -28,7 +28,7 @@
}, },
"productId": { "productId": {
"type": "string", "type": "string",
"example": "2030" "example": "3MPC"
}, },
"amount": { "amount": {
"type": "number", "type": "number",
@@ -49,6 +49,11 @@
"format": "float", "format": "float",
"example": 3.0 "example": 3.0
}, },
"interestFee": {
"type": "number",
"format": "float",
"example": 3000.00
},
"ManagementRate": { "ManagementRate": {
"type": "number", "type": "number",
"format": "float", "format": "float",
@@ -0,0 +1,32 @@
"""Migration on Sat May 10 09:54:34 UTC 2025
Revision ID: 173ea45db189
Revises: 3105abd795d4
Create Date: 2025-05-10 09:54:39.380499
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '173ea45db189'
down_revision = '3105abd795d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
+250
View File
@@ -0,0 +1,250 @@
"""empty message
Revision ID: 2eee4157505f
Revises: 565bc3d0ba6e
Create Date: 2025-05-16 13:24:41.914400
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '2eee4157505f'
down_revision = '565bc3d0ba6e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
# ### end Alembic commands ###
+52
View File
@@ -0,0 +1,52 @@
"""empty message
Revision ID: 3105abd795d4
Revises: 95a52be203c4
Create Date: 2025-05-07 11:44:18.483694
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3105abd795d4'
down_revision = '95a52be203c4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
# Step 1: Drop the default value
batch_op.alter_column('id',
server_default=None,
existing_type=sa.VARCHAR(),
existing_nullable=False
)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
# Step 2: Change the column type
batch_op.alter_column('id',
existing_type=sa.VARCHAR(),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
postgresql_using='id::integer'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("''::character varying"))
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Migration for mloan table
Revision ID: 38acee611d55
Revises: f1e83a993034
Create Date: 2025-04-30 09:55:30.552838
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '38acee611d55'
down_revision = 'f1e83a993034'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('tenor', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('tenor')
# ### end Alembic commands ###
@@ -0,0 +1,34 @@
"""Migration on Sat May 10 12:54:52 UTC 2025
Revision ID: 565bc3d0ba6e
Revises: 173ea45db189
Create Date: 2025-05-10 12:54:56.683215
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '565bc3d0ba6e'
down_revision = '173ea45db189'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('disburse_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('disburse_verify', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('disburse_verify')
batch_op.drop_column('disburse_date')
# ### end Alembic commands ###
+41
View File
@@ -0,0 +1,41 @@
"""empty message
Revision ID: 86e701febdda
Revises: eb99c7fb9e09
Create Date: 2025-04-29 07:59:33.305967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '86e701febdda'
down_revision = 'eb99c7fb9e09'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('transaction_offers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('transaction_offers')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Migration on Sat May 3 21:53:29 UTC 2025
Revision ID: 95a52be203c4
Revises: 38acee611d55
Create Date: 2025-05-03 21:53:32.154029
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '95a52be203c4'
down_revision = '38acee611d55'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('eligible_amount', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('eligible_amount')
# ### end Alembic commands ###
+32
View File
@@ -0,0 +1,32 @@
"""empty message
Revision ID: b3a5e10bc77e
Revises: e8dd9b841ad7
Create Date: 2025-05-27 01:52:48.538333
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b3a5e10bc77e'
down_revision = 'e8dd9b841ad7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('reference', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('reference')
# ### end Alembic commands ###
+36
View File
@@ -0,0 +1,36 @@
"""empty message
Revision ID: e8dd9b841ad7
Revises: 2eee4157505f
Create Date: 2025-05-19 11:46:19.204637
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e8dd9b841ad7'
down_revision = '2eee4157505f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('max_daily_loans', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('max_active_loans', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('max_life_loans', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.drop_column('max_life_loans')
batch_op.drop_column('max_active_loans')
batch_op.drop_column('max_daily_loans')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Migration on Sat Apr 26 19:02:17 UTC 2025
Revision ID: eb99c7fb9e09
Revises: 89759cebb9c6
Create Date: 2025-04-26 19:02:20.443678
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'eb99c7fb9e09'
down_revision = '89759cebb9c6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Migration on Tue Apr 29 20:43:35 UTC 2025
Revision ID: f1e83a993034
Revises: 86e701febdda
Create Date: 2025-04-29 20:43:38.595543
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f1e83a993034'
down_revision = '86e701febdda'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###