Updated auth api for customer profile

This commit is contained in:
2025-09-14 11:59:31 -04:00
parent 8d134f691b
commit 5a6bcc0700
5 changed files with 431 additions and 245 deletions

View File

@@ -1,5 +1,44 @@
## File: your_app/crud.py
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import models, schemas from . import models, schemas
from decimal import Decimal
# --- NEW CRUD FUNCTIONS FOR CIM ---
def get_card_by_id(db: Session, card_id: int):
return db.query(models.Card).filter(models.Card.id == card_id).first()
def update_customer_auth_net_profile_id(db: Session, customer_id: int, profile_id: str):
db_customer = get_customer(db, customer_id)
if db_customer:
db_customer.auth_net_profile_id = profile_id
db.commit()
db.refresh(db_customer)
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:]
try:
exp_year, exp_month = map(int, card_info.expiration_date.split('-'))
except ValueError:
# Handle cases like "MM/YY" if necessary, but "YYYY-MM" is better
raise ValueError("Expiration date must be in YYYY-MM format")
db_card = models.Card(
customer_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
expiration_year=exp_year,
expiration_month=exp_month
)
db.add(db_card)
db.commit()
db.refresh(db_card)
return db_card
# --- YOUR EXISTING CRUD FUNCTIONS (kept for completeness) ---
def get_customer(db: Session, customer_id: int): def get_customer(db: Session, customer_id: int):
return db.query(models.Customer).filter(models.Customer.id == customer_id).first() return db.query(models.Customer).filter(models.Customer.id == customer_id).first()
@@ -11,9 +50,13 @@ def get_customers(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Customer).offset(skip).limit(limit).all() return db.query(models.Customer).offset(skip).limit(limit).all()
def create_transaction(db: Session, transaction: schemas.TransactionBase, customer_id: int, status: int, auth_net_transaction_id: str = None): def create_transaction(db: Session, transaction: schemas.TransactionBase, customer_id: int, status: int, auth_net_transaction_id: str = None):
# Using your existing logic for saving amounts
preauthorize_amount = transaction.preauthorize_amount if status == 0 else Decimal("0.0")
charge_amount = transaction.charge_amount if (transaction.transaction_type != 1 and status == 0) else Decimal("0.0")
db_transaction = models.Transaction( db_transaction = models.Transaction(
preauthorize_amount=transaction.preauthorize_amount if status == 0 else 0, # Only save pre-auth amount if approved preauthorize_amount=preauthorize_amount,
charge_amount=transaction.charge_amount if transaction.transaction_type != 0 or status == 0 else 0, # Only save charge amount for charges if approved charge_amount=charge_amount,
transaction_type=transaction.transaction_type, transaction_type=transaction.transaction_type,
customer_id=customer_id, customer_id=customer_id,
status=status, status=status,
@@ -32,31 +75,26 @@ def create_transaction(db: Session, transaction: schemas.TransactionBase, custom
def get_transaction_by_delivery_id(db: Session, delivery_id: int): def get_transaction_by_delivery_id(db: Session, delivery_id: int):
return db.query(models.Transaction).filter( return db.query(models.Transaction).filter(
models.Transaction.delivery_id == delivery_id, models.Transaction.delivery_id == delivery_id,
models.Transaction.transaction_type == 1, # auth transactions models.Transaction.transaction_type == 1,
models.Transaction.status == 0 # approved models.Transaction.status == 0
).first() ).first()
# --- THIS IS THE FIX ---
# This function was missing, causing the AttributeError.
# It finds a transaction using the unique Authorize.Net transaction ID.
def get_transaction_by_auth_id(db: Session, auth_net_transaction_id: str): def get_transaction_by_auth_id(db: Session, auth_net_transaction_id: str):
return db.query(models.Transaction).filter( return db.query(models.Transaction).filter(
models.Transaction.auth_net_transaction_id == auth_net_transaction_id models.Transaction.auth_net_transaction_id == auth_net_transaction_id
).first() ).first()
# --- END OF FIX ---
def update_transaction_for_capture(db: Session, auth_net_transaction_id: str, charge_amount: float, status: int, rejection_reason: str = None): def update_transaction_for_capture(db: Session, auth_net_transaction_id: str, charge_amount: Decimal, status: int, rejection_reason: str = None):
transaction = db.query(models.Transaction).filter(models.Transaction.auth_net_transaction_id == auth_net_transaction_id).first() transaction = db.query(models.Transaction).filter(models.Transaction.auth_net_transaction_id == auth_net_transaction_id).first()
if not transaction: if not transaction:
return None return None
# Set charge_amount only if approved (status == 0), otherwise set to 0 transaction.charge_amount = charge_amount if status == 0 else Decimal("0.0")
transaction.charge_amount = charge_amount if status == 0 else 0 transaction.transaction_type = 3
transaction.transaction_type = 3 # capture
transaction.status = status transaction.status = status
if rejection_reason: if rejection_reason:
transaction.rejection_reason = rejection_reason transaction.rejection_reason = rejection_reason
db.commit() db.commit()
db.refresh(transaction) db.refresh(transaction)
return transaction return transaction

View File

@@ -1,14 +1,19 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean ## File: your_app/models.py
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, Numeric
from .database import Base from .database import Base
import datetime import datetime
class Customer(Base): class Customer(Base):
__tablename__ = "customers" __tablename__ = "customers"
id = Column(Integer, primary_key=True, index=True) 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)
# --- YOUR EXISTING COLUMNS ---
account_number = Column(String(25)) account_number = Column(String(25))
customer_last_name = Column(String(250)) customer_last_name = Column(String(250))
customer_first_name = Column(String(250)) customer_first_name = Column(String(250))
@@ -27,19 +32,37 @@ class Customer(Base):
customer_longitude = Column(String(250)) customer_longitude = Column(String(250))
correct_address = Column(Boolean) correct_address = Column(Boolean)
# --- ADD THIS ENTIRE NEW MODEL ---
class Card(Base):
__tablename__ = "cards"
id = Column(Integer, primary_key=True, index=True)
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True)
# 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)
expiration_month = Column(Integer, nullable=False)
expiration_year = Column(Integer, nullable=False)
class Transaction(Base): class Transaction(Base):
__tablename__ = "transactions" __tablename__ = "transactions"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
preauthorize_amount = Column(Float, nullable=True) # Amount preauthorized (for auth transactions) # Recommended change: Use Numeric for precision
charge_amount = Column(Float, nullable=True) # Final charge amount (for charge/capture transactions) preauthorize_amount = Column(Numeric(10, 2), nullable=True)
transaction_type = Column(Integer) # 0 = charge, 1 = auth, 3 = capture charge_amount = Column(Numeric(10, 2), nullable=True)
status = Column(Integer) # 0 = approved, 1 = declined
transaction_type = Column(Integer)
status = Column(Integer)
auth_net_transaction_id = Column(String, unique=True, index=True, nullable=True) auth_net_transaction_id = Column(String, unique=True, index=True, nullable=True)
customer_id = Column(Integer) customer_id = Column(Integer, ForeignKey("customers.id"))
service_id = Column(Integer, nullable=True) # Reference to Service_Service.id service_id = Column(Integer, nullable=True)
delivery_id = Column(Integer, nullable=True) # Reference to Delivery_Delivery.id delivery_id = Column(Integer, nullable=True)
card_id = Column(Integer, nullable=True) # Reference to credit card used for payment card_id = Column(Integer, ForeignKey("cards.id"), nullable=True)
payment_gateway = Column(Integer, default=1) # 1 = Authorize.Net, 0 = Other payment_gateway = Column(Integer, default=1)
rejection_reason = Column(String, nullable=True) # Detailed error message when payment is declined rejection_reason = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.datetime.utcnow) created_at = Column(DateTime, default=datetime.datetime.utcnow)

View File

@@ -1,22 +1,18 @@
# ## File: your_app/views.py
# your_app/views.py (or wherever this file is located)
#
import datetime
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Tuple, Optional from typing import Tuple, Optional
import enum import enum
# Assuming these are your project's modules
from .. import crud, models, schemas, database from .. import crud, models, schemas, database
from ..services import payment_service from ..services import payment_service
# Placeholder for the Authorize.Net response object type
AuthNetResponse = object AuthNetResponse = object
router = APIRouter( router = APIRouter(
tags=["Transactions"], # Tags are for documentation only, they don't affect URLs prefix="/payments",
tags=["Payments & Transactions"],
) )
class TransactionStatus(enum.IntEnum): class TransactionStatus(enum.IntEnum):
@@ -28,59 +24,121 @@ class TransactionType(enum.IntEnum):
AUTHORIZE = 1 AUTHORIZE = 1
CAPTURE = 3 CAPTURE = 3
# --- Helper function to avoid repeating response parsing logic --- # 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]]:
""" # ... (Your existing _parse_authnet_response function code) ...
Parses the response from the Authorize.Net service.
(This is the same helper from before, it's a good change to keep)
"""
if response is not None and hasattr(response, 'messages') and response.messages.resultCode == "Ok": if response is not None and hasattr(response, 'messages') and response.messages.resultCode == "Ok":
status = TransactionStatus.APPROVED status = TransactionStatus.APPROVED
auth_net_transaction_id = str(response.transactionResponse.transId) if hasattr(response, 'transactionResponse') else None auth_net_transaction_id = str(response.transactionResponse.transId) if hasattr(response, 'transactionResponse') and response.transactionResponse.transId else None
rejection_reason = None rejection_reason = None
else: else:
status = TransactionStatus.DECLINED status = TransactionStatus.DECLINED
auth_net_transaction_id = None auth_net_transaction_id = None
# Improved rejection reason extraction
rejection_reason = "Payment declined by gateway." rejection_reason = "Payment declined by gateway."
if hasattr(response, '_rejection_reason') and response._rejection_reason: if response is not None:
rejection_reason = str(response._rejection_reason) if hasattr(response, 'transactionResponse') and response.transactionResponse and hasattr(response.transactionResponse, 'errors') and response.transactionResponse.errors:
elif response is not None: error = response.transactionResponse.errors[0]
# Check transaction response for errors rejection_reason = f"{error.errorCode.text}: {error.errorText.text}"
if hasattr(response, 'transactionResponse') and response.transactionResponse: elif hasattr(response, 'messages') and response.messages and hasattr(response.messages, 'message') and response.messages.message:
tr = response.transactionResponse msg = response.messages.message[0]
if hasattr(tr, 'errors') and tr.errors: rejection_reason = f"{msg.code.text}: {msg.text.text}"
for error in tr.errors:
if hasattr(error, 'errorCode') and hasattr(error, 'errorText'):
error_code = error.errorCode.text if error.errorCode else "Unknown"
error_text = error.errorText.text if error.errorText else "No error details"
rejection_reason = f"{error_code}: {error_text}"
break
elif hasattr(tr, 'messages') and tr.messages:
if len(tr.messages) > 0:
msg = tr.messages[0]
if hasattr(msg, 'code') and hasattr(msg, 'description'):
code = msg.code.text if msg.code else "Unknown"
desc = msg.description.text if msg.description else "No description"
rejection_reason = f"{code}: {desc}"
elif response is None:
rejection_reason = "No response received from payment gateway."
return status, auth_net_transaction_id, rejection_reason return status, auth_net_transaction_id, rejection_reason
# --- NEW ENDPOINT TO ADD A CARD ---
@router.post("/authorize/", response_model=schemas.Transaction) @router.post("/customers/{customer_id}/cards", response_model=schemas.Card, summary="Add a new payment card for a customer")
def authorize_card(customer_id: int, transaction: schemas.TransactionAuthorize, db: Session = Depends(database.get_db)): def add_card_to_customer(customer_id: int, card_info: schemas.CardCreate, db: Session = Depends(database.get_db)):
auth_net_response = payment_service.authorize_credit_card(transaction) db_customer = crud.get_customer(db, customer_id=customer_id)
if not db_customer:
raise HTTPException(status_code=404, detail="Customer not found")
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
)
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)
return new_card
# --- REFACTORED CHARGE ENDPOINT ---
@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)):
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:
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:
raise HTTPException(status_code=400, detail="Payment profile is not set up correctly for this customer/card")
auth_net_response = payment_service.charge_customer_profile(
customer_profile_id=db_customer.auth_net_profile_id,
payment_profile_id=db_card.auth_net_payment_profile_id,
transaction_req=transaction_req
)
status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response) status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response)
transaction_data = schemas.TransactionBase( transaction_data = schemas.TransactionBase(
preauthorize_amount=transaction.preauthorize_amount, charge_amount=transaction_req.charge_amount,
transaction_type=TransactionType.AUTHORIZE, transaction_type=TransactionType.CHARGE,
service_id=transaction.service_id, service_id=transaction_req.service_id,
delivery_id=transaction.delivery_id, delivery_id=transaction_req.delivery_id,
card_id=transaction.card_id, card_id=transaction_req.card_id,
rejection_reason=rejection_reason
)
return crud.create_transaction(db=db, transaction=transaction_data, customer_id=customer_id, status=status, auth_net_transaction_id=auth_net_transaction_id)
@router.post("/authorize/saved-card/{customer_id}", response_model=schemas.Transaction, summary="Authorize a payment on a saved card")
def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionAuthorizeByCardID, db: Session = Depends(database.get_db)):
"""
Creates a pre-authorization on a customer's saved card.
This validates the card and reserves the funds.
"""
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:
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:
raise HTTPException(status_code=400, detail="Payment profile is not set up correctly for this customer/card")
# Call the NEW service function for authorization
auth_net_response = payment_service.authorize_customer_profile(
customer_profile_id=db_customer.auth_net_profile_id,
payment_profile_id=db_card.auth_net_payment_profile_id,
transaction_req=transaction_req
)
status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response)
# Create the transaction record in your database with the correct type
transaction_data = schemas.TransactionBase(
preauthorize_amount=transaction_req.preauthorize_amount,
transaction_type=TransactionType.AUTHORIZE, # This is key
service_id=transaction_req.service_id,
delivery_id=transaction_req.delivery_id,
card_id=transaction_req.card_id,
rejection_reason=rejection_reason rejection_reason=rejection_reason
) )
@@ -92,57 +150,25 @@ def authorize_card(customer_id: int, transaction: schemas.TransactionAuthorize,
auth_net_transaction_id=auth_net_transaction_id auth_net_transaction_id=auth_net_transaction_id
) )
@router.post("/charge/{customer_id}", response_model=schemas.Transaction) # --- YOUR EXISTING CAPTURE ENDPOINT NEEDS NO CHANGES ---
def charge_card(customer_id: int, transaction: schemas.TransactionCreate, db: Session = Depends(database.get_db)):
try: @router.post("/capture/", response_model=schemas.Transaction, summary="Capture a previously authorized amount")
auth_net_response = payment_service.charge_credit_card(transaction)
status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response)
transaction_data = schemas.TransactionBase(
charge_amount=transaction.charge_amount,
transaction_type=TransactionType.CHARGE,
service_id=transaction.service_id,
delivery_id=transaction.delivery_id,
card_id=transaction.card_id,
rejection_reason=rejection_reason
)
result = crud.create_transaction(
db=db,
transaction=transaction_data,
customer_id=customer_id,
status=status,
auth_net_transaction_id=auth_net_transaction_id
)
return result
except Exception as e:
print(f"DEBUG: Exception in charge_card: {str(e)}")
import traceback
print(f"DEBUG: Traceback: {traceback.format_exc()}")
raise
@router.post("/capture/", response_model=schemas.Transaction)
def capture_authorized_amount(transaction: schemas.TransactionCapture, db: Session = Depends(database.get_db)): def capture_authorized_amount(transaction: schemas.TransactionCapture, db: Session = Depends(database.get_db)):
# This endpoint continues to work perfectly.
# It finds the original transaction by its ID and captures the funds.
auth_transaction = crud.get_transaction_by_auth_id(db, auth_net_transaction_id=transaction.auth_net_transaction_id) auth_transaction = crud.get_transaction_by_auth_id(db, auth_net_transaction_id=transaction.auth_net_transaction_id)
if not auth_transaction: if not auth_transaction:
raise HTTPException(status_code=404, detail="Authorization transaction not found") raise HTTPException(status_code=404, detail="Authorization transaction not found")
# ... The rest of your existing capture logic remains the same ...
auth_net_response = payment_service.capture_authorized_transaction(transaction) auth_net_response = payment_service.capture_authorized_transaction(transaction)
status, _, rejection_reason = _parse_authnet_response(auth_net_response) status, _, rejection_reason = _parse_authnet_response(auth_net_response) # The capture response doesn't return a new ID
# Use your existing CRUD function to update the transaction
return crud.update_transaction_for_capture( return crud.update_transaction_for_capture(
db=db, db=db,
auth_net_transaction_id=transaction.auth_net_transaction_id, auth_net_transaction_id=transaction.auth_net_transaction_id,
charge_amount=transaction.charge_amount, charge_amount=transaction.charge_amount,
status=status, status=status,
rejection_reason=rejection_reason rejection_reason=rejection_reason
) )
@router.get("/transaction/delivery/{delivery_id}", response_model=schemas.Transaction)
def get_transaction_by_delivery(delivery_id: int, db: Session = Depends(database.get_db)):
transaction = crud.get_transaction_by_delivery_id(db, delivery_id=delivery_id)
if not transaction:
raise HTTPException(status_code=404, detail="No pre-authorized transaction found for this delivery")
return transaction

View File

@@ -1,39 +1,77 @@
## File: your_app/schemas.py
from pydantic import BaseModel from pydantic import BaseModel
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
# --- NEW SCHEMAS FOR CIM WORKFLOW ---
class CardCreate(BaseModel):
# This schema receives sensitive card info just once.
card_number: str
expiration_date: str # Format: "YYYY-MM"
cvv: str
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
expiration_month: int
expiration_year: int
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
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 TransactionBase(BaseModel): class TransactionBase(BaseModel):
preauthorize_amount: Optional[float] = None # Amount preauthorized (for auth transactions) preauthorize_amount: Optional[Decimal] = None
charge_amount: Optional[float] = None # Final charge amount (for charge/capture transactions) charge_amount: Optional[Decimal] = None
transaction_type: int # 0 = charge, 1 = auth, 3 = capture - Required for database transaction_type: int
service_id: Optional[int] = None # Reference to Service_Service.id service_id: Optional[int] = None
delivery_id: Optional[int] = None # Reference to Delivery_Delivery.id delivery_id: Optional[int] = None
card_id: Optional[int] = None # Reference to credit card used for payment card_id: Optional[int] = None
payment_gateway: int = 1 # 1 = Authorize.Net, 0 = Other payment_gateway: int = 1
rejection_reason: Optional[str] = None # Detailed error message when payment is declined rejection_reason: Optional[str] = None
class TransactionCreate(TransactionBase): class TransactionCreate(TransactionBase):
charge_amount: float # Final charge amount charge_amount: Decimal
card_number: str card_number: str
expiration_date: str # MM/YY expiration_date: str # MM/YY
cvv: str cvv: str
class TransactionAuthorize(TransactionBase): class TransactionAuthorize(TransactionBase):
preauthorize_amount: float # Amount to preauthorize preauthorize_amount: Decimal
card_number: str card_number: str
expiration_date: str expiration_date: str
cvv: str cvv: str
class TransactionCapture(BaseModel): class TransactionCapture(BaseModel):
charge_amount: float # Amount to capture charge_amount: Decimal
auth_net_transaction_id: str auth_net_transaction_id: str
class Transaction(TransactionBase): class Transaction(TransactionBase):
id: int id: int
transaction_type: int # 0 = charge, 1 = auth, 3 = capture transaction_type: int
status: int # 0 = approved, 1 = declined status: int
auth_net_transaction_id: Optional[str] = None auth_net_transaction_id: Optional[str] = None
customer_id: int customer_id: int
created_at: datetime
class Config: class Config:
orm_mode = True orm_mode = True
@@ -59,6 +97,27 @@ class CustomerBase(BaseModel):
class Customer(CustomerBase): class Customer(CustomerBase):
id: int id: int
# --- ADD THIS FIELD ---
auth_net_profile_id: Optional[str] = None
class Config: class Config:
orm_mode = True orm_mode = 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
# 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

View File

@@ -1,142 +1,182 @@
## File: your_app/services/payment_service.py
import logging import logging
from authorizenet import apicontractsv1 from authorizenet import apicontractsv1
from authorizenet.apicontrollers import createTransactionController from authorizenet.apicontrollers import (
createTransactionController,
createCustomerProfileController,
createCustomerPaymentProfileController
)
from .. import schemas from .. import schemas
from config import load_config from config import load_config # Assuming you have this
from decimal import Decimal
# Set up logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Load Authorize.net credentials from config # 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
# For sandbox, endpoint is https://apitest.authorize.net/xml/v1/request.api
def safe_string_convert(value): # --- NEW CIM CORE FUNCTIONS ---
"""
Safely convert any value to string, handling lxml objects properly. def create_customer_profile(customer: schemas.Customer, card_info: schemas.CardCreate):
""" logger.info(f"Creating Auth.Net profile for internal customer ID: {customer.id}")
if value is None:
merchantAuth = apicontractsv1.merchantAuthenticationType()
merchantAuth.name = API_LOGIN_ID
merchantAuth.transactionKey = TRANSACTION_KEY
creditCard = apicontractsv1.creditCardType(
cardNumber=card_info.card_number,
expirationDate=card_info.expiration_date,
cardCode=card_info.cvv
)
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,
country="USA"
)
paymentProfile = apicontractsv1.customerPaymentProfileType()
paymentProfile.billTo = billTo
paymentProfile.payment = apicontractsv1.paymentType(creditCard=creditCard)
customerProfile = apicontractsv1.customerProfileType(
merchantCustomerId=str(customer.id),
email=customer.customer_email,
paymentProfiles=[paymentProfile]
)
request = apicontractsv1.createCustomerProfileRequest(
merchantAuthentication=merchantAuth,
profile=customerProfile,
validationMode="liveMode"
)
controller = createCustomerProfileController(request)
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 = 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)
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)
)
request = apicontractsv1.createCustomerPaymentProfileRequest(
merchantAuthentication=merchantAuth,
customerProfileId=customer_profile_id,
paymentProfile=paymentProfile,
validationMode="liveMode"
)
controller = createCustomerPaymentProfileController(request)
controller.execute()
response = controller.getresponse()
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 return None
# Check if it's an lxml object with text attribute
if hasattr(value, 'text'):
return value.text
# Otherwise use standard string conversion
return str(value)
def charge_credit_card(transaction: schemas.TransactionCreate): # --- NEW CHARGE FUNCTION ---
logger.info(f"Processing charge for amount: {transaction.charge_amount}")
merchantAuth = apicontractsv1.merchantAuthenticationType() def charge_customer_profile(customer_profile_id: str, payment_profile_id: str, transaction_req: schemas.TransactionCreateByCardID):
merchantAuth.name = API_LOGIN_ID logger.info(f"Charging profile {customer_profile_id} / payment {payment_profile_id} for ${transaction_req.charge_amount}")
merchantAuth.transactionKey = TRANSACTION_KEY
creditCard = apicontractsv1.creditCardType() merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
creditCard.cardNumber = transaction.card_number
creditCard.expirationDate = transaction.expiration_date
creditCard.cardCode = transaction.cvv
payment = apicontractsv1.paymentType() profile_to_charge = apicontractsv1.profileTransAuthCaptureType(
payment.creditCard = creditCard customerProfileId=customer_profile_id,
paymentProfileId=payment_profile_id
transactionRequest = apicontractsv1.transactionRequestType() )
transactionRequest.transactionType = "authCaptureTransaction"
transactionRequest.amount = transaction.charge_amount # ✅ Fixed: Use charge_amount transactionRequest = apicontractsv1.transactionRequestType(
transactionRequest.payment = payment transactionType="authCaptureTransaction",
amount=f"{transaction_req.charge_amount:.2f}",
createtransactionrequest = apicontractsv1.createTransactionRequest() profile=profile_to_charge
createtransactionrequest.merchantAuthentication = merchantAuth )
createtransactionrequest.refId = "ref_id" # Optional reference ID
createtransactionrequest.transactionRequest = transactionRequest # --- 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 = createTransactionController(createtransactionrequest)
controller.execute() controller.execute()
return controller.getresponse()
response = 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)
# Log response status if payment failed
if response is not None and response.messages is not None:
logger.info(f"Charge response: {response.messages.resultCode}")
if hasattr(response.messages, 'message') and len(response.messages.message) > 0:
for msg in response.messages.message:
logger.info(f"Message: {msg.text.text if msg.text else 'No message text'}")
else:
logger.error("No response from Authorize.net")
return response 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 authorize_credit_card(transaction: schemas.TransactionAuthorize): merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY)
logger.info(f"Processing preauthorization for amount: {transaction.preauthorize_amount}")
merchantAuth = apicontractsv1.merchantAuthenticationType() # Note the type here: profileTransAuthOnlyType
merchantAuth.name = API_LOGIN_ID profile_to_authorize = apicontractsv1.profileTransAuthOnlyType(
merchantAuth.transactionKey = TRANSACTION_KEY customerProfileId=customer_profile_id,
paymentProfileId=payment_profile_id
)
creditCard = apicontractsv1.creditCardType() transactionRequest = apicontractsv1.transactionRequestType(
creditCard.cardNumber = transaction.card_number # The key difference: transactionType is "authOnlyTransaction"
creditCard.expirationDate = transaction.expiration_date transactionType="authOnlyTransaction",
creditCard.cardCode = transaction.cvv amount=f"{transaction_req.preauthorize_amount:.2f}",
profile=profile_to_authorize
payment = apicontractsv1.paymentType() )
payment.creditCard = creditCard
# --- LEVEL 2/3 DATA IS STILL CRITICAL HERE ---
transactionRequest = apicontractsv1.transactionRequestType() # The initial authorization is what the card issuers use to determine your rates.
transactionRequest.transactionType = "authOnlyTransaction" if transaction_req.tax_amount and transaction_req.tax_amount > 0:
transactionRequest.amount = transaction.preauthorize_amount # ✅ Fixed: Use preauthorize_amount transactionRequest.tax = apicontractsv1.extendedAmountType(amount=f"{transaction_req.tax_amount:.2f}", name="Sales Tax")
transactionRequest.payment = payment
createtransactionrequest = apicontractsv1.createTransactionRequest(
createtransactionrequest = apicontractsv1.createTransactionRequest() merchantAuthentication=merchantAuth,
createtransactionrequest.merchantAuthentication = merchantAuth transactionRequest=transactionRequest
createtransactionrequest.refId = "ref_id" )
createtransactionrequest.transactionRequest = transactionRequest
controller = createTransactionController(createtransactionrequest) controller = createTransactionController(createtransactionrequest)
controller.execute() controller.execute()
return controller.getresponse()
response = controller.getresponse()
# Log response status
if response is not None and response.messages is not None:
logger.info(f"Preauthorization response: {response.messages.resultCode}")
if hasattr(response.messages, 'message') and len(response.messages.message) > 0:
for msg in response.messages.message:
logger.info(f"Message: {msg.text.text if msg.text else 'No message text'}")
else:
logger.error("No response from Authorize.net for preauthorization")
return response
def capture_authorized_transaction(transaction: schemas.TransactionCapture):
merchantAuth = apicontractsv1.merchantAuthenticationType()
merchantAuth.name = API_LOGIN_ID
merchantAuth.transactionKey = TRANSACTION_KEY
transactionRequest = apicontractsv1.transactionRequestType()
transactionRequest.transactionType = "priorAuthCaptureTransaction"
transactionRequest.amount = transaction.charge_amount # ✅ Fixed: Use charge_amount
transactionRequest.refTransId = transaction.auth_net_transaction_id
createtransactionrequest = apicontractsv1.createTransactionRequest()
createtransactionrequest.merchantAuthentication = merchantAuth
createtransactionrequest.refId = "ref_id"
createtransactionrequest.transactionRequest = transactionRequest
controller = createTransactionController(createtransactionrequest)
controller.execute()
response = controller.getresponse()
# Log response status
if response is not None and response.messages is not None:
logger.info(f"Capture response: {response.messages.resultCode}")
if hasattr(response.messages, 'message') and len(response.messages.message) > 0:
for msg in response.messages.message:
logger.info(f"Message: {msg.text.text if msg.text else 'No message text'}")
else:
logger.error("No response from Authorize.net for capture")
return response