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')
|
raise ValueError('Suggested price must be >= minimum price')
|
||||||
return v
|
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")
|
@api_router.get("/subscriptions/plans")
|
||||||
async def get_subscription_plans(db: Session = Depends(get_db)):
|
async def get_subscription_plans(db: Session = Depends(get_db)):
|
||||||
"""Get all active subscription plans."""
|
"""Get all active subscription plans."""
|
||||||
@@ -2520,6 +2535,155 @@ async def delete_plan(
|
|||||||
|
|
||||||
return {"message": "Plan deactivated successfully"}
|
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
|
# Admin Document Management Routes
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -2984,6 +3148,15 @@ async def create_checkout(
|
|||||||
):
|
):
|
||||||
"""Create Stripe Checkout session with dynamic pricing and donation tracking."""
|
"""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
|
# Get plan
|
||||||
plan = db.query(SubscriptionPlan).filter(
|
plan = db.query(SubscriptionPlan).filter(
|
||||||
SubscriptionPlan.id == request.plan_id
|
SubscriptionPlan.id == request.plan_id
|
||||||
@@ -3103,6 +3276,50 @@ async def create_checkout(
|
|||||||
logger.error(f"Error creating checkout session: {str(e)}")
|
logger.error(f"Error creating checkout session: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to create checkout session")
|
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")
|
@app.post("/api/webhooks/stripe")
|
||||||
async def stripe_webhook(request: Request, db: Session = Depends(get_db)):
|
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."""
|
"""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