Add all project files
This commit is contained in:
14
Dockerfile.dev
Normal file
14
Dockerfile.dev
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.9
|
||||||
|
ENV PYTHONFAULTHANDLER=1
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
ENV MODE="DEVELOPMENT"
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
14
Dockerfile.local
Normal file
14
Dockerfile.local
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.9
|
||||||
|
ENV PYTHONFAULTHANDLER=1
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
ENV MODE="LOCAL"
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
16
Dockerfile.prod
Normal file
16
Dockerfile.prod
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.9
|
||||||
|
|
||||||
|
ENV PYTHONFAULTHANDLER=1
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
ENV MODE="PRODUCTION"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
BIN
__pycache__/config.cpython-39.pyc
Normal file
BIN
__pycache__/config.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/settings_dev.cpython-39.pyc
Normal file
BIN
__pycache__/settings_dev.cpython-39.pyc
Normal file
Binary file not shown.
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/crud.cpython-39.pyc
Normal file
BIN
app/__pycache__/crud.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-39.pyc
Normal file
BIN
app/__pycache__/database.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-39.pyc
Normal file
BIN
app/__pycache__/main.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/models.cpython-39.pyc
Normal file
BIN
app/__pycache__/models.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/schemas.cpython-39.pyc
Normal file
BIN
app/__pycache__/schemas.cpython-39.pyc
Normal file
Binary file not shown.
61
app/crud.py
Normal file
61
app/crud.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from . import models, schemas
|
||||||
|
|
||||||
|
def get_customer(db: Session, customer_id: int):
|
||||||
|
return db.query(models.Customer).filter(models.Customer.id == customer_id).first()
|
||||||
|
|
||||||
|
def get_customer_by_email(db: Session, email: str):
|
||||||
|
return db.query(models.Customer).filter(models.Customer.customer_email == email).first()
|
||||||
|
|
||||||
|
def get_customers(db: Session, skip: int = 0, limit: int = 100):
|
||||||
|
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):
|
||||||
|
db_transaction = models.Transaction(
|
||||||
|
preauthorize_amount=transaction.preauthorize_amount,
|
||||||
|
charge_amount=transaction.charge_amount,
|
||||||
|
transaction_type=transaction.transaction_type,
|
||||||
|
customer_id=customer_id,
|
||||||
|
status=status,
|
||||||
|
auth_net_transaction_id=auth_net_transaction_id,
|
||||||
|
service_id=transaction.service_id,
|
||||||
|
delivery_id=transaction.delivery_id,
|
||||||
|
card_id=transaction.card_id,
|
||||||
|
payment_gateway=transaction.payment_gateway,
|
||||||
|
rejection_reason=transaction.rejection_reason
|
||||||
|
)
|
||||||
|
db.add(db_transaction)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_transaction)
|
||||||
|
return db_transaction
|
||||||
|
|
||||||
|
def get_transaction_by_delivery_id(db: Session, delivery_id: int):
|
||||||
|
return db.query(models.Transaction).filter(
|
||||||
|
models.Transaction.delivery_id == delivery_id,
|
||||||
|
models.Transaction.transaction_type == 1, # auth transactions
|
||||||
|
models.Transaction.status == 0 # approved
|
||||||
|
).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):
|
||||||
|
return db.query(models.Transaction).filter(
|
||||||
|
models.Transaction.auth_net_transaction_id == auth_net_transaction_id
|
||||||
|
).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):
|
||||||
|
transaction = db.query(models.Transaction).filter(models.Transaction.auth_net_transaction_id == auth_net_transaction_id).first()
|
||||||
|
if not transaction:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transaction.charge_amount = charge_amount
|
||||||
|
transaction.transaction_type = 3 # capture
|
||||||
|
transaction.status = status
|
||||||
|
if rejection_reason:
|
||||||
|
transaction.rejection_reason = rejection_reason
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(transaction)
|
||||||
|
return transaction
|
||||||
34
app/database.py
Normal file
34
app/database.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.engine import URL
|
||||||
|
from config import load_config
|
||||||
|
|
||||||
|
|
||||||
|
ApplicationConfig = load_config()
|
||||||
|
|
||||||
|
|
||||||
|
url = URL.create(
|
||||||
|
drivername="postgresql",
|
||||||
|
username=ApplicationConfig.POSTGRES_USERNAME,
|
||||||
|
password=ApplicationConfig.POSTGRES_PW,
|
||||||
|
host=ApplicationConfig.POSTGRES_SERVER,
|
||||||
|
database=ApplicationConfig.POSTGRES_DBNAME00,
|
||||||
|
port=ApplicationConfig.POSTGRES_PORT
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = create_engine(url)
|
||||||
|
|
||||||
|
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = Session()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
34
app/main.py
Normal file
34
app/main.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from .database import engine
|
||||||
|
from . import models
|
||||||
|
from .routers import payment
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from config import load_config
|
||||||
|
|
||||||
|
|
||||||
|
ApplicationConfig = load_config()
|
||||||
|
|
||||||
|
|
||||||
|
models.Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
# print(ApplicationConfig.origins)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=ApplicationConfig.origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(payment.router, prefix="/api", tags=["payment"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root():
|
||||||
|
return {"message": "Welcome to the HVAC Payment API"}
|
||||||
45
app/models.py
Normal file
45
app/models.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
|
||||||
|
from .database import Base
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Customer(Base):
|
||||||
|
__tablename__ = "customers"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
account_number = Column(String(25))
|
||||||
|
customer_last_name = Column(String(250))
|
||||||
|
customer_first_name = Column(String(250))
|
||||||
|
customer_town = Column(String(140))
|
||||||
|
customer_state = Column(Integer)
|
||||||
|
customer_zip = Column(String(25))
|
||||||
|
customer_first_call = Column(DateTime)
|
||||||
|
customer_email = Column(String(500))
|
||||||
|
customer_automatic = Column(Integer)
|
||||||
|
customer_phone_number = Column(String(25))
|
||||||
|
customer_home_type = Column(Integer)
|
||||||
|
customer_apt = Column(String(140))
|
||||||
|
customer_address = Column(String(1000))
|
||||||
|
company_id = Column(Integer)
|
||||||
|
customer_latitude = Column(String(250))
|
||||||
|
customer_longitude = Column(String(250))
|
||||||
|
correct_address = Column(Boolean)
|
||||||
|
|
||||||
|
class Transaction(Base):
|
||||||
|
__tablename__ = "transactions"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
preauthorize_amount = Column(Float, nullable=True) # Amount preauthorized (for auth transactions)
|
||||||
|
charge_amount = Column(Float, nullable=True) # Final charge amount (for charge/capture transactions)
|
||||||
|
transaction_type = Column(Integer) # 0 = charge, 1 = auth, 3 = capture
|
||||||
|
status = Column(Integer) # 0 = approved, 1 = declined
|
||||||
|
auth_net_transaction_id = Column(String, unique=True, index=True, nullable=True)
|
||||||
|
customer_id = Column(Integer)
|
||||||
|
service_id = Column(Integer, nullable=True) # Reference to Service_Service.id
|
||||||
|
delivery_id = Column(Integer, nullable=True) # Reference to Delivery_Delivery.id
|
||||||
|
card_id = Column(Integer, nullable=True) # Reference to credit card used for payment
|
||||||
|
payment_gateway = Column(Integer, default=1) # 1 = Authorize.Net, 0 = Other
|
||||||
|
rejection_reason = Column(String, nullable=True) # Detailed error message when payment is declined
|
||||||
|
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
BIN
app/routers/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/routers/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/payment.cpython-39.pyc
Normal file
BIN
app/routers/__pycache__/payment.cpython-39.pyc
Normal file
Binary file not shown.
133
app/routers/payment.py
Normal file
133
app/routers/payment.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#
|
||||||
|
# your_app/views.py (or wherever this file is located)
|
||||||
|
#
|
||||||
|
import datetime
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
import enum
|
||||||
|
|
||||||
|
# Assuming these are your project's modules
|
||||||
|
from .. import crud, models, schemas, database
|
||||||
|
from ..services import payment_service
|
||||||
|
|
||||||
|
# Placeholder for the Authorize.Net response object type
|
||||||
|
AuthNetResponse = object
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
tags=["Transactions"], # Tags are for documentation only, they don't affect URLs
|
||||||
|
)
|
||||||
|
|
||||||
|
class TransactionStatus(enum.IntEnum):
|
||||||
|
APPROVED = 0
|
||||||
|
DECLINED = 1
|
||||||
|
|
||||||
|
class TransactionType(enum.IntEnum):
|
||||||
|
CHARGE = 0
|
||||||
|
AUTHORIZE = 1
|
||||||
|
CAPTURE = 3
|
||||||
|
|
||||||
|
# --- Helper function to avoid repeating response parsing logic ---
|
||||||
|
def _parse_authnet_response(response: Optional[AuthNetResponse]) -> Tuple[TransactionStatus, Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
Parses the response from the Authorize.Net service.
|
||||||
|
(This is the same helper from before, it's a good change to keep)
|
||||||
|
"""
|
||||||
|
if response and hasattr(response, 'messages') and response.messages.resultCode == "Ok":
|
||||||
|
status = TransactionStatus.APPROVED
|
||||||
|
auth_net_transaction_id = str(response.transactionResponse.transId) if hasattr(response, 'transactionResponse') else None
|
||||||
|
rejection_reason = None
|
||||||
|
else:
|
||||||
|
status = TransactionStatus.DECLINED
|
||||||
|
auth_net_transaction_id = None
|
||||||
|
if hasattr(response, '_rejection_reason'):
|
||||||
|
rejection_reason = str(response._rejection_reason)
|
||||||
|
elif response is None:
|
||||||
|
rejection_reason = "No response received from payment gateway."
|
||||||
|
else:
|
||||||
|
rejection_reason = "Payment declined by gateway."
|
||||||
|
return status, auth_net_transaction_id, rejection_reason
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/authorize/", response_model=schemas.Transaction)
|
||||||
|
def authorize_card(customer_id: int, transaction: schemas.TransactionAuthorize, db: Session = Depends(database.get_db)):
|
||||||
|
auth_net_response = payment_service.authorize_credit_card(transaction)
|
||||||
|
status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response)
|
||||||
|
|
||||||
|
transaction_data = schemas.TransactionBase(
|
||||||
|
preauthorize_amount=transaction.preauthorize_amount,
|
||||||
|
transaction_type=TransactionType.AUTHORIZE,
|
||||||
|
service_id=transaction.service_id,
|
||||||
|
delivery_id=transaction.delivery_id,
|
||||||
|
card_id=transaction.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("/charge/{customer_id}", response_model=schemas.Transaction)
|
||||||
|
def charge_card(customer_id: int, transaction: schemas.TransactionCreate, db: Session = Depends(database.get_db)):
|
||||||
|
# Add debug logging
|
||||||
|
print(f"DEBUG: Received charge request for customer_id: {customer_id}")
|
||||||
|
print(f"DEBUG: Transaction data: {transaction.dict() if hasattr(transaction, 'dict') else transaction}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
print(f"DEBUG: Transaction data to create: {transaction_data.dict()}")
|
||||||
|
|
||||||
|
result = crud.create_transaction(
|
||||||
|
db=db,
|
||||||
|
transaction=transaction_data,
|
||||||
|
customer_id=customer_id,
|
||||||
|
status=status,
|
||||||
|
auth_net_transaction_id=auth_net_transaction_id
|
||||||
|
)
|
||||||
|
print(f"DEBUG: Created transaction: {result.dict()}")
|
||||||
|
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)):
|
||||||
|
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")
|
||||||
|
|
||||||
|
auth_net_response = payment_service.capture_authorized_transaction(transaction)
|
||||||
|
status, _, rejection_reason = _parse_authnet_response(auth_net_response)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
@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
|
||||||
64
app/schemas.py
Normal file
64
app/schemas.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class TransactionBase(BaseModel):
|
||||||
|
preauthorize_amount: Optional[float] = None # Amount preauthorized (for auth transactions)
|
||||||
|
charge_amount: Optional[float] = None # Final charge amount (for charge/capture transactions)
|
||||||
|
transaction_type: int # 0 = charge, 1 = auth, 3 = capture - Required for database
|
||||||
|
service_id: Optional[int] = None # Reference to Service_Service.id
|
||||||
|
delivery_id: Optional[int] = None # Reference to Delivery_Delivery.id
|
||||||
|
card_id: Optional[int] = None # Reference to credit card used for payment
|
||||||
|
payment_gateway: int = 1 # 1 = Authorize.Net, 0 = Other
|
||||||
|
rejection_reason: Optional[str] = None # Detailed error message when payment is declined
|
||||||
|
|
||||||
|
class TransactionCreate(TransactionBase):
|
||||||
|
charge_amount: float # Final charge amount
|
||||||
|
card_number: str
|
||||||
|
expiration_date: str # MM/YY
|
||||||
|
cvv: str
|
||||||
|
|
||||||
|
class TransactionAuthorize(TransactionBase):
|
||||||
|
preauthorize_amount: float # Amount to preauthorize
|
||||||
|
card_number: str
|
||||||
|
expiration_date: str
|
||||||
|
cvv: str
|
||||||
|
|
||||||
|
class TransactionCapture(BaseModel):
|
||||||
|
charge_amount: float # Amount to capture
|
||||||
|
auth_net_transaction_id: str
|
||||||
|
|
||||||
|
class Transaction(TransactionBase):
|
||||||
|
id: int
|
||||||
|
transaction_type: int # 0 = charge, 1 = auth, 3 = capture
|
||||||
|
status: int # 0 = approved, 1 = declined
|
||||||
|
auth_net_transaction_id: Optional[str] = None
|
||||||
|
customer_id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class CustomerBase(BaseModel):
|
||||||
|
account_number: Optional[str] = None
|
||||||
|
customer_last_name: Optional[str] = None
|
||||||
|
customer_first_name: Optional[str] = None
|
||||||
|
customer_town: Optional[str] = None
|
||||||
|
customer_state: Optional[int] = None
|
||||||
|
customer_zip: Optional[str] = None
|
||||||
|
customer_first_call: Optional[datetime] = None
|
||||||
|
customer_email: Optional[str] = None
|
||||||
|
customer_automatic: Optional[int] = None
|
||||||
|
customer_phone_number: Optional[str] = None
|
||||||
|
customer_home_type: Optional[int] = None
|
||||||
|
customer_apt: Optional[str] = None
|
||||||
|
customer_address: Optional[str] = None
|
||||||
|
company_id: Optional[int] = None
|
||||||
|
customer_latitude: Optional[str] = None
|
||||||
|
customer_longitude: Optional[str] = None
|
||||||
|
correct_address: Optional[bool] = None
|
||||||
|
|
||||||
|
class Customer(CustomerBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
BIN
app/services/__pycache__/payment_service.cpython-39.pyc
Normal file
BIN
app/services/__pycache__/payment_service.cpython-39.pyc
Normal file
Binary file not shown.
235
app/services/payment_service.py
Normal file
235
app/services/payment_service.py
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import logging
|
||||||
|
from authorizenet import apicontractsv1
|
||||||
|
from authorizenet.apicontrollers import createTransactionController
|
||||||
|
from .. import schemas
|
||||||
|
from config import load_config
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load Authorize.net credentials from config
|
||||||
|
ApplicationConfig = load_config()
|
||||||
|
API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID
|
||||||
|
TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY
|
||||||
|
# For sandbox, endpoint is https://apitest.authorize.net/xml/v1/request.api
|
||||||
|
|
||||||
|
def safe_string_convert(value):
|
||||||
|
"""
|
||||||
|
Safely convert any value to string, handling lxml objects properly.
|
||||||
|
"""
|
||||||
|
if value is 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):
|
||||||
|
logger.info(f"Processing charge for amount: {transaction.charge_amount}")
|
||||||
|
|
||||||
|
merchantAuth = apicontractsv1.merchantAuthenticationType()
|
||||||
|
merchantAuth.name = API_LOGIN_ID
|
||||||
|
merchantAuth.transactionKey = TRANSACTION_KEY
|
||||||
|
|
||||||
|
creditCard = apicontractsv1.creditCardType()
|
||||||
|
creditCard.cardNumber = transaction.card_number
|
||||||
|
creditCard.expirationDate = transaction.expiration_date
|
||||||
|
creditCard.cardCode = transaction.cvv
|
||||||
|
|
||||||
|
payment = apicontractsv1.paymentType()
|
||||||
|
payment.creditCard = creditCard
|
||||||
|
|
||||||
|
transactionRequest = apicontractsv1.transactionRequestType()
|
||||||
|
transactionRequest.transactionType = "authCaptureTransaction"
|
||||||
|
transactionRequest.amount = transaction.charge_amount # ✅ Fixed: Use charge_amount
|
||||||
|
transactionRequest.payment = payment
|
||||||
|
|
||||||
|
createtransactionrequest = apicontractsv1.createTransactionRequest()
|
||||||
|
createtransactionrequest.merchantAuthentication = merchantAuth
|
||||||
|
createtransactionrequest.refId = "ref_id" # Optional reference ID
|
||||||
|
createtransactionrequest.transactionRequest = transactionRequest
|
||||||
|
|
||||||
|
controller = createTransactionController(createtransactionrequest)
|
||||||
|
controller.execute()
|
||||||
|
|
||||||
|
response = controller.getresponse()
|
||||||
|
|
||||||
|
# Extract rejection reason if payment failed
|
||||||
|
rejection_reason = None
|
||||||
|
if response is not None and response.messages is not None:
|
||||||
|
logger.info(f"Charge response: {response.messages.resultCode}")
|
||||||
|
|
||||||
|
# If payment was declined (resultCode is "Error"), extract error details
|
||||||
|
if response.messages.resultCode == "Error":
|
||||||
|
rejection_reason = "Authorize.Net Charge Error"
|
||||||
|
if hasattr(response.messages, 'message'):
|
||||||
|
try:
|
||||||
|
if len(response.messages.message) > 0:
|
||||||
|
for msg in response.messages.message:
|
||||||
|
if hasattr(msg, 'code') and hasattr(msg, 'text'):
|
||||||
|
# Convert lxml StringElement objects to Python strings
|
||||||
|
code_str = msg.code.text if msg.code else "Unknown"
|
||||||
|
text_str = msg.text.text if msg.text else "No details available"
|
||||||
|
rejection_reason = f"{code_str}: {text_str}"
|
||||||
|
break # Use the first error message
|
||||||
|
elif hasattr(msg, 'text'):
|
||||||
|
# Convert lxml StringElement to Python string
|
||||||
|
text_str = msg.text.text if msg.text else "No details available"
|
||||||
|
rejection_reason = f"Error: {text_str}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rejection_reason = "Charge declined - no specific error details available"
|
||||||
|
except Exception as e:
|
||||||
|
rejection_reason = f"Charge declined - error details could not be parsed: {str(e)}"
|
||||||
|
else:
|
||||||
|
rejection_reason = "Charge declined - no error message available"
|
||||||
|
|
||||||
|
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")
|
||||||
|
rejection_reason = "No response received from Authorize.Net"
|
||||||
|
|
||||||
|
# Attach rejection reason to response for the router to use
|
||||||
|
if response is not None:
|
||||||
|
response._rejection_reason = rejection_reason
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def authorize_credit_card(transaction: schemas.TransactionAuthorize):
|
||||||
|
logger.info(f"Processing preauthorization for amount: {transaction.preauthorize_amount}")
|
||||||
|
|
||||||
|
merchantAuth = apicontractsv1.merchantAuthenticationType()
|
||||||
|
merchantAuth.name = API_LOGIN_ID
|
||||||
|
merchantAuth.transactionKey = TRANSACTION_KEY
|
||||||
|
|
||||||
|
creditCard = apicontractsv1.creditCardType()
|
||||||
|
creditCard.cardNumber = transaction.card_number
|
||||||
|
creditCard.expirationDate = transaction.expiration_date
|
||||||
|
creditCard.cardCode = transaction.cvv
|
||||||
|
|
||||||
|
payment = apicontractsv1.paymentType()
|
||||||
|
payment.creditCard = creditCard
|
||||||
|
|
||||||
|
transactionRequest = apicontractsv1.transactionRequestType()
|
||||||
|
transactionRequest.transactionType = "authOnlyTransaction"
|
||||||
|
transactionRequest.amount = transaction.preauthorize_amount # ✅ Fixed: Use preauthorize_amount
|
||||||
|
transactionRequest.payment = payment
|
||||||
|
|
||||||
|
createtransactionrequest = apicontractsv1.createTransactionRequest()
|
||||||
|
createtransactionrequest.merchantAuthentication = merchantAuth
|
||||||
|
createtransactionrequest.refId = "ref_id"
|
||||||
|
createtransactionrequest.transactionRequest = transactionRequest
|
||||||
|
|
||||||
|
controller = createTransactionController(createtransactionrequest)
|
||||||
|
controller.execute()
|
||||||
|
|
||||||
|
response = controller.getresponse()
|
||||||
|
|
||||||
|
# Extract rejection reason if payment failed
|
||||||
|
rejection_reason = None
|
||||||
|
if response is not None and response.messages is not None:
|
||||||
|
logger.info(f"Preauthorization response: {response.messages.resultCode}")
|
||||||
|
|
||||||
|
# If payment was declined (resultCode is "Error"), extract error details
|
||||||
|
if response.messages.resultCode == "Error":
|
||||||
|
rejection_reason = "Authorize.Net Error"
|
||||||
|
if hasattr(response.messages, 'message'):
|
||||||
|
try:
|
||||||
|
if len(response.messages.message) > 0:
|
||||||
|
for msg in response.messages.message:
|
||||||
|
if hasattr(msg, 'code') and hasattr(msg, 'text'):
|
||||||
|
# Convert lxml StringElement objects to Python strings
|
||||||
|
code_str = msg.code.text if msg.code else "Unknown"
|
||||||
|
text_str = msg.text.text if msg.text else "No details available"
|
||||||
|
rejection_reason = f"{code_str}: {text_str}"
|
||||||
|
break # Use the first error message
|
||||||
|
elif hasattr(msg, 'text'):
|
||||||
|
# Convert lxml StringElement to Python string
|
||||||
|
text_str = msg.text.text if msg.text else "No details available"
|
||||||
|
rejection_reason = f"Error: {text_str}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rejection_reason = "Payment declined - no specific error details available"
|
||||||
|
except Exception as e:
|
||||||
|
rejection_reason = f"Payment declined - error details could not be parsed: {str(e)}"
|
||||||
|
else:
|
||||||
|
rejection_reason = "Payment declined - no error message available"
|
||||||
|
|
||||||
|
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")
|
||||||
|
rejection_reason = "No response received from Authorize.Net"
|
||||||
|
|
||||||
|
# Attach rejection reason to response for the router to use
|
||||||
|
if response is not None:
|
||||||
|
response._rejection_reason = rejection_reason
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Extract rejection reason if capture failed
|
||||||
|
rejection_reason = None
|
||||||
|
if response is not None and response.messages is not None:
|
||||||
|
logger.info(f"Capture response: {response.messages.resultCode}")
|
||||||
|
|
||||||
|
# If capture was declined (resultCode is "Error"), extract error details
|
||||||
|
if response.messages.resultCode == "Error":
|
||||||
|
rejection_reason = "Authorize.Net Capture Error"
|
||||||
|
if hasattr(response.messages, 'message'):
|
||||||
|
try:
|
||||||
|
if len(response.messages.message) > 0:
|
||||||
|
for msg in response.messages.message:
|
||||||
|
if hasattr(msg, 'code') and hasattr(msg, 'text'):
|
||||||
|
# Convert lxml StringElement objects to Python strings
|
||||||
|
code_str = msg.code.text if msg.code else "Unknown"
|
||||||
|
text_str = msg.text.text if msg.text else "No details available"
|
||||||
|
rejection_reason = f"{code_str}: {text_str}"
|
||||||
|
break # Use the first error message
|
||||||
|
elif hasattr(msg, 'text'):
|
||||||
|
# Convert lxml StringElement to Python string
|
||||||
|
text_str = msg.text.text if msg.text else "No details available"
|
||||||
|
rejection_reason = f"Error: {text_str}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rejection_reason = "Capture declined - no specific error details available"
|
||||||
|
except Exception as e:
|
||||||
|
rejection_reason = f"Capture declined - error details could not be parsed: {str(e)}"
|
||||||
|
else:
|
||||||
|
rejection_reason = "Capture declined - no error message available"
|
||||||
|
|
||||||
|
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")
|
||||||
|
rejection_reason = "No response received from Authorize.Net for capture"
|
||||||
|
|
||||||
|
# Attach rejection reason to response for the router to use
|
||||||
|
if response is not None:
|
||||||
|
response._rejection_reason = rejection_reason
|
||||||
|
|
||||||
|
return response
|
||||||
23
config.py
Normal file
23
config.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def load_config(mode=os.environ.get('MODE')):
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"mode is {mode}")
|
||||||
|
if mode == 'PRODUCTION':
|
||||||
|
from settings_prod import ApplicationConfig
|
||||||
|
return ApplicationConfig
|
||||||
|
|
||||||
|
elif mode == 'LOCAL':
|
||||||
|
from settings_local import ApplicationConfig
|
||||||
|
return ApplicationConfig
|
||||||
|
|
||||||
|
elif mode == 'DEVELOPMENT':
|
||||||
|
from settings_dev import ApplicationConfig
|
||||||
|
return ApplicationConfig
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
from settings_local import ApplicationConfig
|
||||||
|
return ApplicationConfig
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
sqlalchemy
|
||||||
|
psycopg2-binary
|
||||||
|
pydantic
|
||||||
|
python-dotenv
|
||||||
|
authorizenet
|
||||||
34
settings_dev.py
Normal file
34
settings_dev.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
|
||||||
|
class ApplicationConfig:
|
||||||
|
"""
|
||||||
|
Basic Configuration for a generic User
|
||||||
|
"""
|
||||||
|
CURRENT_SETTINGS = 'DEVELOPMENT'
|
||||||
|
# databases info
|
||||||
|
POSTGRES_USERNAME = 'postgres'
|
||||||
|
POSTGRES_PW = 'password'
|
||||||
|
POSTGRES_SERVER = '192.168.1.204'
|
||||||
|
POSTGRES_PORT = '5432'
|
||||||
|
POSTGRES_DBNAME00 = 'eamco'
|
||||||
|
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
|
||||||
|
POSTGRES_PW,
|
||||||
|
POSTGRES_SERVER,
|
||||||
|
POSTGRES_DBNAME00
|
||||||
|
)
|
||||||
|
SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI}
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"http://localhost:9000",
|
||||||
|
"https://localhost:9513",
|
||||||
|
"http://localhost:9514",
|
||||||
|
"http://localhost:9512",
|
||||||
|
"http://localhost:9511",
|
||||||
|
"http://localhost:5173", # Frontend port
|
||||||
|
"http://localhost:9516", # Authorize service port
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
# Authorize.net credentials (Sandbox Test Credentials)
|
||||||
|
API_LOGIN_ID = '5KP3u95bQpv'
|
||||||
|
TRANSACTION_KEY = '346HZ32z3fP4hTG2'
|
||||||
33
settings_local.py
Normal file
33
settings_local.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
|
||||||
|
class ApplicationConfig:
|
||||||
|
"""
|
||||||
|
Basic Configuration for a generic User
|
||||||
|
"""
|
||||||
|
CURRENT_SETTINGS = 'LOCAL'
|
||||||
|
# databases info
|
||||||
|
POSTGRES_USERNAME = 'postgres'
|
||||||
|
POSTGRES_PW = 'password'
|
||||||
|
POSTGRES_SERVER = '192.168.1.204'
|
||||||
|
POSTGRES_PORT = '5432'
|
||||||
|
POSTGRES_DBNAME00 = 'auburnoil'
|
||||||
|
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
|
||||||
|
POSTGRES_PW,
|
||||||
|
POSTGRES_SERVER,
|
||||||
|
POSTGRES_DBNAME00
|
||||||
|
)
|
||||||
|
|
||||||
|
SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI}
|
||||||
|
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"http://192.168.1.204:9000",
|
||||||
|
"http://192.168.1.204:9613",
|
||||||
|
"http://192.168.1.204:9614",
|
||||||
|
"http://192.168.1.204:9612",
|
||||||
|
"http://192.168.1.204:9611",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Authorize.net credentials
|
||||||
|
API_LOGIN_ID = 'bizdev05'
|
||||||
|
TRANSACTION_KEY = '4kJd237rZu59qAZd'
|
||||||
26
settings_prod.py
Normal file
26
settings_prod.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
class ApplicationConfig:
|
||||||
|
"""
|
||||||
|
Basic Configuration for a generic User
|
||||||
|
"""
|
||||||
|
CURRENT_SETTINGS = 'PRODUCTION'
|
||||||
|
# databases info
|
||||||
|
POSTGRES_USERNAME = 'postgres'
|
||||||
|
POSTGRES_PW = 'password'
|
||||||
|
POSTGRES_SERVER = '192.168.1.204'
|
||||||
|
POSTGRES_PORT = '5432'
|
||||||
|
POSTGRES_DBNAME00 = 'auburnoil'
|
||||||
|
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
|
||||||
|
POSTGRES_PW,
|
||||||
|
POSTGRES_SERVER,
|
||||||
|
POSTGRES_DBNAME00
|
||||||
|
)
|
||||||
|
SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI}
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"https://oil.edwineames.com",
|
||||||
|
"https://apiauto.edwineames.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Authorize.net credentials
|
||||||
|
API_LOGIN_ID = 'bizdev05'
|
||||||
|
TRANSACTION_KEY = '4kJd237rZu59qAZd'
|
||||||
Reference in New Issue
Block a user