Updated auth api for customer profile
This commit is contained in:
64
app/crud.py
64
app/crud.py
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user