Files
ponzischeme89 6d44e05de4 v1
2026-04-18 07:23:55 +12:00

168 lines
5.5 KiB
Python

import hashlib
import secrets
import uuid
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.deps import get_current_user
from app.config import settings
from app.database import get_db
from app.middleware.rate_limit import limiter
from app.models.contact_lead import ContactLead
from app.models.member import Member, MagicLinkToken
from app.models.user import User
from app.schemas.contact import (
ContactLeadCreate,
ContactLeadInviteRequest,
ContactLeadInviteResponse,
ContactLeadResponse,
ContactLeadUpdate,
)
from app.services.email import send_onboarding_invite
router = APIRouter(tags=["Contact Leads"])
def _split_name(full_name: str) -> tuple[str, str]:
parts = [part for part in full_name.strip().split() if part]
if not parts:
return "Goodwalk", "Client"
if len(parts) == 1:
return parts[0], "Client"
return parts[0], " ".join(parts[1:])
def _normalise_services(payload: ContactLeadCreate) -> str | None:
if payload.services:
return ", ".join(payload.services)
if payload.service:
return payload.service.strip() or None
return None
@router.post("/api/contact", response_model=ContactLeadResponse, status_code=201)
@limiter.limit("10/minute")
async def submit_contact_lead(
request: Request,
response: Response,
data: ContactLeadCreate,
db: AsyncSession = Depends(get_db),
):
lead = ContactLead(
full_name=data.name.strip(),
email=data.email.strip().lower(),
phone=(data.phone or "").strip() or None,
requested_services=_normalise_services(data),
pet_name=(data.petName or "").strip() or None,
pet_breed=(data.petBreed or "").strip() or None,
suburb=(data.location or "").strip() or None,
service_area_status=(data.serviceAreaStatus or "").strip() or None,
message=(data.message or "").strip() or None,
source=data.source,
status="invite",
metadata_json={
"services": data.services,
"service": data.service,
},
)
db.add(lead)
await db.flush()
await db.refresh(lead)
return lead
@router.get("/api/v1/admin/leads", response_model=list[ContactLeadResponse])
async def admin_list_leads(
_admin: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(ContactLead).order_by(ContactLead.created_at.desc()))
return result.scalars().all()
@router.put("/api/v1/admin/leads/{lead_id}", response_model=ContactLeadResponse)
async def admin_update_lead(
lead_id: uuid.UUID,
data: ContactLeadUpdate,
_admin: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(ContactLead).where(ContactLead.id == lead_id))
lead = result.scalars().first()
if lead is None:
raise HTTPException(status_code=404, detail="Lead not found.")
for field, value in data.model_dump(exclude_unset=True).items():
setattr(lead, field, value)
await db.flush()
await db.refresh(lead)
return lead
@router.post("/api/v1/admin/leads/{lead_id}/invite", response_model=ContactLeadInviteResponse)
async def admin_invite_lead(
lead_id: uuid.UUID,
data: ContactLeadInviteRequest,
_admin: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(ContactLead).where(ContactLead.id == lead_id))
lead = result.scalars().first()
if lead is None:
raise HTTPException(status_code=404, detail="Lead not found.")
existing_member_result = await db.execute(select(Member).where(Member.email == lead.email))
member = existing_member_result.scalars().first()
if member is None:
first_name, last_name = _split_name(lead.full_name)
member = Member(
email=lead.email,
first_name=first_name,
last_name=last_name,
phone=lead.phone,
address=lead.suburb,
onboarding_data={
"dog_name": lead.pet_name,
"dog_breed": lead.pet_breed,
"preferred_service": lead.requested_services,
"lead_message": lead.message,
"service_area_status": lead.service_area_status,
"source": lead.source,
},
is_claimed=False,
is_active=True,
member_status="invited",
)
db.add(member)
await db.flush()
lead.invited_member_id = member.id
lead.invited_at = datetime.now(timezone.utc)
lead.status = "invited"
await db.flush()
await db.refresh(lead)
await db.refresh(member)
if data.send_email:
plaintext_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(plaintext_token.encode()).hexdigest()
magic_token = MagicLinkToken(
member_id=member.id,
token_hash=token_hash,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
)
db.add(magic_token)
await db.flush()
magic_url = f"{settings.MEMBERS_URL.rstrip('/')}/join?token={plaintext_token}"
await send_onboarding_invite(lead.email, member.first_name, magic_url)
return ContactLeadInviteResponse(
lead=ContactLeadResponse.model_validate(lead),
member_id=member.id,
member_status=member.member_status,
)