forked from andika/membership-be
Donation page update and Subscription update on Admin Dashboard
This commit is contained in:
Binary file not shown.
217
server.py
217
server.py
@@ -2265,6 +2265,21 @@ class PlanCreateRequest(BaseModel):
|
||||
raise ValueError('Suggested price must be >= minimum price')
|
||||
return v
|
||||
|
||||
# Pydantic model for updating subscriptions
|
||||
class UpdateSubscriptionRequest(BaseModel):
|
||||
status: Optional[str] = Field(None, pattern="^(active|expired|cancelled)$")
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
# Pydantic model for donation checkout
|
||||
class DonationCheckoutRequest(BaseModel):
|
||||
amount_cents: int = Field(..., ge=100, description="Donation amount in cents (minimum $1.00)")
|
||||
|
||||
@validator('amount_cents')
|
||||
def validate_amount(cls, v):
|
||||
if v < 100:
|
||||
raise ValueError('Donation must be at least $1.00 (100 cents)')
|
||||
return v
|
||||
|
||||
@api_router.get("/subscriptions/plans")
|
||||
async def get_subscription_plans(db: Session = Depends(get_db)):
|
||||
"""Get all active subscription plans."""
|
||||
@@ -2520,6 +2535,155 @@ async def delete_plan(
|
||||
|
||||
return {"message": "Plan deactivated successfully"}
|
||||
|
||||
# ============================================================================
|
||||
# Admin Subscription Management Routes
|
||||
# ============================================================================
|
||||
|
||||
@api_router.get("/admin/subscriptions")
|
||||
async def get_all_subscriptions(
|
||||
status: Optional[str] = None,
|
||||
plan_id: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all subscriptions with optional filters."""
|
||||
# Use explicit join to avoid ambiguous foreign key error
|
||||
query = db.query(Subscription).join(Subscription.user).join(Subscription.plan)
|
||||
|
||||
if status:
|
||||
query = query.filter(Subscription.status == status)
|
||||
if plan_id:
|
||||
query = query.filter(Subscription.plan_id == plan_id)
|
||||
|
||||
subscriptions = query.order_by(Subscription.created_at.desc()).all()
|
||||
|
||||
return [{
|
||||
"id": str(sub.id),
|
||||
"user": {
|
||||
"id": str(sub.user.id),
|
||||
"first_name": sub.user.first_name,
|
||||
"last_name": sub.user.last_name,
|
||||
"email": sub.user.email
|
||||
},
|
||||
"plan": {
|
||||
"id": str(sub.plan.id),
|
||||
"name": sub.plan.name,
|
||||
"billing_cycle": sub.plan.billing_cycle
|
||||
},
|
||||
"status": sub.status.value,
|
||||
"start_date": sub.start_date,
|
||||
"end_date": sub.end_date,
|
||||
"amount_paid_cents": sub.amount_paid_cents,
|
||||
"base_subscription_cents": sub.base_subscription_cents,
|
||||
"donation_cents": sub.donation_cents,
|
||||
"payment_method": sub.payment_method,
|
||||
"stripe_subscription_id": sub.stripe_subscription_id,
|
||||
"created_at": sub.created_at,
|
||||
"updated_at": sub.updated_at
|
||||
} for sub in subscriptions]
|
||||
|
||||
@api_router.get("/admin/subscriptions/stats")
|
||||
async def get_subscription_stats(
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get subscription statistics for admin dashboard."""
|
||||
from sqlalchemy import func
|
||||
|
||||
total = db.query(Subscription).count()
|
||||
active = db.query(Subscription).filter(
|
||||
Subscription.status == SubscriptionStatus.active
|
||||
).count()
|
||||
cancelled = db.query(Subscription).filter(
|
||||
Subscription.status == SubscriptionStatus.cancelled
|
||||
).count()
|
||||
expired = db.query(Subscription).filter(
|
||||
Subscription.status == SubscriptionStatus.expired
|
||||
).count()
|
||||
|
||||
revenue_data = db.query(
|
||||
func.sum(Subscription.amount_paid_cents).label('total_revenue'),
|
||||
func.sum(Subscription.base_subscription_cents).label('total_base'),
|
||||
func.sum(Subscription.donation_cents).label('total_donations')
|
||||
).first()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"active": active,
|
||||
"cancelled": cancelled,
|
||||
"expired": expired,
|
||||
"total_revenue": revenue_data.total_revenue or 0,
|
||||
"total_base": revenue_data.total_base or 0,
|
||||
"total_donations": revenue_data.total_donations or 0
|
||||
}
|
||||
|
||||
@api_router.put("/admin/subscriptions/{subscription_id}")
|
||||
async def update_subscription(
|
||||
subscription_id: str,
|
||||
request: UpdateSubscriptionRequest,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update subscription details (status, dates)."""
|
||||
subscription = db.query(Subscription).filter(
|
||||
Subscription.id == subscription_id
|
||||
).first()
|
||||
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="Subscription not found")
|
||||
|
||||
# Update fields if provided
|
||||
if request.status:
|
||||
subscription.status = SubscriptionStatus[request.status]
|
||||
if request.end_date:
|
||||
subscription.end_date = request.end_date
|
||||
|
||||
subscription.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(subscription)
|
||||
|
||||
logger.info(f"Admin {current_user.email} updated subscription {subscription_id}")
|
||||
|
||||
return {
|
||||
"id": str(subscription.id),
|
||||
"user_id": str(subscription.user_id),
|
||||
"plan_id": str(subscription.plan_id),
|
||||
"status": subscription.status.value,
|
||||
"start_date": subscription.start_date,
|
||||
"end_date": subscription.end_date,
|
||||
"amount_paid_cents": subscription.amount_paid_cents,
|
||||
"updated_at": subscription.updated_at
|
||||
}
|
||||
|
||||
@api_router.post("/admin/subscriptions/{subscription_id}/cancel")
|
||||
async def cancel_subscription(
|
||||
subscription_id: str,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Cancel a subscription."""
|
||||
subscription = db.query(Subscription).filter(
|
||||
Subscription.id == subscription_id
|
||||
).first()
|
||||
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="Subscription not found")
|
||||
|
||||
subscription.status = SubscriptionStatus.cancelled
|
||||
subscription.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# Also update user status if currently active
|
||||
user = subscription.user
|
||||
if user.status == UserStatus.active:
|
||||
user.status = UserStatus.inactive
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} cancelled subscription {subscription_id} for user {user.email}")
|
||||
|
||||
return {"message": "Subscription cancelled successfully"}
|
||||
|
||||
# ============================================================================
|
||||
# Admin Document Management Routes
|
||||
# ============================================================================
|
||||
@@ -2984,6 +3148,15 @@ async def create_checkout(
|
||||
):
|
||||
"""Create Stripe Checkout session with dynamic pricing and donation tracking."""
|
||||
|
||||
# Status validation - only allow payment_pending and inactive users
|
||||
allowed_statuses = [UserStatus.payment_pending, UserStatus.inactive]
|
||||
if current_user.status not in allowed_statuses:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Cannot proceed with payment. User status is '{current_user.status.value}'. "
|
||||
f"Please complete email verification and admin approval first."
|
||||
)
|
||||
|
||||
# Get plan
|
||||
plan = db.query(SubscriptionPlan).filter(
|
||||
SubscriptionPlan.id == request.plan_id
|
||||
@@ -3103,6 +3276,50 @@ async def create_checkout(
|
||||
logger.error(f"Error creating checkout session: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to create checkout session")
|
||||
|
||||
@api_router.post("/donations/checkout")
|
||||
async def create_donation_checkout(
|
||||
request: DonationCheckoutRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create Stripe Checkout session for one-time donation."""
|
||||
|
||||
# Get frontend URL from env
|
||||
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
||||
|
||||
try:
|
||||
# Create Stripe Checkout Session for one-time payment
|
||||
import stripe
|
||||
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
|
||||
|
||||
checkout_session = stripe.checkout.Session.create(
|
||||
payment_method_types=['card'],
|
||||
line_items=[{
|
||||
'price_data': {
|
||||
'currency': 'usd',
|
||||
'unit_amount': request.amount_cents,
|
||||
'product_data': {
|
||||
'name': 'Donation to LOAF',
|
||||
'description': 'Thank you for supporting our community!'
|
||||
}
|
||||
},
|
||||
'quantity': 1
|
||||
}],
|
||||
mode='payment', # One-time payment (not subscription)
|
||||
success_url=f"{frontend_url}/membership/donation-success?session_id={{CHECKOUT_SESSION_ID}}",
|
||||
cancel_url=f"{frontend_url}/membership/donate"
|
||||
)
|
||||
|
||||
logger.info(f"Donation checkout created: ${request.amount_cents/100:.2f}")
|
||||
|
||||
return {"checkout_url": checkout_session.url}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating donation checkout: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Payment processing error: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating donation checkout: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to create donation checkout")
|
||||
|
||||
@app.post("/api/webhooks/stripe")
|
||||
async def stripe_webhook(request: Request, db: Session = Depends(get_db)):
|
||||
"""Handle Stripe webhook events. Note: This endpoint is NOT on the api_router to avoid /api/api prefix."""
|
||||
|
||||
Reference in New Issue
Block a user