Spaces:
Running
Running
Download routes/bff_form.py from Success-dev/venthrive-api: direct link, hf CLI and curl.
- Browser
- Download file 4.59 kB
-
https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/routes/bff_form.py
- Command line
-
hf download hf://spaces/Success-dev/venthrive-api/routes/bff_form.py
-
curl -L -o bff_form.py https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/routes/bff_form.py
4.59 kB
| """ | |
| The public Business Footing Framework at /bff: the WHAT, WHY and HOW questions | |
| from onboarding, with no sign-in, no AI and no follow-up questions. | |
| The browser sends the answers and the person's contact details; this route | |
| checks them and stores one document per submission in bff_submissions. | |
| firestore.rules' default deny keeps the collection backend-only. | |
| """ | |
| import time | |
| from fastapi import APIRouter, HTTPException | |
| from firebase_admin import firestore | |
| from pydantic import BaseModel, Field | |
| from routes.bff import BUSINESS_STAGES | |
| from utils.business_type import describe | |
| from utils.public_forms import RateLimiter, clean_email, clean_phone | |
| router = APIRouter() | |
| COLLECTION = "bff_submissions" | |
| PER_SESSION_LIMIT = (5, 3600) # 5 submissions per hour per browser | |
| GLOBAL_LIMIT = (500, 3600) # 500 per hour across everyone | |
| limiter = RateLimiter() | |
| Short = Field(default="", max_length=200) | |
| Long = Field(default="", max_length=4000) | |
| Choices = Field(default_factory=list, max_length=40) | |
| class What(BaseModel): | |
| businessStage: str = Field(max_length=40) | |
| businessName: str = Field(min_length=1, max_length=120) | |
| sectorId: str = Field(min_length=1, max_length=80) | |
| subsectorId: str = Field(min_length=1, max_length=80) | |
| nicheId: str = Field(min_length=1, max_length=80) | |
| customNiche: str = Short | |
| products: list[str] = Choices | |
| class Why(BaseModel): | |
| whyRaw: str = Field(min_length=1, max_length=4000) | |
| valueProposition: str = Field(min_length=1, max_length=4000) | |
| class Hours(BaseModel): | |
| open: str = Field(max_length=5) | |
| close: str = Field(max_length=5) | |
| off: bool | |
| class How(BaseModel): | |
| locationType: str = Field(default="online", pattern="^(online|physical|hybrid)$") | |
| state: str = Short | |
| lga: str = Short | |
| city: str = Short | |
| address: str = Short | |
| hours: dict[str, Hours] = Field(default_factory=dict, max_length=7) | |
| demographics: dict[str, list[str]] = Field(default_factory=dict, max_length=12) | |
| psychographics: str = Long | |
| channels: dict[str, list[str]] = Field(default_factory=dict, max_length=6) | |
| keyPartners: str = Long | |
| revenueStreams: str = Long | |
| class SubmissionIn(BaseModel): | |
| name: str = Field(min_length=1, max_length=120) | |
| email: str = Field(min_length=3, max_length=254) | |
| phone: str = Field(min_length=1, max_length=30) | |
| consent: bool | |
| what: What | |
| why: Why | |
| how: How | |
| startedAt: int = Field(ge=0) # ms since epoch, from the browser | |
| sessionId: str = Field(min_length=1, max_length=64) | |
| def _clean_list(items: list[str]) -> list[str]: | |
| return [i.strip()[:200] for i in items if i.strip()] | |
| def submit(body: SubmissionIn): | |
| if not limiter.allow(f"s:{body.sessionId}", *PER_SESSION_LIMIT) or not limiter.allow("global", *GLOBAL_LIMIT): | |
| raise HTTPException(status_code=429, detail="Too many submissions. Try again later.") | |
| if not body.consent: | |
| raise HTTPException(status_code=400, detail="Consent is required to send your answers.") | |
| email = clean_email(body.email) | |
| phone = clean_phone(body.phone) | |
| if body.what.businessStage not in BUSINESS_STAGES: | |
| raise HTTPException(status_code=400, detail="Choose how far along your business is.") | |
| if body.what.nicheId == "other" and not body.what.customNiche.strip(): | |
| raise HTTPException(status_code=400, detail="Tell us your niche.") | |
| what = body.what.model_dump() | |
| what["products"] = _clean_list(body.what.products) | |
| how = body.how.model_dump() | |
| how["demographics"] = {k[:40]: _clean_list(v) for k, v in body.how.demographics.items()} | |
| how["channels"] = {k[:40]: _clean_list(v) for k, v in body.how.channels.items()} | |
| now_ms = int(time.time() * 1000) | |
| doc = { | |
| "name": body.name.strip(), | |
| "email": email, | |
| "phone": phone, | |
| "consent": True, | |
| "what": what, | |
| "why": {k: v.strip() for k, v in body.why.model_dump().items()}, | |
| "how": how, | |
| # Readable labels next to the ids, so the collection can be read without the app. | |
| "labels": {**describe(what), "stage": BUSINESS_STAGES[body.what.businessStage]}, | |
| "startedAt": body.startedAt, | |
| "durationSeconds": max(0, (now_ms - body.startedAt) // 1000) if body.startedAt else None, | |
| "submittedAt": firestore.SERVER_TIMESTAMP, | |
| "version": 1, | |
| } | |
| try: | |
| _, ref = firestore.client().collection(COLLECTION).add(doc) | |
| except Exception: | |
| raise HTTPException(status_code=500, detail="Could not save your answers.") | |
| return {"success": True, "id": ref.id} | |