## File: your_app/services/payment_service.py import logging import traceback import re from authorizenet import apicontractsv1 logger = logging.getLogger(__name__) from authorizenet.apicontrollers import ( createTransactionController, createCustomerProfileController, createCustomerPaymentProfileController, getCustomerProfileController ) from authorizenet.constants import constants from .. import schemas from config import load_config # Assuming you have this # Load Authorize.net credentials ApplicationConfig = load_config() # Set Authorize.net environment based on configuration # Set Authorize.net environment based on configuration if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': constants.environment = constants.PRODUCTION VALIDATION_MODE = "liveMode" API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': constants.environment = constants.PRODUCTION VALIDATION_MODE = "liveMode" API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY else: constants.environment = constants.SANDBOX constants.show_url_on_request = True VALIDATION_MODE = "testMode" API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY def _is_e00121_response(response): """ Check if the Authorize.Net response contains E00121 error (invalid payment profile ID). """ if response is None: return False try: if hasattr(response, 'messages') and response.messages is not None: # Check for E00121 in different response message structures if hasattr(response.messages, 'message'): message = response.messages.message # Handle list of messages if isinstance(message, list): for msg in message: if getattr(msg, 'code', '') == 'E00121': logger.debug("E00121 detected in message list") return True # Handle single message elif hasattr(message, 'code'): if message.code == 'E00121': logger.debug(f"E00121 detected: '{getattr(message, 'text', 'No details')}'") return True else: logger.debug(f"Message code: '{message.code}' (not E00121)") return False except Exception as e: logger.debug(f"Error checking for E00121: {str(e)}") return False 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, 'transactionResponse') and response.transactionResponse is not None and hasattr(response.transactionResponse, 'errors') and response.transactionResponse.errors: error = response.transactionResponse.errors[0] return f"Error {error.errorCode}: {error.errorText}" 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.debug(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 create_customer_profile(customer: schemas.Customer, card_info: schemas.CardCreate): """ Creates a new customer profile in Authorize.Net (payment profiles added separately). This version sanitizes and trims customer data before sending. """ # Note: Never log API credentials try: merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) except Exception as e: pass # Will be handled by request failure below # API max lengths: email=255 email = (customer.customer_email or f"no-email-{customer.id}@example.com")[:255] customerProfile = apicontractsv1.customerProfileType( merchantCustomerId=str(customer.id), email=email # No paymentProfiles - will be added separately ) request = apicontractsv1.createCustomerProfileRequest( merchantAuthentication=merchantAuth, profile=customerProfile ) controller = createCustomerProfileController(request) if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': controller.setenvironment(constants.PRODUCTION) controller.execute() elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': controller.setenvironment(constants.PRODUCTION) controller.execute() else: controller.execute() response = controller.getresponse() # Check if response is None (API call failed) if response is None: logger.debug("ERROR: Authorize.net API call returned None - likely a network/connectivity issue") raise ValueError("Could not connect to the payment gateway. Please check network connectivity.") try: if response.messages.resultCode == "Ok": profile_id = response.customerProfileId logger.debug(profile_id) # # Payment profile ID is not available since profiles are added separately # payment_id = "" logger.debug(f"SUCCESS: Created customer profile: {profile_id} (payment profiles added separately)") # Add detailed logging logger.debug(f"API Response - Profile ID: {profile_id}") logger.debug(f"Returning: profile_id='{str(profile_id)}', payment_id=''") return str(profile_id), "" else: error_msg = _get_authnet_error_message(response) logger.debug(f"Failed to create customer profile (API Error): {error_msg}") raise ValueError(error_msg) except ValueError: # Re-raise specific ValueError messages we already set above (like E00039) raise except Exception as e: logger.debug(f"A critical exception occurred during the API call: {traceback.format_exc()}") raise ValueError("Could not connect to the payment gateway.") def authorize_customer_profile(customer_profile_id: str, payment_profile_id: str, transaction_req: schemas.TransactionAuthorizeByCardID, db_session=None, customer_id=None, card_id=None): """ Creates an AUTH_ONLY transaction against a customer profile with automatic E00121 recovery. This holds funds but doesn't capture them, and automatically recovers from invalid payment profiles. """ logger.debug(f"Authorizing profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.preauthorize_amount}") # Validate inputs if not customer_profile_id or customer_profile_id.strip() == "": logger.debug("INVALID: customer_profile_id is None or empty") if not payment_profile_id or payment_profile_id.strip() == "": logger.debug("INVALID: payment_profile_id is None or empty") logger.debug("Payment profile ID must be a valid, non-empty string") # FIRST ATTEMPT - Normal authorization logger.debug("TRANSACTION ATTEMPT 1: Standard authorization") response = _perform_authorization(customer_profile_id, payment_profile_id, transaction_req) # CHECK FOR E00121 ERROR - "invalid payment profile ID" if db_session and customer_id and card_id and _is_e00121_response(response): logger.debug(f"E00121 DETECTED! Invalid payment profile {payment_profile_id}") logger.debug("POOOP") logger.debug(f"AUTO-RECOVERING: Starting payment profile refresh for customer {customer_id}") try: # GET CUSTOMER PROFILE ID (since we have customer_id but need profile_id) from .. import crud customer = crud.get_customer(db_session, customer_id) if customer: # REFRESH ALL PAYMENT PROFILES FOR THIS CUSTOMER logger.debug(f"CALLING REFRESH: customer_id={customer_id}, profile_id={customer.auth_net_profile_id}") from .user_create import refresh_customer_payment_profiles refresh_customer_payment_profiles(db_session, customer_id, customer.auth_net_profile_id) # GET THE UPDATED CARD WITH NEW PAYMENT PROFILE ID updated_card = crud.get_card_by_id(db_session, card_id) if updated_card and updated_card.auth_net_payment_profile_id != payment_profile_id: new_payment_profile_id = updated_card.auth_net_payment_profile_id logger.debug(f"RECOVERY SUCCESS: Old ID '{payment_profile_id}' → New ID '{new_payment_profile_id}'") # SECOND ATTEMPT - With refreshed payment profile ID logger.debug("TRANSACTION ATTEMPT 2: Retry with refreshed payment profile") response = _perform_authorization(customer_profile_id, new_payment_profile_id, transaction_req) if _is_e00121_response(response): logger.debug("E00121 STILL PERSISTS after refresh - manual intervention may be needed") logger.debug(f"Payment profile {new_payment_profile_id} also rejected by Authorize.Net") else: logger.debug(f"SUCCESS! E00121 RESOLVED - Transaction succeeded with refreshed payment profile {new_payment_profile_id}") else: logger.debug(f"RECOVERY FAILED: No updated payment profile ID found for card {card_id}") logger.debug("Database refresh did not provide new payment profile ID") else: logger.debug(f"RECOVERY FAILED: Customer {customer_id} not found in database") except Exception as e: logger.debug(f"AUTO-RECOVERY FAILED: {str(e)}") logger.debug("Exception during payment profile refresh process") return response def _perform_authorization(customer_profile_id: str, payment_profile_id: str, transaction_req: schemas.TransactionAuthorizeByCardID): """ Perform the actual Authorize.Net authorization call. """ merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) profile_to_authorize = apicontractsv1.customerProfilePaymentType() profile_to_authorize.customerProfileId = customer_profile_id profile_to_authorize.customerPaymentProfileId = payment_profile_id transactionRequest = apicontractsv1.transactionRequestType( transactionType="authOnlyTransaction", amount=f"{transaction_req.preauthorize_amount:.2f}", profile=profile_to_authorize ) if transaction_req.tax_amount and transaction_req.tax_amount > 0: transactionRequest.tax = apicontractsv1.extendedAmountType(amount=f"{transaction_req.tax_amount:.2f}", name="Sales Tax") createtransactionrequest = apicontractsv1.createTransactionRequest( merchantAuthentication=merchantAuth, transactionRequest=transactionRequest ) controller = createTransactionController(createtransactionrequest) if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': controller.setenvironment(constants.PRODUCTION) controller.execute() elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': controller.setenvironment(constants.PRODUCTION) controller.execute() else: controller.execute() response = controller.getresponse() # Log response details if response is not None and hasattr(response, 'messages'): result_code = getattr(response.messages, 'resultCode', 'Unknown') logger.debug(f"✅ Authorize response: resultCode='{result_code}'") else: logger.debug("✅ Authorize response: No standard response structure") return response def capture_authorized_transaction(transaction_req: schemas.TransactionCapture): """Captures a previously authorized transaction.""" logger.debug(f"Capturing transaction {transaction_req.auth_net_transaction_id} for {transaction_req.charge_amount}") merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) transactionRequest = apicontractsv1.transactionRequestType( transactionType="priorAuthCaptureTransaction", amount=f"{transaction_req.charge_amount:.2f}", refTransId=transaction_req.auth_net_transaction_id ) createtransactionrequest = apicontractsv1.createTransactionRequest( merchantAuthentication=merchantAuth, transactionRequest=transactionRequest ) controller = createTransactionController(createtransactionrequest) if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': controller.setenvironment(constants.PRODUCTION) controller.execute() elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': controller.setenvironment(constants.PRODUCTION) controller.execute() else: controller.execute() return controller.getresponse() def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.Customer, card_info: schemas.CardCreate, is_default: bool = False): logger.debug(f"Adding {'default ' if is_default else ''}payment profile to Auth.Net customer profile ID: {customer_profile_id}") merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) def sanitize(text, max_len, allow_spaces=False, is_zip=False): if not text: return "" if is_zip: pattern = r'[^a-zA-Z0-9-]' else: pattern = r'[^a-zA-Z0-9]' if not allow_spaces else r'[^a-zA-Z0-9\s]' sanitized = re.sub(pattern, '', str(text)) return sanitized.strip()[:max_len] first_name = sanitize(customer.customer_first_name, 50) or "N/A" last_name = sanitize(customer.customer_last_name, 50) or "N/A" address = sanitize(customer.customer_address, 60, allow_spaces=True) or "N/A" city = sanitize(customer.customer_town, 40) or "N/A" # ========= CHANGE 1.B: ADD STATE HERE ========= state = sanitize(customer.customer_state, 40) or "MA" # Defaulting to MA for safety zip_code = sanitize(customer.customer_zip, 20, is_zip=True) # Fix expiration date format for cards try: expiration_year = int(card_info.expiration_date.split('-')[0]) expiration_month = int(card_info.expiration_date.split('-')[1]) expiration_date = f"{expiration_month:02d}{expiration_year % 100:02d}" except (ValueError, IndexError): sanitized_exp = card_info.expiration_date.replace('/', '').replace('-', '') if len(sanitized_exp) == 4: expiration_date = sanitized_exp else: expiration_date = "0325" # Expiration date parsed successfully creditCard = apicontractsv1.creditCardType( cardNumber=card_info.card_number, expirationDate=expiration_date, cardCode=card_info.cvv ) billTo = apicontractsv1.customerAddressType( firstName=first_name, lastName=last_name, address=address, city=city, state=state, zip=zip_code, country="USA" ) paymentProfile = apicontractsv1.customerPaymentProfileType( billTo=billTo, payment=apicontractsv1.paymentType(creditCard=creditCard), defaultPaymentProfile=is_default ) request = apicontractsv1.createCustomerPaymentProfileRequest( merchantAuthentication=merchantAuth, customerProfileId=customer_profile_id, paymentProfile=paymentProfile, # ========= CHANGE 2.B: USE liveMode ========= validationMode=VALIDATION_MODE ) controller = createCustomerPaymentProfileController(request) try: if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': controller.setenvironment(constants.PRODUCTION) controller.execute() elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': controller.setenvironment(constants.PRODUCTION) controller.execute() else: controller.execute() response = controller.getresponse() # Check if response is None (API call failed) if response is None: logger.debug("ERROR: Authorize.net API call returned None - likely a network/connectivity issue") raise ValueError("Could not connect to the payment gateway. Please check network connectivity.") if response.messages.resultCode == "Ok": # Fix: Proper payment profile ID extraction (same bug fix as above) if hasattr(response, 'customerPaymentProfileId') and response.customerPaymentProfileId: return str(response.customerPaymentProfileId) else: logger.debug("WARNING: Added payment profile but no ID returned") raise ValueError("Payment profile created but ID not found in response") else: error_msg = _get_authnet_error_message(response) logger.debug(f"Failed to add payment profile: {error_msg}") raise ValueError(error_msg) except Exception as e: logger.debug(f"A critical exception occurred during the API call: {traceback.format_exc()}") raise ValueError("Could not connect to the payment gateway.") def get_customer_payment_profiles(customer_profile_id: str): """ Retrieves all payment profile IDs for a given customer profile from Authorize.net. Returns a list of payment profile IDs in the order they were created. """ logger.debug(f"Retrieving payment profiles for customer profile ID: {customer_profile_id}") merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) # Create request to get customer profile request = apicontractsv1.getCustomerProfileRequest( merchantAuthentication=merchantAuth, customerProfileId=customer_profile_id ) controller = getCustomerProfileController(request) try: if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': controller.setenvironment(constants.PRODUCTION) controller.execute() elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': controller.setenvironment(constants.PRODUCTION) controller.execute() else: controller.execute() response = controller.getresponse() # Check if response is None (API call failed) if response is None: logger.debug("ERROR: Authorize.net API call returned None - likely a network/connectivity issue") raise ValueError("Could not connect to the payment gateway. Please check network connectivity.") if response.messages.resultCode == "Ok": payment_profile_ids = [] if response.profile.paymentProfiles is not None: for profile in response.profile.paymentProfiles: payment_profile_ids.append(str(profile.customerPaymentProfileId)) logger.debug(f"Retrieved {len(payment_profile_ids)} payment profile IDs for profile {customer_profile_id}") return payment_profile_ids else: error_msg = _get_authnet_error_message(response) logger.debug(f"Failed to retrieve customer profile {customer_profile_id}: {error_msg}") raise ValueError(f"Could not retrieve customer profile: {error_msg}") except Exception as e: logger.debug(f"Critical exception while retrieving customer profile {customer_profile_id}: {traceback.format_exc()}") raise ValueError("Could not connect to the payment gateway.") def charge_customer_profile(customer_profile_id: str, payment_profile_id: str, transaction_req: schemas.TransactionCreateByCardID): """ Creates an AUTH_CAPTURE transaction (charge now) against a customer profile. This charges the customer immediately for the full amount. """ logger.debug(f"Charging profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.charge_amount}") merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) profile_to_charge = apicontractsv1.customerProfilePaymentType() profile_to_charge.customerProfileId = customer_profile_id profile_to_charge.customerPaymentProfileId = payment_profile_id transactionRequest = apicontractsv1.transactionRequestType( transactionType="authCaptureTransaction", amount=f"{transaction_req.charge_amount:.2f}", profile=profile_to_charge ) createtransactionrequest = apicontractsv1.createTransactionRequest( merchantAuthentication=merchantAuth, transactionRequest=transactionRequest ) controller = createTransactionController(createtransactionrequest) if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': controller.setenvironment(constants.PRODUCTION) controller.execute() elif ApplicationConfig.CURRENT_SETTINGS == 'LOCAL': controller.setenvironment(constants.PRODUCTION) controller.execute() else: controller.execute() # The response is returned directly to the router to be parsed there return controller.getresponse()