""" Payment Router - Main payment processing endpoints for card management and transactions. """ import logging from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from decimal import Decimal from .. import crud, models, schemas, database from ..services import payment_service, user_create, user_delete from ..services.payment_service import parse_authnet_response from ..constants import TransactionStatus, TransactionType, STATE_ID_TO_ABBREVIATION from config import load_config logger = logging.getLogger(__name__) ApplicationConfig = load_config() router = APIRouter( prefix="/payments", tags=["Payments & Transactions"], ) @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: # 1. Create the customer profile (returns ID, but no payment profile yet) profile_id = payment_service.create_customer_profile( customer=customer_schema, card_info=card_info ) # 2. Update local DB with the new profile ID crud.update_customer_auth_net_profile_id(db, customer_id=customer_id, profile_id=profile_id) # 3. Explicitly add the payment profile to get the payment_profile_id payment_profile_id = payment_service.add_payment_profile_to_customer( customer_profile_id=profile_id, customer=customer_schema, card_info=card_info ) 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.debug(f"Payment profile ID is null for card {card_id}, refreshing from Authorize.Net") 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.debug(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: delete_success = user_delete._delete_payment_profile( db_customer.auth_net_profile_id, db_card.auth_net_payment_profile_id ) if delete_success: logger.debug(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.debug(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.debug(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() logger.debug(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) # Calculate amounts to save based on status (Business Logic moved from CRUD) final_charge_amount = transaction_req.charge_amount if status == TransactionStatus.APPROVED else Decimal("0.0") final_preauth_amount = Decimal("0.0") transaction_data = schemas.TransactionBase( charge_amount=final_charge_amount, preauthorize_amount=final_preauth_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. """ logger.debug("🐛 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") logger.debug("🐛 DEBUG: CUSTOMER AND CARD FOUND, STARTING PRE-VALIDATION") # 🚨 ENHANCED PRE-TRANSACTION VALIDATION 🚨 # Proactively check and fix payment profile issues before attempting transaction logger.debug(f"🐛 DEBUG: Starting enhanced pre-validation for customer {customer_id}, card {db_card.id}") logger.debug(f"🔍 PRE-TRANSACTION CHECK: Customer {customer_id}, Card {db_card.id}") logger.debug(f"🔍 Current auth_net_profile_id: '{db_customer.auth_net_profile_id}'") logger.debug(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 logger.debug(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 logger.debug("🐛 DEBUG: NULL/MISSING PAYMENT PROFILE DETECTED") logger.debug(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 logger.debug(f"🐛 DEBUG: Payment profile exists, testing validity in Authorize.net...") logger.debug(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 logger.debug(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) logger.debug(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] logger.debug(f"🐛 DEBUG: Checking if '{current_id}' is in: {profile_ids_as_strings}") if current_id not in profile_ids_as_strings: needs_recovery = True logger.debug(f"🐛 DEBUG: PAYMENT PROFILE {current_id} NOT FOUND - NEEDS RECOVERY") logger.debug(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} NOT FOUND IN AUTHORIZE.NET - Invalid ID!") logger.debug(f"🔧 Available profiles in Authorize.net: {test_profiles}") else: if ApplicationConfig.penny_test_transaction: logger.debug(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: logger.debug(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 test_status, _, test_reason = parse_authnet_response(test_response) if "E00121" in str(test_reason) or test_status == 1: # 1 = DECLINED logger.debug(f"🐛 DEBUG: TEST AUTH FAILED - Payment profile exists but is INVALID!") needs_recovery = True logger.debug(f"🔧 PAYMENT PROFILE {db_card.auth_net_payment_profile_id} EXISTS BUT IS UNUSABLE - E00121 detected during test!") logger.debug(f"🔧 Test transaction failed: {test_reason}") else: logger.debug(f"🐛 DEBUG: Payment profile {db_card.auth_net_payment_profile_id} is VALID and USABLE") logger.debug(f"✅ Payment profile {db_card.auth_net_payment_profile_id} is valid and usable in Authorize.net") except Exception as e: logger.debug(f"🐛 DEBUG: Exception during usability test: {str(e)} - assuming profile needs recreation") needs_recovery = True logger.debug(f"🔧 Could not test payment profile usability: {str(e)} - assuming it needs recreation") else: logger.debug(f"🔍 Skipping penny test transaction (disabled in config)") except Exception as e: logger.debug(f"🐛 DEBUG: Exception during profile validation: {str(e)}") logger.debug(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.debug(f"⚠️ Unable to verify profile validity - proceeding with transaction anyway") if needs_recovery: logger.debug(f"🔧 DETECTED PAYMENT PROFILE ISSUE - Triggering auto-recovery for customer {customer_id}") # Auto-recover: Refresh payment profiles before transaction recovery_success = user_create.refresh_customer_payment_profiles( db, customer_id, db_customer.auth_net_profile_id ) if recovery_success: logger.debug("✅ 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.debug(f"🔍 After recovery - payment_profile_id: '{db_card.auth_net_payment_profile_id}'") else: logger.debug("❌ 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.debug(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.debug(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) logger.debug(transaction_req) # Calculate amounts to save based on status (Business Logic moved from CRUD) final_preauth_amount = transaction_req.preauthorize_amount if transaction_status == TransactionStatus.APPROVED else Decimal("0.0") final_charge_amount = Decimal("0.0") transaction_data = schemas.TransactionBase( preauthorize_amount=final_preauth_amount, charge_amount=final_charge_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 @router.post("/capture", response_model=schemas.Transaction, summary="Capture a previously authorized amount") def capture_authorized_amount(transaction: schemas.TransactionCapture, db: Session = Depends(database.get_db)): # This endpoint captures a previously authorized transaction # It finds the original transaction by its ID and captures the funds logger.info(f"POST /payments/capture - Capturing authorized transaction {transaction.auth_net_transaction_id}") auth_transaction = crud.get_transaction_by_auth_id(db, auth_net_transaction_id=transaction.auth_net_transaction_id) if not auth_transaction: raise HTTPException(status_code=404, detail="Authorization transaction not found") # Call the capture service function auth_net_response = payment_service.capture_authorized_transaction(transaction) status, _, rejection_reason = parse_authnet_response(auth_net_response) # Use the existing CRUD function to update the transaction return crud.update_transaction_for_capture( db=db, auth_net_transaction_id=transaction.auth_net_transaction_id, charge_amount=transaction.charge_amount, status=status, rejection_reason=rejection_reason )