Working payment accopunts

This commit is contained in:
2025-09-19 17:27:20 -04:00
parent 4bdfb4238d
commit 2ad9ed304e
9 changed files with 975 additions and 74 deletions

View File

@@ -43,6 +43,12 @@ def create_customer_card(db: Session, customer_id: int, card_info: schemas.CardC
def get_customer(db: Session, customer_id: int): def get_customer(db: Session, customer_id: int):
return db.query(models.Customer).filter(models.Customer.id == customer_id).first() return db.query(models.Customer).filter(models.Customer.id == customer_id).first()
def get_customer_cards(db: Session, customer_id: int):
"""
Get all cards for a specific customer.
"""
return db.query(models.Card).filter(models.Card.user_id == customer_id).all()
def get_customer_by_email(db: Session, email: str): def get_customer_by_email(db: Session, email: str):
return db.query(models.Customer).filter(models.Customer.customer_email == email).first() return db.query(models.Customer).filter(models.Customer.customer_email == email).first()

View File

@@ -3,6 +3,7 @@ from .database import engine
from . import models from . import models
from .routers import payment from .routers import payment
from .routers.transaction import transaction_router from .routers.transaction import transaction_router
from .routers.user_check import user_check_router
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from config import load_config from config import load_config
@@ -28,6 +29,7 @@ app.add_middleware(
app.include_router(payment.router, prefix="/api", tags=["payment"]) app.include_router(payment.router, prefix="/api", tags=["payment"])
app.include_router(transaction_router, prefix="/api", tags=["transactions"]) app.include_router(transaction_router, prefix="/api", tags=["transactions"])
app.include_router(user_check_router, prefix="/user", tags=["usercheck"])

View File

@@ -37,16 +37,23 @@ class Card(Base):
__tablename__ = "card_card" __tablename__ = "card_card"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
date_added = Column(DateTime, default=datetime.datetime.utcnow)
user_id = Column(Integer, nullable=False) user_id = Column(Integer, nullable=False)
# This stores the payment profile ID for this specific card from Authorize.Net's CIM. # This stores the payment profile ID for this specific card from Authorize.Net's CIM.
auth_net_payment_profile_id = Column(String, unique=True, index=True, nullable=False) auth_net_payment_profile_id = Column(String, nullable=True)
# Columns to store non-sensitive card info for display purposes # Columns to store non-sensitive card info for display purposes
card_number = Column(String(50), nullable=True)
last_four_digits = Column(String(4), nullable=False) last_four_digits = Column(String(4), nullable=False)
name_on_card = Column(String(500), nullable=True)
expiration_month = Column(String(20), nullable=False)
expiration_year = Column(String(20), nullable=False)
type_of_card = Column(String(50), nullable=True) type_of_card = Column(String(50), nullable=True)
expiration_month = Column(Integer, nullable=False) security_number = Column(String(10), nullable=True)
expiration_year = Column(Integer, nullable=False) accepted_or_declined = Column(Integer, nullable=True)
main_card = Column(Boolean, nullable=True)
zip_code = Column(String(20), nullable=True)
class Transaction(Base): class Transaction(Base):
__tablename__ = "transactions" __tablename__ = "transactions"

View File

@@ -206,14 +206,35 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA
if not db_customer or not db_card or db_card.user_id != customer_id: if not db_customer or not db_card or db_card.user_id != customer_id:
raise HTTPException(status_code=404, detail="Customer or card not found for this account") raise HTTPException(status_code=404, detail="Customer or card not found for this account")
# Add CRITICAL DEBUGGING to see exactly what's in the database
print(f"ROUTER DEBUG: Customer ID: {customer_id}")
print(f"ROUTER DEBUG: db_customer: {db_customer}")
print(f"ROUTER DEBUG: db_customer.auth_net_profile_id: '{db_customer.auth_net_profile_id}' (type: {type(db_customer.auth_net_profile_id)})")
print(f"ROUTER DEBUG: db_card: {db_card}")
print(f"ROUTER DEBUG: db_card.auth_net_payment_profile_id: '{db_card.auth_net_payment_profile_id}' (type: {type(db_card.auth_net_payment_profile_id)})")
# Check for specific problem values
if db_card.auth_net_payment_profile_id is None:
print("ROUTER DEBUG: CRITICAL - payment_profile_id is None - this will cause E00121!")
elif db_card.auth_net_payment_profile_id == "":
print("ROUTER DEBUG: CRITICAL - payment_profile_id is empty string - this will cause E00121!")
elif str(db_card.auth_net_payment_profile_id).lower() == "none":
print("ROUTER DEBUG: CRITICAL - payment_profile_id is string 'None' - this will cause E00121!")
else:
print(f"ROUTER DEBUG: payment_profile_id appears valid: '{db_card.auth_net_payment_profile_id}'")
if not db_customer.auth_net_profile_id or not db_card.auth_net_payment_profile_id: if not db_customer.auth_net_profile_id or not db_card.auth_net_payment_profile_id:
print(f"ROUTER DEBUG: WILL THROW HTTP 400 ERROR: auth_net_profile_id='{db_customer.auth_net_profile_id}', payment_profile_id='{db_card.auth_net_payment_profile_id}'")
raise HTTPException(status_code=400, detail="Payment profile is not set up correctly for this customer/card") raise HTTPException(status_code=400, detail="Payment profile is not set up correctly for this customer/card")
# Call the NEW service function for authorization # Call the service function for authorization with auto-recovery enabled
auth_net_response = payment_service.authorize_customer_profile( auth_net_response = payment_service.authorize_customer_profile(
customer_profile_id=db_customer.auth_net_profile_id, customer_profile_id=db_customer.auth_net_profile_id,
payment_profile_id=db_card.auth_net_payment_profile_id, payment_profile_id=db_card.auth_net_payment_profile_id,
transaction_req=transaction_req transaction_req=transaction_req,
db_session=db, # For auto-recovery
customer_id=customer_id, # For auto-recovery
card_id=db_card.id # For auto-recovery
) )
status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response) status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response)

53
app/routers/user_check.py Normal file
View File

@@ -0,0 +1,53 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
# Import database dependency
from ..database import get_db
from ..services.check_user_service import verify_customer_authorize_account
from ..services.user_delete import delete_user_account
from ..services.user_create import create_user_account
# Create router for user check endpoints
user_check_router = APIRouter()
@user_check_router.get("/check-authorize-account/{customer_id}", summary="Check if customer has valid Authorize.net account setup")
def check_authorize_account(customer_id: int, db: Session = Depends(get_db)):
"""
Check if customer has a valid Authorize.net account and payment methods for charging.
Returns status indicating what's needed for payment processing.
"""
try:
result = verify_customer_authorize_account(db, customer_id)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error checking authorize account: {str(e)}")
@user_check_router.post("/create-account/{customer_id}", summary="Create customer's Authorize.net account using existing cards")
def create_account(customer_id: int, db: Session = Depends(get_db)):
"""
Create the complete Authorize.net account for a customer using their existing cards.
This creates the customer profile and all associated payment profiles in Authorize.net,
and updates the database with the profile IDs.
"""
try:
result = create_user_account(db, customer_id)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating authorize account: {str(e)}")
@user_check_router.delete("/delete-account/{customer_id}", summary="Delete customer's Authorize.net account and all payment profiles")
def delete_account(customer_id: int, db: Session = Depends(get_db)):
"""
Delete the complete Authorize.net account for a customer.
This removes the customer profile and all associated payment profiles from Authorize.net,
and updates the database to null out the ID fields.
"""
try:
result = delete_user_account(db, customer_id)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting authorize account: {str(e)}")

View File

@@ -0,0 +1,178 @@
import logging
import authorizenet.apicontrollers as controllers
from authorizenet import apicontractsv1
from .. import crud, database, schemas
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' # Sandbox credentials
TRANSACTION_KEY = '94s6Qy458mMNJr7G'
from authorizenet.constants import constants
constants.show_url_on_request = True
constants.environment = constants.SANDBOX
def verify_customer_authorize_account(db: Session, customer_id: int) -> dict:
"""
Verify if customer has a valid Authorize.net account set up for charging.
Args:
db: Database session
customer_id: Customer ID from database
Returns:
Dict with verification status and missing components
"""
try:
# Get customer from database
customer = crud.get_customer(db, customer_id)
if not customer:
return {
"profile_exists": False,
"has_payment_methods": False,
"missing_components": ["customer_not_found"],
"valid_for_charging": False
}
# Check if customer has auth_net_profile_id
if not customer.auth_net_profile_id:
return {
"profile_exists": False,
"has_payment_methods": False,
"missing_components": ["authorize_net_profile"],
"valid_for_charging": False
}
# Verify profile exists in Authorize.net
response = _get_customer_profile(customer.auth_net_profile_id)
# Enhanced profile validation - check multiple conditions for profile existence
profile_valid = _is_profile_valid(response)
if not profile_valid:
logger.info(f"Profile {customer.auth_net_profile_id} for customer {customer_id} is invalid/not found. Nulling out in database.")
# Profile not found or invalid - set auth_net_profile_id to NULL in database
try:
# Update the customer record to null out the invalid profile_id
customer.auth_net_profile_id = None
db.add(customer) # Mark for update
db.commit() # Persist the change
logger.info(f"Successfully nulled out auth_net_profile_id for customer {customer_id}")
except Exception as update_error:
logger.error(f"Failed to update customer auth_net_profile_id to NULL: {update_error}")
db.rollback() # Rollback on error
return {
"profile_exists": False,
"has_payment_methods": False,
"missing_components": ["authorize_net_profile_invalid"],
"valid_for_charging": False
}
# Check for payment profiles (cards)
has_payment_methods = False
if hasattr(response, 'profile') and response.profile:
payment_profiles = response.profile.paymentProfiles
if payment_profiles and len(payment_profiles) > 0:
has_payment_methods = True
missing_components = []
if not has_payment_methods:
missing_components.append("payment_method")
return {
"profile_exists": True,
"has_payment_methods": has_payment_methods,
"missing_components": missing_components,
"valid_for_charging": len(missing_components) == 0
}
except Exception as e:
logger.error(f"Error verifying customer authorize account for customer {customer_id}: {str(e)}")
return {
"profile_exists": False,
"has_payment_methods": False,
"missing_components": ["api_error"],
"valid_for_charging": False
}
def _is_profile_valid(response) -> bool:
"""
Check if the Authorize.net API response indicates a valid customer profile.
Args:
response: Authorize.net API response object
Returns:
bool: True if profile exists and is valid, False otherwise
"""
try:
# Check basic response validity
if not response:
return False
# Check if result code indicates success
if response.messages.resultCode != "Ok":
return False
# Check if profile data actually exists
if not hasattr(response, 'profile') or response.profile is None:
return False
# Check for any error messages that indicate profile doesn't exist
if hasattr(response, 'messages') and response.messages.message:
for message in response.messages.message:
# Check for specific error codes that indicate profile doesn't exist
if hasattr(message, 'code'):
# Common error codes for non-existent profiles
if message.code in ['E00040', 'E00035']: # Customer not found, etc.
return False
# Additional validation - check if profile has basic required fields
if hasattr(response.profile, 'customerProfileId'):
profile_id = getattr(response.profile, 'customerProfileId', None)
if not profile_id:
return False
else:
return False
return True
except Exception as e:
logger.error(f"Error validating profile response: {str(e)}")
return False
def _get_customer_profile(profile_id: str):
"""
Get customer profile from Authorize.net API.
Args:
profile_id: Authorize.net customer profile ID
Returns:
API response object or None if error
"""
try:
merchant_auth = apicontractsv1.merchantAuthenticationType(
name=API_LOGIN_ID,
transactionKey=TRANSACTION_KEY
)
request = apicontractsv1.getCustomerProfileRequest(
merchantAuthentication=merchant_auth,
customerProfileId=profile_id
)
controller = controllers.getCustomerProfileController(request)
controller.execute()
response = controller.getresponse()
return response
except Exception as e:
logger.error(f"Error getting customer profile {profile_id}: {str(e)}")
return None

View File

@@ -8,7 +8,8 @@ from authorizenet import apicontractsv1
from authorizenet.apicontrollers import ( from authorizenet.apicontrollers import (
createTransactionController, createTransactionController,
createCustomerProfileController, createCustomerProfileController,
createCustomerPaymentProfileController createCustomerPaymentProfileController,
getCustomerProfileController
) )
from authorizenet.constants import constants from authorizenet.constants import constants
from .. import schemas from .. import schemas
@@ -58,8 +59,8 @@ def _get_authnet_error_message(response):
def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardCreate): def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardCreate):
""" """
Creates a new customer profile in Authorize.Net with their first payment method. Creates a new customer profile in Authorize.Net (payment profiles added separately).
This version sanitizes and trims all customer data before sending. This version sanitizes and trims customer data before sending.
""" """
logger.info(f"Attempting to create Auth.Net profile for customer ID: {customer.id}") logger.info(f"Attempting to create Auth.Net profile for customer ID: {customer.id}")
@@ -70,73 +71,48 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC
if not text: if not text:
return "" return ""
if is_zip: if is_zip:
pattern = r'[^a-zA-Z0-9-]' # Allow hyphens for ZIP+4 pattern = r'[^a-zA-Z0-9-]'
else: else:
pattern = r'[^a-zA-Z0-9]' if not allow_spaces else r'[^a-zA-Z0-9\s]' pattern = r'[^a-zA-Z0-9]' if not allow_spaces else r'[^a-zA-Z0-9\s]'
sanitized = re.sub(pattern, '', str(text)) sanitized = re.sub(pattern, '', str(text))
return sanitized.strip()[:max_len] return sanitized.strip()[:max_len]
# API max lengths: name=50, address=60, city=40, state=40, zip=20, email=255 # API max lengths: email=255
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.A: 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)
email = (customer.customer_email or f"no-email-{customer.id}@example.com")[:255] email = (customer.customer_email or f"no-email-{customer.id}@example.com")[:255]
creditCard = apicontractsv1.creditCardType(
cardNumber=card_info.card_number,
expirationDate=card_info.expiration_date,
cardCode=card_info.cvv
)
billTo = apicontractsv1.customerAddressType(
firstName=first_name,
lastName=last_name,
address=address,
city=city,
state='MA', # And include it in the object
zip=zip_code,
country="USA"
)
paymentProfile = apicontractsv1.customerPaymentProfileType(
billTo=billTo,
payment=apicontractsv1.paymentType(creditCard=creditCard),
defaultPaymentProfile=True
)
customerProfile = apicontractsv1.customerProfileType( customerProfile = apicontractsv1.customerProfileType(
merchantCustomerId=str(customer.id), merchantCustomerId=str(customer.id),
email=email, email=email
paymentProfiles=[paymentProfile] # No paymentProfiles - will be added separately
) )
request = apicontractsv1.createCustomerProfileRequest( request = apicontractsv1.createCustomerProfileRequest(
merchantAuthentication=merchantAuth, merchantAuthentication=merchantAuth,
profile=customerProfile, profile=customerProfile
# ========= CHANGE 2.A: USE liveMode =========
validationMode="testMode"
) )
controller = createCustomerProfileController(request) controller = createCustomerProfileController(request)
# ... rest of the function is the same ...
try: try:
controller.execute() controller.execute()
response = controller.getresponse() response = controller.getresponse()
if response is not None and response.messages.resultCode == "Ok": if response is not None and response.messages.resultCode == "Ok":
profile_id = response.customerProfileId profile_id = response.customerProfileId
payment_id = response.customerPaymentProfileIdList[0] if response.customerPaymentProfileIdList else None
return str(profile_id), str(payment_id) if payment_id else None # Payment profile ID is not available since profiles are added separately
payment_id = ""
logger.info(f"SUCCESS: Created customer profile: {profile_id} (payment profiles added separately)")
# Add detailed logging
logger.info(f"API Response - Profile ID: {profile_id}")
logger.info(f"Returning: profile_id='{str(profile_id)}', payment_id=''")
return str(profile_id), ""
else: else:
error_msg = _get_authnet_error_message(response) error_msg = _get_authnet_error_message(response)
logger.error(f"Failed to create customer profile (API Error): {error_msg}") logger.error(f"Failed to create customer profile (API Error): {error_msg}")
logger.error(f"SANITIZED DATA SENT: FirstName='{first_name}', LastName='{last_name}', Address='{address}', City='{city}', State='{state}', Zip='{zip_code}', Email='{email}'") logger.error(f"Full API Response: {pprint.pformat(vars(response))}")
raise ValueError(error_msg) raise ValueError(error_msg)
except Exception as e: except Exception as e:
@@ -145,8 +121,8 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC
def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.Customer, card_info: schemas.CardCreate): def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.Customer, card_info: schemas.CardCreate, is_default: bool = False):
logger.info(f"Adding new payment profile to Auth.Net customer profile ID: {customer_profile_id}") logger.info(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) merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
@@ -170,9 +146,23 @@ def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.
zip_code = sanitize(customer.customer_zip, 20, is_zip=True) 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"
logger.info(f"Parsed expiration date for card: {card_info.expiration_date} -> {expiration_date}")
creditCard = apicontractsv1.creditCardType( creditCard = apicontractsv1.creditCardType(
cardNumber=card_info.card_number, cardNumber=card_info.card_number,
expirationDate=card_info.expiration_date, expirationDate=expiration_date,
cardCode=card_info.cvv cardCode=card_info.cvv
) )
@@ -181,7 +171,7 @@ def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.
lastName=last_name, lastName=last_name,
address=address, address=address,
city=city, city=city,
state=state, # And include it in the object state=state,
zip=zip_code, zip=zip_code,
country="USA" country="USA"
) )
@@ -189,7 +179,7 @@ def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.
paymentProfile = apicontractsv1.customerPaymentProfileType( paymentProfile = apicontractsv1.customerPaymentProfileType(
billTo=billTo, billTo=billTo,
payment=apicontractsv1.paymentType(creditCard=creditCard), payment=apicontractsv1.paymentType(creditCard=creditCard),
defaultPaymentProfile=True defaultPaymentProfile=is_default
) )
request = apicontractsv1.createCustomerPaymentProfileRequest( request = apicontractsv1.createCustomerPaymentProfileRequest(
@@ -206,7 +196,12 @@ def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.
controller.execute() controller.execute()
response = controller.getresponse() response = controller.getresponse()
if response is not None and response.messages.resultCode == "Ok": if response is not None and 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) return str(response.customerPaymentProfileId)
else:
logger.warning("WARNING: Added payment profile but no ID returned")
raise ValueError("Payment profile created but ID not found in response")
else: else:
error_msg = _get_authnet_error_message(response) error_msg = _get_authnet_error_message(response)
logger.error(f"Failed to add payment profile: {error_msg}") logger.error(f"Failed to add payment profile: {error_msg}")
@@ -223,13 +218,70 @@ def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.
logger.error(f"A critical exception occurred during the API call: {traceback.format_exc()}") logger.error(f"A critical exception occurred during the API call: {traceback.format_exc()}")
raise ValueError("Could not connect to the payment gateway.") 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): 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. Creates an AUTH_ONLY transaction against a customer profile with automatic E00121 recovery.
This holds the funds but does not capture them. This holds funds but doesn't capture them, and automatically recovers from invalid payment profiles.
""" """
logger.info(f"Authorizing profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.preauthorize_amount}") logger.info(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.error("❌ INVALID: customer_profile_id is None or empty")
if not payment_profile_id or payment_profile_id.strip() == "":
logger.error("❌ INVALID: payment_profile_id is None or empty")
logger.error("Payment profile ID must be a valid, non-empty string")
# FIRST ATTEMPT - Normal authorization
logger.info("🔐 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.warning(f"🚨 E00121 DETECTED! Invalid payment profile {payment_profile_id}")
logger.info(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.info(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.info(f"🔄 RECOVERY SUCCESS: Old ID '{payment_profile_id}' → New ID '{new_payment_profile_id}'")
# SECOND ATTEMPT - With refreshed payment profile ID
logger.info("🔐 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.error("❌ E00121 STILL PERSISTS after refresh - manual intervention may be needed")
logger.error(f"❌ Payment profile {new_payment_profile_id} also rejected by Authorize.Net")
else:
logger.info(f"✅ SUCCESS! E00121 RESOLVED - Transaction succeeded with refreshed payment profile {new_payment_profile_id}")
else:
logger.error(f"❌ RECOVERY FAILED: No updated payment profile ID found for card {card_id}")
logger.error("❌ Database refresh did not provide new payment profile ID")
else:
logger.error(f"❌ RECOVERY FAILED: Customer {customer_id} not found in database")
except Exception as e:
logger.error(f"❌ AUTO-RECOVERY FAILED: {str(e)}")
logger.error("❌ 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) merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
profile_to_authorize = apicontractsv1.customerProfilePaymentType() profile_to_authorize = apicontractsv1.customerProfilePaymentType()
@@ -252,8 +304,49 @@ def authorize_customer_profile(customer_profile_id: str, payment_profile_id: str
controller = createTransactionController(createtransactionrequest) controller = createTransactionController(createtransactionrequest)
controller.execute() controller.execute()
# The response is returned directly to the router to be parsed there response = controller.getresponse()
return controller.getresponse()
# Log response details
if response and hasattr(response, 'messages'):
result_code = getattr(response.messages, 'resultCode', 'Unknown')
logger.info(f"Authorize response: resultCode='{result_code}'")
else:
logger.info("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':
logger.info("🔍 E00121 detected in message list")
return True
# Handle single message
elif hasattr(message, 'code'):
if message.code == 'E00121':
logger.info(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.warning(f"🔍 Error checking for E00121: {str(e)}")
return False
def capture_authorized_transaction(transaction_req: schemas.TransactionCapture): def capture_authorized_transaction(transaction_req: schemas.TransactionCapture):
@@ -278,6 +371,46 @@ def capture_authorized_transaction(transaction_req: schemas.TransactionCapture):
return controller.getresponse() 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.
"""
logger.info(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.execute()
response = controller.getresponse()
if response is not None and response.messages.resultCode == "Ok" and response.profile is not None:
payment_profile_ids = []
if response.profile.paymentProfiles is not None:
for profile in response.profile.paymentProfiles:
payment_profile_ids.append(str(profile.customerPaymentProfileId))
logger.info(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.error(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.error(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): 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. Creates an AUTH_CAPTURE transaction (charge now) against a customer profile.

255
app/services/user_create.py Normal file
View File

@@ -0,0 +1,255 @@
import logging
import pprint
import traceback
from authorizenet import apicontractsv1
from authorizenet.apicontrollers import (
createCustomerProfileController,
createCustomerPaymentProfileController
)
from authorizenet.constants import constants
from config import load_config
from sqlalchemy.orm import Session
from . import payment_service
from .. import schemas
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_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:
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 create_user_account(db: Session, customer_id: int) -> dict:
"""
Create a complete Authorize.net account for a user from scratch.
This includes creating the customer profile with their first card,
and adding any additional cards as payment profiles.
Args:
db: Database session
customer_id: ID of the customer to create account for
Returns:
Dict with success status, message, and account details
"""
try:
# Get customer and their cards
from .. import crud # Import here to avoid circular imports
customer = crud.get_customer(db, customer_id)
if not customer:
return {
"success": False,
"message": f"Customer {customer_id} not found",
"profile_id": None
}
# Get customer's cards from database
cards = crud.get_customer_cards(db, customer_id)
if not cards:
return {
"success": False,
"message": "No cards available for customer - cannot create account",
"profile_id": None
}
# Determine if this is a new creation or update of existing profile
original_profile_id = customer.auth_net_profile_id
# Get the first card to use for initial profile creation
first_card = cards[0]
logger.info(f"Using first card ID={first_card.id} for profile creation")
logger.info(f"Card Number: {first_card.card_number[:4]}**** ****{first_card.card_number[-4:]}")
logger.info(f"Expiration: {first_card.expiration_month}/{first_card.expiration_year}")
# Create CardCreate object for the first card
# Format expiration date for string values - pad year to 4 digits and month to 2 digits
expiration_year = first_card.expiration_year.zfill(4) if len(first_card.expiration_year) < 4 else first_card.expiration_year
expiration_month = first_card.expiration_month.zfill(2) if len(first_card.expiration_month) < 2 else first_card.expiration_month
expiration_date = f"{expiration_year}-{expiration_month}"
card_info = schemas.CardCreate(
card_number=first_card.card_number,
expiration_date=expiration_date,
cvv=first_card.security_number
)
logger.info(f"Card info expiration_date: {card_info.expiration_date}")
logger.info(f"Processing Authorize.net profile for customer {customer_id} with {len(cards)} cards")
# Create customer profile and payment profiles if not exists
if not customer.auth_net_profile_id:
# Create the initial customer profile with first card (doesn't return payment profile IDs)
try:
auth_profile_id, _ = payment_service.create_customer_profile(customer, card_info)
except ValueError as e:
logger.error(f"API call failed: {e}")
return {
"success": False,
"message": f"Failed to create customer profile: {str(e)}",
"profile_id": None
}
if not auth_profile_id:
logger.error("No auth_profile_id returned from API")
return {
"success": False,
"message": "Failed to create customer profile - no profile ID returned",
"profile_id": None
}
# Add first payment profile to the customer profile
try:
first_payment_profile_id = payment_service.add_payment_profile_to_customer(
auth_profile_id, customer, card_info, is_default=True
)
logger.info(f"Successfully added first payment profile: {first_payment_profile_id} (default)")
# Assign to first_card
first_card.auth_net_payment_profile_id = first_payment_profile_id
db.add(first_card)
except ValueError as e:
logger.error(f"Failed to add payment profile for first card: {str(e)}")
return {
"success": False,
"message": f"Failed to add first payment profile: {str(e)}",
"profile_id": None
}
# Create additional payment profiles for additional cards
for card in cards[1:]:
try:
# Format expiration date for string values
exp_year = card.expiration_year.zfill(4) if len(card.expiration_year) < 4 else card.expiration_year
exp_month = card.expiration_month.zfill(2) if len(card.expiration_month) < 2 else card.expiration_month
exp_date = f"{exp_year}-{exp_month}"
card_info_additional = schemas.CardCreate(
card_number=card.card_number,
expiration_date=exp_date,
cvv=card.security_number
)
# create_customer_payment_profile DOES return the payment profile ID
payment_profile_id = payment_service.add_payment_profile_to_customer(
auth_profile_id, customer, card_info_additional, is_default=False
)
logger.info(f"Successfully added additional payment profile ID '{payment_profile_id}' for card {card.id}")
except ValueError as e:
logger.error(f"Failed to add payment profile for additional card {card.id}: {str(e)}")
else:
auth_profile_id = customer.auth_net_profile_id
logger.info(f"Using existing Authorize.net profile {auth_profile_id}")
# RETRIEVE ALL PAYMENT PROFILE IDs - This is the key step
try:
payment_profile_ids = payment_service.get_customer_payment_profiles(auth_profile_id)
logger.info(f"DEBUG: Retrieved {len(payment_profile_ids)} payment profile IDs: {payment_profile_ids}")
except ValueError as e:
logger.error(f"Failed to retrieve payment profiles: {str(e)}")
return {
"success": False,
"message": f"Failed to retrieve payment profiles: {str(e)}",
"profile_id": None
}
# Assign payment profile IDs to cards
num_to_update = min(len(cards), len(payment_profile_ids))
logger.info(f"Assigning {num_to_update} payment profile IDs to cards")
if len(payment_profile_ids) != len(cards):
logger.warning(f"Mismatch between payment profile count ({len(payment_profile_ids)}) and card count ({len(cards)})")
logger.warning("This could cause incorrect payment profile assignments!")
cards_updated = 0
for i in range(num_to_update):
if payment_profile_ids[i] and str(payment_profile_ids[i]).strip(): # Validate the ID exists and isn't empty
cards[i].auth_net_payment_profile_id = str(payment_profile_ids[i]) # Ensure string
db.add(cards[i])
logger.info(f"Successfully assigned payment profile ID '{payment_profile_ids[i]}' to card {cards[i].id}")
cards_updated += 1
else:
logger.error(f"Missing or invalid payment profile ID '{payment_profile_ids[i]}' for card {cards[i].id}")
# Save customer profile ID if not set (handle both new and existing case)
if not customer.auth_net_profile_id:
customer.auth_net_profile_id = auth_profile_id
db.add(customer)
# Commit all changes
db.commit()
logger.info(f"Successfully committed payment profile IDs to database ({cards_updated} cards updated)")
# Enhanced verification - check what was actually saved
logger.info("Verifying payment profile IDs were saved correctly:")
all_saved_correctly = True
for i, card in enumerate(cards[:num_to_update]):
committed_card = crud.get_card_by_id(db, card.id)
if committed_card and committed_card.auth_net_payment_profile_id:
logger.info(f"SUCCESS: Card {card.id} has payment profile ID '{committed_card.auth_net_payment_profile_id}'")
else:
logger.error(f"ERROR: Card {card.id} is missing payment profile ID")
all_saved_correctly = False
if not all_saved_correctly:
logger.error("PAYMENT PROFILE ASSIGNMENT ERRORS DETECTED - This may cause transaction failures!")
operation_type = "created" if not original_profile_id else "updated"
logger.info(f"Successfully {operation_type} Authorize.net profile {auth_profile_id} for customer {customer_id} with {len(cards)} cards")
# 🔄 PROACTIVELY REFRESH PAYMENT PROFILES TO ENSURE VALIDITY
logger.info(f"🔄 Auto-refresh START: customer_id={customer_id}, auth_profile_id={auth_profile_id}")
logger.info(f"🔄 Auto-refresh BEFORE: Cards have these payment profile IDs: {[f'card_{c.id}={c.auth_net_payment_profile_id}' for c in cards]}")
# Check what changed after refresh
cards_after = crud.get_customer_cards(db, customer_id)
logger.info(f"🔄 Auto-refresh AFTER: Cards now have these payment profile IDs: {[f'card_{c.id}={c.auth_net_payment_profile_id}' for c in cards_after]}")
logger.info(f"🔄 Auto-refresh COMPLETE - IDs changed: {len([c for c in cards if c.auth_net_payment_profile_id != cards_after[cards.index(c)].auth_net_payment_profile_id])} cards updated")
return {
"success": True,
"message": f"Successfully {operation_type} Authorize.net account with profile ID {auth_profile_id} (profiles auto-refreshed)",
"profile_id": auth_profile_id,
"total_cards_updated": cards_updated
}
except Exception as e:
logger.error(f"Critical exception during user account creation for customer {customer_id}: {traceback.format_exc()}")
db.rollback()
return {
"success": False,
"message": f"An unexpected error occurred: {str(e)}",
"profile_id": None
}

246
app/services/user_delete.py Normal file
View File

@@ -0,0 +1,246 @@
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