Adding authnet not tested

This commit is contained in:
2025-09-15 15:30:30 -04:00
parent 5a6bcc0700
commit 47e3fb443b
6 changed files with 285 additions and 157 deletions

View File

@@ -18,7 +18,7 @@ def update_customer_auth_net_profile_id(db: Session, customer_id: int, profile_i
return db_customer return db_customer
def create_customer_card(db: Session, customer_id: int, card_info: schemas.CardCreate, payment_profile_id: str): 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: try:
exp_year, exp_month = map(int, card_info.expiration_date.split('-')) exp_year, exp_month = map(int, card_info.expiration_date.split('-'))
except ValueError: 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") raise ValueError("Expiration date must be in YYYY-MM format")
db_card = models.Card( db_card = models.Card(
customer_id=customer_id, user_id=customer_id,
auth_net_payment_profile_id=payment_profile_id, auth_net_payment_profile_id=payment_profile_id,
last_four=last_four, last_four_digits=last_four_digits,
card_brand="Unknown", # Use a library like 'creditcard' to detect this from the number type_of_card="Unknown", # Use a library like 'creditcard' to detect this from the number
expiration_year=exp_year, expiration_year=exp_year,
expiration_month=exp_month expiration_month=exp_month
) )

View File

@@ -5,13 +5,13 @@ from .database import Base
import datetime import datetime
class Customer(Base): class Customer(Base):
__tablename__ = "customers" __tablename__ = "customer_customer"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
# --- ADD THIS COLUMN --- # --- ADD THIS COLUMN ---
# This stores the master profile ID from Authorize.Net's CIM. # 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 --- # --- YOUR EXISTING COLUMNS ---
account_number = Column(String(25)) account_number = Column(String(25))
@@ -34,17 +34,17 @@ class Customer(Base):
# --- ADD THIS ENTIRE NEW MODEL --- # --- ADD THIS ENTIRE NEW MODEL ---
class Card(Base): class Card(Base):
__tablename__ = "cards" __tablename__ = "card_card"
id = Column(Integer, primary_key=True, index=True) 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. # 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) auth_net_payment_profile_id = Column(String, unique=True, index=True, nullable=False)
# Columns to store non-sensitive card info for display purposes # Columns to store non-sensitive card info for display purposes
last_four = Column(String(4), nullable=False) last_four_digits = Column(String(4), nullable=False)
card_brand = Column(String(50), nullable=True) type_of_card = Column(String(50), nullable=True)
expiration_month = Column(Integer, nullable=False) expiration_month = Column(Integer, nullable=False)
expiration_year = Column(Integer, nullable=False) expiration_year = Column(Integer, nullable=False)

View File

@@ -7,6 +7,8 @@ import enum
from .. import crud, models, schemas, database from .. import crud, models, schemas, database
from ..services import payment_service from ..services import payment_service
import logging
AuthNetResponse = object AuthNetResponse = object
@@ -23,6 +25,16 @@ class TransactionType(enum.IntEnum):
CHARGE = 0 CHARGE = 0
AUTHORIZE = 1 AUTHORIZE = 1
CAPTURE = 3 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. # This helper function is perfect, keep it.
def _parse_authnet_response(response: Optional[AuthNetResponse]) -> Tuple[TransactionStatus, Optional[str], Optional[str]]: 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}" rejection_reason = f"{msg.code.text}: {msg.text.text}"
return status, auth_net_transaction_id, rejection_reason return status, auth_net_transaction_id, rejection_reason
# --- NEW ENDPOINT TO ADD A CARD --- @router.post("/customers/{customer_id}/cards", response_model=schemas.CustomerCardResponse, summary="Add a new payment card for a customer")
@router.post("/customers/{customer_id}/cards", response_model=schemas.Card, 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)): 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) db_customer = crud.get_customer(db, customer_id=customer_id)
if not db_customer: if not db_customer:
raise HTTPException(status_code=404, detail="Customer not found") 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) customer_schema = schemas.Customer.from_orm(db_customer)
payment_profile_id = None payment_profile_id = None
if not db_customer.auth_net_profile_id: try:
profile_id, payment_id = payment_service.create_customer_profile(customer=customer_schema, card_info=card_info) # This part now works because the service hard-codes the state to "MA"
if not profile_id or not payment_id: if not db_customer.auth_net_profile_id:
raise HTTPException(status_code=400, detail="Failed to create payment profile with Authorize.Net") profile_id, payment_id = payment_service.create_customer_profile(
crud.update_customer_auth_net_profile_id(db, customer_id=customer_id, profile_id=profile_id) customer=customer_schema, card_info=card_info
payment_profile_id = payment_id )
else: crud.update_customer_auth_net_profile_id(db, customer_id=customer_id, profile_id=profile_id)
payment_profile_id = payment_service.add_payment_profile_to_customer( payment_profile_id = payment_id
customer_profile_id=db_customer.auth_net_profile_id, else:
customer=customer_schema, payment_profile_id = payment_service.add_payment_profile_to_customer(
card_info=card_info 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")
new_card = crud.create_customer_card(db=db, customer_id=customer_id, card_info=card_info, payment_profile_id=payment_profile_id) # ========= THIS IS THE FIX FOR THE FRONTEND =========
return new_card # 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()
# --- REFACTORED CHARGE ENDPOINT --- # 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
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") @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)): 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_customer = crud.get_customer(db, customer_id=customer_id)
db_card = crud.get_card_by_id(db, card_id=transaction_req.card_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") 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: if not db_customer.auth_net_profile_id or not db_card.auth_net_payment_profile_id:

View File

@@ -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 typing import List, Optional
from datetime import datetime 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): class CardCreate(BaseModel):
# This schema receives sensitive card info just once.
card_number: str card_number: str
expiration_date: str # Format: "YYYY-MM" expiration_date: str # Format: "YYYY-MM"
cvv: str cvv: str
main_card: bool = False
class Card(BaseModel): class Card(BaseModel):
# This schema is for displaying saved card info. It is NOT sensitive.
id: int id: int
customer_id: int user_id: int
last_four: str last_four_digits: str
card_brand: Optional[str] = None type_of_card: Optional[str] = None
expiration_month: int expiration_month: int
expiration_year: int expiration_year: int
class Config: # --- MODIFICATION: This is the new syntax for Pydantic V2 ---
orm_mode = True model_config = ConfigDict(from_attributes=True)
# The line above replaces the old `class Config: orm_mode = True`
class TransactionCreateByCardID(BaseModel): class TransactionCreateByCardID(BaseModel):
# This is the NEW way to create a charge, using an internal card_id.
card_id: int card_id: int
charge_amount: Decimal # Use Decimal charge_amount: Decimal
# Fields for Level 2 data
tax_amount: Optional[Decimal] = Decimal("0.0") tax_amount: Optional[Decimal] = Decimal("0.0")
# Your other business-related IDs
service_id: Optional[int] = None service_id: Optional[int] = None
delivery_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): class TransactionBase(BaseModel):
preauthorize_amount: Optional[Decimal] = None preauthorize_amount: Optional[Decimal] = None
@@ -52,7 +54,7 @@ class TransactionBase(BaseModel):
class TransactionCreate(TransactionBase): class TransactionCreate(TransactionBase):
charge_amount: Decimal charge_amount: Decimal
card_number: str card_number: str
expiration_date: str # MM/YY expiration_date: str
cvv: str cvv: str
class TransactionAuthorize(TransactionBase): class TransactionAuthorize(TransactionBase):
@@ -73,8 +75,8 @@ class Transaction(TransactionBase):
customer_id: int customer_id: int
created_at: datetime created_at: datetime
class Config: # --- MODIFICATION: This is the new syntax for Pydantic V2 ---
orm_mode = True model_config = ConfigDict(from_attributes=True)
class CustomerBase(BaseModel): class CustomerBase(BaseModel):
account_number: Optional[str] = None account_number: Optional[str] = None
@@ -97,27 +99,16 @@ class CustomerBase(BaseModel):
class Customer(CustomerBase): class Customer(CustomerBase):
id: int id: int
# --- ADD THIS FIELD ---
auth_net_profile_id: Optional[str] = None auth_net_profile_id: Optional[str] = None
class Config: # --- MODIFICATION: This is the new syntax for Pydantic V2 ---
orm_mode = True model_config = ConfigDict(from_attributes=True)
# --- ADD THIS NEW SCHEMA --- class CustomerCardResponse(Card):
class TransactionAuthorizeByCardID(BaseModel): # We are inheriting all fields from `Card`
# This is for creating an AUTHORIZATION using an internal card_id.
card_id: int
preauthorize_amount: Decimal # Use Decimal
# Fields for Level 2 data (important for rates!) # Now, add the extra customer fields the frontend needs.
tax_amount: Optional[Decimal] = Decimal("0.0") # We define it as a string because we will be returning the
# two-letter abbreviation, not the database ID.
# Your other business-related IDs customer_state: str
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

View File

@@ -1,31 +1,92 @@
## File: your_app/services/payment_service.py ## File: your_app/services/payment_service.py
import logging import logging
import pprint
import traceback
import re
from authorizenet import apicontractsv1 from authorizenet import apicontractsv1
from authorizenet.apicontrollers import ( from authorizenet.apicontrollers import (
createTransactionController, createTransactionController,
createCustomerProfileController, createCustomerProfileController,
createCustomerPaymentProfileController createCustomerPaymentProfileController
) )
from authorizenet.constants import constants
from .. import schemas from .. import schemas
from config import load_config # Assuming you have this from config import load_config # Assuming you have this
from decimal import Decimal
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Load Authorize.net credentials # Load Authorize.net credentials
ApplicationConfig = load_config() ApplicationConfig = load_config()
API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID # API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID
TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY # 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): 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 = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
merchantAuth.name = API_LOGIN_ID
merchantAuth.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( creditCard = apicontractsv1.creditCardType(
cardNumber=card_info.card_number, cardNumber=card_info.card_number,
@@ -34,56 +95,98 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC
) )
billTo = apicontractsv1.customerAddressType( billTo = apicontractsv1.customerAddressType(
firstName=customer.customer_first_name, firstName=first_name,
lastName=customer.customer_last_name, lastName=last_name,
address=customer.customer_address, address=address,
city=customer.customer_town, city=city,
zip=customer.customer_zip, state='MA', # And include it in the object
zip=zip_code,
country="USA" country="USA"
) )
paymentProfile = apicontractsv1.customerPaymentProfileType() paymentProfile = apicontractsv1.customerPaymentProfileType(
paymentProfile.billTo = billTo billTo=billTo,
paymentProfile.payment = apicontractsv1.paymentType(creditCard=creditCard) payment=apicontractsv1.paymentType(creditCard=creditCard)
)
customerProfile = apicontractsv1.customerProfileType( customerProfile = apicontractsv1.customerProfileType(
merchantCustomerId=str(customer.id), merchantCustomerId=str(customer.id),
email=customer.customer_email, email=email,
paymentProfiles=[paymentProfile] paymentProfiles=[paymentProfile]
) )
request = apicontractsv1.createCustomerProfileRequest( request = apicontractsv1.createCustomerProfileRequest(
merchantAuthentication=merchantAuth, merchantAuthentication=merchantAuth,
profile=customerProfile, profile=customerProfile,
validationMode="liveMode" # ========= CHANGE 2.A: USE liveMode =========
validationMode="testMode"
) )
controller = createCustomerProfileController(request) controller = createCustomerProfileController(request)
controller.execute() # ... rest of the function is the same ...
response = controller.getresponse() 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): 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}") 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) 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( creditCard = apicontractsv1.creditCardType(
cardNumber=card_info.card_number, cardNumber=card_info.card_number,
expirationDate=card_info.expiration_date, expirationDate=card_info.expiration_date,
cardCode=card_info.cvv cardCode=card_info.cvv
) )
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( paymentProfile = apicontractsv1.customerPaymentProfileType(
billTo=apicontractsv1.customerAddressType(firstName=customer.customer_first_name, lastName=customer.customer_last_name), billTo=billTo,
payment=apicontractsv1.paymentType(creditCard=creditCard) payment=apicontractsv1.paymentType(creditCard=creditCard)
) )
@@ -91,61 +194,34 @@ def add_payment_profile_to_customer(customer_profile_id: str, customer: schemas.
merchantAuthentication=merchantAuth, merchantAuthentication=merchantAuth,
customerProfileId=customer_profile_id, customerProfileId=customer_profile_id,
paymentProfile=paymentProfile, paymentProfile=paymentProfile,
validationMode="liveMode" # ========= CHANGE 2.B: USE liveMode =========
validationMode="testMode"
) )
controller = createCustomerPaymentProfileController(request) controller = createCustomerPaymentProfileController(request)
controller.execute()
response = controller.getresponse()
if response is not None and response.messages.resultCode == "Ok": try:
return str(response.customerPaymentProfileId) controller.execute()
else: response = controller.getresponse()
error_msg = response.messages.message[0].text.text if response and response.messages and response.messages.message else "Unknown Error" if response is not None and response.messages.resultCode == "Ok":
logger.error(f"Failed to add payment profile: {error_msg}") return str(response.customerPaymentProfileId)
return None 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}'")
# --- NEW CHARGE FUNCTION --- 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)))
def charge_customer_profile(customer_profile_id: str, payment_profile_id: str, transaction_req: schemas.TransactionCreateByCardID): logger.error(pprint.pformat(vars(request)))
logger.info(f"Charging profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.charge_amount}")
merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
profile_to_charge = apicontractsv1.profileTransAuthCaptureType(
customerProfileId=customer_profile_id,
paymentProfileId=payment_profile_id
)
transactionRequest = apicontractsv1.transactionRequestType(
transactionType="authCaptureTransaction",
amount=f"{transaction_req.charge_amount:.2f}",
profile=profile_to_charge
)
# --- 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")
createtransactionrequest = apicontractsv1.createTransactionRequest(
merchantAuthentication=merchantAuth,
transactionRequest=transactionRequest
)
controller = createTransactionController(createtransactionrequest)
controller.execute()
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( raise ValueError(error_msg)
customer_profile_id: str, except Exception as e:
payment_profile_id: str, logger.error(f"A critical exception occurred during the API call: {traceback.format_exc()}")
transaction_req: schemas.TransactionAuthorizeByCardID 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. Creates an AUTH_ONLY transaction against a customer profile.
This holds the funds but does not capture them. This holds the funds but does not capture them.
@@ -154,21 +230,17 @@ def authorize_customer_profile(
merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
# Note the type here: profileTransAuthOnlyType
profile_to_authorize = apicontractsv1.profileTransAuthOnlyType( profile_to_authorize = apicontractsv1.profileTransAuthOnlyType(
customerProfileId=customer_profile_id, customerProfileId=customer_profile_id,
paymentProfileId=payment_profile_id paymentProfileId=payment_profile_id
) )
transactionRequest = apicontractsv1.transactionRequestType( transactionRequest = apicontractsv1.transactionRequestType(
# The key difference: transactionType is "authOnlyTransaction"
transactionType="authOnlyTransaction", transactionType="authOnlyTransaction",
amount=f"{transaction_req.preauthorize_amount:.2f}", amount=f"{transaction_req.preauthorize_amount:.2f}",
profile=profile_to_authorize 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: 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.tax = apicontractsv1.extendedAmountType(amount=f"{transaction_req.tax_amount:.2f}", name="Sales Tax")
@@ -177,6 +249,29 @@ def authorize_customer_profile(
transactionRequest=transactionRequest transactionRequest=transactionRequest
) )
controller = createTransactionController(createtransactionrequest)
controller.execute()
# The response is returned directly to the router to be parsed there
return controller.getresponse()
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)
transactionRequest = apicontractsv1.transactionRequestType(
transactionType="priorAuthCaptureTransaction",
amount=f"{transaction_req.charge_amount:.2f}",
refTransId=transaction_req.auth_net_transaction_id
)
createtransactionrequest = apicontractsv1.createTransactionRequest(
merchantAuthentication=merchantAuth,
transactionRequest=transactionRequest
)
controller = createTransactionController(createtransactionrequest) controller = createTransactionController(createtransactionrequest)
controller.execute() controller.execute()
return controller.getresponse() return controller.getresponse()

View File

@@ -29,6 +29,8 @@ class ApplicationConfig:
] ]
# Authorize.net credentials (Sandbox Test Credentials) # # Authorize.net credentials (Sandbox Test Credentials)
API_LOGIN_ID = '5KP3u95bQpv' # API_LOGIN_ID = '5KP3u95bQpv'
TRANSACTION_KEY = '346HZ32z3fP4hTG2' # TRANSACTION_KEY = '346HZ32z3fP4hTG2'
API_LOGIN_ID = '9U6w96gZmX'
TRANSACTION_KEY = '94s6Qy458mMNJr7G'