"""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)