You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

310 lines
12 KiB

"""
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"