""" Follow-Up Processor Module This module handles follow-up processing for payment intents and refunds. """ import json import logging from datetime import datetime from typing import Dict, Any from .base_processor import BasePaymentProcessor logger = logging.getLogger(__name__) class FollowUpProcessor(BasePaymentProcessor): """ Processor for payment intent and refund follow-up modes. This class handles checking the status of pending payment intents and refunds, updating the database accordingly. """ def process(self) -> Dict[str, Any]: """ Process follow-up (payment intents by default). This method is required by BasePaymentProcessor abstract class. Use process_payment_intents() or process_refunds() for specific modes. Returns: Dictionary with processing results """ return self.process_payment_intents() def process_payment_intents(self) -> Dict[str, Any]: """ Process follow-up for pending payment intents. Returns: Dictionary with processing results """ start_time = datetime.now() self.log_processing_start("Payment Intent Follow-up") # Get pending payment intents pending_intents = self.payment_repo.get_pending_payment_intents(payment_type="both") total_pending = sum(len(v) for v in pending_intents.values()) if total_pending == 0: self.logger.info("No payment intents requiring follow-up") return { 'success': True, 'total_pending': 0, 'succeeded': 0, 'failed': 0, 'still_pending': 0, 'duration': (datetime.now() - start_time).total_seconds() } # Process payment intents succeeded_count = 0 failed_count = 0 still_pending = 0 for payment_type, payments in pending_intents.items(): self.logger.info(f"Processing {len(payments)} {payment_type} payment intents") for payment in payments: result = self._check_payment_intent(payment, payment_type) if result == "succeeded": succeeded_count += 1 elif result == "failed": failed_count += 1 elif result == "pending": still_pending += 1 # Calculate duration and log completion duration = (datetime.now() - start_time).total_seconds() self.log_processing_complete( "Payment Intent Follow-up", succeeded_count, failed_count, duration, {'still_pending': still_pending, 'total_checked': total_pending} ) # Log using services try: from services import log_payment_intent_followup log_payment_intent_followup(total_pending, succeeded_count, failed_count, still_pending) except Exception as e: self.logger.warning(f"Failed to log payment intent follow-up: {e}") return { 'success': True, 'total_pending': total_pending, 'succeeded': succeeded_count, 'failed': failed_count, 'still_pending': still_pending, 'duration': duration } def process_refunds(self) -> Dict[str, Any]: """ Process follow-up for pending refunds. Returns: Dictionary with processing results """ start_time = datetime.now() self.log_processing_start("Refund Follow-up") # Get pending refunds pending_refunds = self.payment_repo.get_pending_refunds(payment_type="both") total_pending = sum(len(v) for v in pending_refunds.values()) if total_pending == 0: self.logger.info("No refunds requiring follow-up") return { 'success': True, 'total_pending': 0, 'completed': 0, 'failed': 0, 'still_pending': 0, 'duration': (datetime.now() - start_time).total_seconds() } # Process refunds completed_count = 0 failed_count = 0 still_pending = 0 for payment_type, refunds in pending_refunds.items(): self.logger.info(f"Processing {len(refunds)} {payment_type} refunds") for refund in refunds: result = self._check_refund(refund, payment_type) if result == "succeeded": completed_count += 1 elif result == "failed": failed_count += 1 elif result == "pending": still_pending += 1 # Calculate duration and log completion duration = (datetime.now() - start_time).total_seconds() self.log_processing_complete( "Refund Follow-up", completed_count, failed_count, duration, {'still_pending': still_pending, 'total_checked': total_pending} ) # Log using services try: from services import log_activity log_activity( user_id=1, # System user action="refund_followup", entity_type="script", entity_id=None, details=f"Processed {total_pending} pending refunds: {completed_count} completed, {failed_count} failed, {still_pending} still pending" ) except Exception as e: self.logger.warning(f"Failed to log refund follow-up activity: {e}") return { 'success': True, 'total_pending': total_pending, 'completed': completed_count, 'failed': failed_count, 'still_pending': still_pending, 'duration': duration } def _check_payment_intent(self, payment, payment_type: str) -> str: """ Check the status of a payment intent and update the database. Args: payment: Payment record payment_type: "pay" or "singlepay" Returns: Status string: "succeeded", "failed", or "pending" """ try: intent_result = self.stripe_processor.check_payment_intent(payment.Payment_Intent) self.logger.debug(f"Payment intent {payment.Payment_Intent}: {intent_result['status']}") if intent_result['status'] == "succeeded": payment.PI_FollowUp_JSON = json.dumps(intent_result) payment.PI_FollowUp = False payment.PI_Last_Check = datetime.now() payment.Success = True # Process payment result to update Splynx self.handle_payment_result(payment.id, intent_result, payment_type) self.payment_repo.commit() self.logger.info(f"SUCCESS: Payment intent {payment.Payment_Intent} succeeded") return "succeeded" elif intent_result['status'] == "failed": payment.PI_FollowUp_JSON = json.dumps(intent_result) payment.PI_FollowUp = False payment.PI_Last_Check = datetime.now() # Process payment result to update Splynx self.handle_payment_result(payment.id, intent_result, payment_type) self.payment_repo.commit() self.logger.warning(f"ERROR: Payment intent {payment.Payment_Intent} failed") return "failed" else: # Still pending or requires action payment.PI_FollowUp_JSON = json.dumps(intent_result) payment.PI_Last_Check = datetime.now() if intent_result.get('failure_reason'): # Has a failure reason, mark as failed self.handle_payment_result(payment.id, intent_result, payment_type) payment.PI_FollowUp = False payment.Error = json.dumps(intent_result) self.payment_repo.commit() self.logger.warning(f"ERROR: Payment intent {payment.Payment_Intent} failed with reason") return "failed" else: # Still pending self.payment_repo.commit() self.logger.info(f"PENDING: Payment intent {payment.Payment_Intent} still pending") return "pending" except Exception as e: self.logger.error(f"Error checking payment intent {payment.Payment_Intent}: {e}") return "failed" def _check_refund(self, refund_record, payment_type: str) -> str: """ Check the status of a refund and update the database. Args: refund_record: Payment record with refund payment_type: "pay" or "singlepay" Returns: Status string: "succeeded", "failed", or "pending" """ try: if not refund_record.Stripe_Refund_ID: self.logger.error(f"No Stripe refund ID found for {payment_type} record {refund_record.id}") return "failed" refund_result = self.stripe_processor.check_refund_status(refund_record.Stripe_Refund_ID) self.logger.debug(f"Refund {refund_record.Stripe_Refund_ID}: {refund_result.get('status')}") # Check if the API call was successful if not refund_result.get('success', False): self.logger.error(f"Failed to check refund status: {refund_result.get('error', 'Unknown error')}") return "failed" if refund_result['status'] == "succeeded": # Refund completed successfully refund_record.Refund = True refund_record.Refund_FollowUp = False refund_record.Refund_JSON = json.dumps(refund_result) # Delete associated Splynx payment record if in live mode if self.process_live and refund_record.Payment_Intent: delete_result = self.splynx_repo.delete_payment( customer_id=refund_record.Splynx_ID, payment_intent_id=refund_record.Payment_Intent ) if delete_result.get('success'): self.logger.info(f"Deleted Splynx payment for refund completion: customer {refund_record.Splynx_ID}") else: self.logger.warning(f"Failed to delete Splynx payment: {delete_result.get('error')}") self.payment_repo.commit() self.logger.info(f"SUCCESS: Refund completed: {refund_record.Stripe_Refund_ID}") return "succeeded" elif refund_result['status'] in ["failed", "canceled"]: # Refund failed refund_record.Refund_FollowUp = False refund_record.Refund_JSON = json.dumps(refund_result) self.payment_repo.commit() self.logger.warning(f"ERROR: Refund failed: {refund_record.Stripe_Refund_ID} - {refund_result['status']}") return "failed" elif refund_result['status'] == "pending": # Still pending refund_record.Refund_JSON = json.dumps(refund_result) self.payment_repo.commit() self.logger.info(f"PENDING: Refund still pending: {refund_record.Stripe_Refund_ID}") return "pending" else: # Unknown status refund_record.Refund_JSON = json.dumps(refund_result) self.payment_repo.commit() self.logger.warning(f"WARNING: Unknown refund status: {refund_record.Stripe_Refund_ID} - {refund_result['status']}") return "pending" except Exception as e: self.logger.error(f"Error processing refund {refund_record.Stripe_Refund_ID}: {e}") return "failed"