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:
+32
-10
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user