Square Payments
This commit is contained in:
@@ -1,19 +1,33 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from ...core.database import get_db
|
||||
from ...models.models import Payment, PaymentStatus, User, Membership, MembershipStatus
|
||||
from ...models.models import Payment, PaymentStatus, PaymentMethod, User, Membership, MembershipStatus, MembershipTier
|
||||
from ...schemas import (
|
||||
PaymentCreate, PaymentUpdate, PaymentResponse, MessageResponse
|
||||
PaymentCreate, PaymentUpdate, PaymentResponse, MessageResponse,
|
||||
SquarePaymentRequest, SquarePaymentResponse, SquareRefundRequest
|
||||
)
|
||||
from ...api.dependencies import get_current_active_user, get_admin_user
|
||||
from ...services.email_service import email_service
|
||||
from ...services.square_service import square_service
|
||||
from ...core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/config/square")
|
||||
async def get_square_config():
|
||||
"""Get Square configuration for frontend"""
|
||||
return {
|
||||
"application_id": settings.SQUARE_APPLICATION_ID,
|
||||
"location_id": settings.SQUARE_LOCATION_ID,
|
||||
"environment": settings.SQUARE_ENVIRONMENT
|
||||
}
|
||||
|
||||
|
||||
@router.get("/my-payments", response_model=List[PaymentResponse])
|
||||
async def get_my_payments(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
@@ -144,6 +158,181 @@ async def update_payment(
|
||||
return payment
|
||||
|
||||
|
||||
@router.post("/square/process", response_model=SquarePaymentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def process_square_payment(
|
||||
payment_request: SquarePaymentRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Process a Square payment and create membership
|
||||
This endpoint receives a payment token from the Square Web Payments SDK,
|
||||
processes the payment through Square's API, and creates an ACTIVE membership
|
||||
"""
|
||||
# Verify tier exists
|
||||
tier = db.query(MembershipTier).filter(
|
||||
MembershipTier.id == payment_request.tier_id,
|
||||
MembershipTier.is_active == True
|
||||
).first()
|
||||
|
||||
if not tier:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Membership tier not found or not active"
|
||||
)
|
||||
|
||||
# Create a reference ID for tracking
|
||||
reference_id = f"user_{current_user.id}_tier_{tier.id}_{datetime.utcnow().timestamp()}"
|
||||
|
||||
# Process payment with Square
|
||||
square_result = await square_service.create_payment(
|
||||
amount_money=payment_request.amount,
|
||||
source_id=payment_request.source_id,
|
||||
idempotency_key=payment_request.idempotency_key,
|
||||
reference_id=reference_id,
|
||||
note=payment_request.note or f"Membership payment for {tier.name} - {current_user.email}"
|
||||
)
|
||||
|
||||
if not square_result.get('success'):
|
||||
# Payment failed - don't create membership
|
||||
return SquarePaymentResponse(
|
||||
success=False,
|
||||
errors=square_result.get('errors', ['Payment processing failed'])
|
||||
)
|
||||
|
||||
# Payment succeeded - create membership and payment records in a transaction
|
||||
try:
|
||||
# Calculate membership dates
|
||||
start_date = datetime.utcnow().date()
|
||||
end_date = start_date + relativedelta(years=1)
|
||||
|
||||
# Create membership with ACTIVE status
|
||||
membership = Membership(
|
||||
user_id=current_user.id,
|
||||
tier_id=tier.id,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
status=MembershipStatus.ACTIVE
|
||||
)
|
||||
db.add(membership)
|
||||
db.flush() # Get membership ID without committing
|
||||
|
||||
# Create payment record
|
||||
payment = Payment(
|
||||
user_id=current_user.id,
|
||||
membership_id=membership.id,
|
||||
amount=payment_request.amount,
|
||||
payment_method=PaymentMethod.SQUARE,
|
||||
status=PaymentStatus.COMPLETED,
|
||||
transaction_id=square_result.get('payment_id'),
|
||||
payment_date=datetime.utcnow(),
|
||||
notes=payment_request.note
|
||||
)
|
||||
db.add(payment)
|
||||
|
||||
# Commit both together
|
||||
db.commit()
|
||||
db.refresh(membership)
|
||||
db.refresh(payment)
|
||||
|
||||
# Send activation email (non-blocking)
|
||||
try:
|
||||
await email_service.send_membership_activation_email(
|
||||
to_email=current_user.email,
|
||||
first_name=current_user.first_name,
|
||||
membership_tier=tier.name,
|
||||
annual_fee=tier.annual_fee,
|
||||
payment_amount=payment.amount,
|
||||
payment_method="Square",
|
||||
renewal_date=end_date.strftime("%d %B %Y"),
|
||||
db=db
|
||||
)
|
||||
except Exception as e:
|
||||
# Log error but don't fail the payment
|
||||
print(f"Failed to send membership activation email: {e}")
|
||||
|
||||
return SquarePaymentResponse(
|
||||
success=True,
|
||||
payment_id=square_result.get('payment_id'),
|
||||
status=square_result.get('status'),
|
||||
amount=payment_request.amount,
|
||||
currency='GBP',
|
||||
receipt_url=square_result.get('receipt_url'),
|
||||
database_payment_id=payment.id,
|
||||
membership_id=membership.id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
# Payment succeeded but membership creation failed
|
||||
# This is a critical error - we should log it and potentially refund
|
||||
print(f"CRITICAL: Payment succeeded but membership creation failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Payment processed but membership creation failed. Please contact support."
|
||||
)
|
||||
|
||||
|
||||
@router.post("/square/refund", response_model=MessageResponse)
|
||||
async def refund_square_payment(
|
||||
refund_request: SquareRefundRequest,
|
||||
current_user = Depends(get_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Refund a Square payment (admin only)
|
||||
"""
|
||||
# Get the payment from database
|
||||
payment = db.query(Payment).filter(Payment.id == refund_request.payment_id).first()
|
||||
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Payment not found"
|
||||
)
|
||||
|
||||
if payment.payment_method != PaymentMethod.SQUARE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Can only refund Square payments through this endpoint"
|
||||
)
|
||||
|
||||
if payment.status != PaymentStatus.COMPLETED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Can only refund completed payments"
|
||||
)
|
||||
|
||||
if not payment.transaction_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Payment has no Square transaction ID"
|
||||
)
|
||||
|
||||
# Process refund with Square
|
||||
refund_result = await square_service.refund_payment(
|
||||
payment_id=payment.transaction_id,
|
||||
amount_money=refund_request.amount,
|
||||
reason=refund_request.reason
|
||||
)
|
||||
|
||||
if not refund_result.get('success'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Refund failed: {', '.join(refund_result.get('errors', ['Unknown error']))}"
|
||||
)
|
||||
|
||||
# Update payment status
|
||||
payment.status = PaymentStatus.REFUNDED
|
||||
payment.notes = f"{payment.notes or ''}\nRefunded: {refund_request.reason or 'No reason provided'}"
|
||||
db.commit()
|
||||
|
||||
return MessageResponse(
|
||||
message="Payment refunded successfully",
|
||||
detail=f"Refund ID: {refund_result.get('refund_id')}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PaymentResponse])
|
||||
async def list_payments(
|
||||
skip: int = 0,
|
||||
|
||||
@@ -31,6 +31,7 @@ class Settings(BaseSettings):
|
||||
SQUARE_ACCESS_TOKEN: str
|
||||
SQUARE_ENVIRONMENT: str = "sandbox"
|
||||
SQUARE_LOCATION_ID: str
|
||||
SQUARE_APPLICATION_ID: str
|
||||
|
||||
# Email
|
||||
SMTP2GO_API_KEY: str
|
||||
|
||||
@@ -22,6 +22,9 @@ from .schemas import (
|
||||
PaymentCreate,
|
||||
PaymentUpdate,
|
||||
PaymentResponse,
|
||||
SquarePaymentRequest,
|
||||
SquarePaymentResponse,
|
||||
SquareRefundRequest,
|
||||
MessageResponse,
|
||||
EmailTemplateBase,
|
||||
EmailTemplateCreate,
|
||||
@@ -53,6 +56,9 @@ __all__ = [
|
||||
"PaymentCreate",
|
||||
"PaymentUpdate",
|
||||
"PaymentResponse",
|
||||
"SquarePaymentRequest",
|
||||
"SquarePaymentResponse",
|
||||
"SquareRefundRequest",
|
||||
"MessageResponse",
|
||||
"EmailTemplateBase",
|
||||
"EmailTemplateCreate",
|
||||
|
||||
@@ -163,6 +163,36 @@ class PaymentResponse(BaseModel):
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# Square Payment Schemas
|
||||
class SquarePaymentRequest(BaseModel):
|
||||
"""Request schema for Square payment processing"""
|
||||
source_id: str = Field(..., description="Payment source ID from Square Web Payments SDK")
|
||||
tier_id: int = Field(..., description="Membership tier ID to create membership for")
|
||||
amount: float = Field(..., gt=0, description="Payment amount in GBP")
|
||||
idempotency_key: Optional[str] = Field(None, description="Unique key to prevent duplicate payments")
|
||||
note: Optional[str] = Field(None, description="Optional payment note")
|
||||
|
||||
|
||||
class SquarePaymentResponse(BaseModel):
|
||||
"""Response schema for Square payment"""
|
||||
success: bool
|
||||
payment_id: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
amount: Optional[float] = None
|
||||
currency: Optional[str] = None
|
||||
receipt_url: Optional[str] = None
|
||||
errors: Optional[list[str]] = None
|
||||
database_payment_id: Optional[int] = None
|
||||
membership_id: Optional[int] = Field(None, description="Created membership ID")
|
||||
|
||||
|
||||
class SquareRefundRequest(BaseModel):
|
||||
"""Request schema for Square payment refund"""
|
||||
payment_id: int = Field(..., description="Database payment ID")
|
||||
amount: Optional[float] = Field(None, gt=0, description="Amount to refund (None for full refund)")
|
||||
reason: Optional[str] = Field(None, description="Reason for refund")
|
||||
|
||||
|
||||
# Message Response
|
||||
class MessageResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
332
backend/app/services/square_service.py
Normal file
332
backend/app/services/square_service.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Square Payment Service
|
||||
Handles Square payment processing including payment creation, verification, and webhooks
|
||||
"""
|
||||
|
||||
from square.client import Square, SquareEnvironment
|
||||
from typing import Dict, Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from ..core.config import settings
|
||||
|
||||
|
||||
class SquareService:
|
||||
"""Service for handling Square payment processing"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Square client"""
|
||||
# Configure Square client based on environment
|
||||
environment = (
|
||||
SquareEnvironment.SANDBOX
|
||||
if settings.SQUARE_ENVIRONMENT.lower() == 'sandbox'
|
||||
else SquareEnvironment.PRODUCTION
|
||||
)
|
||||
self.client = Square(
|
||||
token=settings.SQUARE_ACCESS_TOKEN,
|
||||
environment=environment
|
||||
)
|
||||
self.location_id = settings.SQUARE_LOCATION_ID
|
||||
|
||||
async def create_payment(
|
||||
self,
|
||||
amount_money: float,
|
||||
source_id: str,
|
||||
idempotency_key: Optional[str] = None,
|
||||
customer_id: Optional[str] = None,
|
||||
reference_id: Optional[str] = None,
|
||||
note: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Create a payment using Square
|
||||
|
||||
Args:
|
||||
amount_money: Amount in the currency's smallest denomination (e.g., cents for GBP)
|
||||
source_id: Payment source ID from the Web Payments SDK
|
||||
idempotency_key: Unique key to prevent duplicate payments
|
||||
customer_id: Optional Square customer ID
|
||||
reference_id: Optional reference ID for internal tracking
|
||||
note: Optional note about the payment
|
||||
|
||||
Returns:
|
||||
Dict with payment result including payment_id, status, and details
|
||||
"""
|
||||
try:
|
||||
# Generate idempotency key if not provided
|
||||
if not idempotency_key:
|
||||
idempotency_key = str(uuid.uuid4())
|
||||
|
||||
# Convert amount to money object (Square expects amount in smallest currency unit)
|
||||
# For GBP, this is pence
|
||||
amount_in_pence = int(amount_money * 100)
|
||||
|
||||
# Create payment - pass parameters directly as keyword arguments
|
||||
result = self.client.payments.create(
|
||||
source_id=source_id,
|
||||
idempotency_key=idempotency_key,
|
||||
amount_money={
|
||||
'amount': amount_in_pence,
|
||||
'currency': 'GBP'
|
||||
},
|
||||
location_id=self.location_id,
|
||||
customer_id=customer_id if customer_id else None,
|
||||
reference_id=reference_id if reference_id else None,
|
||||
note=note if note else None
|
||||
)
|
||||
|
||||
if result.errors:
|
||||
# Payment failed - extract user-friendly error messages
|
||||
error_messages = []
|
||||
for error in result.errors:
|
||||
code = error.code if hasattr(error, 'code') else None
|
||||
detail = error.detail if hasattr(error, 'detail') else str(error)
|
||||
|
||||
# Map Square error codes to user-friendly messages
|
||||
if code == 'GENERIC_DECLINE':
|
||||
error_messages.append('Your card was declined. Please try a different payment method.')
|
||||
elif code == 'CVV_FAILURE':
|
||||
error_messages.append('The CVV code is invalid. Please check and try again.')
|
||||
elif code == 'INVALID_CARD':
|
||||
error_messages.append('The card information is invalid. Please check your card details.')
|
||||
elif code == 'CARD_DECLINED':
|
||||
error_messages.append('Your card was declined. Please contact your bank or try a different card.')
|
||||
elif code == 'INSUFFICIENT_FUNDS':
|
||||
error_messages.append('Insufficient funds. Please try a different payment method.')
|
||||
elif code == 'INVALID_EXPIRATION':
|
||||
error_messages.append('The card expiration date is invalid.')
|
||||
elif code == 'ADDRESS_VERIFICATION_FAILURE':
|
||||
error_messages.append('Address verification failed. Please check your billing address.')
|
||||
else:
|
||||
# For other errors, use the detail message but clean it up
|
||||
if 'Authorization error' in detail:
|
||||
error_messages.append('Payment authorization failed. Please try a different card.')
|
||||
else:
|
||||
error_messages.append(detail)
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'errors': error_messages if error_messages else ['Payment processing failed. Please try again.']
|
||||
}
|
||||
|
||||
# Payment succeeded
|
||||
payment = result.payment
|
||||
return {
|
||||
'success': True,
|
||||
'payment_id': payment.id,
|
||||
'status': payment.status,
|
||||
'amount': amount_money,
|
||||
'currency': 'GBP',
|
||||
'created_at': payment.created_at,
|
||||
'receipt_url': payment.receipt_url if hasattr(payment, 'receipt_url') else None,
|
||||
'reference_id': reference_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Handle Square API exceptions
|
||||
error_message = str(e)
|
||||
|
||||
# Check if this is a Square API error with response body
|
||||
if hasattr(e, 'errors') and e.errors:
|
||||
# Extract user-friendly messages from Square error
|
||||
friendly_errors = []
|
||||
for error in e.errors:
|
||||
code = error.get('code') if isinstance(error, dict) else getattr(error, 'code', None)
|
||||
|
||||
if code == 'GENERIC_DECLINE':
|
||||
friendly_errors.append('Your card was declined. Please try a different payment method.')
|
||||
elif code == 'INSUFFICIENT_FUNDS':
|
||||
friendly_errors.append('Insufficient funds. Please try a different payment method.')
|
||||
elif code == 'CVV_FAILURE':
|
||||
friendly_errors.append('The CVV code is invalid. Please check and try again.')
|
||||
elif code == 'CARD_DECLINED':
|
||||
friendly_errors.append('Your card was declined. Please contact your bank or try a different card.')
|
||||
else:
|
||||
detail = error.get('detail') if isinstance(error, dict) else getattr(error, 'detail', str(error))
|
||||
if 'Authorization error' in str(detail):
|
||||
friendly_errors.append('Payment authorization failed. Please try a different card.')
|
||||
else:
|
||||
friendly_errors.append(str(detail))
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'errors': friendly_errors if friendly_errors else ['Payment processing failed. Please try again.']
|
||||
}
|
||||
|
||||
# Generic error fallback
|
||||
return {
|
||||
'success': False,
|
||||
'errors': ['Payment processing failed. Please try again or contact support.']
|
||||
}
|
||||
|
||||
async def get_payment(self, payment_id: str) -> Dict:
|
||||
"""
|
||||
Retrieve payment details from Square
|
||||
|
||||
Args:
|
||||
payment_id: Square payment ID
|
||||
|
||||
Returns:
|
||||
Dict with payment details
|
||||
"""
|
||||
try:
|
||||
result = self.client.payments.get(payment_id)
|
||||
|
||||
if result.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': [error.detail if hasattr(error, 'detail') else str(error) for error in result.errors]
|
||||
}
|
||||
|
||||
payment = result.payment
|
||||
amount_money = payment.amount_money
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'payment_id': payment.id,
|
||||
'status': payment.status,
|
||||
'amount': amount_money.amount / 100, # Convert pence to pounds
|
||||
'currency': amount_money.currency,
|
||||
'created_at': payment.created_at,
|
||||
'receipt_url': payment.receipt_url if hasattr(payment, 'receipt_url') else None,
|
||||
'reference_id': payment.reference_id if hasattr(payment, 'reference_id') else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': [str(e)]
|
||||
}
|
||||
|
||||
async def refund_payment(
|
||||
self,
|
||||
payment_id: str,
|
||||
amount_money: Optional[float] = None,
|
||||
reason: Optional[str] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Refund a payment
|
||||
|
||||
Args:
|
||||
payment_id: Square payment ID to refund
|
||||
amount_money: Amount to refund (None for full refund)
|
||||
reason: Reason for the refund
|
||||
idempotency_key: Unique key to prevent duplicate refunds
|
||||
|
||||
Returns:
|
||||
Dict with refund result
|
||||
"""
|
||||
try:
|
||||
if not idempotency_key:
|
||||
idempotency_key = str(uuid.uuid4())
|
||||
|
||||
# Prepare parameters
|
||||
amount_in_pence = int(amount_money * 100) if amount_money else None
|
||||
|
||||
# Create refund - pass parameters directly as keyword arguments
|
||||
if amount_in_pence:
|
||||
result = self.client.refunds.refund_payment(
|
||||
idempotency_key=idempotency_key,
|
||||
amount_money={
|
||||
'amount': amount_in_pence,
|
||||
'currency': 'GBP'
|
||||
},
|
||||
payment_id=payment_id,
|
||||
reason=reason if reason else None
|
||||
)
|
||||
else:
|
||||
# Full refund - get payment amount first
|
||||
payment_result = await self.get_payment(payment_id)
|
||||
if not payment_result.get('success'):
|
||||
return payment_result
|
||||
|
||||
amount_in_pence = int(payment_result['amount'] * 100)
|
||||
result = self.client.refunds.refund_payment(
|
||||
idempotency_key=idempotency_key,
|
||||
amount_money={
|
||||
'amount': amount_in_pence,
|
||||
'currency': 'GBP'
|
||||
},
|
||||
payment_id=payment_id,
|
||||
reason=reason if reason else None
|
||||
)
|
||||
|
||||
if result.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': [error.detail if hasattr(error, 'detail') else str(error) for error in result.errors]
|
||||
}
|
||||
|
||||
refund = result.refund
|
||||
return {
|
||||
'success': True,
|
||||
'refund_id': refund.id,
|
||||
'status': refund.status,
|
||||
'payment_id': payment_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': [str(e)]
|
||||
}
|
||||
|
||||
async def create_customer(
|
||||
self,
|
||||
email: str,
|
||||
given_name: str,
|
||||
family_name: str,
|
||||
phone_number: Optional[str] = None,
|
||||
address: Optional[Dict] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Create a Square customer for future payments
|
||||
|
||||
Args:
|
||||
email: Customer email
|
||||
given_name: Customer first name
|
||||
family_name: Customer last name
|
||||
phone_number: Optional phone number
|
||||
address: Optional address dict
|
||||
idempotency_key: Unique key
|
||||
|
||||
Returns:
|
||||
Dict with customer details
|
||||
"""
|
||||
try:
|
||||
if not idempotency_key:
|
||||
idempotency_key = str(uuid.uuid4())
|
||||
|
||||
# Create customer - pass parameters directly as keyword arguments
|
||||
result = self.client.customers.create(
|
||||
idempotency_key=idempotency_key,
|
||||
email_address=email,
|
||||
given_name=given_name,
|
||||
family_name=family_name,
|
||||
phone_number=phone_number if phone_number else None,
|
||||
address=address if address else None
|
||||
)
|
||||
|
||||
if result.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': [error.detail if hasattr(error, 'detail') else str(error) for error in result.errors]
|
||||
}
|
||||
|
||||
customer = result.customer
|
||||
return {
|
||||
'success': True,
|
||||
'customer_id': customer.id,
|
||||
'email': customer.email_address if hasattr(customer, 'email_address') else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': [str(e)]
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
square_service = SquareService()
|
||||
@@ -1,8 +1,8 @@
|
||||
# FastAPI and web server
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
pydantic==2.10.3
|
||||
pydantic-settings==2.6.1
|
||||
python-multipart==0.0.6
|
||||
|
||||
# Database
|
||||
@@ -17,8 +17,8 @@ passlib[bcrypt]==1.7.4
|
||||
python-dotenv==1.0.0
|
||||
bcrypt==4.1.1
|
||||
|
||||
# Payment Integration (to be added later)
|
||||
# squareup==43.2.0.20251016
|
||||
# Payment Integration
|
||||
squareup==43.2.0.20251016
|
||||
|
||||
# Email Service
|
||||
httpx==0.25.2
|
||||
@@ -27,3 +27,4 @@ httpx==0.25.2
|
||||
email-validator==2.1.0
|
||||
aiofiles==23.2.1
|
||||
Jinja2==3.1.2
|
||||
python-dateutil==2.8.2
|
||||
|
||||
Reference in New Issue
Block a user