Files
tipibot/core/economy/house.py
Rene Arumetsa 037628d24f feat(economy): add vanity shop and harden economy money-safety
Vanity shop (/vanity): cosmetic badges/titles as a pure whale coin sink -
purchases burn coins (not credited to the house) and equip a badge shown on
/profile. New vanity_owned/vanity_active fields, schema sync, and tests.

Money-safety and robustness fixes from a codebase review:

- _parse_amount now rejects negative amounts. Every bet/give/request flows
  through it, so a negative value can no longer mint coins on a loss/transfer
  path that trusts the caller's sign (all call sites already guarded <= 0;
  this closes the source).
- do_blackjack_payout no longer raises on a DB failure. The stake was already
  deducted in do_blackjack_bet, so it now logs critical with the owed amount
  (for admin reconciliation) and returns db_error; all payout call sites render
  a clear "payout failed" notice instead of crashing the interaction.
- Instant "kohv" consumable now cancels the pending reminder DMs for the
  cooldowns it wipes (via new INSTANT_RESET_COMMANDS), so no stale/duplicate
  reminders fire.
- Renamed the misleadingly-named _refund_user_safe -> _debit_house_safe (it
  debits the house) and dropped its ignored first arg.
- Added __all__ to vanity.py and consumables.py so `import *` no longer leaks
  incidental imports into the economy namespace.
- Documented Kõrvaklapid's +25 coin daily bonus in README and DEV_NOTES.

Tests: blackjack payout DB-failure safety and INSTANT_RESET_COMMANDS lockstep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
2026-09-04 02:09:25 +03:00

93 lines
3.2 KiB
Python

"""House account: the bot's own balance, fed by fines and lost bets."""
from __future__ import annotations
from .. import pb_client
from ..pb_client import DatabaseError
from .store import _log, _now, get_user, _commit
# ---------------------------------------------------------------------------
# House account (bot user)
# ---------------------------------------------------------------------------
HOUSE_ID: int | None = None
_house_pb_id: str | None = None
def set_house(user_id: int) -> None:
"""Register the bot's Discord user ID as the house account."""
global HOUSE_ID, _house_pb_id
if HOUSE_ID != user_id:
_house_pb_id = None
HOUSE_ID = user_id
async def _house_record_id() -> str | None:
"""PocketBase record id of the house account (cached; creates the record on first use)."""
global _house_pb_id
if HOUSE_ID is None:
return None
if _house_pb_id is None:
house = await get_user(HOUSE_ID)
_house_pb_id = house.get("_pb_id") # type: ignore[typeddict-item]
return _house_pb_id
async def _credit_house(amount: int) -> None:
"""Add `amount` coins to the house via an atomic PocketBase increment.
Deliberately lock-free: callers hold per-user locks, so this must never
acquire one itself (see the locking rules above)."""
if amount <= 0:
return
record_id = await _house_record_id()
if record_id is None:
return
await pb_client.update_record(record_id, {"balance+": amount})
async def get_heist_global_cd() -> float:
"""Return unix timestamp until which no new heist can start. Persisted on house record."""
if HOUSE_ID is None:
return 0.0
house = await get_user(HOUSE_ID)
return float(house.get("heist_global_cd_until") or 0)
async def set_heist_global_cd(until: float) -> None:
"""Persist heist global cooldown expiry to the house account in PocketBase."""
record_id = await _house_record_id()
if record_id is None:
return
await pb_client.update_record(record_id, {"heist_global_cd_until": until})
async def _refund_house_safe(amount: int, context: str, related_uid: int) -> None:
"""Best-effort refund of `amount` to the house. Logs critical if it fails."""
if HOUSE_ID is None or amount <= 0:
return
try:
await _credit_house(amount)
except DatabaseError as exc:
_log.critical(
"House compensation failed (%s, related uid %s, amount %s): %s",
context, related_uid, amount, exc,
)
async def _debit_house_safe(amount: int, context: str, uid: int) -> None:
"""Best-effort atomic debit of `amount` from the house. Compensates a fine
that was credited to the house but whose matching user debit failed to persist
(the coins must be pulled back out of the house). Logs critical if it fails."""
if HOUSE_ID is None or amount <= 0:
return
try:
record_id = await _house_record_id()
if record_id:
await pb_client.update_record(record_id, {"balance-": amount})
except DatabaseError as exc:
_log.critical(
"House debit compensation failed (%s, related uid %s, amount %s): %s",
context, uid, amount, exc,
)