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