Compare commits

...

47 Commits

Author SHA1 Message Date
VivianDee a81313447b [add]: timestamp fix 2025-05-16 13:40:33 +01:00
VivianDee 0369323dd6 Update transaction_offers.py 2025-05-14 20:05:20 +01: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
30 changed files with 738 additions and 461 deletions
+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)
+2 -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,7 +46,7 @@ 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
+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}"
)
+1
View File
@@ -123,6 +123,7 @@ class BaseService:
return {
"interest": interest,
"interest_amount": interest_amount,
"management": management,
"insurance": insurance,
"vat": vat,
+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()
+57 -56
View File
@@ -8,6 +8,9 @@ from app.api.enums import TransactionType
from app.api.integrations import SimbrellaIntegration
from app.extensions import db
from app.models import Offer, RACCheck
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
import random
@@ -42,14 +45,9 @@ 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()
@@ -62,7 +60,7 @@ class EligibilityCheckService(BaseService):
# this chck 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()
@@ -75,42 +73,52 @@ class EligibilityCheckService(BaseService):
if not rac_check:
logger.error(f"Failed to save RACCheck")
return jsonify({
"message": "Failed to save RACCheck."
}), 400
offers = Offer.get_all_offers()
eligible_offers = []
for offer in offers:
# Determine an approved amount
random_float = random.random() # temporary to play data
approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
approved_amount = round(approved_amount, 2)
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id = customer.id,
transaction_id = transaction.transaction_id,
offer_id = offer.id,
min_amount = offer.min_amount,
max_amount = offer.max_amount,
eligible_amount = approved_amount,
product_id = offer.product_id,
tenor = offer.tenor
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()
# Visible offer ID: offer_id + padded(transaction_offer.id)
padded_id = str(transaction_offer.id).zfill(6)
public_offer_id = f"{offer.id}{padded_id}"
# eligible_offers = []
eligible_offers.append({
"offerId": public_offer_id,
"product_id": offer.product_id,
"min_amount": offer.min_amount,
"max_amount": approved_amount,
"tenor": offer.tenor
})
# for offer in offers:
# # Determine an approved amount
# random_float = random.random() # temporary to play data
# approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
# approved_amount = round(approved_amount, 2)
#
# transaction_offer = TransactionOffer.create_transaction_offer(
# customer_id = customer.id,
# transaction_id = transaction.transaction_id,
# offer_id = offer.id,
# min_amount = offer.min_amount,
# max_amount = offer.max_amount,
# eligible_amount = approved_amount,
# product_id = offer.product_id,
# tenor = offer.tenor
# )
#
# # Visible offer ID: offer_id + padded(transaction_offer.id)
# padded_id = str(transaction_offer.id).zfill(6)
# public_offer_id = f"{offer.id}{padded_id}"
#
# eligible_offers.append({
# "offerId": public_offer_id,
# "product_id": offer.product_id,
# "min_amount": offer.min_amount,
# "max_amount": approved_amount,
# "tenor": offer.tenor
# })
# Simulate processing
response_data = {
@@ -119,30 +127,23 @@ class EligibilityCheckService(BaseService):
"countryCode": "NG",
"msisdn": msisdn,
"eligibleOffers": eligible_offers,
"resultDescription": "Successful",
"resultCode": "00",
"accountId": account_id
}
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return jsonify({
"message": "Validation exception"
}) , 422
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+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()
+103 -1
View File
@@ -1,4 +1,8 @@
from app.models import Offer, TransactionOffer
from app.models.loan import Loan
import random
import logging
logger = logging.getLogger(__name__)
class OfferAnalysis:
@@ -10,6 +14,11 @@ class OfferAnalysis:
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:
@@ -20,5 +29,98 @@ class OfferAnalysis:
if not offer:
raise ValueError("Invalid Offer.")
original_transaction = transaction_id
return transaction_offer, offer, eligible_amount
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
+21 -39
View File
@@ -15,6 +15,7 @@ from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta
from app.models import LoanRepaymentSchedule
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -42,6 +43,7 @@ class ProvideLoanService(BaseService):
offer_id = validated_data.get('offerId')
amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId")
channel = validated_data.get('channel')
customer = Customer.is_valid_customer(customer_id)
@@ -50,16 +52,14 @@ class ProvideLoanService(BaseService):
rac_response = RACCheck.get_rac_check(customer_id = customer_id, account_id = account_id)
try:
transaction_offer, offer, eligible_amount = OfferAnalysis.get_offer(
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 jsonify({
"message": str(ve)
}), 400
return ResponseHelper.error(result_description=str(ve))
# transaction_offer_id = int(offer_id[5:]) # The last part is int
@@ -80,14 +80,13 @@ class ProvideLoanService(BaseService):
# "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()
@@ -107,7 +106,6 @@ class ProvideLoanService(BaseService):
vat = charges["vat"]
# Save the loan details
loan = Loan.create_loan(
customer_id = customer_id,
@@ -116,6 +114,7 @@ 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'),
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
@@ -123,29 +122,25 @@ class ProvideLoanService(BaseService):
eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor,
)
if not loan:
logger.error(f"Failed to save loan details")
return jsonify({
"message": "Failed to save loan details."
}), 400
return ResponseHelper.error(result_description="Failed to save loan details.")
db.session.flush()
current_product_id = offer.product_id
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedule:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return jsonify({
"message": "Failed to generate loan repayment schedule."
}), 400
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
# charges = Charge.get_offer_charges(offer.id)
logger.error(f"{charges}")
# logger.info(f"{charges}")
loan_id = loan.id
@@ -154,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 = {
@@ -164,40 +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()
+21 -16
View File
@@ -1,5 +1,6 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.utils.logger import logger
@@ -32,10 +33,14 @@ class SelectOfferService(BaseService):
customer_id = validated_data.get("customerId")
amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId")
offer_id = validated_data.get("offerId")
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
):
@@ -45,9 +50,9 @@ 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)
@@ -63,6 +68,7 @@ class SelectOfferService(BaseService):
insurance = charges["insurance"]
vat = charges["vat"]
repayment_amount = charges["repayment_amount"]
interest_amount = charges["interest_amount"]
# Calculate the repayment dates
@@ -81,11 +87,12 @@ class SelectOfferService(BaseService):
offers = [
{
"offerId": offer.id,
"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,
@@ -122,26 +129,24 @@ class SelectOfferService(BaseService):
"customerId": customer_id,
"accountId": account_id,
"loan": offers,
"resultCode": "00",
"resultDescription": "Successful",
}
db.session.commit()
return response_data
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": "Validation exception"}), 422
except ValueError as err:
db.session.rollback()
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": str(err)}), 400
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({"message": "Internal Server Error"}), 500
db.session.rollback()
return ResponseHelper.internal_server_error()
+5 -5
View File
@@ -5,20 +5,20 @@ from app.api.helpers.response_helper import ResponseHelper
def register_error_handlers(app):
@app.errorhandler(HTTPException)
def handle_http_exception(e):
return jsonify({'error': e.description}), e.code
return ResponseHelper.error(result_description=e.description, result_code=e.code )
@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({"message": "Method Not Allowed"}), 405
return ResponseHelper.method_not_allowed()
@app.errorhandler(404)
def not_found(error):
return jsonify({"message": "Resource not found"}), 404
return ResponseHelper.not_found()
@app.errorhandler(400)
def bad_request(error):
return jsonify({"message": "Bad Request"}), 400
return ResponseHelper.bad_request()
@app.errorhandler(415)
def unsupported_media_type(error):
return jsonify({"message": "Unsupported Media Type"}), 415
return ResponseHelper.error(result_description="Unsupported Media Type", result_code="415")
+3 -2
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), onupdate=func.now())
customer = relationship(
"Customer",
+3 -3
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), onupdate=func.now())
offer = relationship(
"Offer",
primaryjoin="Charge.offer_id == Offer.id",
+3 -3
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), onupdate=func.now())
accounts = relationship(
"Account",
primaryjoin="Customer.id == Account.customer_id",
+61 -5
View File
@@ -6,6 +6,11 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from datetime import timedelta
import logging
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
logger = logging.getLogger(__name__)
class Loan(db.Model):
@@ -33,9 +38,11 @@ class Loan(db.Model):
status = db.Column(db.String(20), default='pending')
tenor = db.Column(db.Integer, nullable=True)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), 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",
@@ -69,6 +76,7 @@ class Loan(db.Model):
initial_loan_amount,
collection_type,
transaction_id,
original_transaction,
upfront_fee,
repayment_amount,
installment_amount,
@@ -92,7 +100,7 @@ class Loan(db.Model):
product_id = product_id,
collection_type = collection_type,
transaction_id = transaction_id,
original_transaction = transaction_id,
original_transaction = original_transaction,
initial_loan_amount = initial_loan_amount,
current_loan_amount = initial_loan_amount,
upfront_fee = upfront_fee,
@@ -125,13 +133,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):
"""
+3 -3
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone, timedelta
from app.extensions import db
from sqlalchemy.orm import relationship
from app.utils.logger import logger
from sqlalchemy.sql import func
class LoanCharge(db.Model):
@@ -16,9 +17,8 @@ class LoanCharge(db.Model):
description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanCharge.loan_id == Loan.id",
+3 -3
View File
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from sqlalchemy.sql import func
class LoanRepaymentSchedule(db.Model):
__tablename__ = 'loan_repayment_schedules'
@@ -17,9 +18,8 @@ class LoanRepaymentSchedule(db.Model):
paid = db.Column(db.Boolean, default=False)
paid_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
+3 -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'
@@ -17,9 +18,8 @@ class Offer(db.Model):
insurance_rate = db.Column(db.Float, default=1.0)
vat_rate = db.Column(db.Float, default=7.5)
list_order = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
charges = relationship(
"Charge",
primaryjoin="Offer.id == Charge.offer_id",
+3 -3
View File
@@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship
from sqlalchemy.exc import IntegrityError
from uuid import uuid4
from sqlalchemy.types import JSON
from sqlalchemy.sql import func
class RACCheck(db.Model):
__tablename__ = 'rac_checks'
@@ -13,9 +14,8 @@ class RACCheck(db.Model):
customer_id = db.Column(db.String, nullable=False)
account_id = db.Column(db.String, nullable=False)
rac_response = db.Column(db.JSON, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
@classmethod
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
+3 -2
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), onupdate=func.now())
transaction_id = db.Column(db.String(50), nullable=True)
@classmethod
+3 -3
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), onupdate=func.now())
def __repr__(self):
return f'<Transaction {self.id}>'
+14 -6
View File
@@ -1,6 +1,11 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import logging
logger = logging.getLogger(__name__)
class TransactionOffer(db.Model):
__tablename__ = 'transaction_offers'
@@ -8,6 +13,7 @@ class TransactionOffer(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
customer_id = db.Column(db.String(50), nullable=False)
transaction_id = db.Column(db.String(50), nullable=False)
original_transaction = db.Column(db.String(50), nullable=True)
offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
min_amount = db.Column(db.Float, nullable=False)
@@ -15,9 +21,8 @@ class TransactionOffer(db.Model):
eligible_amount = db.Column(db.Float, nullable=True)
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
customer = relationship(
"Customer",
primaryjoin="Customer.id == TransactionOffer.customer_id",
@@ -26,26 +31,29 @@ class TransactionOffer(db.Model):
)
@classmethod
def is_valid_transaction_offer(cls, offer_id, customer_id, product_id):
def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id):
transaction_offer = cls.query.filter_by(
id = str(offer_id),
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, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
"""
Class method to create and save a TransactionOffer.
"""
transaction_offer = cls(
customer_id=customer_id,
transaction_id=transaction_id,
original_transaction=original_transaction,
offer_id=offer_id,
min_amount=min_amount,
max_amount=max_amount,
@@ -9,6 +9,10 @@
"type": "string",
"example": "Tr201712RK9232P115"
},
"loanRef": {
"type": "string",
"example": "1620029887USSDAMPC"
},
"customerId": {
"type": "string",
"example": "CN621868"
+1 -1
View File
@@ -28,7 +28,7 @@
},
"productId": {
"type": "string",
"example": "2090"
"example": "3MPC"
},
"offerId": {
"type": "string",
+6 -1
View File
@@ -28,7 +28,7 @@
},
"productId": {
"type": "string",
"example": "2030"
"example": "3MPC"
},
"amount": {
"type": "number",
@@ -49,6 +49,11 @@
"format": "float",
"example": 3.0
},
"interestFee": {
"type": "number",
"format": "float",
"example": 3000.00
},
"ManagementRate": {
"type": "number",
"format": "float",
@@ -0,0 +1,32 @@
"""Migration on Sat May 10 09:54:34 UTC 2025
Revision ID: 173ea45db189
Revises: 3105abd795d4
Create Date: 2025-05-10 09:54:39.380499
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '173ea45db189'
down_revision = '3105abd795d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
+250
View File
@@ -0,0 +1,250 @@
"""empty message
Revision ID: 2eee4157505f
Revises: 565bc3d0ba6e
Create Date: 2025-05-16 13:24:41.914400
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '2eee4157505f'
down_revision = '565bc3d0ba6e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
# ### end Alembic commands ###
@@ -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 ###