Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d2659462 | |||
| 569d4c45d7 |
@@ -68,14 +68,6 @@ class Config:
|
||||
PENAL_CHARGE_INTERVAL_DAYS = os.getenv("PENAL_CHARGE_INTERVAL_DAYS", 30)
|
||||
PENAL_CHARGE_MAXIMUM_COUNT = os.getenv("PENAL_CHARGE_MAXIMUM_COUNT", 6)
|
||||
|
||||
#processing failed disbursement sections
|
||||
FAILED_DISBURSEMENT_BATCH_SIZE = int(os.getenv("FAILED_DISBURSEMENT_BATCH_SIZE", 10))
|
||||
FAILED_DISBURSEMENT_DELAY_SECONDS = int(os.getenv("FAILED_DISBURSEMENT_DELAY_SECONDS", 5))
|
||||
FAILED_DISBURSEMENT_BATCH_DELAY_SECONDS = int(
|
||||
os.getenv("FAILED_DISBURSEMENT_BATCH_DELAY_SECONDS", 5)
|
||||
)
|
||||
FAILED_RETRY_TIME_INTERVAL_SECONDS = int(os.getenv("FAILED_RETRY_TIME_INTERVAL_SECONDS", 86400)) #24 hours = 86400 seconds
|
||||
|
||||
|
||||
BANK_CALL_API_TIME_OUT = os.getenv("BANK_CALL_API_TIME_OUT", 100)
|
||||
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net/api")
|
||||
|
||||
@@ -6,4 +6,3 @@ class LoanStatus(str, Enum):
|
||||
ACTIVE_PARTIAL = "active_partial"
|
||||
START_REPAY = "start_repay"
|
||||
REPAID = "repaid"
|
||||
FAILED = "failed"
|
||||
@@ -137,8 +137,6 @@ class SimbrellaClient:
|
||||
logger.error("")
|
||||
LoanService.set_disbursement_loan_description(loan_data['debtId'],
|
||||
"Disbursement Service url not found (404)")
|
||||
logger.info(f"Loan status updated to {LoanStatus.FAILED} for loan ID {loan_data['debtId']} due to disbursement failure")
|
||||
LoanService.update_status(loan_data['debtId'], LoanStatus.FAILED)
|
||||
return ResponseHelper.error("Disbursement Service url not found (404)", status_code=404)
|
||||
|
||||
logger.info(f"Disbursement response: {response.json()}")
|
||||
@@ -167,8 +165,6 @@ class SimbrellaClient:
|
||||
errorMessage = "Unable to complete Disbursement Service with HTTP status code: " + str(
|
||||
response.status_code)
|
||||
LoanService.set_disbursement_loan_description(loan_data['debtId'], errorMessage)
|
||||
updatedLoan = LoanService.update_status(loan_data['debtId'], LoanStatus.FAILED)
|
||||
logger.info(f"Loan status updated to {updatedLoan.get('status')} for loan ID {loan_data['debtId']} due to disbursement failure")
|
||||
return ResponseHelper.error(errorMessage, status_code=response.status_code)
|
||||
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
|
||||
+1
-37
@@ -9,7 +9,6 @@ from app.utils.logger import logger
|
||||
from app.extensions import db
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import datetime, timezone
|
||||
from app.config import settings
|
||||
|
||||
class Loan(db.Model):
|
||||
__tablename__ = "loans"
|
||||
@@ -47,6 +46,7 @@ class Loan(db.Model):
|
||||
verify_result = db.Column(db.String(10), nullable=True)
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
reference = db.Column(db.String(50), nullable=True)
|
||||
|
||||
total_penal_charge = db.Column(db.Float, default=0.0)
|
||||
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
@@ -103,19 +103,6 @@ class Loan(db.Model):
|
||||
def get_loan_by_transaction_id(cls, transaction_id):
|
||||
return cls.query.filter_by(transaction_id=transaction_id).first()
|
||||
|
||||
#update loan status
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
loan = cls.query.get(loan_id)
|
||||
if loan:
|
||||
loan.status = status
|
||||
db.session.commit()
|
||||
return loan.to_dict()
|
||||
else:
|
||||
raise ValueError(f"Loan with ID {loan_id} not found.")
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_by_loan_id(cls, loan_id):
|
||||
return cls.query.filter_by(id=loan_id).first()
|
||||
@@ -264,29 +251,6 @@ class Loan(db.Model):
|
||||
logger.error(f"Error fetching latest loan without disburse date: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def get_failed_disbursements(cls):
|
||||
"""
|
||||
Get all loans with failed disbursement.
|
||||
"""
|
||||
try:
|
||||
last_time_interval = settings.FAILED_RETRY_TIME_INTERVAL_SECONDS or 0
|
||||
failed_loans = cls.query.filter(
|
||||
cls.disburse_date.is_(None),
|
||||
cls.created_at >= datetime.utcnow() - timedelta(seconds=last_time_interval)
|
||||
).all()
|
||||
|
||||
if not failed_loans:
|
||||
logger.info("No failed disbursements found.")
|
||||
return []
|
||||
|
||||
logger.info(f"Found {len(failed_loans)} failed disbursements.")
|
||||
return failed_loans
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching failed disbursements: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_latest_loan_with_disburse_date(cls):
|
||||
"""
|
||||
|
||||
+10
-110
@@ -21,17 +21,9 @@ from app.config import settings
|
||||
|
||||
autocall_bp = Blueprint("autocall", __name__)
|
||||
|
||||
#refresh-verify-disbursement
|
||||
#
|
||||
#
|
||||
|
||||
@autocall_bp.route("/refresh-verify-disbursement", methods=["GET"])
|
||||
def verify_transaction():
|
||||
"""
|
||||
Get the latest loan with disbursement date and no verification date.
|
||||
Then call the verify transaction endpoint with the loan data.
|
||||
This is called after disbursement to ensure the loan was actually disbursed
|
||||
Then a verification date is set
|
||||
"""
|
||||
logger.info(f"Calling VerifyTransaction Components")
|
||||
|
||||
loan = LoanService.get_latest_loan_with_disburse_date()
|
||||
@@ -55,11 +47,6 @@ def verify_transaction():
|
||||
|
||||
@autocall_bp.route("/refresh-disbursement", methods=["GET"])
|
||||
def disbursement():
|
||||
"""
|
||||
Get the latest loan without disbursement date.
|
||||
Then call the disburse loan endpoint with the loan data.
|
||||
"""
|
||||
|
||||
# data = request.json()
|
||||
logger.info(f"Calling Disbursement Components")
|
||||
loan = LoanService.get_latest_loan_without_disburse_date()
|
||||
@@ -83,14 +70,6 @@ def disbursement():
|
||||
|
||||
@autocall_bp.route("/retry-disbursement", methods=["POST"])
|
||||
def retry_disbursement():
|
||||
"""
|
||||
This takes in a transaction id as input and
|
||||
retries the disbursement for the loan with that transaction id.
|
||||
This is to be used in cases where the disbursement failed due to network issues
|
||||
or other transient issues. It will call the disbursement endpoint
|
||||
with the loan data for the loan with the given transaction id.
|
||||
"""
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
logger.info(f"Retry Transaction ID Data Received for :::: {data}")
|
||||
@@ -122,64 +101,12 @@ def retry_disbursement():
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to call retry disbursement {data}: {e}")
|
||||
|
||||
@autocall_bp.route("/retry-failed-disbursements", methods=["GET"])
|
||||
def retry_failed_disbursements():
|
||||
"""
|
||||
This endpoint is for retrying failed disbursements.
|
||||
It will get the list of failed disbursements and call the disbursement endpoint for each of them.
|
||||
This is to be used in cases where the disbursement failed due to network issues or other transient issues. It will call the disbursement endpoint
|
||||
with the loan data for the loan with the given transaction id.
|
||||
"""
|
||||
|
||||
try:
|
||||
logger.info(f"Calling Retry Failed Disbursements Components")
|
||||
failed_disbursements = LoanService.get_failed_disbursements()
|
||||
if not failed_disbursements:
|
||||
logger.info(f"No failed disbursements found")
|
||||
return ResponseHelper.success(message="No failed disbursements found", status_code=200)
|
||||
|
||||
logger.info(f"Found {len(failed_disbursements)} failed disbursements to retry")
|
||||
#get batch size from settings
|
||||
# Safe config values
|
||||
batch_size = max(1, settings.FAILED_DISBURSEMENT_BATCH_SIZE or 1)
|
||||
delay_seconds = max(0, settings.FAILED_DISBURSEMENT_DELAY_SECONDS or 0)
|
||||
batch_delay = max(0, settings.FAILED_DISBURSEMENT_BATCH_DELAY_SECONDS or 0)
|
||||
for i in range(0, len(failed_disbursements), batch_size):
|
||||
batch = failed_disbursements[i:i + batch_size]
|
||||
for loan in batch:
|
||||
logger.info(f"Retrying disbursement for loan ID {loan.id}")
|
||||
loan_data = loan.to_dict()
|
||||
|
||||
data = {
|
||||
"transactionId": loan_data.get('transactionId'),
|
||||
"FbnTransactionId": loan_data.get('transactionId'),
|
||||
"debtId": str(loan_data.get('debtId')),
|
||||
"customerId": loan_data.get('customerId'),
|
||||
"accountId": loan_data.get('accountId'),
|
||||
"productId": str(loan_data.get('productId', "")),
|
||||
"provideAmount": loan_data.get('currentLoanAmount'),
|
||||
}
|
||||
response = SimbrellaClient.disburse_loan(data)
|
||||
logger.info(f"Retry Disbursement Result Received for loan ID {loan.id}: {response}")
|
||||
time_module.sleep(delay_seconds)
|
||||
time_module.sleep(batch_delay)
|
||||
return ResponseHelper.success(message="Retry Failed Disbursements Request Sent Successfully", status_code=200)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retry disbursements: {e}")
|
||||
return ResponseHelper.error("Failed to retry disbursements", status_code=500, error=str(e))
|
||||
|
||||
|
||||
|
||||
|
||||
@autocall_bp.route("/direct/loan", methods=["POST"])
|
||||
def direct_loan():
|
||||
"""
|
||||
This endpoint is for directly calling the disbursement endpoint with a transaction id.
|
||||
This is to be used in cases where the disbursement failed due to network issues
|
||||
or other transient issues. It will call the disbursement endpoint
|
||||
with the loan data for the loan with the given transaction id.
|
||||
"""
|
||||
|
||||
data = request.get_json()
|
||||
logger.info(f"Data received: {data}")
|
||||
|
||||
@@ -231,10 +158,6 @@ def direct_loan():
|
||||
|
||||
@autocall_bp.route("/direct/repayment", methods=["POST"])
|
||||
def direct_repayment():
|
||||
"""
|
||||
This endpoint is for directly calling the collect loan endpoint with a transaction id.
|
||||
"""
|
||||
|
||||
data = request.get_json()
|
||||
logger.info(f"Data received: {data}")
|
||||
|
||||
@@ -316,11 +239,11 @@ def direct_repayment():
|
||||
response = SimbrellaClient.collect_loan_user_initiated(data_to_process)
|
||||
return response
|
||||
|
||||
|
||||
|
||||
|
||||
@autocall_bp.route("/refresh-verify-collection", methods=["GET"])
|
||||
def refresh_verify_collection():
|
||||
"""
|
||||
This endpoint is for directly calling the verify collection endpoint.
|
||||
"""
|
||||
data = request.get_json()
|
||||
logger.info(f"Calling Verify Collection")
|
||||
|
||||
@@ -330,11 +253,6 @@ def refresh_verify_collection():
|
||||
|
||||
@autocall_bp.route("/refresh-collection", methods=["GET"])
|
||||
def refresh_collection():
|
||||
"""
|
||||
This endpoint is for directly calling the collect loan endpoint.
|
||||
It will get the latest repayment with no repay date and call the collect loan endpoint with that repayment data.
|
||||
This is to be used in cases where the collection failed due to network issues or other transient issues. It will call the collection endpoint
|
||||
"""
|
||||
#data = request.get_json()
|
||||
logger.info(f"Calling Collection ")
|
||||
#grab the last repayments with repay date is none
|
||||
@@ -370,11 +288,7 @@ def payment_callback():
|
||||
return response
|
||||
|
||||
@autocall_bp.route("/penal-charge", methods=["POST"])
|
||||
|
||||
def penal_charge():
|
||||
"""
|
||||
This endpoint is for directly calling the penal charge endpoint.
|
||||
"""
|
||||
data = request.get_json()
|
||||
logger.info(f"Calling Penal Charge Endpoint")
|
||||
|
||||
@@ -385,15 +299,9 @@ def penal_charge():
|
||||
logger.error(f"Error in Penal Charge: {e}")
|
||||
return ResponseHelper.error("Penal charge failed")
|
||||
|
||||
|
||||
@autocall_bp.route("/analytic-salary-detect", methods=["POST"])
|
||||
def salary_detect():
|
||||
"""
|
||||
Creates new salary data
|
||||
Gets the salary list not yet processed
|
||||
then, gets the loan list for each salary and
|
||||
creates repayments for each loan with the salary data,
|
||||
then calls the collect loan endpoint for each repayment created.
|
||||
"""
|
||||
payload = request.get_json()
|
||||
logger.info("Calling Salary Detect endpoint")
|
||||
|
||||
@@ -419,11 +327,13 @@ def salary_detect():
|
||||
logger.info("Finished processing List")
|
||||
return ResponseHelper.success([], "AutoCall Add Salary Successful")
|
||||
|
||||
|
||||
@autocall_bp.route("/analytic-salary-process", methods=["POST"])
|
||||
def salary_process():
|
||||
response = process_salary_list()
|
||||
return ResponseHelper.success([], "AutoCall Successful")
|
||||
|
||||
|
||||
def process_salary_list():
|
||||
# Step 1: Get all pending salaries
|
||||
pending_salaries = SalaryService.get_pending_salaries()
|
||||
@@ -517,9 +427,10 @@ def process_salary_list():
|
||||
|
||||
return ResponseHelper.success([], "Processed all pending salaries")
|
||||
|
||||
|
||||
|
||||
@autocall_bp.route("/report", methods=["GET"])
|
||||
def report():
|
||||
"""This endpoint is for generating the report and sending the email."""
|
||||
try:
|
||||
report_data = get_report_data()
|
||||
logger.info(f"Generated report data: {report_data}")
|
||||
@@ -534,12 +445,6 @@ def report():
|
||||
|
||||
@autocall_bp.route("/process-penal-charges", methods=["GET"])
|
||||
def process_penal_charges():
|
||||
"""
|
||||
This endpoint is for processing penal charges for overdue loans schedule with grace period.
|
||||
It will check for overdue loans, calculate the penal charge,
|
||||
create a new penal charge record, update the loan and repayment schedule with the new penal charge, and call the Simbrella endpoint to update the penal charge on their system.
|
||||
"""
|
||||
|
||||
try:
|
||||
OVERDUE_GRACE_PERIOD_DAYS = settings.OVERDUE_GRACE_PERIOD_DAYS
|
||||
OVERDUE_PROCESSING_LIST_LIMIT = settings.OVERDUE_PROCESSING_LIST_LIMIT
|
||||
@@ -654,12 +559,6 @@ def process_penal_charges():
|
||||
|
||||
@autocall_bp.route("/overdue-loans", methods=["GET"])
|
||||
def overdue_loans():
|
||||
"""
|
||||
This endpoint is for processing overdue loans.
|
||||
It will get all active overdue loan schedules,
|
||||
and then for each loan schedule, it will create a repayment, update the loan status, and call the Simbrella endpoint to collect the loan.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Step 1: Get all active overdue loans
|
||||
overdue_loans = LoanRepaymentScheduleService.get_active_overdue_repayment_schedule()
|
||||
@@ -697,6 +596,7 @@ def overdue_loans():
|
||||
logger.exception(f"Error fetching overdue loans: {e}")
|
||||
return ResponseHelper.error("Failed to fetch overdue loans", status_code=500, error=str(e))
|
||||
|
||||
|
||||
def process_overdue_loan(loan):
|
||||
"""
|
||||
Handles repayment creation, loan status update, and Simbrella call
|
||||
|
||||
@@ -60,21 +60,6 @@ class LoanService:
|
||||
|
||||
return Loan.set_disbursement_message(loan_id, description)
|
||||
|
||||
@classmethod
|
||||
def get_failed_disbursements(cls):
|
||||
"""
|
||||
Get all loans with failed disbursement.
|
||||
"""
|
||||
return Loan.get_failed_disbursements()
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
Update the status of the loan with the given loan_id.
|
||||
"""
|
||||
# Retrieve loan
|
||||
return Loan.update_status(loan_id, status)
|
||||
|
||||
|
||||
@classmethod
|
||||
def set_disburse_verify_result(cls, loan_id, result, description):
|
||||
|
||||
@@ -110,12 +110,6 @@ paths:
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/retry-failed-disbursements:
|
||||
get:
|
||||
summary: Retry failed disbursements
|
||||
responses:
|
||||
200:
|
||||
description: A successful response
|
||||
/autocall/refresh-disbursement:
|
||||
get:
|
||||
summary: Refresh the disbursement
|
||||
|
||||
Reference in New Issue
Block a user