ba8b493a31
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
57 lines
2.3 KiB
Python
57 lines
2.3 KiB
Python
"""storefronts — the storefront + membership service (SD-0001 §6.5).
|
|
|
|
Owns the storefront entity, the account<->storefront membership (INV-5), the entry-routing
|
|
answer (storefront_for), and INV-4's one-storefront guard — the single deletable check that
|
|
makes one-per-account an MVP rule, not a schema law. Never mints identity: the BFF passes
|
|
account_id and email down.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import psycopg
|
|
|
|
from .errors import AlreadyOwnsStorefront
|
|
from .models import Storefront
|
|
|
|
|
|
def _default_name(email: str) -> str:
|
|
"""§6.3: a blank name stores a generated default — never NULL (corpus 14.01.0026)."""
|
|
return f"{email.split('@', 1)[0]}'s storefront"
|
|
|
|
|
|
def storefront_for(conn: psycopg.Connection, account_id: int) -> Storefront | None:
|
|
"""The entry-routing answer (§6.5): which storefront, if any, this account has."""
|
|
row = conn.execute(
|
|
"SELECT s.id, s.name FROM storefront s"
|
|
" JOIN storefront_membership m ON m.storefront_id = s.id"
|
|
" WHERE m.account_id = %s ORDER BY m.created_at LIMIT 1",
|
|
(account_id,),
|
|
).fetchone()
|
|
return Storefront(id=row[0], name=row[1]) if row else None
|
|
|
|
|
|
def create_storefront(
|
|
conn: psycopg.Connection, account_id: int, email: str, name: str | None
|
|
) -> Storefront:
|
|
"""Create the account's one storefront + owner membership (PUC-4; INV-4, INV-5).
|
|
|
|
Guard + insert run as one atomic unit: a transaction-scoped advisory lock keyed by
|
|
account_id serializes concurrent creates for the same account, so the second of two
|
|
racing requests sees the first's committed membership and is refused (§6.5).
|
|
"""
|
|
conn.execute("SELECT pg_advisory_xact_lock(%s)", (account_id,))
|
|
existing = storefront_for(conn, account_id)
|
|
if existing is not None:
|
|
conn.rollback() # release the advisory lock; nothing was written
|
|
raise AlreadyOwnsStorefront()
|
|
final_name = (name or "").strip() or _default_name(email)
|
|
sf_id = conn.execute(
|
|
"INSERT INTO storefront (name) VALUES (%s) RETURNING id", (final_name,)
|
|
).fetchone()[0]
|
|
conn.execute(
|
|
"INSERT INTO storefront_membership (account_id, storefront_id, role)"
|
|
" VALUES (%s, %s, 'owner')",
|
|
(account_id, sf_id),
|
|
)
|
|
conn.commit()
|
|
return Storefront(id=sf_id, name=final_name)
|