import logging import traceback from authorizenet import apicontractsv1 from authorizenet.apicontrollers import ( deleteCustomerProfileController, deleteCustomerPaymentProfileController ) from authorizenet.constants import constants from config import load_config from sqlalchemy.orm import Session logger = logging.getLogger(__name__) # Load Authorize.net credentials ApplicationConfig = load_config() API_LOGIN_ID = '9U6w96gZmX' TRANSACTION_KEY = '94s6Qy458mMNJr7G' constants.show_url_on_request = True constants.environment = constants.SANDBOX def _get_authnet_error_message(response): """ Robust error parsing function that correctly handles the API's response format. """ if response is None: return "No response from payment gateway." try: if hasattr(response, 'messages') and response.messages is not None: if hasattr(response.messages, 'message'): message_list = response.messages.message if not isinstance(message_list, list): message_list = [message_list] if message_list: msg = message_list[0] code = msg.code if hasattr(msg, 'code') else 'Unknown' text = msg.text if hasattr(msg, 'text') else 'No details provided.' return f"Error {code}: {text}" except Exception as e: logger.error(f"Error while parsing Auth.Net error message: {e}") return "An unparsable error occurred with the payment gateway." return "An unknown error occurred with the payment gateway." def delete_user_account(db: Session, customer_id: int) -> dict: """ Delete the complete Authorize.net account for a user. This removes the customer profile and all associated payment profiles from Authorize.net, then updates the database to null out the ID fields. Args: db: Database session customer_id: ID of the customer to delete account for Returns: Dict with success status, message, and details """ try: # Import crud functions here to avoid circular imports from .. import crud # Get customer from database customer = crud.get_customer(db, customer_id) if not customer: return { "success": False, "message": f"Customer {customer_id} not found", "deleted_profile_id": None } # Check if customer has an Authorize.net profile if not customer.auth_net_profile_id: return { "success": False, "message": "Customer does not have an Authorize.net profile", "deleted_profile_id": None } profile_id_to_delete = customer.auth_net_profile_id # Get customer's payment profiles/cards from database cards = crud.get_customer_cards(db, customer_id) logger.info(f"Starting deletion of Authorize.net account for customer {customer_id} (Profile ID: {profile_id_to_delete})") # Step 1: Delete payment profiles first (must delete these before customer profile) deleted_payment_profiles = [] if cards: logger.info(f"Found {len(cards)} cards to delete from Authorize.net") for card_index, card in enumerate(cards): if card.auth_net_payment_profile_id: try: logger.info(f"Deleting payment profile {card.auth_net_payment_profile_id} for card {card.id}") # Delete payment profile from Authorize.net success = _delete_payment_profile(profile_id_to_delete, card.auth_net_payment_profile_id) if success: deleted_payment_profiles.append(card.auth_net_payment_profile_id) logger.info(f"Successfully deleted payment profile {card.auth_net_payment_profile_id}") else: logger.warning(f"Failed to delete payment profile {card.auth_net_payment_profile_id} - it may not exist or already deleted") except Exception as e: logger.error(f"Error deleting payment profile {card.auth_net_payment_profile_id}: {str(e)}") # Continue with other payment profiles - we want to delete as much as possible # Always null out the payment profile ID in database (even if API delete failed) card.auth_net_payment_profile_id = None db.add(card) # Step 2: Delete customer profile logger.info(f"Deleting customer profile {profile_id_to_delete}") profile_deleted_success = _delete_customer_profile(profile_id_to_delete) # Step 3: Update database regardless of API results customer.auth_net_profile_id = None db.add(customer) # Commit all database changes db.commit() if profile_deleted_success: logger.info(f"Successfully deleted Authorize.net account for customer {customer_id}") return { "success": True, "message": f"Successfully deleted Authorize.net account with profile ID {profile_id_to_delete}", "deleted_profile_id": profile_id_to_delete, "deleted_payment_profiles_count": len(deleted_payment_profiles), "deleted_payment_profiles": deleted_payment_profiles } else: logger.warning(f"Customer profile {profile_id_to_delete} may not have been completely removed from Authorize.net, but database has been updated") return { "success": False, "message": f"Profile {profile_id_to_delete} may not have been completely removed from Authorize.net, but database has been cleaned up", "deleted_profile_id": profile_id_to_delete, "deleted_payment_profiles_count": len(deleted_payment_profiles), "deleted_payment_profiles": deleted_payment_profiles } except Exception as e: logger.error(f"Critical exception during account deletion for customer {customer_id}: {traceback.format_exc()}") db.rollback() return { "success": False, "message": f"An unexpected error occurred: {str(e)}", "deleted_profile_id": None } def _delete_customer_profile(profile_id: str) -> bool: """ Delete customer profile from Authorize.net. Args: profile_id: Authorize.net customer profile ID Returns: bool: True if successfully deleted or profile doesn't exist, False on error """ try: merchant_auth = apicontractsv1.merchantAuthenticationType( name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY ) request = apicontractsv1.deleteCustomerProfileRequest( merchantAuthentication=merchant_auth, customerProfileId=profile_id ) controller = deleteCustomerProfileController(request) controller.execute() response = controller.getresponse() if response is None: logger.warning(f"No response received when trying to delete profile {profile_id}") return False if hasattr(response, 'messages') and response.messages.resultCode == "Ok": logger.info(f"Successfully deleted customer profile {profile_id}") return True else: error_msg = _get_authnet_error_message(response) logger.warning(f"Failed to delete customer profile {profile_id}: {error_msg}") # Still count as success if the profile was already deleted/not found if "not found" in error_msg.lower() or "E00040" in error_msg or "E00035" in error_msg: logger.info(f"Profile {profile_id} was already deleted or doesn't exist") return True return False except Exception as e: logger.error(f"Exception during delete customer profile {profile_id}: {str(e)}") return False def _delete_payment_profile(customer_profile_id: str, payment_profile_id: str) -> bool: """ Delete payment profile from Authorize.net. Args: customer_profile_id: Authorize.net customer profile ID payment_profile_id: Authorize.net payment profile ID to delete Returns: bool: True if successfully deleted or profile doesn't exist, False on error """ try: merchant_auth = apicontractsv1.merchantAuthenticationType( name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY ) request = apicontractsv1.deleteCustomerPaymentProfileRequest( merchantAuthentication=merchant_auth, customerProfileId=customer_profile_id, customerPaymentProfileId=payment_profile_id ) controller = deleteCustomerPaymentProfileController(request) controller.execute() response = controller.getresponse() if response is None: logger.warning(f"No response received when trying to delete payment profile {payment_profile_id}") return False if hasattr(response, 'messages') and response.messages.resultCode == "Ok": logger.info(f"Successfully deleted payment profile {payment_profile_id}") return True else: error_msg = _get_authnet_error_message(response) logger.warning(f"Failed to delete payment profile {payment_profile_id}: {error_msg}") # Still count as success if the payment profile was already deleted/not found if "not found" in error_msg.lower() or "E00040" in error_msg or "E00035" in error_msg: logger.info(f"Payment profile {payment_profile_id} was already deleted or doesn't exist") return True return False except Exception as e: logger.error(f"Exception during delete payment profile {payment_profile_id}: {str(e)}") return False