Adding authnet not tested
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,21 +56,28 @@ 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
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This part now works because the service hard-codes the state to "MA"
|
||||||
if not db_customer.auth_net_profile_id:
|
if not db_customer.auth_net_profile_id:
|
||||||
profile_id, payment_id = payment_service.create_customer_profile(customer=customer_schema, card_info=card_info)
|
profile_id, payment_id = payment_service.create_customer_profile(
|
||||||
if not profile_id or not payment_id:
|
customer=customer_schema, card_info=card_info
|
||||||
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)
|
crud.update_customer_auth_net_profile_id(db, customer_id=customer_id, profile_id=profile_id)
|
||||||
payment_profile_id = payment_id
|
payment_profile_id = payment_id
|
||||||
else:
|
else:
|
||||||
@@ -67,13 +86,34 @@ def add_card_to_customer(customer_id: int, card_info: schemas.CardCreate, db: Se
|
|||||||
customer=customer_schema,
|
customer=customer_schema,
|
||||||
card_info=card_info
|
card_info=card_info
|
||||||
)
|
)
|
||||||
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 creates the card in our local database
|
||||||
return new_card
|
new_card = crud.create_customer_card(
|
||||||
|
db=db,
|
||||||
|
customer_id=customer_id,
|
||||||
|
card_info=card_info,
|
||||||
|
payment_profile_id=payment_profile_id
|
||||||
|
)
|
||||||
|
|
||||||
# --- REFACTORED CHARGE ENDPOINT ---
|
# ========= 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
|
||||||
|
|
||||||
|
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:
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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,31 +95,36 @@ 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)
|
||||||
|
# ... rest of the function is the same ...
|
||||||
|
try:
|
||||||
controller.execute()
|
controller.execute()
|
||||||
response = controller.getresponse()
|
response = controller.getresponse()
|
||||||
|
|
||||||
@@ -67,23 +133,60 @@ def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardC
|
|||||||
payment_id = response.customerPaymentProfileIdList[0] if response.customerPaymentProfileIdList else None
|
payment_id = response.customerPaymentProfileIdList[0] if response.customerPaymentProfileIdList else None
|
||||||
return str(profile_id), str(payment_id) if payment_id else None
|
return str(profile_id), str(payment_id) if payment_id else None
|
||||||
else:
|
else:
|
||||||
error_msg = response.messages.message[0].text.text if response and response.messages and response.messages.message else "Unknown Error"
|
error_msg = _get_authnet_error_message(response)
|
||||||
logger.error(f"Failed to create customer profile: {error_msg}")
|
logger.error(f"Failed to create customer profile (API Error): {error_msg}")
|
||||||
return None, None
|
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.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
try:
|
||||||
controller.execute()
|
controller.execute()
|
||||||
response = controller.getresponse()
|
response = controller.getresponse()
|
||||||
|
|
||||||
if response is not None and response.messages.resultCode == "Ok":
|
if response is not None and response.messages.resultCode == "Ok":
|
||||||
return str(response.customerPaymentProfileId)
|
return str(response.customerPaymentProfileId)
|
||||||
else:
|
else:
|
||||||
error_msg = response.messages.message[0].text.text if response and response.messages and response.messages.message else "Unknown Error"
|
error_msg = _get_authnet_error_message(response)
|
||||||
logger.error(f"Failed to add payment profile: {error_msg}")
|
logger.error(f"Failed to add payment profile: {error_msg}")
|
||||||
return None
|
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()
|
||||||
@@ -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'
|
||||||
Reference in New Issue
Block a user