diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..508ae4b --- /dev/null +++ b/Dockerfile.dev @@ -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"] \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..a9e93e0 --- /dev/null +++ b/Dockerfile.local @@ -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"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..92320fc --- /dev/null +++ b/Dockerfile.prod @@ -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"] \ No newline at end of file diff --git a/__pycache__/config.cpython-39.pyc b/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000..3614ff2 Binary files /dev/null and b/__pycache__/config.cpython-39.pyc differ diff --git a/__pycache__/settings_dev.cpython-39.pyc b/__pycache__/settings_dev.cpython-39.pyc new file mode 100644 index 0000000..3f6484c Binary files /dev/null and b/__pycache__/settings_dev.cpython-39.pyc differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..78a8abf Binary files /dev/null and b/app/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/__pycache__/crud.cpython-39.pyc b/app/__pycache__/crud.cpython-39.pyc new file mode 100644 index 0000000..3cfbd43 Binary files /dev/null and b/app/__pycache__/crud.cpython-39.pyc differ diff --git a/app/__pycache__/database.cpython-39.pyc b/app/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000..5caf837 Binary files /dev/null and b/app/__pycache__/database.cpython-39.pyc differ diff --git a/app/__pycache__/main.cpython-39.pyc b/app/__pycache__/main.cpython-39.pyc new file mode 100644 index 0000000..013728c Binary files /dev/null and b/app/__pycache__/main.cpython-39.pyc differ diff --git a/app/__pycache__/models.cpython-39.pyc b/app/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000..fde198b Binary files /dev/null and b/app/__pycache__/models.cpython-39.pyc differ diff --git a/app/__pycache__/schemas.cpython-39.pyc b/app/__pycache__/schemas.cpython-39.pyc new file mode 100644 index 0000000..38031ce Binary files /dev/null and b/app/__pycache__/schemas.cpython-39.pyc differ diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 0000000..06069e1 --- /dev/null +++ b/app/crud.py @@ -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 \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..d586acb --- /dev/null +++ b/app/database.py @@ -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() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0d0e2e9 --- /dev/null +++ b/app/main.py @@ -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"} \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..eac8591 --- /dev/null +++ b/app/models.py @@ -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) diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/__pycache__/__init__.cpython-39.pyc b/app/routers/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..6dba933 Binary files /dev/null and b/app/routers/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/routers/__pycache__/payment.cpython-39.pyc b/app/routers/__pycache__/payment.cpython-39.pyc new file mode 100644 index 0000000..1886df4 Binary files /dev/null and b/app/routers/__pycache__/payment.cpython-39.pyc differ diff --git a/app/routers/payment.py b/app/routers/payment.py new file mode 100644 index 0000000..678d473 --- /dev/null +++ b/app/routers/payment.py @@ -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 diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..a5710da --- /dev/null +++ b/app/schemas.py @@ -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 diff --git a/app/services/__pycache__/payment_service.cpython-39.pyc b/app/services/__pycache__/payment_service.cpython-39.pyc new file mode 100644 index 0000000..466d3e2 Binary files /dev/null and b/app/services/__pycache__/payment_service.cpython-39.pyc differ diff --git a/app/services/payment_service.py b/app/services/payment_service.py new file mode 100644 index 0000000..b67aa80 --- /dev/null +++ b/app/services/payment_service.py @@ -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 diff --git a/config.py b/config.py new file mode 100644 index 0000000..2b79c46 --- /dev/null +++ b/config.py @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0bdfdfb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +sqlalchemy +psycopg2-binary +pydantic +python-dotenv +authorizenet \ No newline at end of file diff --git a/settings_dev.py b/settings_dev.py new file mode 100644 index 0000000..d2edf9a --- /dev/null +++ b/settings_dev.py @@ -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' diff --git a/settings_local.py b/settings_local.py new file mode 100644 index 0000000..572bc99 --- /dev/null +++ b/settings_local.py @@ -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' diff --git a/settings_prod.py b/settings_prod.py new file mode 100644 index 0000000..18bf840 --- /dev/null +++ b/settings_prod.py @@ -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'