Compare commits

...

105 Commits

Author SHA1 Message Date
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
ameye 5f9b1f4cb8 Added rates with offers 2025-04-26 08:53:06 -04:00
ameye ed95865834 Merge branch 'loan_repayment_schedules' of DigiFi/digifi-BankToProductCore into master 2025-04-25 15:58:25 +00:00
VivianDee 6973630845 Update loan_repayment_schedule.py 2025-04-25 16:14:44 +01:00
VivianDee 5d37ba30fb [update]: repayment schedule table 2025-04-25 16:04:44 +01:00
ameye e8044d8fed Merge branch 'loan_repayment_schedules' of DigiFi/digifi-BankToProductCore into master 2025-04-25 14:42:09 +00:00
VivianDee cf0502459b [chore]: remove redundant code 2025-04-25 15:34:46 +01:00
VivianDee 851422c335 [fix]: loan amount 2025-04-25 15:33:58 +01:00
VivianDee ddbabcaca9 [fix]: Repayment schedule model 2025-04-25 15:10:07 +01:00
Vivian Dee c216c55928 [add]: Lona repayment schedule 2025-04-25 14:29:13 +01:00
Vivian Dee 0995f08aea [update]: Loan and Offers 2025-04-25 13:31:11 +01:00
ameye e034c0ff9d Merge branch 'loan_repayment_dates' of DigiFi/digifi-BankToProductCore into master 2025-04-25 11:33:54 +00:00
VivianDee 4d4e4fcd3e Update base_service.py 2025-04-25 11:33:05 +01:00
VivianDee 1cce111d1f Update offer.py 2025-04-25 11:30:43 +01:00
VivianDee b9b7988877 Update base_service.py 2025-04-25 11:12:23 +01:00
VivianDee 841393c470 [fix]: Loan fees 2025-04-25 10:59:24 +01:00
VivianDee bbb903b27c [update]: RACCheck 2025-04-24 18:44:31 +01:00
VivianDee c895cc36e0 [add]: Loan table extention 2025-04-24 18:29:38 +01:00
CHIEFSOFT\ameye 67c6d909f8 Adjusted the respose 2025-04-24 12:44:18 -04:00
ameye e08dfe9894 Merge branch 'loan_repayment_dates' of DigiFi/digifi-BankToProductCore into master 2025-04-24 10:28:10 +00:00
VivianDee 7d691db7a5 [update]: Select Offer 2025-04-23 20:59:07 +01:00
Vivian Dee 4b92c33d5a [fix]: loan charges and instalment amount 2025-04-23 18:57:22 +01:00
Vivian Dee 8cfa957cc0 [update]: Select Offer 2025-04-23 18:35:47 +01:00
ameye 5768b537b1 Merge branch 'charges_model' of DigiFi/digifi-BankToProductCore into master 2025-04-17 16:12:09 +00:00
ameye bc894c7856 Merge branch 'charges_model' of DigiFi/digifi-BankToProductCore into master 2025-04-17 15:13:48 +00:00
ameye 829bd976b2 Merge branch 'loan_charges_on_loans' of DigiFi/digifi-BankToProductCore into master 2025-04-17 10:45:18 +00:00
ameye e04f54bf83 Merge branch 'loan_charges_on_loans' of DigiFi/digifi-BankToProductCore into master 2025-04-17 10:03:16 +00:00
48 changed files with 1948 additions and 472 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"
+56 -195
View File
@@ -1,251 +1,112 @@
from flask import jsonify
from typing import List, Dict, Union, Optional, Any
from typing import Optional, Union, Dict, List, Any
class ResponseHelper:
"""
A helper class for building standardized JSON responses in Flask.
A helper class for building standardized JSON responses using resultCode and resultDescription.
"""
@staticmethod
def build_response(
status: bool,
message: str,
data: Optional[Union[Dict, List, str]] = None,
status_code: int = 200,
error: Optional[Union[Dict, str]] = None,
result_code: str,
result_description: str,
data: Optional[Union[Dict, List, str]] = None
) -> Dict[str, Any]:
"""
Build a standardized JSON response.
Args:
status (bool): Indicates whether the request was successful.
message (str): A message describing the result of the request.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
status_code (int): The HTTP status code for the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
response = {
"status": status,
"statusCode": status_code,
"message": message,
"data": data if data is not None else {},
"error": error if error is not None else {},
"resultCode": result_code,
"resultDescription": result_description
}
return jsonify(response), status_code
if isinstance(data, dict):
response.update(data)
return jsonify(response)
@staticmethod
def success(
data: Optional[Union[Dict, List, str]] = None,
message: str = "Successful",
status_code: int = 200,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Successful",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a success response.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
status_code (int): The HTTP status code for the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, status_code, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def error(
message: str = "An error occurred",
status_code: int = 400,
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "An error occurred",
result_code: str = "01",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return an error response.
Args:
message (str): A message describing the error.
status_code (int): The HTTP status code for the response.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, status_code, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def created(
data: Optional[Union[Dict, List, str]] = None,
message: str = "Resource created successfully",
error: Optional[Union[Dict, str]] = None,
result_description: str = "Resource created successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a created resource.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, 201, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def updated(
data: Optional[Union[Dict, List, str]] = None,
message: str = "Resource updated successfully",
error: Optional[Union[Dict, str]] = None,
result_description: str = "Resource updated successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an updated resource.
Args:
data (Optional[Union[Dict, List, str]]): The data to return in the response.
message (str): A message describing the result of the request.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(True, message, data, 200, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def internal_server_error(
message: str = "Internal Server Error",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Internal Server Error",
result_code: str = "500",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an internal server error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 500, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unauthorized(
message: str = "Unauthorized",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Unauthorized",
result_code: str = "401",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an unauthorized request.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 401, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def forbidden(
message: str = "Forbidden",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Forbidden",
result_code: str = "403",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a forbidden request.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 403, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def not_found(
message: str = "Resource not found",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Resource not found",
result_code: str = "404",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a not found resource.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 404, error)
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unprocessable_entity(
message: str = "Unprocessable entity",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Unprocessable entity",
result_code: str = "422",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for an unprocessable entity.
return ResponseHelper.build_response(result_code, result_description, data)
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 422, error)
@staticmethod
def method_not_allowed(
message: str = "Method Not Allowed",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Method Not Allowed",
result_code: str = "405",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a method not allowed error.
return ResponseHelper.build_response(result_code, result_description, data)
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 405, error)
@staticmethod
def bad_request(
message: str = "Bad Request",
data: Optional[Union[Dict, List, str]] = None,
error: Optional[Union[Dict, str]] = None,
result_description: str = "Bad Request",
result_code: str = "400",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Return a response for a bad request error.
Args:
message (str): A message describing the error.
data (Optional[Union[Dict, List, str]]): The data to return in the response.
error (Optional[Union[Dict, str]]): Any error details to include in the response.
Returns:
Dict[str, Any]: A dictionary representing the JSON response.
"""
return ResponseHelper.build_response(False, message, data, 400, error)
return ResponseHelper.build_response(result_code, result_description, data)
+4 -2
View File
@@ -35,7 +35,7 @@ class SimbrellaIntegration:
],
}
logger.error(f"This is PayLoad: {str(payload)}", exc_info=True)
# logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = {
"Content-Type": "application/json",
@@ -46,9 +46,11 @@ class SimbrellaIntegration:
try:
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
except Exception as e:
logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True)
raise Exception(f"RACCheck API call failed: {str(e)}")
+1
View File
@@ -9,5 +9,6 @@ class SelectOfferSchema(Schema):
msisdn = fields.Str(required=True)
requestedAmount = fields.Float(required=True)
productId = fields.Str(required=True)
offerId = 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.notification_callback import NotificationCallbackService
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")
if not data:
return ResponseHelper.bad_request(message="Missing JSON in request")
return ResponseHelper.bad_request(result_description="Missing JSON in request")
# Validate input data using the Authorization schema
schema = AuthorizeRequestSchema()
@@ -44,7 +44,7 @@ class AuthorizationService(BaseService):
validated_data["username"] != USERNAME
or validated_data["password"] != PASSWORD
):
return ResponseHelper.unauthorized(message="Invalid credentials")
return ResponseHelper.unauthorized(result_description="Invalid credentials")
access_token = create_access_token(identity=validated_data["username"])
refresh_token = create_refresh_token(identity=validated_data["username"])
@@ -56,17 +56,17 @@ class AuthorizationService(BaseService):
}
return ResponseHelper.success(
data=response_data, message="Authorization processed successfully"
data={"data": response_data}, result_description="Authorization processed successfully"
)
except ValidationError as e:
logger.error(f"Validation error: {e}")
return ResponseHelper.bad_request(message=f"Validation error: {e}")
return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
except Exception as e:
logger.error(f"Error processing Authorization request: {e}")
return ResponseHelper.internal_server_error(
message=f"Error processing Authorization request: {e}"
result_description=f"Error processing Authorization request: {e}"
)
@staticmethod
@@ -92,11 +92,11 @@ class AuthorizationService(BaseService):
}
return ResponseHelper.success(
data=response_data, message="RefreshToken processed successfully"
data={"data": response_data}, result_description="RefreshToken processed successfully"
)
except Exception as e:
logger.error(f"Error processing RefreshToken request: {e}")
return ResponseHelper.internal_server_error(
message=f"Error processing RefreshToken request: {e}"
result_description=f"Error processing RefreshToken request: {e}"
)
+111
View File
@@ -60,3 +60,114 @@ class BaseService:
def async_send_to_kafka(cls, loan_data, request_id, topic):
KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id, topic = topic)
KafkaIntegration.flush()
@classmethod
def calculate_charges(cls, offer, amount):
"""
Calculates and returns the charges for the given offer and amount.
Args:
offer (Offer): The offer object that contains the charges.
amount (float): The requested loan amount.
Returns:
dict: A dictionary containing the calculated charges.
"""
if not offer or not offer.charges:
logger.error(f"No charges found for offer ID {offer.id}")
return {"error": "No charges found for the offer"}
loan_charges = offer.charges
tenor = offer.schedule # offer.tenor // 30 # Convert to months
interest = cls.get_charge_detail(rates = offer.interest_rate, charges = loan_charges, code = "INTEREST", amount = amount)
management = cls.get_charge_detail(rates = offer.management_rate, charges = loan_charges, code = "MGTFEE", amount = amount)
insurance = cls.get_charge_detail(rates = offer.insurance_rate, charges = loan_charges, code = "INSURANCE", amount = amount)
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
upfront_fees = [
fee["fee"]
for fee in [interest, management, insurance, vat]
if fee["due_days"] == 0
]
postpaid_fees = [
fee["fee"]
for fee in [interest, management, insurance, vat]
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)
# 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 = amount + (sum(postpaid_fees) * tenor)
# Total amount: (upfront_payment + repayment_amount)
total_amount = upfront_payment + repayment_amount
# Calculate the installment amount
installment_amount = repayment_amount / offer.schedule
return {
"interest": interest,
"interest_amount": interest_amount,
"management": management,
"insurance": insurance,
"vat": vat,
"upfront_payment": round(upfront_payment, 2),
"repayment_amount": round(repayment_amount, 2),
"installment_amount": round(installment_amount, 2),
"total_amount": round(total_amount, 2)
}
@classmethod
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.
Returns default values if not found.
"""
fee = 0.0
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": 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 {"rate": 0, "fee": 0, "due_days": 0}
+7 -24
View File
@@ -1,4 +1,5 @@
from flask import request, jsonify
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService
from marshmallow import ValidationError
from app.utils.logger import logger
@@ -34,44 +35,26 @@ class CustomerConsentService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Request is received"
}
db.session.commit()
return response_data
return ResponseHelper.success(result_description="Request is received")
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
return ResponseHelper.internal_server_error()
+114 -28
View File
@@ -1,4 +1,6 @@
from flask import session, jsonify
from app.models.loan import Loan
from app.models.transaction_offers import TransactionOffer
from app.utils.logger import logger
from app.api.services.base_service import BaseService
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.integrations import SimbrellaIntegration
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):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@@ -39,29 +46,86 @@ class EligibilityCheckService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
db.session.flush()
# Determine Loan count
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
if not is_eligible:
return ResponseHelper.error(result_description="Max loan count reached")
# Call RACCheck
response = SimbrellaIntegration.rac_check(
customer_id = customer_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:
return jsonify({"message": "RACCheck failed"}), 400
return ResponseHelper.error(result_description="RACCheck failed")
response = response.json()
rac_check = RACCheck.add_rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.transaction_id,
data = response['RACResponse']
)
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
)
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description= str(ve))
# -----------------------------------------------------------------------
# s = Offer.get_all_offers()
offers = [offer.to_dict() for offer in 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
response_data = {
@@ -69,31 +133,53 @@ class EligibilityCheckService(BaseService):
"transactionId": transactionId,
"countryCode": "NG",
"msisdn": msisdn,
"eligibleOffers": offers,
"resultDescription": "Successful",
"resultCode": "00",
"eligibleOffers": eligible_offers,
"accountId": account_id
}
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
@staticmethod
def check_loan_limits(customer_id):
"""
Checks if a customer has exceeded the loan limits for given offer.
"""
loan = Loan.get_customer_last_loan(customer_id)
if not loan:
return True
offer_id = loan.offer_id[:5]
offer = Offer.get_offer_by_id(offer_id)
if not offer:
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
return False
daily_count = TransactionOffer.get_daily_loan_count(customer_id, offer_id)
logger.error(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
if offer.max_daily_loans is not None and daily_count >= offer.max_daily_loans:
return False
return True
+9 -22
View File
@@ -6,7 +6,8 @@ from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.extensions import db
from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class LoanStatusService(BaseService):
@@ -43,13 +44,9 @@ class LoanStatusService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# loans = [
@@ -76,33 +73,23 @@ class LoanStatusService(BaseService):
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": total_debt_amount,
"resultCode": "00",
"resultDescription": "Successful"
}
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
return ResponseHelper.internal_server_error()
+8 -24
View File
@@ -5,6 +5,7 @@ from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.notification_callback import NotificationCallbackSchema
from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class NotificationCallbackService(BaseService):
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
@@ -27,37 +28,20 @@ class NotificationCallbackService(BaseService):
schema = NotificationCallbackSchema()
validated_data = schema.load(data) # Raises ValidationError if invalid
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success(
# data=response_data,
# message="Notification callback processed successfully"
# )
return response_data
return ResponseHelper.success()
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+126
View File
@@ -0,0 +1,126 @@
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 decide_offer(transaction_id, rac_check, validated_data, customer_id):
eligible_offers = []
# if we have active offers - we have to feed off it
logger.info(f"LOOOOOOOOOOOOOOOOOO** {customer_id}")
# 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
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
+87 -48
View File
@@ -8,9 +8,14 @@ from app.models.loan_charge import LoanCharge
from app.utils.logger import logger
from app.api.schemas.provide_loan import ProvideLoanSchema
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.extensions import db
from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta
from app.models import LoanRepaymentSchedule
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -36,29 +41,70 @@ class ProvideLoanService(BaseService):
collection_type = validated_data.get('collectionType')
transaction_id = validated_data.get('transactionId')
offer_id = validated_data.get('offerId')
amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId")
channel = validated_data.get('channel')
customer = Customer.is_valid_customer(customer_id)
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:
logger.error(f"Invalid Offer")
return jsonify({
"message": "Invalid Offer."
}), 400
try:
transaction_offer, offer, eligible_amount, original_transaction = OfferAnalysis.get_offer(
transaction_id=transaction_id,
rac_response=rac_response,
validated_data=validated_data
)
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description=str(ve))
# 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
transaction = ProvideLoanService.log_transaction(validated_data=validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
db.session.flush()
charges = ProvideLoanService.calculate_charges(offer, amount)
upfront_fee = charges["upfront_payment"]
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"]
interest = charges["interest"]
management = charges["management"]
insurance = charges["insurance"]
vat = charges["vat"]
# Save the loan details
loan = Loan.create_loan(
@@ -68,23 +114,33 @@ class ProvideLoanService(BaseService):
product_id = offer.product_id,
collection_type = collection_type,
transaction_id = validated_data.get('transactionId'),
original_transaction = transaction_offer.original_transaction,
initial_loan_amount = validated_data.get('requestedAmount'),
status= LoanStatus.ACTIVE
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
installment_amount = installment_amount,
eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor,
)
db.session.flush()
if not loan:
logger.error(f"Failed to save loan details")
return jsonify({
"message": "Failed to save loan details."
}), 400
charges = Charge.get_offer_charges(offer.id)
return ResponseHelper.error(result_description="Failed to save loan details.")
logger.error(f"{charges}")
db.session.flush()
current_product_id = offer.product_id
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedule:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
# charges = Charge.get_offer_charges(offer.id)
# logger.info(f"{charges}")
loan_id = loan.id
@@ -93,9 +149,7 @@ class ProvideLoanService(BaseService):
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Invalid Customer or Account")
response_data = {
@@ -103,44 +157,29 @@ class ProvideLoanService(BaseService):
"transactionId": transaction_id,
"customerId": customer_id,
"accountId": account_id,
"msisdn": customer.msisdn,
"resultCode": "00",
"resultDescription": "Successful"
"msisdn": customer.msisdn
}
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# Call Kafka in a background thread
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start()
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+12 -32
View File
@@ -1,6 +1,7 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.api.helpers.response_helper import ResponseHelper
from app.models import Repayment
from app.models.customer import Customer
from app.models.loan import Loan
@@ -49,9 +50,7 @@ class RepaymentService(BaseService):
if not repayment:
logger.error(f"Failed to save repayment details")
return jsonify({
"message": "Failed to save repayment details."
}), 400
return ResponseHelper.error(result_description="Failed to save repayment details.")
#Update Loan status
@@ -61,13 +60,9 @@ class RepaymentService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
return ResponseHelper.error(result_description="Invalid Customer or Account")
@@ -76,43 +71,28 @@ class RepaymentService(BaseService):
"transactionId": transaction_id,
"customerId": customer_id,
"productId": product_id,
"debtId": loan_id,
"resultCode": "00",
"resultDescription": "Successful"
"debtId": loan_id
}
# return ResponseHelper.success(
# data=response_data,
# message="Repayment processed successfully"
# )
# Call Kafka in a background thread
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start()
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+90 -31
View File
@@ -1,11 +1,14 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.select_offer import SelectOfferSchema
from app.extensions import db
from app.models import Offer
from datetime import date
from dateutil.relativedelta import relativedelta
class SelectOfferService(BaseService):
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
@@ -28,6 +31,15 @@ class SelectOfferService(BaseService):
)
account_id = validated_data.get("accountId")
customer_id = validated_data.get("customerId")
amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId")
transaction_offer_id = validated_data.get("offerId")
transaction_id = validated_data.get("transactionId")
request_id = validated_data.get("requestId")
offer_id = int(transaction_offer_id[5:]) # The last part is int
#"offerId": "SAL30001129",
if SelectOfferService.validate_account_ownership(
account_id=account_id, customer_id=customer_id
@@ -38,56 +50,103 @@ class SelectOfferService(BaseService):
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({"message": "Failed to log transaction."}), 400
else:
return jsonify({"message": "Invalid Customer or Account"}), 400
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Get the offer by product ID
offer = Offer.get_offer_by_product_id(product_id)
db.session.flush()
charges = SelectOfferService.calculate_charges(offer, amount)
upfront_payment = charges["upfront_payment"]
total_amount = charges["total_amount"]
installment_amount = charges["installment_amount"]
interest = charges["interest"]
management = charges["management"]
insurance = charges["insurance"]
vat = charges["vat"]
repayment_amount = charges["repayment_amount"]
interest_amount = charges["interest_amount"]
# Calculate the repayment dates
tenor = offer.tenor
start_date = date.today()
# Convert tenor to months
months = offer.schedule # tenor // 30
recommended_repayment_dates = [
(start_date + relativedelta(months=i + 1)).isoformat()
for i in range(months)
]
offers = [
{
"offerId": "SAL90",
"productId": "2030",
"amount": 10000.0,
"upfrontPayment": 1000.0,
"interestRate": 3.0,
"managementRate": 1.0,
"managementFee": 1.0,
"insuranceRate": 1.0,
"insuranceFee": 100.0,
"VATRate": 7.5,
"VATAmount": 100.0,
"recommendedRepaymentDates": ["2022-11-30"],
"installmentAmount": 11000.0,
"totalRepaymentAmount": 11000.0,
"offerId": transaction_offer_id,
"productId": product_id,
"amount": amount,
"upfrontPayment": upfront_payment,
"interestRate": offer.interest_rate,
"interestFee": interest_amount,
"managementRate": offer.management_rate,
"managementFee": management["fee"],
"insuranceRate": offer.insurance_rate,
"insuranceFee": insurance["fee"],
"VATRate": offer.vat_rate,
"VATAmount": vat["fee"],
"recommendedRepaymentDates": recommended_repayment_dates,
"repaymentAmount": repayment_amount,
"installmentAmount": installment_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
response_data = {
"outstandingDebtAmount": 0,
"requestId": "202111170001371256908",
"transactionId": transaction.id,
"requestId": request_id,
"transactionId": transaction_id,
"customerId": customer_id,
"accountId": account_id,
"loan": offers,
"resultCode": "00",
"resultDescription": "Successful",
}
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": "Validation exception"}), 422
except ValueError as err:
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": str(err)}), 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({"message": "Internal Server Error"}), 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+2 -1
View File
@@ -31,7 +31,8 @@ class Config:
"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")
settings = Config()
+5 -5
View File
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
def register_error_handlers(app):
@app.errorhandler(HTTPException)
def handle_http_exception(e):
return jsonify({'error': e.description}), e.code
return ResponseHelper.error(result_description=e.description, result_code=e.code )
@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({"message": "Method Not Allowed"}), 405
return ResponseHelper.method_not_allowed()
@app.errorhandler(404)
def not_found(error):
return jsonify({"message": "Resource not found"}), 404
return ResponseHelper.not_found()
@app.errorhandler(400)
def bad_request(error):
return jsonify({"message": "Bad Request"}), 400
return ResponseHelper.bad_request()
@app.errorhandler(415)
def unsupported_media_type(error):
return jsonify({"message": "Unsupported Media Type"}), 415
return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
+4 -1
View File
@@ -6,6 +6,9 @@ from .repayment import Repayment
from .loan_charge import LoanCharge
from .offer import Offer
from .charge import Charge
from .rac_checks import RACCheck
from .loan_repayment_schedule import LoanRepaymentSchedule
from .transaction_offers import TransactionOffer
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge']
__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 app.extensions import db
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Account(db.Model):
__tablename__ = 'accounts'
@@ -11,8 +12,8 @@ class Account(db.Model):
account_type = db.Column(db.String(50))
status = db.Column(db.String(20), default='active')
lien_amount = db.Column(db.Float, default=0.0)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship(
"Customer",
@@ -26,7 +27,9 @@ class Account(db.Model):
account = cls(
id=id,
customer_id=customer_id,
account_type=account_type
account_type=account_type,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+6 -4
View File
@@ -1,6 +1,7 @@
from datetime import datetime, timezone, timedelta
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
class Charge(db.Model):
@@ -12,9 +13,8 @@ class Charge(db.Model):
percent = db.Column(db.Float, default=0.0)
description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
offer = relationship(
"Offer",
primaryjoin="Charge.offer_id == Offer.id",
@@ -57,7 +57,9 @@ class Charge(db.Model):
code = code,
percent = percent,
description = description,
due = due_days
due = due_days,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(charge_obj)
+17 -4
View File
@@ -3,6 +3,7 @@ from sqlalchemy.orm import relationship
from app.extensions import db
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Customer(db.Model):
__tablename__ = 'customers'
@@ -10,9 +11,8 @@ class Customer(db.Model):
id = db.Column(db.String(50), primary_key=True)
msisdn = db.Column(db.String(20), unique=True, nullable=False)
country_code = db.Column(db.String(3), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
accounts = relationship(
"Account",
primaryjoin="Customer.id == Account.customer_id",
@@ -27,6 +27,13 @@ class Customer(db.Model):
back_populates="customer",
)
transaction_offers = relationship(
"TransactionOffer",
primaryjoin="Customer.id == TransactionOffer.customer_id",
foreign_keys="TransactionOffer.customer_id",
back_populates="customer",
)
@classmethod
def is_valid_customer(cls, customer_id):
customer = cls.query.filter_by(id=customer_id).first()
@@ -40,7 +47,13 @@ class Customer(db.Model):
raise ValueError("Customer already exists")
# Create the customer
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
customer = cls(
id=id,
msisdn=msisdn,
country_code=country_code,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
db.session.add(customer)
+107 -11
View File
@@ -4,7 +4,13 @@ from app.models.customer import Customer
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship
from app.models.loan_charge import LoanCharge
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):
@@ -17,6 +23,7 @@ class Loan(db.Model):
)
customer_id = db.Column(db.String(50), nullable=False)
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)
offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
@@ -25,10 +32,17 @@ class Loan(db.Model):
initial_loan_amount = db.Column(db.Float, nullable=False)
default_penalty_fee = db.Column(db.Float, default=0)
continuous_fee = db.Column(db.Float, default=0)
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
status = db.Column(db.String(20), default='pending')
tenor = db.Column(db.Integer, nullable=True)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
disburse_date = db.Column(db.DateTime, nullable=True)
disburse_verify = db.Column(db.DateTime, nullable=True)
customer = relationship(
"Customer",
@@ -39,20 +53,44 @@ class Loan(db.Model):
loan_charges = relationship(
"LoanCharge",
primaryjoin="Loan.id == LoanCharge.loan_id",
primaryjoin="LoanCharge.loan_id == Loan.id",
foreign_keys="LoanCharge.loan_id",
back_populates="loan",
)
@classmethod
def create_loan(cls, customer_id, account_id, offer_id, product_id, initial_loan_amount, collection_type, transaction_id, status='pending'):
loan_repayment_schedules = relationship(
"LoanRepaymentSchedule",
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
foreign_keys="LoanRepaymentSchedule.loan_id",
back_populates="loan",
)
@classmethod
def create_loan(
cls,
customer_id,
account_id,
offer_id,
product_id,
initial_loan_amount,
collection_type,
transaction_id,
original_transaction,
upfront_fee,
repayment_amount,
installment_amount,
tenor,
eligible_amount,
status = "pending",
):
# Check if customer exists
customer = Customer.is_valid_customer(customer_id)
if not customer:
raise ValueError("Customer does not exist")
now = datetime.now(timezone.utc)
due_date = now + timedelta(days=tenor)
# Create and save the loan
loan = cls(
@@ -62,10 +100,18 @@ class Loan(db.Model):
product_id = product_id,
collection_type = collection_type,
transaction_id = transaction_id,
original_transaction = original_transaction,
initial_loan_amount = initial_loan_amount,
current_loan_amount = initial_loan_amount,
due_date=now,
status = status
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
installment_amount = installment_amount,
due_date=due_date,
tenor = tenor,
status = status,
eligible_amount =eligible_amount,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
@@ -74,7 +120,6 @@ class Loan(db.Model):
raise ValueError(f"Database integrity error: {err}")
return loan
@classmethod
def has_active_loans(cls, customer_id):
active_loans = cls.query.filter_by(
@@ -90,13 +135,61 @@ class Loan(db.Model):
@classmethod
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()
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
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
def update_status(cls, loan_id, status):
"""
@@ -125,6 +218,9 @@ class Loan(db.Model):
'defaultPenaltyFee': self.default_penalty_fee,
'continuousFee': self.continuous_fee,
'collectionType': self.collection_type,
'upfrontFee': self.upfront_fee,
'repaymentAmount': self.repayment_amount,
'installmentAmount': self.installment_amount,
'status': self.status,
'dueDate': self.due_date.isoformat() if self.due_date 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 app.extensions import db
from sqlalchemy.orm import relationship
from app.utils.logger import logger
from sqlalchemy.sql import func
class LoanCharge(db.Model):
@@ -15,9 +17,8 @@ class LoanCharge(db.Model):
description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanCharge.loan_id == Loan.id",
@@ -35,8 +36,8 @@ class LoanCharge(db.Model):
charges (list): A list of dictionaries with keys:
code (str), amount (float), percent (float), description (str), due (int)
"""
if not charges or not isinstance(charges, list):
raise ValueError("Charges must be a non-empty list of dictionaries")
# if not charges or not isinstance(charges, list):
# raise ValueError("Charges must be a non-empty list of dictionaries")
if loan_id is None:
raise ValueError("loan_id cannot be None")
@@ -44,23 +45,27 @@ class LoanCharge(db.Model):
loan_charges = []
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:
amount = (percent / 100.0) * referenced_amount
subset_keys = ['interest', 'management', 'insurance', 'vat']
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(
loan_id = loan_id,
transaction_id = transaction_id,
code = getattr(charge, "code"),
code = code,
amount = round(amount, 2),
percent = percent,
description = getattr(charge, "description", ""),
description = description,
due = due_days,
due_date = now + timedelta(days=due_days)
due_date = now + timedelta(days=due_days),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(charge_obj)
+72
View File
@@ -0,0 +1,72 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from sqlalchemy.sql import func
class LoanRepaymentSchedule(db.Model):
__tablename__ = 'loan_repayment_schedules'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
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)
installment_number = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=False)
installment_amount= db.Column(db.Float, default=0.0)
total_repayment_amount = db.Column(db.Float, default=0.0)
paid = db.Column(db.Boolean, default=False)
paid_at = db.Column(db.DateTime, 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())
loan = relationship(
"Loan",
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
foreign_keys=[loan_id],
back_populates="loan_repayment_schedules",
)
@classmethod
def add_repayment_schedule(cls, loan, num_schedules, transaction_id):
"""
Add repayment schedules for a given loan.
"""
now = datetime.now(timezone.utc)
schedules = []
for i in range(num_schedules):
due_date = now + relativedelta(months=i + 1)
schedule = LoanRepaymentSchedule(
loan_id=loan.id,
installment_number=i + 1,
due_date=due_date,
total_repayment_amount = round(loan.repayment_amount, 2),
installment_amount=round(loan.installment_amount, 2),
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)
schedules.append(schedule)
return schedules
def to_dict(self):
return {
'id': self.id,
'loanId': self.loan_id,
'installmentNumber': self.installment_number,
'dueDate': self.due_date.isoformat(),
'principalAmount': self.principal_amount,
'interestAmount': self.interest_amount,
'totalInstallment': self.total_installment,
'paid': self.paid,
'paidAt': self.paid_at.isoformat() if self.paid_at else None
}
def __repr__(self):
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
+43 -3
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
from app.extensions import db
from app.models.charge import Charge
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
class Offer(db.Model):
__tablename__ = 'offers'
@@ -11,8 +12,17 @@ class Offer(db.Model):
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
tenor = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
schedule = db.Column(db.Integer, nullable=True)
interest_rate = db.Column(db.Float, default=3.0)
management_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)
list_order = db.Column(db.Integer, nullable=True)
max_daily_loans = db.Column(db.Integer, nullable=True)
max_active_loans = db.Column(db.Integer, nullable=True)
max_life_loans = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
charges = relationship(
"Charge",
@@ -40,6 +50,28 @@ class Offer(db.Model):
if not offer:
return False
return offer
@classmethod
def get_offer_by_id(cls, offer_id):
"""
Return an offer by its ID.
"""
offer = cls.query.filter_by(id=str(offer_id)).first()
if not offer:
raise ValueError(f"Offer with ID {offer_id} not found")
return offer
@classmethod
def get_offer_by_product_id(cls, product_id):
"""
Return an offer by its product ID.
"""
offer = cls.query.filter_by(product_id=str(product_id)).first()
if not offer:
raise ValueError(f"Offer with Product ID {product_id} not found")
return offer
def to_dict(self):
return {
@@ -47,7 +79,15 @@ class Offer(db.Model):
"productId": self.product_id,
"minAmount": self.min_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):
+75
View File
@@ -0,0 +1,75 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.exc import IntegrityError
from uuid import uuid4
from sqlalchemy.types import JSON
from sqlalchemy.sql import func
class RACCheck(db.Model):
__tablename__ = 'rac_checks'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String, nullable=False)
account_id = db.Column(db.String, nullable=False)
rac_response = db.Column(db.JSON, nullable=False)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@classmethod
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
# 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
def get_all_rac_checks(cls):
"""
Return all RAC checks in dictionary format.
"""
rac_checks = cls.query.all()
if not rac_checks:
return None
return rac_checks
@classmethod
def get_rac_check(cls, customer_id, account_id):
"""
Return a RAC check by its ID.
"""
rac_check = cls.query.filter_by( customer_id = customer_id,
account_id = account_id,).first()
if not rac_check:
raise ValueError(f"RAC Check for customer not found")
return rac_check
def to_dict(self):
return {
"id": str(self.id),
"transactionId": str(self.transaction_id),
"customerId": self.customer_id,
"accountId": self.account_id,
"racResponse": self.rac_response,
"createdAt": self.created_at.isoformat(),
"updatedAt": self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<RACCheck {self.id}>'
+6 -3
View File
@@ -4,6 +4,7 @@ from app.extensions import db
from app.models.customer import Customer
from app.models.loan import Loan
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
class Repayment(db.Model):
@@ -17,8 +18,8 @@ class Repayment(db.Model):
loan_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
transaction_id = db.Column(db.String(50), nullable=True)
@classmethod
@@ -41,7 +42,9 @@ class Repayment(db.Model):
customer_id=customer_id,
loan_id=loan_id,
product_id=product_id,
transaction_id = transaction_id
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+6 -4
View File
@@ -3,6 +3,7 @@ from app.extensions import db
from app.models import account
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
class Transaction(db.Model):
__tablename__ = 'transactions'
@@ -16,9 +17,8 @@ class Transaction(db.Model):
customer_id = db.Column(db.String(50), nullable=True)
type = db.Column(db.String(50), nullable=False)
channel = db.Column(db.String(50), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
def __repr__(self):
return f'<Transaction {self.id}>'
@@ -38,7 +38,9 @@ class Transaction(db.Model):
customer_id = customer_id,
account_id = account_id,
type = type,
channel = channel
channel = channel,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+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}>'
+17 -4
View File
@@ -47,13 +47,26 @@
"productId": {
"type": "string",
"example": "101"
},
"installment": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"repaymentDate": {
"type": "string",
"example": "2025-04-24 10:31:"
}
}
}
},
"totalDebtAmount": {
"type": "integer",
"example": 8500
}
}
}
},
"resultCode": {
"type": "string",
@@ -9,6 +9,10 @@
"type": "string",
"example": "Tr201712RK9232P115"
},
"loanRef": {
"type": "string",
"example": "1620029887USSDAMPC"
},
"customerId": {
"type": "string",
"example": "CN621868"
@@ -27,6 +27,10 @@
"example": "ACN8263457"
},
"productId": {
"type": "string",
"example": "3MPC"
},
"offerId": {
"type": "string",
"example": "101"
},
+11 -2
View File
@@ -28,13 +28,17 @@
},
"productId": {
"type": "string",
"example": "2030"
"example": "3MPC"
},
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"dueDate": {
"type": "string",
"example": "2025-04-24 10:31:"
},
"upfrontPayment": {
"type": "number",
"format": "float",
@@ -45,6 +49,11 @@
"format": "float",
"example": 3.0
},
"interestFee": {
"type": "number",
"format": "float",
"example": 3000.00
},
"ManagementRate": {
"type": "number",
"format": "float",
@@ -75,7 +84,7 @@
"format": "float",
"example": 100.0
},
"recommendedRepaymentDates": {
"installmentRepaymentDates": {
"type": "array",
"items": {
"type": "string"
@@ -0,0 +1,32 @@
"""Migration on Sat May 10 09:54:34 UTC 2025
Revision ID: 173ea45db189
Revises: 3105abd795d4
Create Date: 2025-05-10 09:54:39.380499
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '173ea45db189'
down_revision = '3105abd795d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -0,0 +1,53 @@
"""Migration on Thu Apr 24 17:42:25 UTC 2025
Revision ID: 1b2339f43824
Revises: de9ad96ba34e
Create Date: 2025-04-24 17:43:09.589626
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1b2339f43824'
down_revision = 'de9ad96ba34e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('rac_checks',
sa.Column('id', sa.String(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(), nullable=False),
sa.Column('account_id', sa.String(), nullable=False),
sa.Column('rac_response', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.NUMERIC(precision=10, scale=2),
type_=sa.Float(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('installment_amount')
batch_op.drop_column('repayment_amount')
batch_op.drop_column('upfront_fee')
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.Float(),
type_=sa.NUMERIC(precision=10, scale=2),
existing_nullable=True)
op.drop_table('rac_checks')
# ### end Alembic commands ###
@@ -0,0 +1,42 @@
"""Migration on Fri Apr 25 15:01:00 UTC 2025
Revision ID: 2a45dd99c9cb
Revises: 2cf0c177ca02
Create Date: 2025-04-25 15:01:51.129681
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a45dd99c9cb'
down_revision = '2cf0c177ca02'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('total_repayment_amount', sa.Float(), nullable=True))
batch_op.drop_column('principal_amount')
batch_op.drop_column('interest_amount')
batch_op.drop_column('total_installment')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('total_installment', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('interest_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('principal_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.drop_column('total_repayment_amount')
batch_op.drop_column('installment_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -0,0 +1,41 @@
"""Migration on Fri Apr 25 14:02:01 UTC 2025
Revision ID: 2cf0c177ca02
Revises: 1b2339f43824
Create Date: 2025-04-25 14:02:42.244146
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2cf0c177ca02'
down_revision = '1b2339f43824'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('principal_amount', sa.Float(), nullable=True),
sa.Column('interest_amount', sa.Float(), nullable=True),
sa.Column('total_installment', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('loan_repayment_schedules')
# ### end Alembic commands ###
+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,38 @@
"""Migration on Sat Apr 26 12:50:46 UTC 2025
Revision ID: 89759cebb9c6
Revises: 2a45dd99c9cb
Create Date: 2025-04-26 12:50:49.771355
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '89759cebb9c6'
down_revision = '2a45dd99c9cb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('interest_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('management_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('insurance_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('vat_rate', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.drop_column('vat_rate')
batch_op.drop_column('insurance_rate')
batch_op.drop_column('management_rate')
batch_op.drop_column('interest_rate')
# ### end Alembic commands ###
@@ -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 ###
+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 ###
+4
View File
@@ -35,3 +35,7 @@ flask-jwt-extended
# Kafka
confluent-kafka==1.9.2
python-dateutil