aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-09-02 13:27:05 -0600
committerstuppie2026-09-02 13:27:05 -0600
commitdc79688a2ddc3b50b5df5c42323a4d237be2c47a (patch)
tree5eb62e2855b9089fe5638aaeb038f3b9a8d872f6
parentce5a2d02389b0879598dc5c5fbab989ea0913028 (diff)
downloadgeneralresearch-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.py35
-rw-r--r--generalresearch/managers/thl/session.py56
-rw-r--r--generalresearch/models/thl/session.py7
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