# retoor from sqlalchemy import text from devplacepy.database import CONSENTS_TABLE, _now_iso, db, get_table from .agreements import Agreement from .pending import has_gate_column, not_withdrawn, satisfied_clause CONSENT_COLUMNS = ( "uid", "owner_kind", "owner_id", "kind", "version", "state", "granted_at", "withdrawn_at", "created_at", "deleted_at", "deleted_by", ) def _gate_set_clause(agreement: Agreement) -> str: users = get_table("users") assignments = [f"{agreement.gate_column} = :version"] if agreement.stamp_column and users.has_column(agreement.stamp_column): assignments.append(f"{agreement.stamp_column} = :now") return ", ".join(assignments) def _insert_consent_sql(precondition: str) -> str: columns = ", ".join(CONSENT_COLUMNS) sql = ( f"INSERT INTO {CONSENTS_TABLE} ({columns}) " "SELECT :row_uid, 'user', :uid, :kind, :version, 'granted', " ":now, '', :now, NULL, NULL" ) if precondition: return f"{sql} WHERE {precondition}" return sql def _consent_params(agreement: Agreement, uid: str, version: str, now: str) -> dict: from devplacepy.utils import generate_uid return { "row_uid": generate_uid(), "uid": uid, "kind": agreement.kind, "version": version, "now": now, } def _claim_gate_column(agreement: Agreement, uid: str, version: str, now: str) -> bool: claim = ( f"UPDATE users SET {_gate_set_clause(agreement)} " "WHERE uid = :uid " f"AND COALESCE({agreement.gate_column}, '') != :version " f"AND {not_withdrawn('users.uid')}" ) params = _consent_params(agreement, uid, version, now) with db: claimed = db.executable.execute(text(claim), params).rowcount if claimed != 1: return False db.executable.execute(text(_insert_consent_sql("")), params) return True def _claim_ledger(agreement: Agreement, uid: str, version: str, now: str) -> bool: precondition = ( f"{not_withdrawn(':uid')} AND {satisfied_clause(agreement, ':uid', '')}" ) params = _consent_params(agreement, uid, version, now) with db: written = db.executable.execute( text(_insert_consent_sql(precondition)), params ).rowcount return written == 1 def _record(agreement: Agreement, user: dict, version: str) -> None: from devplacepy.services.audit import record as audit uid = user["uid"] username = user.get("username") or "" links = [audit.target("user", uid, username)] if agreement.kind == "terms": audit.record_system( "terms.accept", actor_kind="service", actor_username="acceptance", target_type="user", target_uid=uid, target_label=username, new_value=version, summary=f"{username} accepted terms version {version}", links=links, ) return audit.record_system( "consent.grant", actor_kind="service", actor_username="acceptance", target_type="user", target_uid=uid, target_label=username, new_value="granted", metadata={"kind": agreement.kind}, summary=f"granted {agreement.kind} consent for {username}", links=links, ) def converge_user(agreement: Agreement, user: dict, version: str) -> bool: uid = user.get("uid") if not uid: return False now = _now_iso() if has_gate_column(agreement): won = _claim_gate_column(agreement, uid, version, now) else: won = _claim_ledger(agreement, uid, version, now) if not won: return False _record(agreement, user, version) return True