## File: your_app/services/payment_service.py import pprint import traceback import re from authorizenet import apicontractsv1 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 if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': constants.environment = constants.PRODUCTION print("Payment Service Production") VALIDATION_MODE = "liveMode" API_LOGIN_ID = '4d2Mn6H23R' TRANSACTION_KEY = '7B94d8xfTQXv37WS' else: constants.environment = constants.SANDBOX VALIDATION_MODE = "testMode" print("Payment Service Sandbox") API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY constants.show_url_on_request = True # Very useful for debugging 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 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: print(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. """ print(f"Attempting to create Auth.Net profile for customer ID: {customer.id}") print("dogpoop") print(ApplicationConfig.API_LOGIN_ID) print(ApplicationConfig.TRANSACTION_KEY) try: merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) except Exception as e: print(e) print("here1234") # --- DATA SANITIZATION LOGIC --- 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] # 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) try: controller.setenvironment(constants.PRODUCTION) controller.execute() response = controller.getresponse() print("************") print(response) print(response.messages) print(response.messages.resultCode) print("************") except Exception as e: print("^^^") print(response) print(response.messages) print(response.messages.resultCode) print("^^^") print(f"API execution failed: {traceback.format_exc()}") raise ValueError("API call execution error") try: if response.messages.resultCode == "Ok": profile_id = response.customerProfileId print(profile_id) # Payment profile ID is not available since profiles are added separately payment_id = "" print(f"SUCCESS: Created customer profile: {profile_id} (payment profiles added separately)") # Add detailed logging print(f"API Response - Profile ID: {profile_id}") print(f"Returning: profile_id='{str(profile_id)}', payment_id=''") return str(profile_id), "" else: error_msg = _get_authnet_error_message(response) print(f"Failed to create customer profile (API Error): {error_msg}") print(f"Full API Response: {pprint.pformat(vars(response))}") raise ValueError(error_msg) except ValueError: # Re-raise specific ValueError messages we already set above (like E00039) raise except Exception as e: print(f"A critical exception occurred during the API call: {traceback.format_exc()}") raise ValueError("Could not connect to the payment gateway.") def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.Customer, card_info: schemas.CardCreate, is_default: bool = False): print(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" print(f"Parsed expiration date for card: {card_info.expiration_date} -> {expiration_date}") 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: controller.setenvironment(constants.PRODUCTION) controller.execute() response = controller.getresponse() 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: print("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) print(f"Failed to add payment profile: {error_msg}") print(f"SANITIZED DATA SENT FOR ADD PROFILE: FirstName='{first_name}', LastName='{last_name}', Address='{address}', City='{city}', State='{state}', Zip='{zip_code}'") print(f"Card info: number='{card_info.card_number}', exp='{card_info.expiration_date}', cvv='{card_info.cvv}'") # Mask if sensitive print(pprint.pformat(vars(billTo))) print(pprint.pformat(vars(request))) raise ValueError(error_msg) except Exception as e: print(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. """ print(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() == "": print("INVALID: customer_profile_id is None or empty") if not payment_profile_id or payment_profile_id.strip() == "": print("INVALID: payment_profile_id is None or empty") print("Payment profile ID must be a valid, non-empty string") # FIRST ATTEMPT - Normal authorization print("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): print(f"E00121 DETECTED! Invalid payment profile {payment_profile_id}") print("POOOP") print(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 print(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 print(f"RECOVERY SUCCESS: Old ID '{payment_profile_id}' → New ID '{new_payment_profile_id}'") # SECOND ATTEMPT - With refreshed payment profile ID print("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): print("E00121 STILL PERSISTS after refresh - manual intervention may be needed") print(f"Payment profile {new_payment_profile_id} also rejected by Authorize.Net") else: print(f"SUCCESS! E00121 RESOLVED - Transaction succeeded with refreshed payment profile {new_payment_profile_id}") else: print(f"RECOVERY FAILED: No updated payment profile ID found for card {card_id}") print("Database refresh did not provide new payment profile ID") else: print(f"RECOVERY FAILED: Customer {customer_id} not found in database") except Exception as e: print(f"AUTO-RECOVERY FAILED: {str(e)}") print("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) controller.setenvironment(constants.PRODUCTION) controller.execute() response = controller.getresponse() # Log response details if response and hasattr(response, 'messages'): result_code = getattr(response.messages, 'resultCode', 'Unknown') print(f"Authorize response: resultCode='{result_code}'") else: print("Authorize response: No standard response structure") return response 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': print("E00121 detected in message list") return True # Handle single message elif hasattr(message, 'code'): if message.code == 'E00121': print(f"E00121 detected: '{getattr(message, 'text', 'No details')}'") return True else: print(f"Message code: '{message.code}' (not E00121)") return False except Exception as e: print(f"Error checking for E00121: {str(e)}") return False def capture_authorized_transaction(transaction_req: schemas.TransactionCapture): """Captures a previously authorized transaction.""" print(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) controller.setenvironment(constants.PRODUCTION) controller.execute() return controller.getresponse() 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. """ print(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: controller.setenvironment(constants.PRODUCTION) controller.execute() response = controller.getresponse() 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)) print(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) print(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: print(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. """ print(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) controller.setenvironment(constants.PRODUCTION) controller.execute() # The response is returned directly to the router to be parsed there return controller.getresponse()