Files
eamco_authorize/app/routers/payment.py
Edwin Eames 1bd2b105ae Refactor imports in payment router
- Move local imports of user_create and user_delete to top level
- Clean up function bodies
2026-02-01 12:36:58 -05:00

383 lines
19 KiB
Python

"""
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
)