diff --git a/app/models/loan.py b/app/models/loan.py index 6a595ec..845e9c2 100644 --- a/app/models/loan.py +++ b/app/models/loan.py @@ -218,4 +218,14 @@ 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.") + return customer_loans \ No newline at end of file diff --git a/app/models/repayment.py b/app/models/repayment.py index 7fd9ea2..8997b8e 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,12 +41,55 @@ 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() 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..38c2a63 100644 --- a/app/routes/autocall.py +++ b/app/routes/autocall.py @@ -113,53 +113,74 @@ def penal_charge(): @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(f"Calling Salary Detect endpoint") + # Attempt to add the incoming 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.info(f"Failed to save salary: ", e) + # Fetch 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") + return ResponseHelper.success([], "No pending salaries") - logger.info(f"Found {len(pending_salaries)} pending salaries") + logger.info(f"Found pending salaries {len(pending_salaries)}" ) - #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' - # ) + 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 + # Step 1: Update status to PROCESSING early to avoid race condition + try: + SalaryService.update_status(pending_salary.id, "PROCESSING") + except Exception as e: + logger.info(f"Failed to update status to PROCESSING for salary ID : {pending_salary.id} {e}" ) + continue + + # Step 2: Fetch loans for this salary's customer + 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.info(f"Error fetching loans for customer ID : {pending_salary.customer_id} {e}") + continue + + # Step 3: Loop through loans and create repayment entries + 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.info(f"Error creating repayment for loan ID : {loan.id} {e}" ) + continue + + # Step 4: Optionally update salary to DONE after all loan processing + try: + SalaryService.update_status(pending_salary.id, "DONE") + except Exception as e: + logger.info(f"Failed to mark salary ID as DONE: {pending_salary.id} {e}") + + # Step 5: Call Simbrella integration after processing all salaries try: - SimbrellaClient.collect_loan_user_salary_detect(data) + SimbrellaClient.collect_loan_user_salary_detect(payload) except Exception as e: - logger.info(f"Failed to call collect_loan_user_salary_detect: {e}") + logger.info(f"Failed to call Simbrella client: {e}") - - return_data=[] - - return ResponseHelper.success(return_data, "AutoCall Successful") + return ResponseHelper.success([], "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..6e52215 100644 --- a/app/services/repayment.py +++ b/app/services/repayment.py @@ -55,4 +55,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