from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from decimal import Decimal import requests import logging from typing import Dict, Any, Optional, Tuple from database import get_db from models import Card, Transaction, Customer_Customer, Account_User from schemas import PaymentCreate, PaymentResponse, CardCreate, CardResponse, CardUpdate from routes.auth.current_user import get_current_user from config import load_config router = APIRouter() logger = logging.getLogger(__name__) # State code to abbreviation mapping STATE_MAPPING = {0: "MA", 1: "NH"} # Add more states as needed # Authorize.net API endpoints AUTHNET_SANDBOX_URL = "https://apitest.authorize.net/xml/v1/request.api" AUTHNET_PRODUCTION_URL = "https://api.authorize.net/xml/v1/request.api" def get_auth_net_credentials() -> Tuple[str, str]: """Get Authorize.net API credentials from config.""" config = load_config() return config.AUTH_NET_API_LOGIN_ID, config.AUTH_NET_TRANSACTION_KEY def get_authnet_url() -> str: """Get the appropriate Authorize.net URL based on environment.""" config = load_config() # Use sandbox for development, production for prod if hasattr(config, 'CURRENT_SETTINGS') and config.CURRENT_SETTINGS == 'PRODUCTION': return AUTHNET_PRODUCTION_URL return AUTHNET_SANDBOX_URL def validate_payment_credentials(): """Validate that payment credentials are configured.""" config = load_config() config.validate_payment_config() def get_merchant_auth() -> Dict[str, str]: """Get merchantAuthentication block for API requests.""" api_login_id, transaction_key = get_auth_net_credentials() return { "name": api_login_id, "transactionKey": transaction_key } def make_auth_net_request(request_type: str, request_data: Dict[str, Any]) -> Dict[str, Any]: """ Make a request to Authorize.net API. Args: request_type: The API request type (e.g., 'createCustomerProfileRequest') request_data: The request-specific data (merchantAuthentication will be added) Returns: The API response as a dictionary Raises: HTTPException: If the API returns an error """ # Validate credentials are configured try: validate_payment_credentials() except ValueError as e: raise HTTPException(status_code=500, detail=str(e)) url = get_authnet_url() # Build the full request payload with merchant authentication payload = { request_type: { "merchantAuthentication": get_merchant_auth(), **request_data } } headers = {"Content-Type": "application/json"} try: response = requests.post(url, json=payload, headers=headers, timeout=30) response.raise_for_status() except requests.exceptions.Timeout: logger.error("Authorize.net API timeout") raise HTTPException(status_code=504, detail="Payment gateway timeout") except requests.exceptions.RequestException as e: logger.error(f"Authorize.net API request failed: {e}") raise HTTPException(status_code=500, detail="Payment gateway error") # Parse response - Authorize.net returns JSON with a BOM sometimes response_text = response.text.lstrip('\ufeff') try: result = requests.compat.json.loads(response_text) except ValueError: logger.error(f"Invalid JSON response from Authorize.net: {response_text[:200]}") raise HTTPException(status_code=500, detail="Invalid payment gateway response") # Check for API-level errors messages = result.get("messages", {}) if messages.get("resultCode") == "Error": error_messages = messages.get("message", []) if error_messages: error_code = error_messages[0].get("code", "Unknown") error_text = error_messages[0].get("text", "Unknown error") logger.error(f"Authorize.net error {error_code}: {error_text}") raise HTTPException(status_code=400, detail=f"Payment error: {error_text}") raise HTTPException(status_code=400, detail="Payment processing failed") return result def build_bill_to_from_customer(customer: Customer_Customer) -> Dict[str, str]: """ Build the billTo object from customer data for Authorize.net API requests. Args: customer: The customer record with address information Returns: Dictionary with billing address fields for Authorize.net """ # Map state integer to abbreviation state_abbrev = STATE_MAPPING.get(customer.customer_state, "") return { "firstName": customer.customer_first_name or "", "lastName": customer.customer_last_name or "", "company": "", # No company field in customer model currently "address": customer.customer_address or "", "city": customer.customer_town or "", "state": state_abbrev, "zip": customer.customer_zip or "", "country": "USA", "phoneNumber": customer.customer_phone_number or "" } async def get_or_create_customer_profile( db: AsyncSession, customer: Customer_Customer ) -> str: """ Get existing customer profile ID or create a new one in Authorize.net CIM. Args: db: Database session customer: The customer record Returns: The Authorize.net customer profile ID """ # Return existing profile if we have one if customer.auth_net_profile_id: return customer.auth_net_profile_id # Build email - use customer email or generate a placeholder email = customer.customer_email or f"no-email-{customer.id}@example.com" # Create new customer profile with full address information # Element order matters for Authorize.net XML schema: # merchantCustomerId, description, email, paymentProfiles, shipToList, profileType request_data = { "profile": { "merchantCustomerId": str(customer.id), "description": f"{customer.customer_first_name} {customer.customer_last_name}".strip(), "email": email, "paymentProfiles": [], "shipToList": [ { "firstName": customer.customer_first_name or "", "lastName": customer.customer_last_name or "", "address": customer.customer_address or "", "city": customer.customer_town or "", "state": STATE_MAPPING.get(customer.customer_state, ""), "zip": customer.customer_zip or "", "country": "USA", "phoneNumber": customer.customer_phone_number or "" } ] } } response = make_auth_net_request("createCustomerProfileRequest", request_data) customer_profile_id = response.get("customerProfileId") if not customer_profile_id: raise HTTPException(status_code=500, detail="Failed to create customer profile") # Save the profile ID to the customer record customer.auth_net_profile_id = customer_profile_id await db.commit() logger.info(f"Created Authorize.net customer profile {customer_profile_id} for customer {customer.id}") return customer_profile_id async def create_payment_profile( db: AsyncSession, customer_profile_id: str, card_details: CardCreate, customer: Customer_Customer, ) -> Card: """ Create a payment profile (tokenize card) in Authorize.net CIM. Args: db: Database session customer_profile_id: The Authorize.net customer profile ID card_details: The card information customer: The customer record Returns: The saved Card record with the payment profile ID """ # Build the billTo from customer data, with fallback to card name bill_to = build_bill_to_from_customer(customer) # Override firstName/lastName from card if name_on_card is provided if card_details.name_on_card: name_parts = card_details.name_on_card.split() bill_to["firstName"] = name_parts[0] if name_parts else bill_to["firstName"] bill_to["lastName"] = " ".join(name_parts[1:]) if len(name_parts) > 1 else bill_to["lastName"] # Override zip from card details if provided (card billing zip may differ from service address) if card_details.zip_code: bill_to["zip"] = card_details.zip_code # Build the payment profile request request_data = { "customerProfileId": customer_profile_id, "paymentProfile": { "billTo": bill_to, "payment": { "creditCard": { "cardNumber": card_details.card_number, "expirationDate": f"{card_details.expiration_year}-{card_details.expiration_month.zfill(2)}", "cardCode": card_details.security_number } } }, "validationMode": "testMode" # Use "liveMode" in production for $0 auth validation } response = make_auth_net_request("createCustomerPaymentProfileRequest", request_data) payment_profile_id = response.get("customerPaymentProfileId") if not payment_profile_id: raise HTTPException(status_code=500, detail="Failed to create payment profile") # Extract last four digits digits = ''.join(filter(str.isdigit, card_details.card_number)) if len(digits) < 4: raise HTTPException(status_code=400, detail="Invalid card number") last_four = int(digits[-4:]) # Determine card type from first digit card_type = None if digits.startswith('4'): card_type = 'Visa' elif digits.startswith(('51', '52', '53', '54', '55')) or digits.startswith(('22', '23', '24', '25', '26', '27')): card_type = 'Mastercard' elif digits.startswith(('34', '37')): card_type = 'American Express' elif digits.startswith('6011') or digits.startswith('65'): card_type = 'Discover' # Save the card record saved_card = Card( user_id=customer.id, auth_net_payment_profile_id=payment_profile_id, last_four_digits=last_four, name_on_card=card_details.name_on_card, expiration_month=card_details.expiration_month, expiration_year=card_details.expiration_year, zip_code=card_details.zip_code, type_of_card=card_type, accepted_or_declined=1, ) db.add(saved_card) await db.commit() await db.refresh(saved_card) logger.info(f"Created payment profile {payment_profile_id} for customer {customer.id}") return saved_card def update_customer_profile( customer_profile_id: str, customer: Customer_Customer ) -> bool: """ Update the customer profile in Authorize.net with current customer data. Args: customer_profile_id: The Authorize.net customer profile ID customer: The customer record with updated information Returns: True if update was successful, False otherwise """ try: email = customer.customer_email or f"no-email-{customer.id}@example.com" # Element order matters for Authorize.net XML schema # For updateCustomerProfileRequest, only these fields can be updated request_data = { "profile": { "merchantCustomerId": str(customer.id), "description": f"{customer.customer_first_name} {customer.customer_last_name}".strip(), "email": email, "customerProfileId": customer_profile_id } } make_auth_net_request("updateCustomerProfileRequest", request_data) print(f"[PAYMENT] Updated customer profile {customer_profile_id}") return True except Exception as e: print(f"[PAYMENT] Failed to update customer profile {customer_profile_id}: {e}") return False def update_payment_profile_billing( customer_profile_id: str, payment_profile_id: str, customer: Customer_Customer, card: Card ) -> bool: """ Update the billing address on an existing payment profile. Args: customer_profile_id: The Authorize.net customer profile ID payment_profile_id: The Authorize.net payment profile ID customer: The customer record with billing information card: The card record with last four digits and expiration info Returns: True if update was successful, False otherwise """ try: bill_to = build_bill_to_from_customer(customer) # Override zip from card if it has a billing zip if card.zip_code: bill_to["zip"] = card.zip_code # Zero-pad last four digits (e.g., 2 -> "0002") last_four_str = str(card.last_four_digits).zfill(4) print(f"[PAYMENT] Updating payment profile {payment_profile_id} with billTo: {bill_to}") # Element order matters for Authorize.net XML schema: # paymentProfile must contain: billTo, payment, customerPaymentProfileId (in that order) request_data = { "customerProfileId": customer_profile_id, "paymentProfile": { "billTo": bill_to, "payment": { "creditCard": { "cardNumber": f"XXXX{last_four_str}", "expirationDate": f"{card.expiration_year}-{card.expiration_month.zfill(2)}" } }, "customerPaymentProfileId": payment_profile_id } } make_auth_net_request("updateCustomerPaymentProfileRequest", request_data) print(f"[PAYMENT] Updated billing info for payment profile {payment_profile_id}") return True except Exception as e: print(f"[PAYMENT] Failed to update billing info for payment profile {payment_profile_id}: {e}") return False def charge_customer_profile( customer_profile_id: str, payment_profile_id: str, amount: Decimal, customer: Optional[Customer_Customer] = None, order_description: Optional[str] = None ) -> Dict[str, Any]: """ Charge a saved card using customer and payment profile IDs. Args: customer_profile_id: The Authorize.net customer profile ID payment_profile_id: The Authorize.net payment profile ID amount: The amount to charge customer: Optional customer record for billing information order_description: Optional order description Returns: The transaction response """ request_data = { "transactionRequest": { "transactionType": "authOnlyTransaction", "amount": str(amount), "profile": { "customerProfileId": customer_profile_id, "paymentProfile": { "paymentProfileId": payment_profile_id } }, "tax": { "amount": "0", "name": "Tax Exempt", "description": "Charity organization - tax exempt" }, "taxExempt": "true" } } # Add order description if order_description: request_data["transactionRequest"]["order"] = { "description": order_description[:255] } # Add customer info for transaction records (billTo cannot be sent with payment profile - # billing address is already stored in the payment profile itself) if customer: request_data["transactionRequest"]["customer"] = { "id": str(customer.id), "email": customer.customer_email or f"no-email-{customer.id}@example.com" } return make_auth_net_request("createTransactionRequest", request_data) def charge_card_direct( card_details: CardCreate, amount: Decimal, customer: Optional[Customer_Customer] = None, order_description: Optional[str] = None ) -> Dict[str, Any]: """ Charge a card directly without saving it. Args: card_details: The card information amount: The amount to charge customer: Optional customer record for billing information order_description: Optional order description Returns: The transaction response """ request_data = { "transactionRequest": { "transactionType": "authOnlyTransaction", "amount": str(amount), "payment": { "creditCard": { "cardNumber": card_details.card_number, "expirationDate": f"{card_details.expiration_year}-{card_details.expiration_month.zfill(2)}", "cardCode": card_details.security_number } }, "tax": { "amount": "0", "name": "Tax Exempt", "description": "Charity organization - tax exempt" }, "taxExempt": "true" } } # Add order description if order_description: request_data["transactionRequest"]["order"] = { "description": order_description[:255] } # Add customer and billing information (customer must come before billTo in schema) if customer: request_data["transactionRequest"]["customer"] = { "id": str(customer.id), "email": customer.customer_email or f"no-email-{customer.id}@example.com" } bill_to = build_bill_to_from_customer(customer) # Override firstName/lastName from card if name_on_card is provided if card_details.name_on_card: name_parts = card_details.name_on_card.split() bill_to["firstName"] = name_parts[0] if name_parts else bill_to["firstName"] bill_to["lastName"] = " ".join(name_parts[1:]) if len(name_parts) > 1 else bill_to["lastName"] # Override zip from card details if provided if card_details.zip_code: bill_to["zip"] = card_details.zip_code request_data["transactionRequest"]["billTo"] = bill_to return make_auth_net_request("createTransactionRequest", request_data) @router.post("/process", response_model=PaymentResponse) async def process_payment( payment_data: PaymentCreate, current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """ Process a payment using Authorize.net. Supports two modes: 1. New card: Provide card_details. Optionally set save_card=True to tokenize. 2. Saved card: Provide card_id to charge an existing saved card. """ if not current_user.user_id: raise HTTPException(status_code=404, detail="Customer not found") # Get customer info customer_result = await db.execute( select(Customer_Customer).where(Customer_Customer.id == current_user.user_id) ) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=404, detail="Customer not found") auth_net_transaction_id = None saved_card = None card_id_used = payment_data.card_id try: if payment_data.card_id: # Charge a saved card using CIM profile card_result = await db.execute( select(Card).where( Card.id == payment_data.card_id, Card.user_id == current_user.user_id ) ) saved_card = card_result.scalar_one_or_none() if not saved_card: raise HTTPException(status_code=404, detail="Saved card not found") if not saved_card.auth_net_payment_profile_id: raise HTTPException(status_code=400, detail="Card is not properly tokenized") if not customer.auth_net_profile_id: raise HTTPException(status_code=400, detail="Customer profile not found") # Log customer data for debugging print(f"[PAYMENT] Processing payment for customer {customer.id}: " f"name={customer.customer_first_name} {customer.customer_last_name}, " f"address={customer.customer_address}, town={customer.customer_town}, " f"state={customer.customer_state}, zip={customer.customer_zip}, " f"phone={customer.customer_phone_number}") # Update customer profile with current customer data update_customer_profile(customer.auth_net_profile_id, customer) # Update billing info on the payment profile (ensures existing cards have current address) update_payment_profile_billing( customer_profile_id=customer.auth_net_profile_id, payment_profile_id=saved_card.auth_net_payment_profile_id, customer=customer, card=saved_card ) # Charge using the saved profile response = charge_customer_profile( customer_profile_id=customer.auth_net_profile_id, payment_profile_id=saved_card.auth_net_payment_profile_id, amount=payment_data.amount, customer=customer, order_description=f"Delivery #{payment_data.delivery_id}" if payment_data.delivery_id else None ) # Extract transaction ID from response trans_response = response.get("transactionResponse", {}) auth_net_transaction_id = trans_response.get("transId") elif payment_data.card_details: # Process with new card details if payment_data.card_details.save_card: # Create/get customer profile and save the card first customer_profile_id = await get_or_create_customer_profile(db, customer) # Create payment profile (tokenize the card) saved_card = await create_payment_profile( db=db, customer_profile_id=customer_profile_id, card_details=payment_data.card_details, customer=customer, ) card_id_used = saved_card.id # Now charge using the newly created profile response = charge_customer_profile( customer_profile_id=customer_profile_id, payment_profile_id=saved_card.auth_net_payment_profile_id, amount=payment_data.amount, customer=customer, order_description=f"Delivery #{payment_data.delivery_id}" if payment_data.delivery_id else None ) trans_response = response.get("transactionResponse", {}) auth_net_transaction_id = trans_response.get("transId") else: # One-time charge without saving the card response = charge_card_direct( card_details=payment_data.card_details, amount=payment_data.amount, customer=customer, order_description=f"Delivery #{payment_data.delivery_id}" if payment_data.delivery_id else None ) trans_response = response.get("transactionResponse", {}) auth_net_transaction_id = trans_response.get("transId") else: raise HTTPException(status_code=400, detail="Must provide either card_id or card_details") # Verify we got a transaction ID if not auth_net_transaction_id: raise HTTPException(status_code=500, detail="Transaction failed - no transaction ID returned") # Check transaction response code trans_response = response.get("transactionResponse", {}) response_code = trans_response.get("responseCode") # Response codes: 1 = Approved, 2 = Declined, 3 = Error, 4 = Held for Review if response_code == "2": # Declined errors = trans_response.get("errors", []) error_text = errors[0].get("errorText", "Transaction declined") if errors else "Transaction declined" raise HTTPException(status_code=400, detail=f"Payment declined: {error_text}") elif response_code == "3": # Error errors = trans_response.get("errors", []) error_text = errors[0].get("errorText", "Transaction error") if errors else "Transaction error" raise HTTPException(status_code=400, detail=f"Payment error: {error_text}") elif response_code == "4": # Held for review - still save the transaction but mark status differently logger.warning(f"Transaction {auth_net_transaction_id} held for review") # Determine transaction status status = 1 if response_code == "1" else (2 if response_code == "4" else 0) # Save transaction record transaction_record = Transaction( preauthorize_amount=payment_data.amount, charge_amount=None, customer_id=customer.id, transaction_type=1, # pre-authorization status=status, auth_net_transaction_id=auth_net_transaction_id, delivery_id=payment_data.delivery_id, card_id=card_id_used, payment_gateway=1, # Authorize.net ) db.add(transaction_record) await db.commit() return PaymentResponse( transaction_id=str(transaction_record.id), status="approved" if response_code == "1" else "held_for_review", amount=payment_data.amount, auth_net_transaction_id=auth_net_transaction_id ) except HTTPException: await db.rollback() raise except Exception as e: await db.rollback() logger.error(f"Payment processing error: {e}") raise HTTPException(status_code=500, detail=f"Payment processing error: {str(e)}") @router.get("/cards", response_model=list[CardResponse]) async def get_saved_cards( current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get all saved cards for the current customer.""" if not current_user.user_id: return [] cards = await db.execute( select(Card).where(Card.user_id == current_user.user_id) ) return cards.scalars().all() @router.post("/cards", response_model=CardResponse) async def save_card( card_details: CardCreate, current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """ Save a card without charging it (tokenize only). Creates a customer profile in Authorize.net CIM if one doesn't exist, then creates a payment profile for the card. """ if not current_user.user_id: raise HTTPException(status_code=404, detail="Customer not found") # Get customer info customer_result = await db.execute( select(Customer_Customer).where(Customer_Customer.id == current_user.user_id) ) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=404, detail="Customer not found") try: # Create/get customer profile customer_profile_id = await get_or_create_customer_profile(db, customer) # Create payment profile (tokenize the card) saved_card = await create_payment_profile( db=db, customer_profile_id=customer_profile_id, card_details=card_details, customer=customer, ) return saved_card except HTTPException: await db.rollback() raise except Exception as e: await db.rollback() logger.error(f"Error saving card: {e}") raise HTTPException(status_code=500, detail=f"Error saving card: {str(e)}") @router.delete("/cards/{card_id}") async def delete_card( card_id: int, current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """ Delete a saved card. Removes the payment profile from Authorize.net CIM and deletes the local record. """ if not current_user.user_id: raise HTTPException(status_code=404, detail="Customer not found") # Get the card (verify it belongs to this customer) card_result = await db.execute( select(Card).where( Card.id == card_id, Card.user_id == current_user.user_id ) ) card = card_result.scalar_one_or_none() if not card: raise HTTPException(status_code=404, detail="Card not found") # Get customer for the Authorize.net profile ID customer_result = await db.execute( select(Customer_Customer).where(Customer_Customer.id == current_user.user_id) ) customer = customer_result.scalar_one_or_none() try: # Delete from Authorize.net if we have the profile IDs if card.auth_net_payment_profile_id and customer and customer.auth_net_profile_id: request_data = { "customerProfileId": customer.auth_net_profile_id, "customerPaymentProfileId": card.auth_net_payment_profile_id } try: make_auth_net_request("deleteCustomerPaymentProfileRequest", request_data) logger.info(f"Deleted payment profile {card.auth_net_payment_profile_id} from Authorize.net") except HTTPException as e: # Log but don't fail if Authorize.net deletion fails # The profile might already be deleted or invalid logger.warning(f"Failed to delete payment profile from Authorize.net: {e.detail}") # Delete the local record await db.delete(card) await db.commit() return {"message": "Card deleted successfully"} except Exception as e: await db.rollback() logger.error(f"Error deleting card: {e}") raise HTTPException(status_code=500, detail=f"Error deleting card: {str(e)}") @router.post("/cards/{card_id}/set-default") async def set_default_card( card_id: int, current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Set a card as the default payment method.""" if not current_user.user_id: raise HTTPException(status_code=404, detail="Customer not found") # Verify the card belongs to the customer card_result = await db.execute( select(Card).where( Card.id == card_id, Card.user_id == current_user.user_id ) ) card = card_result.scalar_one_or_none() if not card: raise HTTPException(status_code=404, detail="Card not found") # Unset any existing default for this customer all_cards = await db.execute( select(Card).where(Card.user_id == current_user.user_id) ) for c in all_cards.scalars(): c.main_card = False # Set this card as default card.main_card = True await db.commit() return {"message": "Default card updated successfully"} @router.put("/cards/{card_id}", response_model=CardResponse) async def update_card( card_id: int, card_update: CardUpdate, current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """ Update a saved card's details. Updates both the local database and syncs to Authorize.Net CIM. Only name_on_card, expiration_month, expiration_year, and zip_code can be updated. """ if not current_user.user_id: raise HTTPException(status_code=404, detail="Customer not found") # Get the card (verify it belongs to this customer) card_result = await db.execute( select(Card).where( Card.id == card_id, Card.user_id == current_user.user_id ) ) card = card_result.scalar_one_or_none() if not card: raise HTTPException(status_code=404, detail="Card not found") # Get customer for Authorize.net profile ID customer_result = await db.execute( select(Customer_Customer).where(Customer_Customer.id == current_user.user_id) ) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=404, detail="Customer not found") try: # Update Authorize.Net payment profile if we have the profile IDs if card.auth_net_payment_profile_id and customer.auth_net_profile_id: # Build billTo from customer data bill_to = build_bill_to_from_customer(customer) # Override firstName/lastName from card name if provided name_on_card = card_update.name_on_card if card_update.name_on_card else card.name_on_card if name_on_card: name_parts = name_on_card.split() bill_to["firstName"] = name_parts[0] if name_parts else bill_to["firstName"] bill_to["lastName"] = " ".join(name_parts[1:]) if len(name_parts) > 1 else bill_to["lastName"] exp_month = card_update.expiration_month if card_update.expiration_month else card.expiration_month exp_year = card_update.expiration_year if card_update.expiration_year else card.expiration_year # Override zip from card details if provided if card_update.zip_code: bill_to["zip"] = card_update.zip_code elif card.zip_code: bill_to["zip"] = card.zip_code # Zero-pad last four digits (e.g., 2 -> "0002") last_four_str = str(card.last_four_digits).zfill(4) # Element order matters for Authorize.net XML schema request_data = { "customerProfileId": customer.auth_net_profile_id, "paymentProfile": { "billTo": bill_to, "payment": { "creditCard": { "cardNumber": f"XXXX{last_four_str}", "expirationDate": f"{exp_year}-{exp_month.zfill(2)}" } }, "customerPaymentProfileId": card.auth_net_payment_profile_id } } try: make_auth_net_request("updateCustomerPaymentProfileRequest", request_data) print(f"[PAYMENT] Updated payment profile {card.auth_net_payment_profile_id} in Authorize.net") except HTTPException as e: print(f"[PAYMENT] Failed to update payment profile in Authorize.net: {e.detail}") # Continue with local update even if Authorize.net update fails # Update local database fields if card_update.name_on_card is not None: card.name_on_card = card_update.name_on_card if card_update.expiration_month is not None: card.expiration_month = card_update.expiration_month if card_update.expiration_year is not None: card.expiration_year = card_update.expiration_year if card_update.zip_code is not None: card.zip_code = card_update.zip_code await db.commit() await db.refresh(card) return card except HTTPException: await db.rollback() raise except Exception as e: await db.rollback() logger.error(f"Error updating card: {e}") raise HTTPException(status_code=500, detail=f"Error updating card: {str(e)}") @router.post("/sync-billing-info") async def sync_billing_info( current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """ Sync customer billing information to Authorize.net. Updates the customer profile and all payment profiles with current customer data. Call this after updating customer address or contact information. """ if not current_user.user_id: raise HTTPException(status_code=404, detail="Customer not found") # Get customer info customer_result = await db.execute( select(Customer_Customer).where(Customer_Customer.id == current_user.user_id) ) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=404, detail="Customer not found") if not customer.auth_net_profile_id: return {"message": "No Authorize.net profile to update", "updated_cards": 0} results = { "customer_profile_updated": False, "cards_updated": 0, "cards_failed": 0 } try: # Update customer profile if update_customer_profile(customer.auth_net_profile_id, customer): results["customer_profile_updated"] = True # Get all saved cards for the customer cards_result = await db.execute( select(Card).where(Card.user_id == current_user.user_id) ) cards = cards_result.scalars().all() # Update billing info on each payment profile for card in cards: if card.auth_net_payment_profile_id: if update_payment_profile_billing( customer_profile_id=customer.auth_net_profile_id, payment_profile_id=card.auth_net_payment_profile_id, customer=customer, card=card ): results["cards_updated"] += 1 else: results["cards_failed"] += 1 return { "message": "Billing info sync completed", **results } except Exception as e: logger.error(f"Error syncing billing info: {e}") raise HTTPException(status_code=500, detail=f"Error syncing billing info: {str(e)}")