File size: 4,589 Bytes
b324c71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""
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()]


@router.post("/submissions")
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}