diff --git a/app/integrations/simbrella.py b/app/integrations/simbrella.py index 19a9c99..54f622b 100644 --- a/app/integrations/simbrella.py +++ b/app/integrations/simbrella.py @@ -182,7 +182,7 @@ class SimbrellaClient: def collect_loan_user_initiated(data): # InitiatedBy = USER_INITIATED logger.info(f"Calling CollectLoan collect_loan_user_initiated ******* endpoint with data: {data}") - return SimbrellaClient._collect_loan(data,1) + return SimbrellaClient._collect_loan(data,"1") @staticmethod def collect_loan_user_salary_detect(data): @@ -198,28 +198,28 @@ class SimbrellaClient: # return ResponseHelper.error(message="Failed to call salary endpoint", # status_code=400, # error=str(e) ) - return SimbrellaClient._collect_loan(data,2) + return SimbrellaClient._collect_loan(data,"2") @staticmethod def collect_loan_user_due_payment(data): # InitiatedBy = REPAYMENT_DUE - return SimbrellaClient._collect_loan(data,3) + return SimbrellaClient._collect_loan(data,"3") @staticmethod - def _collect_loan(data, collectionMethod: int): + def _collect_loan(data, collectionMethod: str): api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}{SimbrellaClient.BANK_CALL_COLLECT_LOAN_ENDPOINT}" logger.info(f"Calling CollectLoan api_url==> : {api_url}") logger.info(f"Calling CollectLoan endpoint with data: {data}") # Check if the repayment exists logger.info(f"Checking if repayment exists") - repayment = RepaymentService.get_repayment_by_transaction_id(transaction_id=data['transactionId']) + repayment = RepaymentService.get_repayment_by_id(id=data['Id']) logger.info(f"Repayment Response From Database ** : {repayment}") if not repayment: - logger.info(f"Repayment with transactionId: {data['transactionId']}, was not found") + logger.info(f"Repayment with id: {data['Id']}, was not found") return ResponseHelper.error("Repayment not found") logger.info(f"Repayment Response From Database ** : {repayment.to_dict()}") repayment_data = repayment.to_dict() @@ -252,7 +252,7 @@ class SimbrellaClient: "customerId": repayment_data['customerId'], "accountId": loan_data['accountId'], "productId": repayment_data['productId'], - "collectAmount": loan_data['repaymentAmount'], + "collectAmount": loan_data['repaymentAmount'] or 0, "penalCharge": 5, "channel": "USSD", "collectionMethod": collectionMethod, @@ -263,7 +263,7 @@ class SimbrellaClient: try: logger.info(f"Here is your CollectLoan Request data ***** : {collect_loan_data}") - response = requests.post(api_url, json=collect_loan_data, headers=get_headers()) + response = requests.post(api_url, json=collect_loan_data,timeout=30, headers=get_headers()) logger.info(f"CollectLoan response: {response.json()}") RepaymentService.set_repay_result(repayment_data['Id'], response.json().get('responseCode', ''), response.json().get('responseMessage', '')) diff --git a/app/models/loan.py b/app/models/loan.py index 6a595ec..d26b5cc 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -218,4 +218,15 @@ class Loan(db.Model): return cls.query.filter( cls.disburse_date.isnot(None), cls.disburse_verify.is_(None) - ).order_by(cls.created_at.desc()).first() \ No newline at end of file + ).order_by(cls.created_at.desc()).first() + + @classmethod + def get_customer_loans(cls, customer_id): + """ + Get customer's active loans by customer_id. + """ + customer_loans = cls.query.filter_by( customer_id = customer_id).all() + if not customer_loans: + raise ValueError(f"Customer with Id {customer_id} does not have any loan.") + logger.info(f"Found {len(customer_loans)} loans for customer ID: {customer_id}") + return customer_loans \ No newline at end of file diff --git a/app/models/repayment.py b/app/models/repayment.py index 7fd9ea2..25fc866 100644 --- a/app/models/repayment.py +++ b/app/models/repayment.py @@ -14,6 +14,8 @@ class Repayment(db.Model): customer_id = db.Column(db.String(50), nullable=False) product_id = db.Column(db.String(20), nullable=True) transaction_id = db.Column(db.String(50), nullable=False) + initiated_by = db.Column(db.String(50), nullable=True) + salary_amount = db.Column(db.Float, nullable=True, 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)) repay_date = db.Column(db.DateTime, nullable=True) @@ -39,15 +41,61 @@ class Repayment(db.Model): 'verifyResult': self.verify_result, 'verifyDescription': self.verify_description, 'transactionId': self.transaction_id, + 'initiatedBy':self.initiated_by, + 'salaryAmount':self.salary_amount, 'repayDate': self.repay_date.isoformat() if self.repay_date else None, 'VerifyDate': self.verify_date.isoformat() if self.verify_date else None, } + @classmethod + def add_repayment(cls, data: dict): + """ + Create and persist a new repayment record. + """ + logger.info(f"Received repayment data: {data}") + try: + new_repayment = cls( + loan_id=data["loanId"], + customer_id=data["customerId"], + product_id=data.get("productId"), + transaction_id=data["transactionId"], + initiated_by=data.get("initiatedBy"), + salary_amount=float(data.get("salaryAmount", 0.0)), + repay_date=( + datetime.strptime(data["repayDate"], "%Y-%m-%d") + .replace(tzinfo=timezone.utc) + if data.get("repayDate") + else None + ), + repay_result=data.get("repayResult"), + repay_description=data.get("repayDescription"), + verify_result=data.get("verifyResult"), + verify_description=data.get("verifyDescription"), + verify_date=( + datetime.strptime(data["verifyDate"], "%Y-%m-%d") + .replace(tzinfo=timezone.utc) + if data.get("verifyDate") + else None + ), + ) + + db.session.add(new_repayment) + db.session.commit() + logger.info("Repayment record committed.") + return new_repayment + + except Exception as e: + db.session.rollback() + logger.error(f"Error adding repayment data: {e}") + raise @classmethod def get_repayment_by_transaction_id(cls, transaction_id): return cls.query.filter_by(transaction_id=transaction_id).first() + @classmethod + def get_repayment_by_id(cls, id): + return cls.query.filter_by(id=id).first() @classmethod def set_repay_date(cls, repayment_id, customer_id): diff --git a/app/models/salary.py b/app/models/salary.py index 52c21b9..d11315b 100644 --- a/app/models/salary.py +++ b/app/models/salary.py @@ -45,7 +45,7 @@ class Salary(db.Model): try: new_data = cls( customer_id=data.get('customerId'), - amount=data.get('salaryAmount', 0.0), + amount=data.get('amount', 0.0), status='START', salary_date = datetime.strptime(data.get('salaryDate'), "%Y-%m-%d").date() if data.get('salaryDate') else None, account_id=data.get('accountId') @@ -69,3 +69,30 @@ class Salary(db.Model): except Exception as e: logger.error(f"Error fetching pending salaries: {str(e)}") return [] + @classmethod + def update_status(cls, salary_id, status): + """ + Update the status of the salary record with the given salary_id. + """ + try: + # Retrieve salary record + salary = cls.query.get(salary_id) + + if not salary: + raise ValueError(f"Salary with ID {salary_id} does not exist.") + + if salary.status == status: + return salary.to_dict() # Still return the current state if no change + + # Update status and timestamp + salary.status = status + salary.updated_at = datetime.now(timezone.utc) # Manually update timestamp if not auto-updating + db.session.commit() + + logger.info("Salary status updated and committed.") + return salary.to_dict() + + except Exception as e: + db.session.rollback() + logger.error(f"Error updating salary status: {e}") + raise Exception(f"Error updating salary status: {str(e)}") diff --git a/app/routes/autocall.py b/app/routes/autocall.py index c2aff66..b0c551f 100644 --- a/app/routes/autocall.py +++ b/app/routes/autocall.py @@ -75,7 +75,7 @@ def refresh_collection(): repayment = RepaymentService.get_latest_repayment_without_repay_date() #repayment = RepaymentService.get_latest_repayment_with_loanId(13735) if not repayment: - logger.info(f"No repayment found without disbursement date") + logger.info(f"No repayment found without repay date") return 0 logger.info(f"Calling repay loan endpoint with data: {repayment}") repayment_data = repayment.to_dict() @@ -86,6 +86,7 @@ def refresh_collection(): "debtId": repayment_data['loanId'], "customerId": repayment_data['customerId'], "productId": repayment_data['productId'], + "Id":repayment_data['Id'] } logger.info(f"Data being sent to Simbrella: {data}") logger.info(f"calling simbrella") @@ -105,61 +106,85 @@ def payment_callback(): @autocall_bp.route("/penal-charge", methods=["POST"]) def penal_charge(): data = request.get_json() - logger.info(f"Calling Penal Charge Endpoints") + logger.info(f"Calling Penal Charge Endpoint") - response = SimbrellaClient.penal_charge(data[0]) + try: + response = SimbrellaClient.penal_charge(data[0]) + return response + except Exception as e: + logger.error(f"Error in Penal Charge: {e}") + return ResponseHelper.error("Penal charge failed") - return response @autocall_bp.route("/analytic-salary-detect", methods=["POST"]) def salary_detect(): - #*************************************************** - # PART 1 Accept any new import of salary detection - #************************************************** - data = request.get_json() - logger.info(f"Calling Salary Detect Endpoints") + payload = request.get_json() + logger.info("Calling Salary Detect endpoint") + # Step 1: Try to add new salary data try: - salary = SalaryService.add_salary_data(data) - if salary: - logger.info(f"Successful Salary Added") - # return ResponseHelper.success(salary.to_dict(), "Successful") + new_salary = SalaryService.add_salary_data(payload) + if new_salary: + logger.info(f"Salary added: {new_salary.id}") except Exception as e: - logger.info(f"Failed to save salary: {e}") - - # *************************************************** - # PART 2 SELECT * FROM salaries WHERE status IS 'START' ORDER BY id ASC - # ************************************************** + logger.error(f"Failed to save salary: {e}") + # Step 2: Get all pending salaries pending_salaries = SalaryService.get_pending_salaries() - if not pending_salaries: - logger.info(f"No pending salaries found") - # return ResponseHelper.success([], "No pending salaries found") + logger.info("No pending salaries found") + return ResponseHelper.success([], "No pending salaries") - logger.info(f"Found {len(pending_salaries)} pending salaries") + logger.info(f"Found {len(pending_salaries)} pending salaries to process") - #in the loop - # USE the customerID to find thu user Loan - # if loan is/are found for the user - # INSERT INTO repayments TABLE - # repayment = cls( - # customer_id=customer_id, - # loan_id=loan.id, - # product_id=loan.product_id, - # transaction_id = transaction_id, - # created_at=datetime.now(timezone.utc), - # updated_at=datetime.now(timezone.utc), - # initiated_by='SALARY_DETECT' - # ) + # Step 3: Process each salary + for pending_salary in pending_salaries: + logger.info(f"Processing salary ID: {pending_salary.id}") - # by the time you call this you must be on repayment table - try: - SimbrellaClient.collect_loan_user_salary_detect(data) - except Exception as e: - logger.info(f"Failed to call collect_loan_user_salary_detect: {e}") + # Step 3.1: Update status to PROCESSING + try: + SalaryService.update_status(pending_salary.id, "PROCESSING") + except Exception as e: + logger.warning(f"Could not update status for salary ID {pending_salary.id}: {e}") + continue + + # Step 3.2: Get loans + try: + loans = LoanService.get_customer_loans(pending_salary.customer_id) + if not loans: + logger.warning(f"No loans found for customer ID: {pending_salary.customer_id}") + continue + except Exception as e: + logger.error(f"Error fetching loans for customer ID {pending_salary.customer_id}: {e}") + continue + + # Step 3.3: Create repayments + for loan in loans: + try: + loan_dict = loan.to_dict() + repayment_data = { + "customerId": pending_salary.customer_id, + "loanId": loan_dict["debtId"], + "productId": loan_dict["productId"], + "transactionId": loan_dict["transactionId"], + "initiatedBy": "SALARY_DETECT", + "salaryAmount": pending_salary.amount, + } + logger.info(f"Creating repayment for loan ID {loan_dict['debtId']}") + repayment = RepaymentService.add_repayment(repayment_data) + logger.info(f"Created repayment ID: {repayment.id}") + except Exception as e: + logger.error(f"Error creating repayment for loan ID {loan.id}: {e}") + continue + # Step 4: Simbrella integration call after all processing + try: + SimbrellaClient.collect_loan_user_salary_detect(repayment.to_dict()) + except Exception as e: + logger.error(f"Failed to call Simbrella client: {e}") + + + + logger.info(f"Finished processing salary ID: {pending_salary.id}") + return ResponseHelper.success([], "AutoCall Successful") - return_data=[] - - return ResponseHelper.success(return_data, "AutoCall Successful") diff --git a/app/services/loan.py b/app/services/loan.py index 6d1379c..f1d9910 100644 --- a/app/services/loan.py +++ b/app/services/loan.py @@ -70,3 +70,11 @@ class LoanService: Get the latest loan without a disbursement date. """ return Loan.get_latest_loan_with_disburse_date() + + @classmethod + def get_customer_loans(cls, customer_id): + """ + Get customer's active loans by customer_id. + """ + + return Loan.get_customer_loans(customer_id=customer_id) diff --git a/app/services/repayment.py b/app/services/repayment.py index 3784a98..e212ff2 100644 --- a/app/services/repayment.py +++ b/app/services/repayment.py @@ -8,6 +8,12 @@ class RepaymentService: Get the repayment by transaction ID """ return Repayment.get_repayment_by_transaction_id(transaction_id) + @staticmethod + def get_repayment_by_id(id): + """ + Get the repayment by ID + """ + return Repayment.get_repayment_by_id(id) @classmethod def set_repay_date(cls, repayment_id, customer_id): @@ -55,4 +61,10 @@ class RepaymentService: """ Get the latest repayment with a repay date and no verification date. """ - return Repayment.get_latest_loan_with_repay_date() \ No newline at end of file + return Repayment.get_latest_loan_with_repay_date() + @classmethod + def add_repayment(cls, data): + """ + Add a new repayment entry. + """ + return Repayment.add_repayment(data) \ No newline at end of file diff --git a/app/services/salary.py b/app/services/salary.py index c0cbbf3..f85e9af 100644 --- a/app/services/salary.py +++ b/app/services/salary.py @@ -15,4 +15,10 @@ class SalaryService: """ Get the pending salary for a given customer. """ - return Salary.get_pending_salaries() \ No newline at end of file + return Salary.get_pending_salaries() + @classmethod + def update_status(cls, salary_id, status): + """ + Update the status of the salary with the given salary_id. + """ + return Salary.update_status(salary_id, status) \ No newline at end of file