feat(slice-3): POST /api/storefronts + storefronts-fed entry routing (§6.4/§6.5)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:36:50 -07:00
parent e11fb032b1
commit 306b5c1e5d
2 changed files with 122 additions and 10 deletions
+32 -10
View File
@@ -3,8 +3,8 @@
SLICE-1 mounted /healthz; SLICE-2 adds the /api/auth/* identity endpoints (§6.4). The BFF
translates HTTP <-> domain calls and owns no business logic (INV-6): every rule lives in
the accounts domain. create_app() opens the pool, self-migrates (INV-1, INV-7), and builds
the configured mailer (INV-8) at startup. storefront is hard-wired null this slice; the
storefronts domain (SLICE-3) replaces the _storefront_for seam.
the configured mailer (INV-8) at startup. SLICE-3 adds POST /api/storefronts and feeds the
_storefront_for seam from the storefronts domain.
"""
from __future__ import annotations
@@ -18,7 +18,7 @@ from fastapi import Depends, FastAPI, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from app.domains import accounts
from app.domains import accounts, storefronts
from app.platform import config, db
from app.platform import mailer as mailer_mod
from app.platform.deps import SESSION_COOKIE, get_conn, get_mailer, get_session
@@ -35,18 +35,19 @@ class VerifyBody(BaseModel):
code: str
class CreateStorefrontBody(BaseModel):
name: str | None = None
def _error(status: int, code: str, message: str, **extra: Any) -> JSONResponse:
"""The shared §6.4 error envelope: {"error": {"code", "message", ...}}."""
return JSONResponse(status_code=status, content={"error": {"code": code, "message": message, **extra}})
def _storefront_for(conn: psycopg.Connection, account: accounts.Account) -> dict | None:
"""The entry-routing answer: which storefront, if any, this account has (§6.5).
Hard-wired null in SLICE-2 (identity only). SLICE-3 replaces this with a storefronts
domain call; the verify/me responses already carry the field.
"""
return None
"""The entry-routing answer: which storefront, if any, this account has (§6.5)."""
sf = storefronts.storefront_for(conn, account.id)
return {"id": sf.id, "name": sf.name} if sf else None
def _ensure_app_logging() -> None:
@@ -93,7 +94,7 @@ def create_app(database_url: str | None = None) -> FastAPI:
finally:
app.state.pool.close()
app = FastAPI(title="ecomm", version="0.2", lifespan=lifespan)
app = FastAPI(title="ecomm", version="0.3", lifespan=lifespan)
@app.get("/healthz")
def healthz(response: Response, conn: psycopg.Connection = Depends(get_conn)):
@@ -170,6 +171,27 @@ def create_app(database_url: str | None = None) -> FastAPI:
resp.delete_cookie(SESSION_COOKIE, path="/")
return resp
@app.post("/api/storefronts")
def create_storefront(
body: CreateStorefrontBody,
conn: psycopg.Connection = Depends(get_conn),
sess: dict | None = Depends(get_session),
):
"""Create the account's one storefront (§6.4; PUC-4, INV-4/PUC-7 on refusal)."""
if sess is None:
return _error(401, "unauthenticated", "You are not signed in.")
account = accounts.get_account(conn, sess["account_id"])
if account is None:
return _error(401, "unauthenticated", "You are not signed in.")
try:
sf = storefronts.create_storefront(conn, account.id, account.email, body.name)
except storefronts.AlreadyOwnsStorefront:
return _error(
409, "already_owns_storefront",
"Your account already has its storefront — ecomm is one storefront per account today.",
)
return JSONResponse(status_code=201, content={"id": sf.id, "name": sf.name})
return app