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,
|
||||
|
||||
Reference in New Issue
Block a user