
Features: - User registration and authentication with JWT tokens - Multi-level admin access (Admin and Super Admin) - Gym management with membership plans - Subscription management with payment integration - Stripe and Paystack payment gateway support - Role-based access control - SQLite database with Alembic migrations - Comprehensive API endpoints with FastAPI - Database models for users, gyms, memberships, subscriptions, and transactions - Admin endpoints for user management and financial reporting - Health check and documentation endpoints Core Components: - FastAPI application with CORS support - SQLAlchemy ORM with relationship mapping - JWT-based authentication with bcrypt password hashing - Payment service abstraction for multiple gateways - Pydantic schemas for request/response validation - Alembic database migration system - Admin dashboard functionality - Environment variable configuration
182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.db.session import get_db
|
|
from app.core.deps import get_current_active_user
|
|
from app.models.user import User
|
|
from app.models.membership import MembershipPlan
|
|
from app.models.subscription import Subscription, SubscriptionStatus
|
|
from app.models.transaction import Transaction, TransactionStatus
|
|
from app.schemas.transaction import PaymentRequest, Transaction as TransactionSchema
|
|
from app.services.payment import payment_service
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/initialize")
|
|
def initialize_payment(
|
|
payment_request: PaymentRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_active_user),
|
|
):
|
|
plan = (
|
|
db.query(MembershipPlan)
|
|
.filter(
|
|
MembershipPlan.id == payment_request.membership_plan_id,
|
|
MembershipPlan.is_active,
|
|
)
|
|
.first()
|
|
)
|
|
if not plan:
|
|
raise HTTPException(status_code=404, detail="Membership plan not found")
|
|
|
|
existing_active_subscription = (
|
|
db.query(Subscription)
|
|
.filter(
|
|
Subscription.user_id == current_user.id,
|
|
Subscription.membership_plan_id == payment_request.membership_plan_id,
|
|
Subscription.status == SubscriptionStatus.ACTIVE,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if existing_active_subscription:
|
|
raise HTTPException(
|
|
status_code=400, detail="Active subscription already exists for this plan"
|
|
)
|
|
|
|
# Create transaction record
|
|
transaction = Transaction(
|
|
user_id=current_user.id,
|
|
amount=plan.price,
|
|
payment_gateway=payment_request.payment_gateway,
|
|
description=f"Subscription to {plan.name}",
|
|
)
|
|
db.add(transaction)
|
|
db.commit()
|
|
db.refresh(transaction)
|
|
|
|
metadata = {
|
|
"user_id": str(current_user.id),
|
|
"transaction_id": str(transaction.id),
|
|
"plan_id": str(plan.id),
|
|
}
|
|
|
|
try:
|
|
payment_data = payment_service.initialize_payment(
|
|
gateway=payment_request.payment_gateway,
|
|
amount=plan.price,
|
|
currency="USD"
|
|
if payment_request.payment_gateway.value == "stripe"
|
|
else "NGN",
|
|
email=current_user.email,
|
|
metadata=metadata,
|
|
)
|
|
|
|
# Update transaction with gateway reference
|
|
if payment_request.payment_gateway.value == "stripe":
|
|
transaction.gateway_reference = payment_data.get("payment_intent_id")
|
|
else:
|
|
transaction.gateway_reference = payment_data.get("reference")
|
|
|
|
db.commit()
|
|
|
|
return {"transaction_id": transaction.id, "payment_data": payment_data}
|
|
except Exception as e:
|
|
transaction.status = TransactionStatus.FAILED
|
|
db.commit()
|
|
raise e
|
|
|
|
|
|
@router.post("/verify/{transaction_id}")
|
|
def verify_payment(
|
|
transaction_id: int,
|
|
background_tasks: BackgroundTasks,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_active_user),
|
|
):
|
|
transaction = (
|
|
db.query(Transaction)
|
|
.filter(
|
|
Transaction.id == transaction_id, Transaction.user_id == current_user.id
|
|
)
|
|
.first()
|
|
)
|
|
if not transaction:
|
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
|
|
|
if transaction.status == TransactionStatus.COMPLETED:
|
|
return {"message": "Payment already verified"}
|
|
|
|
try:
|
|
verification_data = payment_service.verify_payment(
|
|
gateway=transaction.payment_gateway, reference=transaction.gateway_reference
|
|
)
|
|
|
|
if verification_data["status"] in ["succeeded", "success"]:
|
|
transaction.status = TransactionStatus.COMPLETED
|
|
transaction.gateway_transaction_id = verification_data.get(
|
|
"reference"
|
|
) or verification_data.get("payment_intent_id")
|
|
|
|
# Create subscription
|
|
plan = (
|
|
db.query(MembershipPlan)
|
|
.filter(MembershipPlan.id == transaction.subscription_id)
|
|
.first()
|
|
)
|
|
if not plan:
|
|
# Find plan from metadata or transaction description
|
|
# This is a fallback - ideally we'd store plan_id in transaction
|
|
raise HTTPException(
|
|
status_code=500, detail="Cannot create subscription: plan not found"
|
|
)
|
|
|
|
start_date = datetime.utcnow()
|
|
end_date = start_date + timedelta(days=plan.duration_days)
|
|
|
|
subscription = Subscription(
|
|
user_id=current_user.id,
|
|
membership_plan_id=plan.id,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
amount_paid=transaction.amount,
|
|
payment_reference=transaction.gateway_reference,
|
|
status=SubscriptionStatus.ACTIVE,
|
|
)
|
|
|
|
transaction.subscription_id = subscription.id
|
|
db.add(subscription)
|
|
db.commit()
|
|
|
|
return {"message": "Payment verified and subscription created successfully"}
|
|
else:
|
|
transaction.status = TransactionStatus.FAILED
|
|
db.commit()
|
|
raise HTTPException(status_code=400, detail="Payment verification failed")
|
|
|
|
except Exception as e:
|
|
transaction.status = TransactionStatus.FAILED
|
|
db.commit()
|
|
raise HTTPException(
|
|
status_code=400, detail=f"Payment verification error: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/transactions", response_model=list[TransactionSchema])
|
|
def get_user_transactions(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_active_user),
|
|
):
|
|
transactions = (
|
|
db.query(Transaction)
|
|
.filter(Transaction.user_id == current_user.id)
|
|
.offset(skip)
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
return transactions
|