118 lines
No EOL
3.7 KiB
Python
118 lines
No EOL
3.7 KiB
Python
"""Subscribers API endpoints with tier enforcement.
|
|
|
|
When adding a subscriber to an organization, the org's subscriber limit
|
|
is checked against the org's tier.
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_db, verify_api_key
|
|
from app.models.models import Subscriber
|
|
from app.models.saas_models import Organization
|
|
from app.services.tier_enforcement import enforce_subscriber_limit
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class SubscriberCreate(BaseModel):
|
|
email: str = Field(..., max_length=255)
|
|
organization_id: str | None = None # Optional: for tier enforcement
|
|
|
|
|
|
@router.get("/")
|
|
async def list_subscribers(db: AsyncSession = Depends(get_db)):
|
|
"""List all subscribers."""
|
|
result = await db.execute(select(Subscriber))
|
|
subscribers = result.scalars().all()
|
|
return [
|
|
{
|
|
"id": s.id,
|
|
"email": s.email,
|
|
"organization_id": s.organization_id,
|
|
"is_confirmed": s.is_confirmed,
|
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
|
}
|
|
for s in subscribers
|
|
]
|
|
|
|
|
|
@router.post("/", status_code=status.HTTP_201_CREATED)
|
|
async def create_subscriber(
|
|
data: SubscriberCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
api_key: str = Depends(verify_api_key),
|
|
):
|
|
"""Add a new subscriber.
|
|
|
|
If organization_id is provided, tier enforcement is applied to ensure
|
|
the org hasn't exceeded its subscriber limit.
|
|
"""
|
|
org = None
|
|
if data.organization_id:
|
|
result = await db.execute(
|
|
select(Organization).where(Organization.id == data.organization_id)
|
|
)
|
|
org = result.scalar_one_or_none()
|
|
if org is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Organization '{data.organization_id}' not found",
|
|
)
|
|
|
|
# Tier enforcement when org context is provided
|
|
if org is not None:
|
|
await enforce_subscriber_limit(db, org)
|
|
|
|
import uuid
|
|
subscriber = Subscriber(
|
|
email=data.email,
|
|
confirm_token=str(uuid.uuid4()),
|
|
organization_id=data.organization_id,
|
|
)
|
|
db.add(subscriber)
|
|
await db.flush()
|
|
await db.refresh(subscriber)
|
|
return {
|
|
"id": subscriber.id,
|
|
"email": subscriber.email,
|
|
"organization_id": subscriber.organization_id,
|
|
"confirm_token": subscriber.confirm_token,
|
|
}
|
|
|
|
|
|
@router.delete("/{subscriber_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_subscriber(
|
|
subscriber_id: UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
api_key: str = Depends(verify_api_key),
|
|
):
|
|
"""Remove a subscriber."""
|
|
result = await db.execute(select(Subscriber).where(Subscriber.id == str(subscriber_id)))
|
|
subscriber = result.scalar_one_or_none()
|
|
if not subscriber:
|
|
raise HTTPException(status_code=404, detail="Subscriber not found")
|
|
await db.delete(subscriber)
|
|
|
|
|
|
@router.post("/{subscriber_id}/confirm")
|
|
async def confirm_subscriber(
|
|
subscriber_id: UUID,
|
|
token: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Confirm a subscriber's email address."""
|
|
result = await db.execute(select(Subscriber).where(Subscriber.id == str(subscriber_id)))
|
|
subscriber = result.scalar_one_or_none()
|
|
if not subscriber:
|
|
raise HTTPException(status_code=404, detail="Subscriber not found")
|
|
if subscriber.confirm_token != token:
|
|
raise HTTPException(status_code=400, detail="Invalid confirmation token")
|
|
subscriber.is_confirmed = True
|
|
subscriber.confirm_token = None
|
|
await db.flush()
|
|
return {"message": "Subscriber confirmed", "email": subscriber.email} |