diff --git a/app/routers/payment.py b/app/routers/payment.py index 496217d..a73fa97 100644 --- a/app/routers/payment.py +++ b/app/routers/payment.py @@ -4,12 +4,13 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import Tuple, Optional import enum -import logging from .. import crud, models, schemas, database from ..services import payment_service +from config import load_config + +ApplicationConfig = load_config() -logger = logging.getLogger(__name__) @@ -193,13 +194,13 @@ def update_card_for_customer(customer_id: int, card_id: int, card_info: schemas. try: # If payment profile ID is null, refresh from Authorize.Net to sync if not db_card.auth_net_payment_profile_id: - logger.info(f"Payment profile ID is null for card {card_id}, refreshing from Authorize.Net") + print(f"Payment profile ID is null for card {card_id}, refreshing from Authorize.Net") from ..services import user_create user_create.refresh_customer_payment_profiles(db, customer_id, db_customer.auth_net_profile_id) # Re-fetch the card to get the updated payment profile ID db.refresh(db_card) - logger.info(f"After refresh, card {card_id} has payment_profile_id: {db_card.auth_net_payment_profile_id}") + print(f"After refresh, card {card_id} has payment_profile_id: {db_card.auth_net_payment_profile_id}") # Delete existing payment profile if it exists if db_card.auth_net_payment_profile_id: @@ -208,13 +209,13 @@ def update_card_for_customer(customer_id: int, card_id: int, card_info: schemas. db_customer.auth_net_profile_id, db_card.auth_net_payment_profile_id ) if delete_success: - logger.info(f"Successfully deleted old payment profile {db_card.auth_net_payment_profile_id} for card {card_id}") + print(f"Successfully deleted old payment profile {db_card.auth_net_payment_profile_id} for card {card_id}") # Clear the payment profile ID since it was deleted db_card.auth_net_payment_profile_id = None db.add(db_card) db.commit() else: - logger.warning(f"Failed to delete old payment profile {db_card.auth_net_payment_profile_id} for card {card_id}") + print(f"Failed to delete old payment profile {db_card.auth_net_payment_profile_id} for card {card_id}") # Create new payment profile with updated card information new_payment_profile_id = payment_service.add_payment_profile_to_customer( @@ -228,7 +229,7 @@ def update_card_for_customer(customer_id: int, card_id: int, card_info: schemas. db.add(db_card) db.commit() - logger.info(f"Successfully updated card {card_id} with new payment profile {new_payment_profile_id}") + print(f"Successfully updated card {card_id} with new payment profile {new_payment_profile_id}") # Return the new payment_profile_id return {"payment_profile_id": new_payment_profile_id} @@ -293,9 +294,9 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA # 🚨 ENHANCED PRE-TRANSACTION VALIDATION 🚨 # Proactively check and fix payment profile issues before attempting transaction print(f"🐛 DEBUG: Starting enhanced pre-validation for customer {customer_id}, card {db_card.id}") - logger.info(f"🔍 PRE-TRANSACTION CHECK: Customer {customer_id}, Card {db_card.id}") - logger.info(f"🔍 Current auth_net_profile_id: '{db_customer.auth_net_profile_id}'") - logger.info(f"🔍 Current payment_profile_id: '{db_card.auth_net_payment_profile_id}'") + print(f"🔍 PRE-TRANSACTION CHECK: Customer {customer_id}, Card {db_card.id}") + print(f"🔍 Current auth_net_profile_id: '{db_customer.auth_net_profile_id}'") + print(f"🔍 Current payment_profile_id: '{db_card.auth_net_payment_profile_id}'") # Check for missing payment profiles OR test validity of existing ones needs_recovery = False @@ -309,11 +310,11 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA # Missing/null payment profile - needs recovery needs_recovery = True print("🐛 DEBUG: NULL/MISSING PAYMENT PROFILE DETECTED") - logger.warning(f"🔧 NULL/MISSING PAYMENT PROFILE DETECTED - Triggering auto-recovery") + print(f"🔧 NULL/MISSING PAYMENT PROFILE DETECTED - Triggering auto-recovery") else: # Payment profile exists in DB, but let's test if it's valid in Authorize.net print(f"🐛 DEBUG: Payment profile exists, testing validity in Authorize.net...") - logger.info(f"🔍 Payment profile exists, testing validity in Authorize.net...") + print(f"🔍 Payment profile exists, testing validity in Authorize.net...") try: # Quick test: try to retrieve customer payment profiles to see if our ID is valid print(f"🐛 DEBUG: Calling get_customer_payment_profiles for profile_id: {db_customer.auth_net_profile_id}") @@ -326,55 +327,60 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA if current_id not in profile_ids_as_strings: needs_recovery = True print(f"🐛 DEBUG: PAYMENT PROFILE {current_id} NOT FOUND - NEEDS RECOVERY") - logger.warning(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} NOT FOUND IN AUTHORIZE.NET - Invalid ID!") - logger.warning(f"🔧 Available profiles in Authorize.net: {test_profiles}") + print(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} NOT FOUND IN AUTHORIZE.NET - Invalid ID!") + print(f"🔧 Available profiles in Authorize.net: {test_profiles}") else: - print(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} exists in Authorize.net, testing usability...") - # Profile exists in Authorize.net, but test if it's actually USABLE - # by doing a quick test authorization with minimal amount - try: - print(f"🐛 DEBUG: Testing if payment profile is actually usable...") - # Create a tiny test transaction (like $0.01) to validate the card works - test_transaction_req = schemas.TransactionAuthorizeByCardID( - card_id=db_card.id, - preauthorize_amount="0.01" # Minimal test amount - ) + if ApplicationConfig.penny_test_transaction: + print(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} exists in Authorize.net, testing usability...") + # Profile exists in Authorize.net, but test if it's actually USABLE + # by doing a quick test authorization with minimal amount + try: + print(f"🐛 DEBUG: Testing if payment profile is actually usable...") + # Create a tiny test transaction (like $0.01) to validate the card works + test_transaction_req = schemas.TransactionAuthorizeByCardID( + card_id=db_card.id, + preauthorize_amount="0.01" # Minimal test amount + ) - test_response = payment_service.authorize_customer_profile( - customer_profile_id=db_customer.auth_net_profile_id, - payment_profile_id=db_card.auth_net_payment_profile_id, - transaction_req=test_transaction_req, - db_session=None, # Don't pass DB session for test transaction - customer_id=None, - card_id=None - ) + test_response = payment_service.authorize_customer_profile( + customer_profile_id=db_customer.auth_net_profile_id, + payment_profile_id=db_card.auth_net_payment_profile_id, + transaction_req=test_transaction_req, + db_session=None, # Don't pass DB session for test transaction + customer_id=None, + card_id=None + ) - # Check if the test authorization worked - from ..services import payment_service as ps # Need access to _parse_authnet_response - test_status, _, test_reason = _parse_authnet_response(test_response) + # Check if the test authorization worked + from ..services import payment_service as ps # Need access to _parse_authnet_response + test_status, _, test_reason = _parse_authnet_response(test_response) - if "E00121" in str(test_reason) or test_status == 1: # 1 = DECLINED - print(f"🐛 DEBUG: TEST AUTH FAILED - Payment profile exists but is INVALID!") + if "E00121" in str(test_reason) or test_status == 1: # 1 = DECLINED + print(f"🐛 DEBUG: TEST AUTH FAILED - Payment profile exists but is INVALID!") + needs_recovery = True + print(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} EXISTS BUT IS UNUSABLE - E00121 detected during test!") + print(f"🔧 Test transaction failed: {test_reason}") + else: + print(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} is VALID and USABLE") + print(f"✅ Payment profile {db_card.auth_net_payment_profile_id} is valid and usable in Authorize.net") + + except Exception as e: + print(f"🐛 DEBUG: Exception during usability test: {str(e)} - assuming profile needs recreation") needs_recovery = True - logger.warning(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} EXISTS BUT IS UNUSABLE - E00121 detected during test!") - logger.warning(f"🔧 Test transaction failed: {test_reason}") - else: - print(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} is VALID and USABLE") - logger.info(f"✅ Payment profile {db_card.auth_net_payment_profile_id} is valid and usable in Authorize.net") + print(f"🔧 Could not test payment profile usability: {str(e)} - assuming it needs recreation") + else: + print(f"🔍 Skipping penny test transaction (disabled in config)") - except Exception as e: - print(f"🐛 DEBUG: Exception during usability test: {str(e)} - assuming profile needs recreation") - needs_recovery = True - logger.warning(f"🔧 Could not test payment profile usability: {str(e)} - assuming it needs recreation") + except Exception as e: print(f"🐛 DEBUG: Exception during profile validation: {str(e)}") - logger.warning(f"🔧 Could not verify payment profile validity in Authorize.net: {str(e)}") + print(f"🔧 Could not verify payment profile validity in Authorize.net: {str(e)}") # If we can't verify, assume it's okay and let the transaction proceed # (better to try and fail than to block legitimate transactions) - logger.info(f"⚠️ Unable to verify profile validity - proceeding with transaction anyway") + print(f"⚠️ Unable to verify profile validity - proceeding with transaction anyway") if needs_recovery: - logger.warning(f"🔧 DETECTED PAYMENT PROFILE ISSUE - Triggering auto-recovery for customer {customer_id}") + print(f"🔧 DETECTED PAYMENT PROFILE ISSUE - Triggering auto-recovery for customer {customer_id}") # Auto-recover: Refresh payment profiles before transaction from ..services import user_create @@ -383,14 +389,14 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA ) if recovery_success: - logger.info("✅ Auto-recovery successful - proceeding with transaction") + print("✅ Auto-recovery successful - proceeding with transaction") # Re-fetch card data to get updated payment profile ID db.refresh(db_card) db.refresh(db_customer) - logger.info(f"🔍 After recovery - payment_profile_id: '{db_card.auth_net_payment_profile_id}'") + print(f"🔍 After recovery - payment_profile_id: '{db_card.auth_net_payment_profile_id}'") else: - logger.error("❌ Auto-recovery failed - cannot proceed with transaction") + print("❌ Auto-recovery failed - cannot proceed with transaction") raise HTTPException( status_code=400, detail="Payment profile setup error detected and auto-recovery failed. Please contact support." @@ -398,13 +404,13 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA # Final validation before proceeding if not db_customer.auth_net_profile_id or not db_card.auth_net_payment_profile_id: - logger.error(f"❌ CRITICAL: Payment profile validation failed after recovery attempt") + print(f"❌ CRITICAL: Payment profile validation failed after recovery attempt") raise HTTPException( status_code=400, detail="Payment profile is not set up correctly for this customer/card" ) - logger.info(f"✅ Payment profile validation passed - proceeding with authorization") + print(f"✅ Payment profile validation passed - proceeding with authorization") # 🚨 ENHANCED E00121 ERROR HANDLING 🚨 # If transaction still fails with E00121 despite pre-validation, force-nuke the problematic ID diff --git a/app/routers/payment.py.backup b/app/routers/payment.py.backup new file mode 100644 index 0000000..496217d --- /dev/null +++ b/app/routers/payment.py.backup @@ -0,0 +1,536 @@ +## File: your_app/views.py + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import Tuple, Optional +import enum +import logging + +from .. import crud, models, schemas, database +from ..services import payment_service + +logger = logging.getLogger(__name__) + + + +AuthNetResponse = object + +router = APIRouter( + prefix="/payments", + tags=["Payments & Transactions"], +) + +class TransactionStatus(enum.IntEnum): + APPROVED = 0 + DECLINED = 1 + +class TransactionType(enum.IntEnum): + CHARGE = 0 + AUTHORIZE = 1 + CAPTURE = 3 + + + +# --- NEW CIM CORE FUNCTIONS --- +STATE_ID_TO_ABBREVIATION = { +0: "MA", +1: "RI", +2: "NH", +3: "ME", +4: "VT", +5: "CT", +6: "NY" +} + +def _parse_authnet_response(response: Optional[AuthNetResponse]) -> Tuple[TransactionStatus, Optional[str], Optional[str]]: + """ + Parse Authorize.net response with proper attribute access for SDK objects. + Authorize.net response objects don't have .text properties, they're direct attributes. + """ + print(f"DEBUG: Parsing response, type: {type(response)}") + print(f"DEBUG: Response exists: {response is not None}") + + if response is not None: + print("DEBUG: Checking for messages attribute...") + if hasattr(response, 'messages'): + print(f"DEBUG: Messages exist, resultCode: {getattr(response.messages, 'resultCode', 'NO resultCode')}") + else: + print("DEBUG: No messages attribute") + + if response.messages.resultCode == "Ok": + print("DEBUG: Taking APPROVED path") + status = TransactionStatus.APPROVED + auth_net_transaction_id = None + + # Extract transaction ID with proper error handling + try: + if hasattr(response, 'transactionResponse') and response.transactionResponse: + if hasattr(response.transactionResponse, 'transId') and response.transactionResponse.transId: + auth_net_transaction_id = str(response.transactionResponse.transId) + print(f"DEBUG: FOUND transaction ID: {auth_net_transaction_id}") + else: + print("DEBUG: transactionResponse exists but no transId") + else: + print("DEBUG: No transactionResponse in approved response") + except Exception as e: + print(f"DEBUG: Exception extracting transaction ID: {e}") + print(f"DEBUG: Response object inspection:") + print(type(response)) + if hasattr(response, 'transactionResponse'): + print(f"TransactionResponse type: {type(response.transactionResponse)}") + print(dir(response.transactionResponse)) + + rejection_reason = None + print(f"DEBUG: APPROVED - ID: {auth_net_transaction_id}, rejection: {rejection_reason}") + + else: + print("DEBUG: Taking DECLINED path") + status = TransactionStatus.DECLINED + auth_net_transaction_id = None + rejection_reason = "Payment declined by gateway." + + if response is not None: + # Handle transaction response errors + if hasattr(response, 'transactionResponse') and response.transactionResponse: + if hasattr(response.transactionResponse, 'errors') and response.transactionResponse.errors: + print("DEBUG: Using transactionResponse.errors") + try: + error = response.transactionResponse.errors[0] + # Remove the .text access - use direct attributes + error_code = getattr(error, 'errorCode', 'Unknown') + error_text = getattr(error, 'errorText', 'Unknown error') + rejection_reason = f"{error_code}: {error_text}" + print(f"DEBUG: Transaction error: {rejection_reason}") + except Exception as e: + print(f"DEBUG: Exception parsing transaction error: {e}") + rejection_reason = "Failed to parse transaction error" + + # Handle message-level errors + elif hasattr(response, 'messages') and response.messages: + if hasattr(response.messages, 'message') and response.messages.message: + print("DEBUG: Using response.messages.message") + try: + msg = response.messages.message + if isinstance(msg, list): + msg = msg[0] if msg else None + if msg: + code = getattr(msg, 'code', 'Unknown') + text = getattr(msg, 'text', 'Unknown error') + rejection_reason = f"{code}: {text}" + print(f"DEBUG: Message error: {rejection_reason}") + except Exception as e: + print(f"DEBUG: Exception parsing message error: {e}") + rejection_reason = "Failed to parse message error" + + print(f"DEBUG: FINAL RESULT - Status: {status}, ID: {auth_net_transaction_id}, Reason: {rejection_reason}") + return status, auth_net_transaction_id, rejection_reason + +@router.post("/customers/{customer_id}/cards", summary="Add a new payment card for a customer") +def add_card_to_customer(customer_id: int, card_info: schemas.CardCreate, db: Session = Depends(database.get_db)): + """ + Adds a new credit card to a customer. + - If the customer doesn't have an Authorize.Net profile, it creates one. + - If they do, it adds a new payment method to their existing profile. + Returns the payment_profile_id from Authorize.Net. + """ + db_customer = crud.get_customer(db, customer_id=customer_id) + if not db_customer: + raise HTTPException(status_code=404, detail="Customer not found") + + # We still need this schema for the payment service call + customer_schema = schemas.Customer.from_orm(db_customer) + + payment_profile_id = None + + try: + # This part now works because the service hard-codes the state to "MA" + if not db_customer.auth_net_profile_id: + profile_id, payment_id = payment_service.create_customer_profile( + customer=customer_schema, card_info=card_info + ) + crud.update_customer_auth_net_profile_id(db, customer_id=customer_id, profile_id=profile_id) + payment_profile_id = payment_id + else: + payment_profile_id = payment_service.add_payment_profile_to_customer( + customer_profile_id=db_customer.auth_net_profile_id, + customer=customer_schema, + card_info=card_info + ) + + # Return the payment_profile_id + return {"payment_profile_id": payment_profile_id} + + except ValueError as e: + # This will catch errors from the payment service + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + # This will catch any other unexpected errors, like from the database + raise HTTPException(status_code=500, detail="An internal server error occurred.") + + +@router.put("/customers/{customer_id}/cards/{card_id}", summary="Update an existing payment card for a customer") +def update_card_for_customer(customer_id: int, card_id: int, card_info: schemas.CardCreate, db: Session = Depends(database.get_db)): + """ + Updates an existing credit card for a customer. + - If the card has an existing Authorize.Net payment profile, deletes it first. + - Creates a new payment profile with updated card information. + - Updates the database card record with the new payment_profile_id. + Returns the new payment_profile_id from Authorize.Net. + """ + db_customer = crud.get_customer(db, customer_id=customer_id) + if not db_customer: + raise HTTPException(status_code=404, detail="Customer not found") + + db_card = crud.get_card_by_id(db, card_id=card_id) + if not db_card or db_card.user_id != customer_id: + raise HTTPException(status_code=404, detail="Card not found for this customer") + + if not db_customer.auth_net_profile_id: + raise HTTPException(status_code=400, detail="Customer does not have an Authorize.Net profile") + + customer_schema = schemas.Customer.from_orm(db_customer) + + try: + # If payment profile ID is null, refresh from Authorize.Net to sync + if not db_card.auth_net_payment_profile_id: + logger.info(f"Payment profile ID is null for card {card_id}, refreshing from Authorize.Net") + from ..services import user_create + user_create.refresh_customer_payment_profiles(db, customer_id, db_customer.auth_net_profile_id) + + # Re-fetch the card to get the updated payment profile ID + db.refresh(db_card) + logger.info(f"After refresh, card {card_id} has payment_profile_id: {db_card.auth_net_payment_profile_id}") + + # Delete existing payment profile if it exists + if db_card.auth_net_payment_profile_id: + from ..services import user_delete + delete_success = user_delete._delete_payment_profile( + db_customer.auth_net_profile_id, db_card.auth_net_payment_profile_id + ) + if delete_success: + logger.info(f"Successfully deleted old payment profile {db_card.auth_net_payment_profile_id} for card {card_id}") + # Clear the payment profile ID since it was deleted + db_card.auth_net_payment_profile_id = None + db.add(db_card) + db.commit() + else: + logger.warning(f"Failed to delete old payment profile {db_card.auth_net_payment_profile_id} for card {card_id}") + + # Create new payment profile with updated card information + new_payment_profile_id = payment_service.add_payment_profile_to_customer( + customer_profile_id=db_customer.auth_net_profile_id, + customer=customer_schema, + card_info=card_info + ) + + # Update the database card with the new payment profile ID + db_card.auth_net_payment_profile_id = new_payment_profile_id + db.add(db_card) + db.commit() + + logger.info(f"Successfully updated card {card_id} with new payment profile {new_payment_profile_id}") + + # Return the new payment_profile_id + return {"payment_profile_id": new_payment_profile_id} + + except ValueError as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + db.rollback() + print(f"Failed to update card {card_id}: {str(e)}") + raise HTTPException(status_code=500, detail="An internal server error occurred.") + +@router.post("/charge/saved-card/{customer_id}", response_model=schemas.Transaction, summary="Charge a customer using a saved card") +def charge_saved_card(customer_id: int, transaction_req: schemas.TransactionCreateByCardID, db: Session = Depends(database.get_db)): + db_customer = crud.get_customer(db, customer_id=customer_id) + db_card = crud.get_card_by_id(db, card_id=transaction_req.card_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") + + if not db_customer.auth_net_profile_id or not db_card.auth_net_payment_profile_id: + raise HTTPException(status_code=400, detail="Payment profile is not set up correctly for this customer/card") + + auth_net_response = payment_service.charge_customer_profile( + customer_profile_id=db_customer.auth_net_profile_id, + payment_profile_id=db_card.auth_net_payment_profile_id, + transaction_req=transaction_req + ) + + status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response) + + transaction_data = schemas.TransactionBase( + charge_amount=transaction_req.charge_amount, + transaction_type=TransactionType.CHARGE, + service_id=transaction_req.service_id, + delivery_id=transaction_req.delivery_id, + card_id=transaction_req.card_id, + rejection_reason=rejection_reason + ) + + return crud.create_transaction(db=db, transaction=transaction_data, customer_id=customer_id, status=status, auth_net_transaction_id=auth_net_transaction_id) + + + + +@router.post("/authorize/saved-card/{customer_id}", response_model=schemas.Transaction, summary="Authorize a payment on a saved card") +def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionAuthorizeByCardID, db: Session = Depends(database.get_db)): + """ + Creates a pre-authorization on a customer's saved card. + This validates the card and reserves the funds. + """ + print("🐛 DEBUG: ENTERING authorize_saved_card ROUTER FUNCTION") + + db_customer = crud.get_customer(db, customer_id=customer_id) + db_card = crud.get_card_by_id(db, card_id=transaction_req.card_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") + + print("🐛 DEBUG: CUSTOMER AND CARD FOUND, STARTING PRE-VALIDATION") + + # 🚨 ENHANCED PRE-TRANSACTION VALIDATION 🚨 + # Proactively check and fix payment profile issues before attempting transaction + print(f"🐛 DEBUG: Starting enhanced pre-validation for customer {customer_id}, card {db_card.id}") + logger.info(f"🔍 PRE-TRANSACTION CHECK: Customer {customer_id}, Card {db_card.id}") + logger.info(f"🔍 Current auth_net_profile_id: '{db_customer.auth_net_profile_id}'") + logger.info(f"🔍 Current payment_profile_id: '{db_card.auth_net_payment_profile_id}'") + + # Check for missing payment profiles OR test validity of existing ones + needs_recovery = False + print(f"🐛 DEBUG: Checking payment profiles - customer_id: {db_customer.auth_net_profile_id}, card_id: {db_card.auth_net_payment_profile_id}") + + if (not db_customer.auth_net_profile_id or + not db_card.auth_net_payment_profile_id or + db_card.auth_net_payment_profile_id.strip() == "" or + str(db_card.auth_net_payment_profile_id).lower() == "none" or + db_card.auth_net_payment_profile_id is None): + # Missing/null payment profile - needs recovery + needs_recovery = True + print("🐛 DEBUG: NULL/MISSING PAYMENT PROFILE DETECTED") + logger.warning(f"🔧 NULL/MISSING PAYMENT PROFILE DETECTED - Triggering auto-recovery") + else: + # Payment profile exists in DB, but let's test if it's valid in Authorize.net + print(f"🐛 DEBUG: Payment profile exists, testing validity in Authorize.net...") + logger.info(f"🔍 Payment profile exists, testing validity in Authorize.net...") + try: + # Quick test: try to retrieve customer payment profiles to see if our ID is valid + print(f"🐛 DEBUG: Calling get_customer_payment_profiles for profile_id: {db_customer.auth_net_profile_id}") + test_profiles = payment_service.get_customer_payment_profiles(db_customer.auth_net_profile_id) + print(f"🐛 DEBUG: Got profiles from Authorize.net: {test_profiles}") + current_id = str(db_card.auth_net_payment_profile_id) + profile_ids_as_strings = [str(pid) for pid in test_profiles if pid] + print(f"🐛 DEBUG: Checking if '{current_id}' is in: {profile_ids_as_strings}") + + if current_id not in profile_ids_as_strings: + needs_recovery = True + print(f"🐛 DEBUG: PAYMENT PROFILE {current_id} NOT FOUND - NEEDS RECOVERY") + logger.warning(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} NOT FOUND IN AUTHORIZE.NET - Invalid ID!") + logger.warning(f"🔧 Available profiles in Authorize.net: {test_profiles}") + else: + print(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} exists in Authorize.net, testing usability...") + # Profile exists in Authorize.net, but test if it's actually USABLE + # by doing a quick test authorization with minimal amount + try: + print(f"🐛 DEBUG: Testing if payment profile is actually usable...") + # Create a tiny test transaction (like $0.01) to validate the card works + test_transaction_req = schemas.TransactionAuthorizeByCardID( + card_id=db_card.id, + preauthorize_amount="0.01" # Minimal test amount + ) + + test_response = payment_service.authorize_customer_profile( + customer_profile_id=db_customer.auth_net_profile_id, + payment_profile_id=db_card.auth_net_payment_profile_id, + transaction_req=test_transaction_req, + db_session=None, # Don't pass DB session for test transaction + customer_id=None, + card_id=None + ) + + # Check if the test authorization worked + from ..services import payment_service as ps # Need access to _parse_authnet_response + test_status, _, test_reason = _parse_authnet_response(test_response) + + if "E00121" in str(test_reason) or test_status == 1: # 1 = DECLINED + print(f"🐛 DEBUG: TEST AUTH FAILED - Payment profile exists but is INVALID!") + needs_recovery = True + logger.warning(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} EXISTS BUT IS UNUSABLE - E00121 detected during test!") + logger.warning(f"🔧 Test transaction failed: {test_reason}") + else: + print(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} is VALID and USABLE") + logger.info(f"✅ Payment profile {db_card.auth_net_payment_profile_id} is valid and usable in Authorize.net") + + except Exception as e: + print(f"🐛 DEBUG: Exception during usability test: {str(e)} - assuming profile needs recreation") + needs_recovery = True + logger.warning(f"🔧 Could not test payment profile usability: {str(e)} - assuming it needs recreation") + except Exception as e: + print(f"🐛 DEBUG: Exception during profile validation: {str(e)}") + logger.warning(f"🔧 Could not verify payment profile validity in Authorize.net: {str(e)}") + # If we can't verify, assume it's okay and let the transaction proceed + # (better to try and fail than to block legitimate transactions) + logger.info(f"⚠️ Unable to verify profile validity - proceeding with transaction anyway") + + if needs_recovery: + logger.warning(f"🔧 DETECTED PAYMENT PROFILE ISSUE - Triggering auto-recovery for customer {customer_id}") + + # Auto-recover: Refresh payment profiles before transaction + from ..services import user_create + recovery_success = user_create.refresh_customer_payment_profiles( + db, customer_id, db_customer.auth_net_profile_id + ) + + if recovery_success: + logger.info("✅ Auto-recovery successful - proceeding with transaction") + + # Re-fetch card data to get updated payment profile ID + db.refresh(db_card) + db.refresh(db_customer) + logger.info(f"🔍 After recovery - payment_profile_id: '{db_card.auth_net_payment_profile_id}'") + else: + logger.error("❌ Auto-recovery failed - cannot proceed with transaction") + raise HTTPException( + status_code=400, + detail="Payment profile setup error detected and auto-recovery failed. Please contact support." + ) + + # Final validation before proceeding + if not db_customer.auth_net_profile_id or not db_card.auth_net_payment_profile_id: + logger.error(f"❌ CRITICAL: Payment profile validation failed after recovery attempt") + raise HTTPException( + status_code=400, + detail="Payment profile is not set up correctly for this customer/card" + ) + + logger.info(f"✅ Payment profile validation passed - proceeding with authorization") + + # 🚨 ENHANCED E00121 ERROR HANDLING 🚨 + # If transaction still fails with E00121 despite pre-validation, force-nuke the problematic ID + auth_net_response = payment_service.authorize_customer_profile( + customer_profile_id=db_customer.auth_net_profile_id, + payment_profile_id=db_card.auth_net_payment_profile_id, + transaction_req=transaction_req, + db_session=db, + customer_id=customer_id, + card_id=db_card.id + ) + + # Parse the transaction response (no need for E00121 nuclear cleanup since pre-validation should have caught it) + transaction_status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response) + + print(transaction_req) + transaction_data = schemas.TransactionBase( + preauthorize_amount=transaction_req.preauthorize_amount, + transaction_type=TransactionType.AUTHORIZE, # This is key + service_id=transaction_req.service_id, + delivery_id=transaction_req.delivery_id, + auto_id=transaction_req.auto_id, + card_id=transaction_req.card_id, + rejection_reason=rejection_reason + ) + + db_transaction = crud.create_transaction( + db=db, + transaction=transaction_data, + customer_id=customer_id, + status=transaction_status, + auth_net_transaction_id=auth_net_transaction_id + ) + + return db_transaction + + +def _nuclear_e00121_payment_profile_cleanup(db: Session, customer_id: int, card_id: int, corrupted_profile_id: str, customer_profile_id: str) -> bool: + """ + DEDICATED E00121 NUKER: Forcefully removes problematic payment profile IDs from both database and Authorize.net. + This is the nuclear option for when E00121 payment profile errors persist despite all attempts to recover. + + Args: + db: Database session + customer_id: Customer ID + card_id: Card ID that has the corrupted profile + corrupted_profile_id: The problematic payment profile ID causing E00121 + customer_profile_id: Customer profile ID in Authorize.net + + Returns: + bool: True if cleanup successful and card can be retried + """ + print("💣 NUCLEAR E00121 CLEANUP INITIATED! 💣") + print(f"💣 Target: Card {card_id}, Profile {corrupted_profile_id}") + + try: + from .. import crud + from ..services import user_delete + + # Step 1: Get the card from database + card = crud.get_card_by_id(db, card_id) + if not card: + print(f"💣 ERROR: Card {card_id} not found in database") + return False + + # Step 2: NUKE FROM AUTHORIZE.NET FIRST + print(f"💣 Deleting corrupted profile {corrupted_profile_id} from Authorize.net...") + try: + delete_success = user_delete._delete_payment_profile(customer_profile_id, corrupted_profile_id) + if delete_success: + print("✅ Successfully deleted corrupted profile from Authorize.net") + else: + print("⚠️ Profile may not have existed in Authorize.net or delete failed") + except Exception as e: + print(f"⚠️ Exception deleting from Authorize.net: {str(e)} - continuing anyway") + + # Step 3: NUKE FROM DATABASE REGARDLESS + print(f"💣 Clearing corrupted profile ID {corrupted_profile_id} from database card {card_id}") + card.auth_net_payment_profile_id = None + db.add(card) + db.commit() + print("✅ Successfully cleared corrupted profile ID from database") + + # Step 4: Attempt immediate recreation of payment profile + print("💣 Attempting immediate recreation of payment profile...") + try: + customer = crud.get_customer(db, customer_id) + + # Format card data for recreation + # Convert to string first to handle cases where database returns int instead of string + exp_year_str = str(card.expiration_year) + exp_month_str = str(card.expiration_month) + exp_year = exp_year_str.zfill(4) if len(exp_year_str) < 4 else exp_year_str + exp_month = exp_month_str.zfill(2) if len(exp_month_str) < 2 else exp_month_str + exp_date = f"{exp_year}-{exp_month}" + + card_create_data = schemas.CardCreate( + card_number=card.card_number, + expiration_date=exp_date, + cvv=card.security_number + ) + + from ..services import payment_service + new_profile_id = payment_service.add_payment_profile_to_customer( + customer_profile_id, customer, card_create_data, is_default=(card.main_card == True) + ) + + if new_profile_id: + print(f"✅ IMMEDIATE RECREATION SUCCESSFUL: New profile {new_profile_id}") + card.auth_net_payment_profile_id = str(new_profile_id) + db.add(card) + db.commit() + print(f"💣 Nuclear cleanup COMPLETE - Card {card_id} has new profile {new_profile_id}") + return True + else: + print("❌ Immediate recreation failed - no new profile ID") + return False + + except Exception as recreate_e: + print(f"❌ Immediate recreation failed: {str(recreate_e)}") + print("💣 Database cleared but recreation failed - needs manual recreation later") + return False + + except Exception as nuke_e: + print(f"💣 CRITICAL ERROR in nuclear cleanup: {str(nuke_e)}") + db.rollback() + return False + +# --- CAPTURE ENDPOINT MOVED TO TRANSACTION ROUTER --- diff --git a/app/services/check_user_service.py b/app/services/check_user_service.py index 2de54c2..f407bc4 100644 --- a/app/services/check_user_service.py +++ b/app/services/check_user_service.py @@ -4,21 +4,32 @@ from .. import crud, database, schemas from config import load_config from sqlalchemy.orm import Session -# Load Authorize.net credentials -ApplicationConfig = load_config() -API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID -TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY + # Set Authorize.net environment based on configuration from authorizenet.constants import constants +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 + 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 + + -constants.show_url_on_request = True def verify_customer_authorize_account(db: Session, customer_id: int) -> dict: """ @@ -173,8 +184,13 @@ def _get_customer_profile(profile_id: str): ) controller = controllers.getCustomerProfileController(request) - controller.setenvironment(constants.PRODUCTION) - controller.execute() + + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': + controller.setenvironment(constants.PRODUCTION) + controller.execute() + else: + controller.execute() + response = controller.getresponse() return response diff --git a/app/services/payment_service.py b/app/services/payment_service.py index c92a612..a01cdbb 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -20,20 +20,48 @@ 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' + API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID + TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY 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 _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 _get_authnet_error_message(response): """ @@ -67,24 +95,11 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC 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] @@ -102,30 +117,21 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC controller = createCustomerProfileController(request) - try: + + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': 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") + else: + controller.execute() + response = controller.getresponse() + + 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 = "" + # # 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 @@ -149,102 +155,6 @@ 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, 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. @@ -331,8 +241,13 @@ def _perform_authorization(customer_profile_id: str, payment_profile_id: str, tr ) controller = createTransactionController(createtransactionrequest) - controller.setenvironment(constants.PRODUCTION) - controller.execute() + + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': + controller.setenvironment(constants.PRODUCTION) + controller.execute() + else: + controller.execute() + response = controller.getresponse() # Log response details @@ -345,37 +260,7 @@ def _perform_authorization(customer_profile_id: str, payment_profile_id: str, tr 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): @@ -396,11 +281,115 @@ def capture_authorized_transaction(transaction_req: schemas.TransactionCapture): ) controller = createTransactionController(createtransactionrequest) - controller.setenvironment(constants.PRODUCTION) - controller.execute() + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': + 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): + 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: + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': + controller.setenvironment(constants.PRODUCTION) + controller.execute() + else: + 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 get_customer_payment_profiles(customer_profile_id: str): """ @@ -420,8 +409,12 @@ def get_customer_payment_profiles(customer_profile_id: str): controller = getCustomerProfileController(request) try: - controller.setenvironment(constants.PRODUCTION) - controller.execute() + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': + controller.setenvironment(constants.PRODUCTION) + controller.execute() + else: + controller.execute() + response = controller.getresponse() if response.messages.resultCode == "Ok": @@ -467,7 +460,11 @@ def charge_customer_profile(customer_profile_id: str, payment_profile_id: str, t ) controller = createTransactionController(createtransactionrequest) - controller.setenvironment(constants.PRODUCTION) - controller.execute() + if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': + controller.setenvironment(constants.PRODUCTION) + controller.execute() + else: + controller.execute() + # The response is returned directly to the router to be parsed there return controller.getresponse() diff --git a/app/services/user_create.py b/app/services/user_create.py index 5c4061c..df008f6 100644 --- a/app/services/user_create.py +++ b/app/services/user_create.py @@ -22,51 +22,18 @@ ApplicationConfig = load_config() # Set Authorize.net environment based on configuration if ApplicationConfig.CURRENT_SETTINGS == 'PRODUCTION': #constants.environment = constants.PRODUCTION - print("Constants:", ApplicationConfig.CURRENT_SETTINGS) - print("Authorize.Net URL:", constants.environment) - API_LOGIN_ID = '4d2Mn6H23R' - TRANSACTION_KEY = '7B94d8xfTQXv37WS' + API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID + TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY # Override to standard production endpoint - constants.environment = "https://api.authorize.net/xml/v1/request.api" - print("Overridden Authorize.Net URL:", constants.environment) else: - print("Constants:", ApplicationConfig.CURRENT_SETTINGS) constants.environment = constants.SANDBOX - print("Authorize.Net URL:", constants.environment) API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY -constants.show_url_on_request = True + constants.show_url_on_request = True -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: """ @@ -92,7 +59,7 @@ def create_user_account(db: Session, customer_id: int) -> dict: "message": f"Customer {customer_id} not found", "profile_id": None } - print(customer) + print(f"Found customer id={customer}") # Get customer's cards from database cards = crud.get_customer_cards(db, customer_id) if not cards: @@ -107,9 +74,9 @@ def create_user_account(db: Session, customer_id: int) -> dict: # 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}") + print(f"Using first card ID={first_card.id} for profile creation") + print(f"Card Number: {first_card.card_number[:4]}**** ****{first_card.card_number[-4:]}") + print(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 @@ -125,8 +92,8 @@ def create_user_account(db: Session, customer_id: int) -> dict: 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") + print(f"Card info expiration_date: {card_info.expiration_date}") + print(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: @@ -135,7 +102,7 @@ def create_user_account(db: Session, customer_id: int) -> dict: auth_profile_id, _ = payment_service.create_customer_profile(customer, card_info) except ValueError as e: error_str = str(e) - logger.error(f"API call failed: {error_str}") + print(f"API call failed: {error_str}") return { "success": False, @@ -146,7 +113,7 @@ def create_user_account(db: Session, customer_id: int) -> dict: } if not auth_profile_id: - logger.error("No auth_profile_id returned from API") + print("No auth_profile_id returned from API") return { "success": False, "message": "Failed to create customer profile - no profile ID returned", @@ -158,12 +125,12 @@ def create_user_account(db: Session, customer_id: int) -> dict: 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)") + print(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)}") + print(f"Failed to add payment profile for first card: {str(e)}") return { "success": False, "message": f"Failed to add first payment profile: {str(e)}", @@ -190,19 +157,19 @@ def create_user_account(db: Session, customer_id: int) -> dict: 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}") + print(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)}") + print(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}") + print(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}") + print(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)}") + print(f"Failed to retrieve payment profiles: {str(e)}") return { "success": False, "message": f"Failed to retrieve payment profiles: {str(e)}", @@ -211,7 +178,7 @@ def create_user_account(db: Session, customer_id: int) -> dict: # 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") + print(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)})") @@ -222,10 +189,10 @@ def create_user_account(db: Session, customer_id: int) -> dict: 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}") + print(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}") + print(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: @@ -234,35 +201,35 @@ def create_user_account(db: Session, customer_id: int) -> dict: # Commit all changes db.commit() - logger.info(f"Successfully committed payment profile IDs to database ({cards_updated} cards updated)") + print(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:") + print("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}'") + print(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") + print(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!") + print("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") + print(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]}") + print(f"🔄 Auto-refresh START: customer_id={customer_id}, auth_profile_id={auth_profile_id}") + print(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") + print(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]}") + print(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, @@ -272,7 +239,7 @@ def create_user_account(db: Session, customer_id: int) -> dict: } except Exception as e: - logger.error(f"Critical exception during user account creation for customer {customer_id}: {traceback.format_exc()}") + print(f"Critical exception during user account creation for customer {customer_id}: {traceback.format_exc()}") db.rollback() return { "success": False, @@ -303,70 +270,74 @@ def refresh_customer_payment_profiles(db: Session, customer_id: int, auth_profil # Get customer's current cards from database cards_before = crud.get_customer_cards(db, customer_id) customer = crud.get_customer(db, customer_id) - logger.info(f"🔄 Refresh START: customer_id={customer_id}, profile_id={auth_profile_id}, current cards={len(cards_before)}") + print(f"🔄 Refresh START: customer_id={customer_id}, profile_id={auth_profile_id}, current cards={len(cards_before)}") # STEP 1: Try to get actual payment profiles from Authorize.net payment_profile_ids = [] try: payment_profile_ids = payment_service.get_customer_payment_profiles(auth_profile_id) - logger.info(f"🔄 Retrieved {len(payment_profile_ids)} payment profiles from Authorize.net: {payment_profile_ids}") + print(f"🔄 Retrieved {len(payment_profile_ids)} payment profiles from Authorize.net: {payment_profile_ids}") except Exception as e: logger.warning(f"🔄 Could not retrieve payment profiles from Authorize.net: {str(e)}") - logger.info("🔄 Will attempt to recreate missing payment profiles") + print("🔄 Will attempt to recreate missing payment profiles") # STEP 2: Check if we have enough payment profiles for our cards cards_need_update = [] for card in cards_before: if not card.auth_net_payment_profile_id or card.auth_net_payment_profile_id.strip() == "": cards_need_update.append(card) - logger.info(f"🔄 Card {card.id} needs payment profile ID assignment") + print(f"🔄 Card {card.id} needs payment profile ID assignment") elif str(card.auth_net_payment_profile_id) not in [str(pid) for pid in payment_profile_ids if pid]: # Payment profile ID exists in DB but not found in Authorize.net - likely invalid cards_need_update.append(card) logger.warning(f"🔄 Card {card.id} has payment profile ID {card.auth_net_payment_profile_id} but it's not found in Authorize.net - NEEDS RECREATION") else: - # Profile exists in Authorize.net, but let's double-check it's usable by doing a quick test - logger.info(f"🔄 Card {card.id} has payment profile ID {card.auth_net_payment_profile_id} in Authorize.net - testing usability...") - try: - test_req = schemas.TransactionAuthorizeByCardID( - card_id=card.id, - preauthorize_amount="0.01" - ) - test_response = payment_service.authorize_customer_profile( - customer_profile_id=customer.auth_net_profile_id, - payment_profile_id=card.auth_net_payment_profile_id, - transaction_req=test_req, - db_session=None, customer_id=None, card_id=None - ) + if ApplicationConfig.penny_test_transaction: + # Profile exists in Authorize.net, but let's double-check it's usable by doing a quick test + print(f"🔄 Card {card.id} has payment profile ID {card.auth_net_payment_profile_id} in Authorize.net - testing usability...") + try: + test_req = schemas.TransactionAuthorizeByCardID( + card_id=card.id, + preauthorize_amount="0.01" + ) + test_response = payment_service.authorize_customer_profile( + customer_profile_id=customer.auth_net_profile_id, + payment_profile_id=card.auth_net_payment_profile_id, + transaction_req=test_req, + db_session=None, customer_id=None, card_id=None + ) - # Check test result - _, _, test_reason = payment_service._parse_authnet_response(test_response) - if "E00121" in str(test_reason): - cards_need_update.append(card) - logger.warning(f"🔄 Card {card.id} has profile {card.auth_net_payment_profile_id} that EXISTS but is CORRUPTED - NEEDS RECREATION") - # Explicitly delete the corrupted profile first - try: - from . import user_delete - delete_success = user_delete._delete_payment_profile(customer.auth_net_profile_id, card.auth_net_payment_profile_id) - if delete_success: - logger.info(f"🔄 Successfully deleted corrupted payment profile {card.auth_net_payment_profile_id}") - card.auth_net_payment_profile_id = None - db.add(card) - except Exception as del_e: - logger.warning(f"🔄 Failed to delete corrupted profile {card.auth_net_payment_profile_id}: {str(del_e)}") - else: - logger.debug(f"🔄 Card {card.id} has valid and usable payment profile ID {card.auth_net_payment_profile_id}") - except Exception as test_e: - logger.warning(f"🔄 Could not test usability of profile {card.auth_net_payment_profile_id} for card {card.id}: {str(test_e)} - assuming it's okay") + # Check test result + _, _, test_reason = payment_service._parse_authnet_response(test_response) + if "E00121" in str(test_reason): + cards_need_update.append(card) + logger.warning(f"🔄 Card {card.id} has profile {card.auth_net_payment_profile_id} that EXISTS but is CORRUPTED - NEEDS RECREATION") + # Explicitly delete the corrupted profile first + try: + from . import user_delete + delete_success = user_delete._delete_payment_profile(customer.auth_net_profile_id, card.auth_net_payment_profile_id) + if delete_success: + print(f"🔄 Successfully deleted corrupted payment profile {card.auth_net_payment_profile_id}") + card.auth_net_payment_profile_id = None + db.add(card) + except Exception as del_e: + logger.warning(f"🔄 Failed to delete corrupted profile {card.auth_net_payment_profile_id}: {str(del_e)}") + else: + logger.debug(f"🔄 Card {card.id} has valid and usable payment profile ID {card.auth_net_payment_profile_id}") + except Exception as test_e: + logger.warning(f"🔄 Could not test usability of profile {card.auth_net_payment_profile_id} for card {card.id}: {str(test_e)} - assuming it's okay") + else: + print(f"🔄 Skipping penny test transaction for card {card.id} (disabled in config)") + logger.debug(f"🔄 Card {card.id} has payment profile ID {card.auth_net_payment_profile_id} in Authorize.net - skipping usability test (config disabled)") # STEP 3: If we don't have enough valid payment profiles, recreate missing ones if len(cards_need_update) > 0: - logger.info(f"🔄 Need to recreate {len(cards_need_update)} payment profiles") + print(f"🔄 Need to recreate {len(cards_need_update)} payment profiles") # Clear payment profile IDs for cards that need recreation (they're invalid anyway) for card in cards_need_update: if card.auth_net_payment_profile_id: - logger.info(f"🔄 Clearing invalid payment profile ID {card.auth_net_payment_profile_id} for card {card.id}") + print(f"🔄 Clearing invalid payment profile ID {card.auth_net_payment_profile_id} for card {card.id}") card.auth_net_payment_profile_id = None db.add(card) @@ -388,7 +359,7 @@ def refresh_customer_payment_profiles(db: Session, customer_id: int, auth_profil cvv=card.security_number ) - logger.info(f"🔄 Recreating payment profile for card {card.id} (**** **** **** {card.last_four_digits})") + print(f"🔄 Recreating payment profile for card {card.id} (**** **** **** {card.last_four_digits})") new_payment_profile_id = payment_service.add_payment_profile_to_customer( auth_profile_id, customer, card_create_data, is_default=(card.main_card == True) ) @@ -397,43 +368,43 @@ def refresh_customer_payment_profiles(db: Session, customer_id: int, auth_profil card.auth_net_payment_profile_id = str(new_payment_profile_id) db.add(card) recreated_cards.append(card) - logger.info(f"✅ Successfully recreated payment profile {new_payment_profile_id} for card {card.id}") + print(f"✅ Successfully recreated payment profile {new_payment_profile_id} for card {card.id}") else: - logger.error(f"❌ Failed to recreate payment profile for card {card.id} - no ID returned") + print(f"❌ Failed to recreate payment profile for card {card.id} - no ID returned") except Exception as e: - logger.error(f"❌ Failed to recreate payment profile for card {card.id}: {str(e)}") + print(f"❌ Failed to recreate payment profile for card {card.id}: {str(e)}") continue if recreated_cards: db.commit() - logger.info(f"✅ Successfully recreated and saved {len(recreated_cards)} payment profiles") + print(f"✅ Successfully recreated and saved {len(recreated_cards)} payment profiles") else: - logger.error("❌ No payment profiles could be recreated - this is a critical failure") + print("❌ No payment profiles could be recreated - this is a critical failure") return False else: - logger.info(f"🔄 All {len(cards_before)} cards have valid payment profile IDs") + print(f"🔄 All {len(cards_before)} cards have valid payment profile IDs") # STEP 4: Final verification that everything looks good cards_final = crud.get_customer_cards(db, customer_id) - logger.info("🔄 FINAL VERIFICATION:") + print("🔄 FINAL VERIFICATION:") all_valid = True for card in cards_final: status = "✅ VALID" if card.auth_net_payment_profile_id and card.auth_net_payment_profile_id.strip() else "❌ INVALID" - logger.info(f"🔄 {status} Card {card.id}: auth_net_payment_profile_id='{card.auth_net_payment_profile_id}'") + print(f"🔄 {status} Card {card.id}: auth_net_payment_profile_id='{card.auth_net_payment_profile_id}'") if not card.auth_net_payment_profile_id or card.auth_net_payment_profile_id.strip() == "": all_valid = False if all_valid: - logger.info(f"🔄 Refresh COMPLETE: All {len(cards_final)} cards have valid payment profile IDs") + print(f"🔄 Refresh COMPLETE: All {len(cards_final)} cards have valid payment profile IDs") return True else: logger.warning("🔄 Refresh PARTIAL: Some cards may still have invalid payment profile IDs") return False except Exception as e: - logger.error(f"🔄 Refresh FAILED for customer {customer_id}: {str(e)}") - logger.error(f"🔄 Refresh traceback: {traceback.format_exc()}") + print(f"🔄 Refresh FAILED for customer {customer_id}: {str(e)}") + print(f"🔄 Refresh traceback: {traceback.format_exc()}") db.rollback() return False diff --git a/settings_local.py b/settings_local.py index 572bc99..a75dcbb 100644 --- a/settings_local.py +++ b/settings_local.py @@ -31,3 +31,6 @@ class ApplicationConfig: # Authorize.net credentials API_LOGIN_ID = 'bizdev05' TRANSACTION_KEY = '4kJd237rZu59qAZd' + + # Payment processing flags + penny_test_transaction = False # Run penny test in development diff --git a/settings_prod.py b/settings_prod.py index c39733b..ff1e055 100644 --- a/settings_prod.py +++ b/settings_prod.py @@ -25,3 +25,6 @@ class ApplicationConfig: # Authorize.net credentials API_LOGIN_ID = '4d2Mn6H23R' TRANSACTION_KEY = '7B94d8xfTQXv37WS' + + # Payment processing flags + penny_test_transaction = False # Skip penny test in production