Donation page update and Subscription update on Admin Dashboard

This commit is contained in:
Koncept Kit
2025-12-11 23:14:13 +07:00
parent 7d55d29362
commit 834d65ec49
2 changed files with 217 additions and 0 deletions

217
server.py
View File

@@ -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."""