"""Author RAG Chatbot SaaS — Visitor Model. One row per unique visitor per author. A visitor is identified by visitor_uid (from widget localStorage) with a server-side SHA-256 fingerprint as fallback. Privacy rules: - Raw IP is NEVER stored — only country/city derived from it. - visitor_uid is a random UUID generated server-side (not derived from IP). - fingerprint is a one-way hash — not reversible. """ import datetime from sqlalchemy import DateTime, Index, Integer, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, generate_uuid class Visitor(Base): """One unique visitor per author. Created on first session/init. Updated (lastSeen, pageViews) on every subsequent session/init from the same browser. """ __tablename__ = "visitors" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) author_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) # Identity — primary (localStorage UUID) and fallback (hash) visitor_uid: Mapped[str] = mapped_column(String(36), nullable=False, index=True) fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, default="") # Lifecycle first_seen: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False) last_seen: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False) page_views: Mapped[int] = mapped_column(Integer, default=1, nullable=False) # Geography — derived from IP at visit time, never the raw IP country_code: Mapped[str | None] = mapped_column(String(2), nullable=True) country_name: Mapped[str | None] = mapped_column(String(100), nullable=True) region: Mapped[str | None] = mapped_column(String(100), nullable=True) city: Mapped[str | None] = mapped_column(String(100), nullable=True) # Device device_type: Mapped[str | None] = mapped_column(String(20), nullable=True) browser: Mapped[str | None] = mapped_column(String(100), nullable=True) os: Mapped[str | None] = mapped_column(String(100), nullable=True) __table_args__ = ( # Each visitor_uid is unique per author (one row per browser per author) UniqueConstraint("author_id", "visitor_uid", name="uq_visitor_author_uid"), Index("ix_visitors_author_fp", "author_id", "fingerprint"), ) def __repr__(self) -> str: return f""