diff options
| author | stuppie | 2026-09-02 13:27:05 -0600 |
|---|---|---|
| committer | stuppie | 2026-09-02 13:27:05 -0600 |
| commit | dc79688a2ddc3b50b5df5c42323a4d237be2c47a (patch) | |
| tree | 5eb62e2855b9089fe5638aaeb038f3b9a8d872f6 | |
| parent | ce5a2d02389b0879598dc5c5fbab989ea0913028 (diff) | |
| download | generalresearch-dc79688a2ddc3b50b5df5c42323a4d237be2c47a.tar.gz generalresearch-dc79688a2ddc3b50b5df5c42323a4d237be2c47a.zip | |
add a claim_latest_attempt_credit method for un-finished/abandoned sessions
| -rw-r--r-- | generalresearch/managers/thl/ledger_manager/thl_ledger.py | 35 | ||||
| -rw-r--r-- | generalresearch/managers/thl/session.py | 56 | ||||
| -rw-r--r-- | generalresearch/models/thl/session.py | 7 |
3 files changed, 66 insertions, 32 deletions
diff --git a/generalresearch/managers/thl/ledger_manager/thl_ledger.py b/generalresearch/managers/thl/ledger_manager/thl_ledger.py index fa09a45..2d7a588 100644 --- a/generalresearch/managers/thl/ledger_manager/thl_ledger.py +++ b/generalresearch/managers/thl/ledger_manager/thl_ledger.py @@ -57,7 +57,7 @@ from generalresearch.models.thl.payout import UserPayoutEvent from generalresearch.models.thl.payout_format import format_payout_format from generalresearch.models.thl.product import Product from generalresearch.models.thl.session import Session, Status, Wall -from generalresearch.models.thl.user import User +from generalresearch.models.thl.user import BPUIDStr, User from generalresearch.models.thl.wallet import PayoutType from generalresearch.models.thl.wallet.user_wallet import ( UserLedgerWallet, @@ -66,6 +66,8 @@ from generalresearch.models.thl.wallet.user_wallet import ( if TYPE_CHECKING: from generalresearch.models.thl.contest.contest import ContestWinner + from generalresearch.managers.thl.session import SessionManager + logging.basicConfig() logger = logging.getLogger("LedgerManager") @@ -1531,7 +1533,7 @@ class ThlLedgerManager(LedgerManager): created: datetime | None = None, skip_flag_check: bool = False, ) -> LedgerTransaction: - """Record conditional credit for an eligible failed session. + """Record conditional credit for an eligible session attempt. Eligibility is determined by the caller. This method verifies that the session failed and records the credit exactly once per session. @@ -1547,8 +1549,7 @@ class ThlLedgerManager(LedgerManager): ) amount = USDCent(round(config.failed_attempt_credit * 100)) - assert session.status == Status.FAIL, "Attempt credit requires a failed session" - assert session.is_attempt_credit_eligible, ( + assert session.is_attempt_credit_eligible(), ( "Session is not eligible for attempt credit" ) @@ -1593,6 +1594,32 @@ class ThlLedgerManager(LedgerManager): skip_flag_check=skip_flag_check, ) + def claim_latest_attempt_credit( + self, + user: User, + session_manager: SessionManager, + skip_flag_check: bool = False, + ) -> LedgerTransaction: + """Claim attempt credit for a product user's most recent session. + This must be for an abandoned session, as when a session if finished + and is eligible for an attempt credit, then the credit is automatically + given.""" + + session = session_manager.get_latest_for_user( + user_id=user.user_id + ) + if session is None: + raise ValueError("User has no session to claim attempt credit for") + if session.status is not None: + raise ValueError("User's latest session is already finalized") + + assert user.product + session.user.product = user.product + return self.create_tx_attempt_credit( + session=session, + skip_flag_check=skip_flag_check, + ) + def create_tx_user_enter_contest( self, contest_uuid: UUIDStr, diff --git a/generalresearch/managers/thl/session.py b/generalresearch/managers/thl/session.py index 746a518..8834f8d 100644 --- a/generalresearch/managers/thl/session.py +++ b/generalresearch/managers/thl/session.py @@ -114,6 +114,12 @@ class SessionManager(PostgresManager): assert len(res) == 1 return self.session_from_mysql(res[0]) + def get_latest_for_user(self, user_id: int) -> Session | None: + """Return the most recently started session for a product user.""" + res = self.filter_paginated(user_id=user_id, order_by="-started", size=1) + if res: + return res[0] + def get_from_id(self, session_id: int) -> Session: query = """ SELECT @@ -190,14 +196,12 @@ class SessionManager(PostgresManager): # re-run model_validate after finished = finished if finished else datetime.now(tz=timezone.utc) session.update( - **{ - "status": status, - "status_code_1": status_code_1, - "status_code_2": status_code_2, - "finished": finished, - "payout": payout, - "user_payout": user_payout, - } + status=status, + status_code_1=status_code_1, + status_code_2=status_code_2, + finished=finished, + payout=payout, + user_payout=user_payout, ) d = session.model_dump_mysql() self.pg_config.execute_write( @@ -453,15 +457,15 @@ class SessionManager(PostgresManager): if started_before or started_after: started_after = started_after or datetime(2017, 1, 1, tzinfo=timezone.utc) started_before = started_before or datetime.now(tz=timezone.utc) - assert ( - started_after.tzinfo == timezone.utc - ), "started_after must be tz-aware as UTC" - assert ( - started_before.tzinfo == timezone.utc - ), "started_before must be tz-aware as UTC" - assert ( - started_after < started_before - ), "started_after must be before started_before" + assert started_after.tzinfo == timezone.utc, ( + "started_after must be tz-aware as UTC" + ) + assert started_before.tzinfo == timezone.utc, ( + "started_before must be tz-aware as UTC" + ) + assert started_after < started_before, ( + "started_after must be before started_before" + ) filters.append("started BETWEEN %(started_after)s AND %(started_before)s") params["started_after"] = started_after params["started_before"] = started_before @@ -469,15 +473,15 @@ class SessionManager(PostgresManager): if adjusted_before or adjusted_after: adjusted_after = adjusted_after or datetime(2017, 1, 1, tzinfo=timezone.utc) adjusted_before = adjusted_before or datetime.now(tz=timezone.utc) - assert ( - adjusted_after.tzinfo == timezone.utc - ), "adjusted_after must be tz-aware as UTC" - assert ( - adjusted_before.tzinfo == timezone.utc - ), "adjusted_before must be tz-aware as UTC" - assert ( - adjusted_after < adjusted_before - ), "adjusted_after must be before adjusted_before" + assert adjusted_after.tzinfo == timezone.utc, ( + "adjusted_after must be tz-aware as UTC" + ) + assert adjusted_before.tzinfo == timezone.utc, ( + "adjusted_before must be tz-aware as UTC" + ) + assert adjusted_after < adjusted_before, ( + "adjusted_after must be before adjusted_before" + ) filters.append( "adjusted_timestamp BETWEEN %(adjusted_after)s AND %(adjusted_before)s" ) diff --git a/generalresearch/models/thl/session.py b/generalresearch/models/thl/session.py index a270257..b5e4401 100644 --- a/generalresearch/models/thl/session.py +++ b/generalresearch/models/thl/session.py @@ -823,21 +823,24 @@ class Session(BaseModel): self.model_config["validate_assignment"] = True self.__class__.model_validate(self) - @property def is_attempt_credit_eligible(self) -> bool: """Return whether this session qualifies for attempt credit. The status of the BP's user_wallet_config.failed_attempt_credit_enabled does not matter here. """ + now = datetime.now(tz=timezone.utc) min_session_length = timedelta(minutes=1) + if self.status is None: + return now - self.started >= min_session_length + ineligible_status_codes = { StatusCode1.SESSION_START_FAIL, StatusCode1.SESSION_START_QUALITY_FAIL, StatusCode1.SESSION_CONTINUE_QUALITY_FAIL, StatusCode1.BUYER_QUALITY_FAIL, StatusCode1.PS_BLOCKED, - StatusCode1.PS_QUALITY + StatusCode1.PS_QUALITY, } return ( self.status == Status.FAIL |
