import uuid from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from app.db import Base class AwardClaim(Base): """One row per milestone a seeker has ACTUALLY been paid for a presence. The three reward faucets in `app/ws.py` (the ritual milestone, a verdict, a Passage beat) each used to be guarded by a set held on the in-memory `SeanceState`. That guard is per-CONNECTION: a client that disconnects and reconnects — or simply opens a second websocket — got a fresh, empty guard and could be paid all over again for the same spirit. `summon_limiter` bounded the rate of that, never the total. This table is the durable form of those guards. The UNIQUE constraint on (user_id, entity_id, award_key) is the actual enforcement, not a belt-and-braces afterthought: `app.ws._claim_award` claims a milestone by INSERTing here and treating an IntegrityError as "somebody already paid this", which is what makes two connections racing the same award resolve to exactly one payout instead of two. `award_key` is the milestone within a presence, not a currency: `ritual`, `judgment:`, `passage:`, and `crossing` (claimed by BOTH roads to a crossing — the Passage's `release` beat and the `cross_over` verdict — so one spirit crosses once whichever road got there first). """ __tablename__ = "award_claims" __table_args__ = ( UniqueConstraint( "user_id", "entity_id", "award_key", name="uq_award_claims_user_entity_key" ), ) id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), index=True) entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entities.id"), index=True) award_key: Mapped[str] = mapped_column(String(64)) claimed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) )