diff --git a/app/crud.py b/app/crud.py index ec6084d..17fb441 100644 --- a/app/crud.py +++ b/app/crud.py @@ -18,7 +18,7 @@ def update_customer_auth_net_profile_id(db: Session, customer_id: int, profile_i return db_customer def create_customer_card(db: Session, customer_id: int, card_info: schemas.CardCreate, payment_profile_id: str): - last_four = card_info.card_number[-4:] + last_four_digits = card_info.card_number[-4:] try: exp_year, exp_month = map(int, card_info.expiration_date.split('-')) except ValueError: @@ -26,10 +26,10 @@ def create_customer_card(db: Session, customer_id: int, card_info: schemas.CardC raise ValueError("Expiration date must be in YYYY-MM format") db_card = models.Card( - customer_id=customer_id, + user_id=customer_id, auth_net_payment_profile_id=payment_profile_id, - last_four=last_four, - card_brand="Unknown", # Use a library like 'creditcard' to detect this from the number + last_four_digits=last_four_digits, + type_of_card="Unknown", # Use a library like 'creditcard' to detect this from the number expiration_year=exp_year, expiration_month=exp_month ) diff --git a/app/models.py b/app/models.py index 97dded2..23a9c72 100644 --- a/app/models.py +++ b/app/models.py @@ -5,13 +5,13 @@ from .database import Base import datetime class Customer(Base): - __tablename__ = "customers" + __tablename__ = "customer_customer" id = Column(Integer, primary_key=True, index=True) # --- ADD THIS COLUMN --- # This stores the master profile ID from Authorize.Net's CIM. - auth_net_profile_id = Column(String, unique=True, index=True, nullable=True) + auth_net_profile_id = Column(String(100)) # --- YOUR EXISTING COLUMNS --- account_number = Column(String(25)) @@ -34,17 +34,17 @@ class Customer(Base): # --- ADD THIS ENTIRE NEW MODEL --- class Card(Base): - __tablename__ = "cards" + __tablename__ = "card_card" id = Column(Integer, primary_key=True, index=True) - customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True) + user_id = Column(Integer, nullable=False) # This stores the payment profile ID for this specific card from Authorize.Net's CIM. auth_net_payment_profile_id = Column(String, unique=True, index=True, nullable=False) # Columns to store non-sensitive card info for display purposes - last_four = Column(String(4), nullable=False) - card_brand = Column(String(50), nullable=True) + last_four_digits = Column(String(4), nullable=False) + type_of_card = Column(String(50), nullable=True) expiration_month = Column(Integer, nullable=False) expiration_year = Column(Integer, nullable=False) diff --git a/app/routers/payment.py b/app/routers/payment.py index 5909ccd..c6abf00 100644 --- a/app/routers/payment.py +++ b/app/routers/payment.py @@ -7,6 +7,8 @@ import enum from .. import crud, models, schemas, database from ..services import payment_service +import logging + AuthNetResponse = object @@ -23,6 +25,16 @@ 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" +} # This helper function is perfect, keep it. def _parse_authnet_response(response: Optional[AuthNetResponse]) -> Tuple[TransactionStatus, Optional[str], Optional[str]]: @@ -44,36 +56,64 @@ def _parse_authnet_response(response: Optional[AuthNetResponse]) -> Tuple[Transa rejection_reason = f"{msg.code.text}: {msg.text.text}" return status, auth_net_transaction_id, rejection_reason -# --- NEW ENDPOINT TO ADD A CARD --- - -@router.post("/customers/{customer_id}/cards", response_model=schemas.Card, summary="Add a new payment card for a customer") +@router.post("/customers/{customer_id}/cards", response_model=schemas.CustomerCardResponse, 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. + """ 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 - if not db_customer.auth_net_profile_id: - profile_id, payment_id = payment_service.create_customer_profile(customer=customer_schema, card_info=card_info) - if not profile_id or not payment_id: - raise HTTPException(status_code=400, detail="Failed to create payment profile with Authorize.Net") - 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 + 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 + ) + + # This creates the card in our local database + new_card = crud.create_customer_card( + db=db, + customer_id=customer_id, + card_info=card_info, + payment_profile_id=payment_profile_id ) - if not payment_profile_id: - raise HTTPException(status_code=400, detail="Failed to add new card to Authorize.Net profile") + + # ========= THIS IS THE FIX FOR THE FRONTEND ========= + # 1. Convert the newly created card object into a Pydantic model, then a dictionary. + # Make sure your schemas.Card uses `user_id` to match your model. + response_data = schemas.Card.from_orm(new_card).model_dump() + + # 2. Manually add the 'customer_state' field that the frontend needs. + response_data['customer_state'] = "MA" + + # 3. Return the complete dictionary. FastAPI validates it against CustomerCardResponse + # and sends it to the frontend. + return response_data - new_card = crud.create_customer_card(db=db, customer_id=customer_id, card_info=card_info, payment_profile_id=payment_profile_id) - return new_card - -# --- REFACTORED CHARGE ENDPOINT --- + 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 + logger.error(f"An unexpected error occurred: {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)): @@ -117,7 +157,7 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA 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.customer_id != customer_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: diff --git a/app/schemas.py b/app/schemas.py index 4ec031e..c7f1c45 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,43 +1,45 @@ -## File: your_app/schemas.py +## File: app/schemas.py (or your equivalent path) -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict # --- MODIFICATION: Import ConfigDict from typing import List, Optional from datetime import datetime -from decimal import Decimal # Use Decimal for currency +from decimal import Decimal -# --- NEW SCHEMAS FOR CIM WORKFLOW --- +# --- NEW SCHEMAS FOR CIM WORKFLOW (Now with correct Pydantic V2 config) --- class CardCreate(BaseModel): - # This schema receives sensitive card info just once. card_number: str expiration_date: str # Format: "YYYY-MM" cvv: str + main_card: bool = False class Card(BaseModel): - # This schema is for displaying saved card info. It is NOT sensitive. id: int - customer_id: int - last_four: str - card_brand: Optional[str] = None + user_id: int + last_four_digits: str + type_of_card: Optional[str] = None expiration_month: int expiration_year: int - class Config: - orm_mode = True + # --- MODIFICATION: This is the new syntax for Pydantic V2 --- + model_config = ConfigDict(from_attributes=True) + # The line above replaces the old `class Config: orm_mode = True` class TransactionCreateByCardID(BaseModel): - # This is the NEW way to create a charge, using an internal card_id. card_id: int - charge_amount: Decimal # Use Decimal - - # Fields for Level 2 data + charge_amount: Decimal tax_amount: Optional[Decimal] = Decimal("0.0") - - # Your other business-related IDs service_id: Optional[int] = None delivery_id: Optional[int] = None -# --- YOUR EXISTING SCHEMAS (UPDATED) --- +class TransactionAuthorizeByCardID(BaseModel): + card_id: int + preauthorize_amount: Decimal + tax_amount: Optional[Decimal] = Decimal("0.0") + service_id: Optional[int] = None + delivery_id: Optional[int] = None + +# --- YOUR EXISTING SCHEMAS (UPDATED for Pydantic V2) --- class TransactionBase(BaseModel): preauthorize_amount: Optional[Decimal] = None @@ -52,7 +54,7 @@ class TransactionBase(BaseModel): class TransactionCreate(TransactionBase): charge_amount: Decimal card_number: str - expiration_date: str # MM/YY + expiration_date: str cvv: str class TransactionAuthorize(TransactionBase): @@ -73,8 +75,8 @@ class Transaction(TransactionBase): customer_id: int created_at: datetime - class Config: - orm_mode = True + # --- MODIFICATION: This is the new syntax for Pydantic V2 --- + model_config = ConfigDict(from_attributes=True) class CustomerBase(BaseModel): account_number: Optional[str] = None @@ -97,27 +99,16 @@ class CustomerBase(BaseModel): class Customer(CustomerBase): id: int - # --- ADD THIS FIELD --- auth_net_profile_id: Optional[str] = None - class Config: - orm_mode = True + # --- MODIFICATION: This is the new syntax for Pydantic V2 --- + model_config = ConfigDict(from_attributes=True) -# --- ADD THIS NEW SCHEMA --- -class TransactionAuthorizeByCardID(BaseModel): - # This is for creating an AUTHORIZATION using an internal card_id. - card_id: int - preauthorize_amount: Decimal # Use Decimal +class CustomerCardResponse(Card): + # We are inheriting all fields from `Card` - # Fields for Level 2 data (important for rates!) - tax_amount: Optional[Decimal] = Decimal("0.0") - - # Your other business-related IDs - service_id: Optional[int] = None - delivery_id: Optional[int] = None - -# --- Your TransactionCapture schema is perfect and needs NO changes --- -class TransactionCapture(BaseModel): - charge_amount: Decimal - auth_net_transaction_id: str \ No newline at end of file + # Now, add the extra customer fields the frontend needs. + # We define it as a string because we will be returning the + # two-letter abbreviation, not the database ID. + customer_state: str \ No newline at end of file diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 68b5119..db7978c 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -1,31 +1,92 @@ ## File: your_app/services/payment_service.py import logging +import pprint +import traceback +import re from authorizenet import apicontractsv1 from authorizenet.apicontrollers import ( createTransactionController, createCustomerProfileController, createCustomerPaymentProfileController ) +from authorizenet.constants import constants from .. import schemas from config import load_config # Assuming you have this -from decimal import Decimal logger = logging.getLogger(__name__) # Load Authorize.net credentials ApplicationConfig = load_config() -API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID -TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY +# API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID +# TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY + # Authorize.net credentials (Sandbox Test Credentials) +API_LOGIN_ID = '9U6w96gZmX' +TRANSACTION_KEY = '94s6Qy458mMNJr7G' +# --- MODIFICATION: Set the environment globally --- +# Set this to SANDBOX for testing, PRODUCTION for live +constants.show_url_on_request = True # Very useful for debugging +constants.environment = constants.SANDBOX + + + +def _get_authnet_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." -# --- NEW CIM CORE FUNCTIONS --- def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardCreate): - logger.info(f"Creating Auth.Net profile for internal customer ID: {customer.id}") + """ + Creates a new customer profile in Authorize.Net with their first payment method. + This version sanitizes and trims all customer data before sending. + """ + logger.info(f"Attempting to create Auth.Net profile for customer ID: {customer.id}") - merchantAuth = apicontractsv1.merchantAuthenticationType() - merchantAuth.name = API_LOGIN_ID - merchantAuth.transactionKey = TRANSACTION_KEY + merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) + + # --- 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-]' # Allow hyphens for ZIP+4 + 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: name=50, address=60, city=40, state=40, zip=20, email=255 + 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.A: 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) + email = (customer.customer_email or f"no-email-{customer.id}@example.com")[:255] creditCard = apicontractsv1.creditCardType( cardNumber=card_info.card_number, @@ -34,96 +95,152 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC ) billTo = apicontractsv1.customerAddressType( - firstName=customer.customer_first_name, - lastName=customer.customer_last_name, - address=customer.customer_address, - city=customer.customer_town, - zip=customer.customer_zip, + firstName=first_name, + lastName=last_name, + address=address, + city=city, + state='MA', # And include it in the object + zip=zip_code, country="USA" ) - - paymentProfile = apicontractsv1.customerPaymentProfileType() - paymentProfile.billTo = billTo - paymentProfile.payment = apicontractsv1.paymentType(creditCard=creditCard) + + paymentProfile = apicontractsv1.customerPaymentProfileType( + billTo=billTo, + payment=apicontractsv1.paymentType(creditCard=creditCard) + ) customerProfile = apicontractsv1.customerProfileType( merchantCustomerId=str(customer.id), - email=customer.customer_email, + email=email, paymentProfiles=[paymentProfile] ) - + request = apicontractsv1.createCustomerProfileRequest( merchantAuthentication=merchantAuth, profile=customerProfile, - validationMode="liveMode" + # ========= CHANGE 2.A: USE liveMode ========= + validationMode="testMode" ) - + controller = createCustomerProfileController(request) - controller.execute() - response = controller.getresponse() + # ... rest of the function is the same ... + try: + controller.execute() + response = controller.getresponse() + + if response is not None and response.messages.resultCode == "Ok": + profile_id = response.customerProfileId + payment_id = response.customerPaymentProfileIdList[0] if response.customerPaymentProfileIdList else None + return str(profile_id), str(payment_id) if payment_id else None + else: + error_msg = _get_authnet_error_message(response) + logger.error(f"Failed to create customer profile (API Error): {error_msg}") + logger.error(f"SANITIZED DATA SENT: FirstName='{first_name}', LastName='{last_name}', Address='{address}', City='{city}', State='{state}', Zip='{zip_code}', Email='{email}'") + raise ValueError(error_msg) + + except Exception as e: + logger.error(f"A critical exception occurred during the API call: {traceback.format_exc()}") + raise ValueError("Could not connect to the payment gateway.") + - if response is not None and response.messages.resultCode == "Ok": - profile_id = response.customerProfileId - payment_id = response.customerPaymentProfileIdList[0] if response.customerPaymentProfileIdList else None - return str(profile_id), str(payment_id) if payment_id else None - else: - error_msg = response.messages.message[0].text.text if response and response.messages and response.messages.message else "Unknown Error" - logger.error(f"Failed to create customer profile: {error_msg}") - return None, None def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.Customer, card_info: schemas.CardCreate): logger.info(f"Adding new 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) + creditCard = apicontractsv1.creditCardType( cardNumber=card_info.card_number, expirationDate=card_info.expiration_date, cardCode=card_info.cvv ) - - paymentProfile = apicontractsv1.customerPaymentProfileType( - billTo=apicontractsv1.customerAddressType(firstName=customer.customer_first_name, lastName=customer.customer_last_name), - payment=apicontractsv1.paymentType(creditCard=creditCard) + + billTo = apicontractsv1.customerAddressType( + firstName=first_name, + lastName=last_name, + address=address, + city=city, + state=state, # And include it in the object + zip=zip_code, + country="USA" ) + paymentProfile = apicontractsv1.customerPaymentProfileType( + billTo=billTo, + payment=apicontractsv1.paymentType(creditCard=creditCard) + ) + request = apicontractsv1.createCustomerPaymentProfileRequest( merchantAuthentication=merchantAuth, customerProfileId=customer_profile_id, paymentProfile=paymentProfile, - validationMode="liveMode" + # ========= CHANGE 2.B: USE liveMode ========= + validationMode="testMode" ) controller = createCustomerPaymentProfileController(request) - controller.execute() - response = controller.getresponse() + + try: + controller.execute() + response = controller.getresponse() + if response is not None and response.messages.resultCode == "Ok": + return str(response.customerPaymentProfileId) + else: + error_msg = _get_authnet_error_message(response) + logger.error(f"Failed to add payment profile: {error_msg}") + logger.error(f"SANITIZED DATA SENT FOR ADD PROFILE: FirstName='{first_name}', LastName='{last_name}', Address='{address}', City='{city}', State='{state}', Zip='{zip_code}'") + + logger.error(f"Card info: number='{card_info.card_number}', exp='{card_info.expiration_date}', cvv='{card_info.cvv}'") # Mask if sensitive + logger.error(pprint.pformat(vars(billTo))) + logger.error(pprint.pformat(vars(request))) - if response is not None and response.messages.resultCode == "Ok": - return str(response.customerPaymentProfileId) - else: - error_msg = response.messages.message[0].text.text if response and response.messages and response.messages.message else "Unknown Error" - logger.error(f"Failed to add payment profile: {error_msg}") - return None -# --- NEW CHARGE FUNCTION --- -def charge_customer_profile(customer_profile_id: str, payment_profile_id: str, transaction_req: schemas.TransactionCreateByCardID): - logger.info(f"Charging profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.charge_amount}") + raise ValueError(error_msg) + except Exception as e: + logger.error(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): + """ + Creates an AUTH_ONLY transaction against a customer profile. + This holds the funds but does not capture them. + """ + logger.info(f"Authorizing profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.preauthorize_amount}") merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) - profile_to_charge = apicontractsv1.profileTransAuthCaptureType( + profile_to_authorize = apicontractsv1.profileTransAuthOnlyType( customerProfileId=customer_profile_id, paymentProfileId=payment_profile_id ) transactionRequest = apicontractsv1.transactionRequestType( - transactionType="authCaptureTransaction", - amount=f"{transaction_req.charge_amount:.2f}", - profile=profile_to_charge + transactionType="authOnlyTransaction", + amount=f"{transaction_req.preauthorize_amount:.2f}", + profile=profile_to_authorize ) - # --- THIS IS THE KEY FOR LOWER RATES (LEVEL 2/3 DATA) --- if transaction_req.tax_amount and transaction_req.tax_amount > 0: transactionRequest.tax = apicontractsv1.extendedAmountType(amount=f"{transaction_req.tax_amount:.2f}", name="Sales Tax") @@ -134,43 +251,21 @@ def charge_customer_profile(customer_profile_id: str, payment_profile_id: str, t controller = createTransactionController(createtransactionrequest) controller.execute() + # The response is returned directly to the router to be parsed there return controller.getresponse() -# --- Your existing authorize/capture functions can remain --- -# (They are not included here for brevity but should be kept in your file if you still need them) - - -def authorize_customer_profile( - customer_profile_id: str, - payment_profile_id: str, - transaction_req: schemas.TransactionAuthorizeByCardID -): - """ - Creates an AUTH_ONLY transaction against a customer profile. - This holds the funds but does not capture them. - """ - logger.info(f"Authorizing profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.preauthorize_amount}") +def capture_authorized_transaction(transaction_req: schemas.TransactionCapture): + """Captures a previously authorized transaction.""" + logger.info(f"Capturing transaction {transaction_req.auth_net_transaction_id} for {transaction_req.charge_amount}") merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) - - # Note the type here: profileTransAuthOnlyType - profile_to_authorize = apicontractsv1.profileTransAuthOnlyType( - customerProfileId=customer_profile_id, - paymentProfileId=payment_profile_id - ) - - transactionRequest = apicontractsv1.transactionRequestType( - # The key difference: transactionType is "authOnlyTransaction" - transactionType="authOnlyTransaction", - amount=f"{transaction_req.preauthorize_amount:.2f}", - profile=profile_to_authorize - ) - # --- LEVEL 2/3 DATA IS STILL CRITICAL HERE --- - # The initial authorization is what the card issuers use to determine your rates. - if transaction_req.tax_amount and transaction_req.tax_amount > 0: - transactionRequest.tax = apicontractsv1.extendedAmountType(amount=f"{transaction_req.tax_amount:.2f}", name="Sales Tax") + transactionRequest = apicontractsv1.transactionRequestType( + transactionType="priorAuthCaptureTransaction", + amount=f"{transaction_req.charge_amount:.2f}", + refTransId=transaction_req.auth_net_transaction_id + ) createtransactionrequest = apicontractsv1.createTransactionRequest( merchantAuthentication=merchantAuth, diff --git a/settings_dev.py b/settings_dev.py index d2edf9a..4794f9d 100644 --- a/settings_dev.py +++ b/settings_dev.py @@ -29,6 +29,8 @@ class ApplicationConfig: ] - # Authorize.net credentials (Sandbox Test Credentials) - API_LOGIN_ID = '5KP3u95bQpv' - TRANSACTION_KEY = '346HZ32z3fP4hTG2' + # # Authorize.net credentials (Sandbox Test Credentials) + # API_LOGIN_ID = '5KP3u95bQpv' + # TRANSACTION_KEY = '346HZ32z3fP4hTG2' +API_LOGIN_ID = '9U6w96gZmX' +TRANSACTION_KEY = '94s6Qy458mMNJr7G' \ No newline at end of file