## File: app/routers/main.py (or your equivalent path) from fastapi import APIRouter, HTTPException from datetime import datetime from sqlalchemy.orm import Session from sqlalchemy import func # Your existing imports from database import session from pyowm import OWM from app.models.customer import Customer_Customer from app.models.cards import Card_Card # Imports needed for the migration task. # YOU MUST ADJUST THESE IMPORTS TO MATCH YOUR PROJECT'S ACTUAL STRUCTURE. from database import session # Assuming your SessionLocal is here for a clean session from authorizenet import apicontractsv1 from authorizenet.constants import constants from authorizenet.apicontrollers import ( createCustomerProfileController, createCustomerPaymentProfileController ) # --- ROUTER DEFINITION --- router = APIRouter( prefix="/main", tags=["main"], responses={404: {"description": "Not found"}}, ) API_LOGIN_ID = '9U6w96gZmX' TRANSACTION_KEY = '94s6Qy458mMNJr7G' constants.show_url_on_request = True constants.environment = constants.SANDBOX state_abbrevs = ['MA', 'RI', 'NH', 'ME', 'VT', 'CT', 'NY'] @router.post("/maintenance/migrate-cards-now") def run_card_migration_synchronously(): """ Triggers a one-time, SYNCHRONOUS task to migrate all existing, insecurely stored credit cards from the `card_card` table to secure Authorize.Net CIM profiles. This function is self-contained and does not use `crud` or `schemas`. WARNING: This is a long-running process and the request may time out. Monitor server logs for completion. This endpoint should be removed after use. """ def _create_authnet_profile(customer_obj: Customer_Customer, card_obj: Card_Card): """Helper to create a new Authorize.Net Customer Profile.""" merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) expiration_date_str = f"{card_obj.expiration_year}-{str(card_obj.expiration_month).zfill(2)}" # Validate expiration month = None year = None if card_obj.expiration_month: try: month = int(card_obj.expiration_month) except: pass if card_obj.expiration_year: try: year = int(card_obj.expiration_year) except: pass if not year or not month or month < 1 or month > 12 or year < 2000 or year > 2050: raise ValueError(f"Invalid expiration year={card_obj.expiration_year}, month={card_obj.expiration_month} for card ID {card_obj.id}") expiration_date_str = f"{year:04d}-{month:02d}" # // FIX 1: Sanitize card number by removing all non-digit characters sanitized_card_number = ''.join(filter(str.isdigit, str(card_obj.card_number))) if len(sanitized_card_number) < 13 or len(sanitized_card_number) > 19 or not sanitized_card_number.isdigit(): raise ValueError(f"Invalid card number length {len(sanitized_card_number)} or non-numeric for card ID {card_obj.id}") # Sanitize CVV cardCode = str(card_obj.security_number).strip() if len(cardCode) < 3 or len(cardCode) > 4 or not cardCode.isdigit(): raise ValueError(f"Invalid CVV length {len(cardCode)} or non-numeric for card ID {card_obj.id}") # Get dynamic state the_state = state_abbrevs[customer_obj.customer_state] if 0 <= customer_obj.customer_state < len(state_abbrevs) else 'MA' # Debug print print(f"DEBUG SEND CREATE: cardNumber='{sanitized_card_number}', expirationDate='{expiration_date_str}', cardCode='{cardCode}', customerID={customer_obj.id}, name='{customer_obj.customer_first_name} {customer_obj.customer_last_name}', address='{customer_obj.customer_address}', city='{customer_obj.customer_town}', state='{the_state}', zip='{customer_obj.customer_zip}', country='USA', phone='{customer_obj.customer_phone_number}', email='{customer_obj.customer_email}'") creditCard = apicontractsv1.creditCardType( cardNumber=sanitized_card_number, expirationDate=expiration_date_str, cardCode=cardCode ) billTo = apicontractsv1.customerAddressType( firstName=customer_obj.customer_first_name, lastName=customer_obj.customer_last_name, address=customer_obj.customer_address, city=customer_obj.customer_town, state=the_state, zip=customer_obj.customer_zip, country="USA", phoneNumber=customer_obj.customer_phone_number ) paymentProfile = apicontractsv1.customerPaymentProfileType( billTo=billTo, payment=apicontractsv1.paymentType(creditCard=creditCard), defaultPaymentProfile=True ) customerProfile = apicontractsv1.customerProfileType( merchantCustomerId=str(customer_obj.id), email=customer_obj.customer_email, paymentProfiles=[paymentProfile] ) request = apicontractsv1.createCustomerProfileRequest( merchantAuthentication=merchantAuth, profile=customerProfile, validationMode="testMode" ) controller = createCustomerProfileController(request) controller.execute() response = controller.getresponse() if response is not None and hasattr(response, 'messages') and response.messages.resultCode == "Ok": profile_id = response.customerProfileId if hasattr(response, 'customerProfileId') else None payment_ids = response.customerPaymentProfileIdList.numericString if hasattr(response, 'customerPaymentProfileIdList') and hasattr(response.customerPaymentProfileIdList, 'numericString') else [] payment_id = payment_ids[0] if payment_ids else None return str(profile_id), str(payment_id) if payment_id else None else: # Handle errors error_msg = "Unknown Authorize.Net Error" if response is not None and hasattr(response, 'messages') and response.messages is not None and hasattr(response.messages, 'message') and response.messages.message: msg = response.messages.message[0] if isinstance(response.messages.message, list) else response.messages.message if msg and hasattr(msg, 'text') and msg.text is not None: if isinstance(msg.text, str): error_msg = msg.text else: # Handle case where text might be an object with 'text' attribute error_msg = msg.text.text if hasattr(msg.text, 'text') else str(msg.text) print(f" AUTH.NET ERROR: {error_msg}") return None, None def _add_authnet_payment_profile(profile_id: str, customer_obj: Customer_Customer, card_obj: Card_Card): """Helper to add a new Payment Profile to an existing Customer Profile.""" merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) expiration_date_str = f"{card_obj.expiration_year}-{str(card_obj.expiration_month).zfill(2)}" # State abbreviations list # // FIX 1 (Applied here as well): Sanitize card number sanitized_card_number = ''.join(filter(str.isdigit, str(card_obj.card_number))) creditCard = apicontractsv1.creditCardType( cardNumber=sanitized_card_number, expirationDate=expiration_date_str, cardCode=str(card_obj.security_number).strip() ) # Get dynamic state the_state = state_abbrevs[customer_obj.customer_state] if 0 <= customer_obj.customer_state < len(state_abbrevs) else 'MA' billTo = apicontractsv1.customerAddressType( firstName=customer_obj.customer_first_name, lastName=customer_obj.customer_last_name, address=customer_obj.customer_address, city=customer_obj.customer_town, state=the_state, zip=customer_obj.customer_zip, country="USA", phoneNumber=customer_obj.customer_phone_number ) paymentProfile = apicontractsv1.customerPaymentProfileType( billTo=billTo, payment=apicontractsv1.paymentType(creditCard=creditCard), defaultPaymentProfile=False ) request = apicontractsv1.createCustomerPaymentProfileRequest( merchantAuthentication=merchantAuth, customerProfileId=profile_id, paymentProfile=paymentProfile, validationMode="testMode" ) controller = createCustomerPaymentProfileController(request) controller.execute() response = controller.getresponse() if response is not None and response.messages.resultCode == "Ok": return str(response.customerPaymentProfileId) else: # // FIX 2 (Applied here as well): Robust error message parsing error_msg = "Unknown Authorize.Net Error" if response is not None and hasattr(response, 'messages') and response.messages is not None and hasattr(response.messages, 'message') and response.messages.message: msg = response.messages.message[0] if hasattr(msg, 'text') and msg.text is not None: error_msg = msg.text.text print(f" AUTH.NET ERROR: {error_msg}") return None # --- MIGRATION SCRIPT LOGIC --- print("="*60) print("MIGRATION STARTED: Migrating all customer cards to Authorize.Net CIM.") print(f"Start Time: {datetime.now()}") print("="*60) customers_processed = 0 cards_migrated = 0 error_count = 0 try: customers_to_migrate = (session .query(Customer_Customer) .filter(Customer_Customer.auth_net_profile_id == None) .all()) total_customers = len(customers_to_migrate) print(f"Found {total_customers} customers to migrate.") for index, customer in enumerate(customers_to_migrate): customers_processed += 1 print(f"\n--- Processing Customer {index + 1}/{total_customers} (ID: {customer.id}, Name: {customer.customer_first_name}) ---") insecure_cards = (session .query(Card_Card) .filter(Card_Card.user_id == customer.id) .all()) print(f"DEBUG: Customer {customer.id} has {len(insecure_cards)} cards") customer_profile_id_for_session = None # # Check for existing profile # existing_profile, existing_payment_ids = _get_existing_profile(str(customer.id)) # if existing_profile and existing_payment_ids: # print(f" -> Found existing Authorize.Net profile ID {existing_profile}") # customer.auth_net_profile_id = existing_profile # session.add(customer) # session.commit() # for i, card_data in enumerate(insecure_cards): # if i < len(existing_payment_ids): # display_card_number = ''.join(filter(str.isdigit, str(card_data.card_number))) # if not display_card_number: # print(f" SKIPPING CARD: Card number is empty after sanitization for card ID {card_data.id}") # error_count += 1 # continue # card_data.auth_net_payment_profile_id = existing_payment_ids[i] # session.add(card_data) # session.commit() # print(f" SUCCESS: Mapped existing Payment ID {existing_payment_ids[i]} to card ending ...{display_card_number[-4:]}") # cards_migrated += 1 # else: # print(f" WARNING: No existing payment profile Kawasaki for card {card_data.id}") # error_count += 1 # continue # skip creation loop for i, card_data in enumerate(insecure_cards): try: # Basic data validation before sending if not card_data.card_number or not card_data.expiration_year or not card_data.expiration_month: print(f" SKIPPING CARD: Incomplete card data for card ID {card_data.id}.") error_count += 1 continue print(f"DEBUG RAW: Card ID {card_data.id}: number='{card_data.card_number}', year='{card_data.expiration_year}', month='{card_data.expiration_month}', cvv='{card_data.security_number}', name='{customer.customer_first_name} {customer.customer_last_name}'") # Sanitize card number for display display_card_number = ''.join(filter(str.isdigit, str(card_data.card_number))) if not display_card_number: print(f" SKIPPING CARD: Card number is empty after sanitization for card ID {card_data.id}.") error_count += 1 continue if i == 0: print(f" -> Creating new CIM profile with card ending in ...{display_card_number[-4:]}") profile_id, payment_id = _create_authnet_profile(customer, card_data) if profile_id and payment_id: customer.auth_net_profile_id = profile_id session.add(customer) customer_profile_id_for_session = profile_id card_data.auth_net_payment_profile_id = payment_id session.add(card_data) session.commit() print(f" SUCCESS: Created Profile ID {profile_id} and Payment ID {payment_id}") cards_migrated += 1 else: print(f" ERROR: Failed to create profile for customer {customer.id}.") error_count += 1 break else: if not display_card_number: print(f" SKIPPING CARD: Card number is empty after sanitization for card ID {card_data.id}.") error_count += 1 continue print(f" -> Adding additional card ending in ...{display_card_number[-4:]}") payment_id = _add_authnet_payment_profile(customer_profile_id_for_session, customer, card_data) if payment_id: card_data.auth_net_payment_profile_id = payment_id session.add(card_data) session.commit() print(f" SUCCESS: Added Payment ID {payment_id}") cards_migrated += 1 else: print(f" ERROR: Failed to add additional card for customer {customer.id}.") error_count += 1 except Exception as e: print(f" CRITICAL ERROR processing a card for customer {customer.id}: {str(e)}") error_count += 1 session.rollback() except Exception as e: print(f"A critical error occurred during the migration process: {e}") session.rollback() return {"ok": False, "error": f"Migration failed with a critical error: {e}"}, 500 finally: print("="*60) print("MIGRATION FINISHED!") print(f"End Time: {datetime.now()}") print(f"Summary: Processed {customers_processed} customers, migrated {cards_migrated} cards, encountered {error_count} errors.") print("="*60) return { "ok": True, "message": "Migration process completed successfully.", "customers_processed": customers_processed, "cards_migrated": cards_migrated, "errors": error_count, }, 200