An instance kept production-identical for extended manual testing is otherwise taxed forever by its own safety controls: five consents, a versioned terms gate on every mutating request, and every account predating the trust and safety commit reading terms_version NULL because init_db deliberately never backfills it. AcceptanceService grants each agreement to each account that has not declined it, so the instance stays production byte for byte while nobody clicks the same dialog again. It is opt-in, dry run by default, and one switch per agreement type. The application is not allowed to know it exists. One registration line in main.py is the only import anywhere, there is no route, schema, template, Devii tool or environment flag, and a unit test greps the tree and fails the suite if a second importer appears. The decline register needs no storage: the ledger is append-only in effect, the service only ever grants, so any withdrawn row was written by a human and that pair is never touched again. No provenance column, nothing to observe. Satisfaction is the gate's own expression, never a proxy, which is why the ordering is created_at then id exactly as consent_state selects, and why the live-account clauses are built with has_column: init_db ensures terms_version and deletion_requested_at but not is_active, so a hardcoded reference raises no such column on an instance where nobody was ever suspended. Every write is one conditional statement decided on the real rowcount, proven with sixteen processes racing one account to exactly one ledger row and one audit row. The two existing audit keys carry it, with actor kind service, because a service that silently mutated consent state would be the worst possible exception to the append-only rule. lensfl.md is the source brief accept.md records the design against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
79 lines
2.4 KiB
Python
79 lines
2.4 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
from devplacepy.database import CONSENTS_TABLE, db, get_setting, get_table
|
|
|
|
from .agreements import Agreement
|
|
|
|
DEFAULT_VERSION = "1"
|
|
|
|
LIVE_ACCOUNT_CLAUSES: tuple[tuple[str, str], ...] = (
|
|
("deletion_requested_at", "COALESCE(u.deletion_requested_at, '') = ''"),
|
|
("is_active", "COALESCE(u.is_active, 1) != 0"),
|
|
("deleted_at", "u.deleted_at IS NULL"),
|
|
)
|
|
|
|
|
|
def latest_consent(column: str, user_ref: str) -> str:
|
|
return (
|
|
f"COALESCE((SELECT c.{column} FROM {CONSENTS_TABLE} c "
|
|
f"WHERE c.owner_kind = 'user' AND c.owner_id = {user_ref} "
|
|
"AND c.kind = :kind AND c.deleted_at IS NULL "
|
|
"ORDER BY c.created_at DESC, c.id DESC LIMIT 1), '')"
|
|
)
|
|
|
|
|
|
def not_withdrawn(user_ref: str) -> str:
|
|
return f"{latest_consent('state', user_ref)} != 'withdrawn'"
|
|
|
|
|
|
def has_gate_column(agreement: Agreement) -> bool:
|
|
if not agreement.gate_column:
|
|
return False
|
|
if "users" not in db.tables:
|
|
return False
|
|
return get_table("users").has_column(agreement.gate_column)
|
|
|
|
|
|
def satisfied_clause(agreement: Agreement, user_ref: str, column_prefix: str) -> str:
|
|
if has_gate_column(agreement):
|
|
return f"COALESCE({column_prefix}{agreement.gate_column}, '') != :version"
|
|
if agreement.version_setting:
|
|
return f"{latest_consent('version', user_ref)} != :version"
|
|
return f"{latest_consent('state', user_ref)} != 'granted'"
|
|
|
|
|
|
def live_account_clauses() -> list[str]:
|
|
users = get_table("users")
|
|
return [
|
|
clause for column, clause in LIVE_ACCOUNT_CLAUSES if users.has_column(column)
|
|
]
|
|
|
|
|
|
def current_version(agreement: Agreement) -> str:
|
|
if not agreement.version_setting:
|
|
return DEFAULT_VERSION
|
|
return get_setting(agreement.version_setting, DEFAULT_VERSION) or DEFAULT_VERSION
|
|
|
|
|
|
def pending(agreement: Agreement, limit: int) -> list[dict]:
|
|
if "users" not in db.tables or CONSENTS_TABLE not in db.tables:
|
|
return []
|
|
bound = max(1, int(limit))
|
|
clauses = [
|
|
*live_account_clauses(),
|
|
not_withdrawn("u.uid"),
|
|
satisfied_clause(agreement, "u.uid", "u."),
|
|
]
|
|
sql = (
|
|
"SELECT u.uid, u.username FROM users u "
|
|
f"WHERE {' AND '.join(clauses)} "
|
|
"ORDER BY u.id LIMIT :limit"
|
|
)
|
|
rows = db.query(
|
|
sql,
|
|
kind=agreement.kind,
|
|
version=current_version(agreement),
|
|
limit=bound,
|
|
)
|
|
return [dict(row) for row in rows]
|