sidequest_common.models.epgp ============================ .. py:module:: sidequest_common.models.epgp .. autoapi-nested-parse:: EPGP loot system: WoW characters, raid sessions/encounters, loot awards, and the EP/GP ledger. Source of truth is the site (``local://addon-contract.md``); the ``sidequest-sync`` addon exports raid facts and ledger entries as an ``SQX1:`` string, an officer pastes it (or uploads SavedVariables) into ``POST /api/epgp/imports``, and the import service upserts by each record's addon-assigned ``external_id`` so re-importing the same string is a no-op. GP/EP decay (15% per weekly reset) is never stored: it's applied on read from ``EpgpLedgerEntry.at`` (see ``sidequest_api.domain.epgp.services``). A voided row (``voided_at`` set) is kept forever and excluded from standings; it can never be un-voided. Classes ------- .. autoapisummary:: sidequest_common.models.epgp.Encounter sidequest_common.models.epgp.EpgpDeviceAuthorization sidequest_common.models.epgp.EpgpImportAudit sidequest_common.models.epgp.EpgpLedgerEntry sidequest_common.models.epgp.EpgpSyncToken sidequest_common.models.epgp.LootAward sidequest_common.models.epgp.RaidSession sidequest_common.models.epgp.SessionCharacterPresence sidequest_common.models.epgp.WowCharacter Module Contents --------------- .. py:class:: Encounter Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One boss pull (or kill) within a raid session, for the loot log and the progression board. ``characters``/``standby`` are the addon-reported character keys present for the pull; kept as JSON rather than a join table since nothing queries them per-character (only the session's ``SessionCharacterPresence`` rows drive attendance and standings). .. py:attribute:: characters :type: sqlalchemy.orm.Mapped[list[str]] .. py:attribute:: difficulty_id :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: encounter_id :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: ended_at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: external_id :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: name :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: session_id :type: sqlalchemy.orm.Mapped[uuid.UUID] .. py:attribute:: size :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: standby :type: sqlalchemy.orm.Mapped[list[str]] .. py:attribute:: started_at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: success :type: sqlalchemy.orm.Mapped[bool] .. py:class:: EpgpDeviceAuthorization Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` A browser sign-in for SideQuest Companion, device-flow style (``gh auth login``). The companion gets a ``device_code``/``user_code`` pair from ``POST /api/epgp/device-authorizations``, opens ``verification_uri_complete`` for the officer to approve, and polls ``POST .../token`` with ``device_code``. ``device_code_hash`` is the sha256 hex digest of the device code; the plaintext is handed to the device once and never stored. ``user_code`` is kept normalized (8 chars, uppercase, no dash). The approving officer is re-checked for officer access on the first poll after approval, when the sync token is minted; ``consumed_at`` then marks the row so a later poll can't replay the token. .. py:attribute:: approved_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: approved_by_member_id :type: sqlalchemy.orm.Mapped[int | None] .. py:attribute:: consumed_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: denied_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: device_code_hash :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: device_name :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: expires_at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: last_polled_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: sync_token_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: user_code :type: sqlalchemy.orm.Mapped[str] .. py:class:: EpgpImportAudit Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One ``POST /api/epgp/imports`` call: who ran it, which addon recorder it came from, and what changed. .. py:attribute:: counts :type: sqlalchemy.orm.Mapped[dict] .. py:attribute:: member_id :type: sqlalchemy.orm.Mapped[int | None] .. py:attribute:: recorder :type: sqlalchemy.orm.Mapped[str] .. py:class:: EpgpLedgerEntry Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One EP or GP change: an addon-exported award, a loot GP charge, or a site-made officer adjustment. ``external_id`` is ``null`` (and so excluded from the unique constraint's enforcement across site rows) for adjustments made directly on the site (``POST /api/epgp/adjustments``); addon-exported rows always carry it, and re-importing the same row is a no-op. ``character_id``/``member_id`` are both nullable: an addon row ties to the character that earned it (``member_id`` resolved through the character's link at read time for the ledger/standings), while a site adjustment ties directly to the target member with no character. .. py:attribute:: amount :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: character_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: external_id :type: sqlalchemy.orm.Mapped[str | None] .. py:attribute:: kind :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: loot_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: member_id :type: sqlalchemy.orm.Mapped[int | None] .. py:attribute:: note :type: sqlalchemy.orm.Mapped[str | None] .. py:attribute:: officer :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: reason :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: session_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: source :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: voided_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:class:: EpgpSyncToken Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` A long-lived bearer token for the ``sidequest-sync`` uploader or SideQuest Companion, scoped to one member. ``token_hash`` is the sha256 hex digest of the token (format ``sqs_`` + 40 url-safe chars); the plaintext is shown to the officer once, at creation, and never stored. ``POST /api/epgp/imports`` and ``GET /api/epgp/game-import`` both accept the token only while its member still holds an officer role, checked at request time (not cached on the row). .. py:attribute:: last_used_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: member_id :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: name :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: revoked_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: token_hash :type: sqlalchemy.orm.Mapped[str] .. py:class:: LootAward Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One item awarded during a raid session, with the GP charge recorded as a twin ``EpgpLedgerEntry``. ``officer`` is the awarding officer's in-game character name (addon data, not a site member link). .. py:attribute:: at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: character_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: encounter_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: external_id :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: gp :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: inv_type :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: item_id :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: item_name :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: officer :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: quality :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: response :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: session_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: voided_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:class:: RaidSession Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One raid night, as recorded by the addon's session recorder and exported by ``id``. ``event_id`` links to the site's ``GuildEvent`` when the officer started it from a scheduled raid (drives the attendance hand-off); ``null`` for an ad-hoc session with no matching event. ``recorder`` is the officer character that exported the session, used for the one-recorder-per-raid-night conflict rule. .. py:attribute:: difficulty_id :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: ended_at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: event_id :type: sqlalchemy.orm.Mapped[uuid.UUID | None] .. py:attribute:: external_id :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: instance_id :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: instance_name :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: recorder :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: size :type: sqlalchemy.orm.Mapped[int] .. py:attribute:: started_at :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:class:: SessionCharacterPresence Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One character's attendance window within one raid session, standby (bench) or not. Drives the attendance hand-off: every member linked to a session character (standby included, as bench) gets marked attended on the linked event's sign-up. .. py:attribute:: character_id :type: sqlalchemy.orm.Mapped[uuid.UUID] .. py:attribute:: first_seen :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: last_seen :type: sqlalchemy.orm.Mapped[datetime.datetime] .. py:attribute:: session_id :type: sqlalchemy.orm.Mapped[uuid.UUID] .. py:attribute:: standby :type: sqlalchemy.orm.Mapped[bool] .. py:class:: WowCharacter Bases: :py:obj:`advanced_alchemy.base.UUIDAuditBase` One in-game character, keyed ``"name-realm"`` lowercase (case-insensitive match per the contract). ``member_id`` links the character to a site member, sharing that member's EP/GP balance; ``source`` records how the link happened (``claim``: the member self-claimed it on the account page; ``officer``: an officer set it on the characters admin screen; ``signup``: auto-linked because the name matched exactly one member's event sign-up character; ``import``: seen in an addon import with no link yet). ``is_main`` flags the character shown as a member's primary on standings. .. py:attribute:: is_main :type: sqlalchemy.orm.Mapped[bool] .. py:attribute:: key :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: last_seen_at :type: sqlalchemy.orm.Mapped[datetime.datetime | None] .. py:attribute:: member_id :type: sqlalchemy.orm.Mapped[int | None] .. py:attribute:: name :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: realm :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: source :type: sqlalchemy.orm.Mapped[str] .. py:attribute:: wow_class :type: sqlalchemy.orm.Mapped[str | None]