Refactor payment service, fix DB session, and consolidate endpoints
- Fix critical NameError in database.py by restoring Session factory - Refactor payment_service.py and crud.py to use shared constants.py and utils.py - Deduplicate state mapping and input sanitization logic - Move transaction amount calculation logic from CRUD to Router layer - Enforce type safety in schemas using IntEnum for TransactionType/Status - Move capture endpoint from transaction.py to payment.py (now /payments/capture) - Update create_customer_profile signature for clarity
This commit is contained in:
@@ -1,133 +1,25 @@
|
||||
## File: your_app/views.py
|
||||
|
||||
"""
|
||||
Payment Router - Main payment processing endpoints for card management and transactions.
|
||||
"""
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Tuple, Optional
|
||||
import enum
|
||||
|
||||
from decimal import Decimal
|
||||
from .. import crud, models, schemas, database
|
||||
from ..services import payment_service
|
||||
from ..services.payment_service import parse_authnet_response
|
||||
from ..constants import TransactionStatus, TransactionType, STATE_ID_TO_ABBREVIATION
|
||||
from config import load_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ApplicationConfig = load_config()
|
||||
|
||||
|
||||
|
||||
|
||||
AuthNetResponse = object
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/payments",
|
||||
tags=["Payments & Transactions"],
|
||||
)
|
||||
|
||||
class TransactionStatus(enum.IntEnum):
|
||||
APPROVED = 0
|
||||
DECLINED = 1
|
||||
|
||||
class TransactionType(enum.IntEnum):
|
||||
CHARGE = 0
|
||||
AUTHORIZE = 1
|
||||
CAPTURE = 3
|
||||
|
||||
|
||||
|
||||
# --- NEW CIM CORE FUNCTIONS ---
|
||||
STATE_ID_TO_ABBREVIATION = {
|
||||
0: "MA",
|
||||
1: "RI",
|
||||
2: "NH",
|
||||
3: "ME",
|
||||
4: "VT",
|
||||
5: "CT",
|
||||
6: "NY"
|
||||
}
|
||||
|
||||
def _parse_authnet_response(response: Optional[AuthNetResponse]) -> Tuple[TransactionStatus, Optional[str], Optional[str]]:
|
||||
"""
|
||||
Parse Authorize.net response with proper attribute access for SDK objects.
|
||||
Authorize.net response objects don't have .text properties, they're direct attributes.
|
||||
"""
|
||||
logger.debug(f"DEBUG: Parsing response, type: {type(response)}")
|
||||
logger.debug(f"DEBUG: Response exists: {response is not None}")
|
||||
|
||||
if response is not None:
|
||||
logger.debug("DEBUG: Checking for messages attribute...")
|
||||
if hasattr(response, 'messages'):
|
||||
logger.debug(f"DEBUG: Messages exist, resultCode: {getattr(response.messages, 'resultCode', 'NO resultCode')}")
|
||||
else:
|
||||
logger.debug("DEBUG: No messages attribute")
|
||||
|
||||
if response.messages.resultCode == "Ok":
|
||||
logger.debug("✅✅ DEBUG: Taking APPROVED path")
|
||||
status = TransactionStatus.APPROVED
|
||||
auth_net_transaction_id = None
|
||||
|
||||
# Extract transaction ID with proper error handling
|
||||
try:
|
||||
if hasattr(response, 'transactionResponse') and response.transactionResponse is not None:
|
||||
if hasattr(response.transactionResponse, 'transId') and response.transactionResponse.transId:
|
||||
auth_net_transaction_id = str(response.transactionResponse.transId)
|
||||
logger.debug(f"✅✅ DEBUG: FOUND transaction ID: {auth_net_transaction_id}")
|
||||
else:
|
||||
logger.debug("DEBUG: transactionResponse exists but no transId")
|
||||
else:
|
||||
logger.debug("DEBUG: No transactionResponse in approved response")
|
||||
except Exception as e:
|
||||
logger.debug(f"DEBUG: Exception extracting transaction ID: {e}")
|
||||
logger.debug(f"DEBUG: Response object inspection:")
|
||||
logger.debug(type(response))
|
||||
if hasattr(response, 'transactionResponse'):
|
||||
logger.debug(f"TransactionResponse type: {type(response.transactionResponse)}")
|
||||
logger.debug(dir(response.transactionResponse))
|
||||
|
||||
rejection_reason = None
|
||||
logger.debug(f"✅✅✅ DEBUG: APPROVED - ID: {auth_net_transaction_id}, rejection: {rejection_reason}")
|
||||
|
||||
else:
|
||||
logger.debug("DEBUG: Taking DECLINED path")
|
||||
status = TransactionStatus.DECLINED
|
||||
auth_net_transaction_id = None
|
||||
rejection_reason = "Payment declined by gateway."
|
||||
|
||||
if response is not None:
|
||||
# Handle transaction response errors
|
||||
if hasattr(response, 'transactionResponse') and response.transactionResponse is not None:
|
||||
if hasattr(response.transactionResponse, 'errors') and response.transactionResponse.errors:
|
||||
logger.debug("DEBUG: Using transactionResponse.errors")
|
||||
try:
|
||||
error = response.transactionResponse.errors[0]
|
||||
# Remove the .text access - use direct attributes
|
||||
error_code = getattr(error, 'errorCode', 'Unknown')
|
||||
error_text = getattr(error, 'errorText', 'Unknown error')
|
||||
rejection_reason = f"{error_code}: {error_text}"
|
||||
logger.debug(f"DEBUG: Transaction error: {rejection_reason}")
|
||||
except Exception as e:
|
||||
logger.debug(f"DEBUG: Exception parsing transaction error: {e}")
|
||||
rejection_reason = "Failed to parse transaction error"
|
||||
|
||||
# Handle message-level errors
|
||||
elif hasattr(response, 'messages') and response.messages:
|
||||
if hasattr(response.messages, 'message') and response.messages.message:
|
||||
logger.debug("DEBUG: Using response.messages.message")
|
||||
try:
|
||||
msg = response.messages.message
|
||||
if isinstance(msg, list):
|
||||
msg = msg[0] if msg else None
|
||||
if msg:
|
||||
code = getattr(msg, 'code', 'Unknown')
|
||||
text = getattr(msg, 'text', 'Unknown error')
|
||||
rejection_reason = f"{code}: {text}"
|
||||
logger.debug(f"DEBUG: Message error: {rejection_reason}")
|
||||
except Exception as e:
|
||||
logger.debug(f"DEBUG: Exception parsing message error: {e}")
|
||||
rejection_reason = "Failed to parse message error"
|
||||
|
||||
logger.debug(f"✅✅✅ DEBUG: FINAL RESULT - Status: {status}, ID: {auth_net_transaction_id}, Reason: {rejection_reason}")
|
||||
return status, auth_net_transaction_id, rejection_reason
|
||||
|
||||
@router.post("/customers/{customer_id}/cards", 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)):
|
||||
@@ -149,11 +41,19 @@ def add_card_to_customer(customer_id: int, card_info: schemas.CardCreate, db: Se
|
||||
try:
|
||||
# This part now works because the service hard-codes the state to "MA"
|
||||
if not db_customer.auth_net_profile_id:
|
||||
profile_id, payment_id = payment_service.create_customer_profile(
|
||||
# 1. Create the customer profile (returns ID, but no payment profile yet)
|
||||
profile_id = payment_service.create_customer_profile(
|
||||
customer=customer_schema, card_info=card_info
|
||||
)
|
||||
# 2. Update local DB with the new profile ID
|
||||
crud.update_customer_auth_net_profile_id(db, customer_id=customer_id, profile_id=profile_id)
|
||||
payment_profile_id = payment_id
|
||||
|
||||
# 3. Explicitly add the payment profile to get the payment_profile_id
|
||||
payment_profile_id = payment_service.add_payment_profile_to_customer(
|
||||
customer_profile_id=profile_id,
|
||||
customer=customer_schema,
|
||||
card_info=card_info
|
||||
)
|
||||
else:
|
||||
payment_profile_id = payment_service.add_payment_profile_to_customer(
|
||||
customer_profile_id=db_customer.auth_net_profile_id,
|
||||
@@ -262,10 +162,16 @@ def charge_saved_card(customer_id: int, transaction_req: schemas.TransactionCrea
|
||||
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)
|
||||
|
||||
# Calculate amounts to save based on status (Business Logic moved from CRUD)
|
||||
final_charge_amount = transaction_req.charge_amount if status == TransactionStatus.APPROVED else Decimal("0.0")
|
||||
final_preauth_amount = Decimal("0.0")
|
||||
|
||||
transaction_data = schemas.TransactionBase(
|
||||
charge_amount=transaction_req.charge_amount,
|
||||
charge_amount=final_charge_amount,
|
||||
preauthorize_amount=final_preauth_amount,
|
||||
transaction_type=TransactionType.CHARGE,
|
||||
service_id=transaction_req.service_id,
|
||||
delivery_id=transaction_req.delivery_id,
|
||||
@@ -355,8 +261,7 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA
|
||||
)
|
||||
|
||||
# Check if the test authorization worked
|
||||
from ..services import payment_service as ps # Need access to _parse_authnet_response
|
||||
test_status, _, test_reason = _parse_authnet_response(test_response)
|
||||
test_status, _, test_reason = parse_authnet_response(test_response)
|
||||
|
||||
if "E00121" in str(test_reason) or test_status == 1: # 1 = DECLINED
|
||||
logger.debug(f"🐛 DEBUG: TEST AUTH FAILED - Payment profile exists but is INVALID!")
|
||||
@@ -427,11 +332,17 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA
|
||||
)
|
||||
|
||||
# Parse the transaction response (no need for E00121 nuclear cleanup since pre-validation should have caught it)
|
||||
transaction_status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response)
|
||||
transaction_status, auth_net_transaction_id, rejection_reason = parse_authnet_response(auth_net_response)
|
||||
|
||||
logger.debug(transaction_req)
|
||||
|
||||
# Calculate amounts to save based on status (Business Logic moved from CRUD)
|
||||
final_preauth_amount = transaction_req.preauthorize_amount if transaction_status == TransactionStatus.APPROVED else Decimal("0.0")
|
||||
final_charge_amount = Decimal("0.0")
|
||||
|
||||
transaction_data = schemas.TransactionBase(
|
||||
preauthorize_amount=transaction_req.preauthorize_amount,
|
||||
preauthorize_amount=final_preauth_amount,
|
||||
charge_amount=final_charge_amount,
|
||||
transaction_type=TransactionType.AUTHORIZE, # This is key
|
||||
service_id=transaction_req.service_id,
|
||||
delivery_id=transaction_req.delivery_id,
|
||||
@@ -449,3 +360,26 @@ def authorize_saved_card(customer_id: int, transaction_req: schemas.TransactionA
|
||||
)
|
||||
|
||||
return db_transaction
|
||||
|
||||
|
||||
@router.post("/capture", response_model=schemas.Transaction, summary="Capture a previously authorized amount")
|
||||
def capture_authorized_amount(transaction: schemas.TransactionCapture, db: Session = Depends(database.get_db)):
|
||||
# This endpoint captures a previously authorized transaction
|
||||
# It finds the original transaction by its ID and captures the funds
|
||||
logger.info(f"POST /payments/capture - Capturing authorized transaction {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:
|
||||
raise HTTPException(status_code=404, detail="Authorization transaction not found")
|
||||
|
||||
# Call the capture service function
|
||||
auth_net_response = payment_service.capture_authorized_transaction(transaction)
|
||||
status, _, rejection_reason = parse_authnet_response(auth_net_response)
|
||||
|
||||
# Use the existing CRUD function to update the transaction
|
||||
return crud.update_transaction_for_capture(
|
||||
db=db,
|
||||
auth_net_transaction_id=transaction.auth_net_transaction_id,
|
||||
charge_amount=transaction.charge_amount,
|
||||
status=status,
|
||||
rejection_reason=rejection_reason
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user