2025-02-18 08:04:02 +01:00
|
|
|
import logging
|
2025-02-16 22:36:18 +01:00
|
|
|
from dataclasses import dataclass
|
2025-02-18 08:04:02 +01:00
|
|
|
from datetime import datetime, timezone
|
|
|
|
from logging import Logger
|
|
|
|
from typing import Any, Dict, Optional
|
2025-02-16 22:36:18 +01:00
|
|
|
|
|
|
|
from bson import ObjectId
|
2025-02-18 08:04:02 +01:00
|
|
|
from pymongo.results import InsertOneResult
|
|
|
|
|
2025-02-20 22:51:01 +01:00
|
|
|
from classes.errors.wallet import (
|
2025-02-20 14:39:19 +01:00
|
|
|
WalletBalanceLimitExceeded,
|
|
|
|
WalletNotFoundError,
|
|
|
|
WalletOverdraftLimitExceeded,
|
2025-02-20 22:51:01 +01:00
|
|
|
WalletInsufficientFunds,
|
2025-02-20 14:39:19 +01:00
|
|
|
)
|
2025-02-18 08:04:02 +01:00
|
|
|
from modules.database import col_wallets
|
|
|
|
|
|
|
|
logger: Logger = logging.getLogger(__name__)
|
2025-02-16 22:36:18 +01:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Wallet:
|
|
|
|
_id: ObjectId
|
|
|
|
owner_id: int
|
|
|
|
guild_id: int
|
2025-02-18 08:04:02 +01:00
|
|
|
balance: float
|
2025-02-16 22:36:18 +01:00
|
|
|
is_frozen: bool
|
|
|
|
created: datetime
|
2025-02-18 08:04:02 +01:00
|
|
|
|
|
|
|
# TODO Write a docstring
|
|
|
|
@classmethod
|
|
|
|
async def from_id(
|
2025-02-18 21:20:14 +01:00
|
|
|
cls, owner_id: int, guild_id: int, allow_creation: bool = True
|
2025-02-18 08:04:02 +01:00
|
|
|
) -> "Wallet":
|
2025-02-20 14:39:19 +01:00
|
|
|
db_entry = await col_wallets.find_one({"owner_id": owner_id, "guild_id": guild_id})
|
2025-02-18 08:04:02 +01:00
|
|
|
|
|
|
|
if db_entry is None:
|
|
|
|
if not allow_creation:
|
|
|
|
raise WalletNotFoundError(owner_id, guild_id)
|
|
|
|
|
|
|
|
db_entry = Wallet.get_defaults(owner_id, guild_id)
|
|
|
|
|
|
|
|
insert_result: InsertOneResult = await col_wallets.insert_one(db_entry)
|
|
|
|
|
|
|
|
db_entry["_id"] = insert_result.inserted_id
|
|
|
|
|
|
|
|
return cls(**db_entry)
|
|
|
|
|
2025-02-20 22:51:01 +01:00
|
|
|
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
|
|
|
|
"""Convert Wallet object to a JSON representation.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Dict[str, Any]: JSON representation of Wallet
|
|
|
|
"""
|
2025-02-18 08:04:02 +01:00
|
|
|
return {
|
2025-02-20 22:51:01 +01:00
|
|
|
"_id": self._id if not json_compatible else str(self._id),
|
2025-02-18 08:04:02 +01:00
|
|
|
"owner_id": self.owner_id,
|
|
|
|
"guild_id": self.guild_id,
|
|
|
|
"balance": self.balance,
|
|
|
|
"is_frozen": self.is_frozen,
|
|
|
|
"created": self.created,
|
|
|
|
}
|
|
|
|
|
|
|
|
async def _set(self, key: str, value: Any) -> None:
|
|
|
|
if not hasattr(self, key):
|
|
|
|
raise AttributeError()
|
|
|
|
|
|
|
|
setattr(self, key, value)
|
|
|
|
|
2025-02-20 14:39:19 +01:00
|
|
|
await col_wallets.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True)
|
2025-02-18 08:04:02 +01:00
|
|
|
|
2025-02-20 14:39:19 +01:00
|
|
|
logger.info("Set attribute '%s' of the wallet %s to '%s'", key, str(self._id), value)
|
2025-02-18 08:04:02 +01:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_defaults(
|
2025-02-18 21:20:14 +01:00
|
|
|
owner_id: Optional[int] = None, guild_id: Optional[int] = None
|
2025-02-18 08:04:02 +01:00
|
|
|
) -> Dict[str, Any]:
|
|
|
|
return {
|
|
|
|
"owner_id": owner_id,
|
|
|
|
"guild_id": guild_id,
|
|
|
|
"balance": 0.0,
|
|
|
|
"is_frozen": False,
|
|
|
|
"created": datetime.now(tz=timezone.utc),
|
|
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_default_value(key: str) -> Any:
|
|
|
|
if key not in Wallet.get_defaults():
|
|
|
|
raise KeyError(f"There's no default value for key '{key}' in Wallet")
|
|
|
|
|
|
|
|
return Wallet.get_defaults()[key]
|
|
|
|
|
|
|
|
# TODO Write a docstring
|
|
|
|
async def freeze(self) -> None:
|
|
|
|
await self._set("is_frozen", True)
|
|
|
|
|
|
|
|
# TODO Write a docstring
|
|
|
|
async def unfreeze(self) -> None:
|
|
|
|
await self._set("is_frozen", False)
|
|
|
|
|
2025-02-18 20:25:57 +01:00
|
|
|
# TODO Write a docstring
|
2025-02-20 14:39:19 +01:00
|
|
|
async def deposit(self, amount: float, balance_limit: Optional[float] = None) -> float:
|
|
|
|
new_balance: float = round(self.balance + amount, 2)
|
|
|
|
|
|
|
|
if balance_limit is not None and new_balance > balance_limit:
|
|
|
|
raise WalletBalanceLimitExceeded(self, amount, balance_limit)
|
|
|
|
|
|
|
|
await self._set("balance", new_balance)
|
|
|
|
|
|
|
|
return new_balance
|
2025-02-18 08:04:02 +01:00
|
|
|
|
2025-02-18 20:25:57 +01:00
|
|
|
# TODO Write a docstring
|
|
|
|
async def withdraw(
|
2025-02-18 21:20:14 +01:00
|
|
|
self,
|
|
|
|
amount: float,
|
|
|
|
allow_overdraft: bool = False,
|
|
|
|
overdraft_limit: Optional[float] = None,
|
2025-02-20 14:39:19 +01:00
|
|
|
) -> float:
|
|
|
|
if amount > self.balance:
|
|
|
|
if not allow_overdraft or overdraft_limit is None:
|
|
|
|
raise WalletInsufficientFunds(self, amount)
|
|
|
|
|
|
|
|
if allow_overdraft and amount > overdraft_limit:
|
|
|
|
raise WalletOverdraftLimitExceeded(self, amount, overdraft_limit)
|
|
|
|
|
|
|
|
new_balance: float = round(self.balance - amount, 2)
|
|
|
|
|
|
|
|
await self._set("balance", new_balance)
|
|
|
|
|
|
|
|
return new_balance
|
|
|
|
|
|
|
|
async def transfer(
|
|
|
|
self,
|
|
|
|
wallet_owner_id: int,
|
|
|
|
wallet_guild_id: int,
|
|
|
|
amount: float,
|
|
|
|
balance_limit: Optional[float] = None,
|
|
|
|
allow_overdraft: bool = False,
|
|
|
|
overdraft_limit: Optional[float] = None,
|
2025-02-18 20:25:57 +01:00
|
|
|
) -> None:
|
2025-02-20 14:39:19 +01:00
|
|
|
# TODO Replace with a concrete exception
|
|
|
|
if amount < 0:
|
|
|
|
raise ValueError()
|
|
|
|
|
|
|
|
wallet: Wallet = await self.from_id(
|
|
|
|
wallet_owner_id, wallet_guild_id, allow_creation=False
|
|
|
|
)
|
|
|
|
|
|
|
|
if balance_limit is not None and amount + wallet.balance > balance_limit:
|
|
|
|
raise WalletBalanceLimitExceeded(wallet, amount, balance_limit)
|
|
|
|
|
|
|
|
if amount > self.balance:
|
|
|
|
if not allow_overdraft or overdraft_limit is None:
|
|
|
|
raise WalletInsufficientFunds(self, amount)
|
|
|
|
|
|
|
|
if allow_overdraft and amount > overdraft_limit:
|
|
|
|
raise WalletOverdraftLimitExceeded(self, amount, overdraft_limit)
|
2025-02-18 20:25:57 +01:00
|
|
|
|
2025-02-20 14:39:19 +01:00
|
|
|
# TODO Make a sanity check to revert the transaction if anything goes wrong
|
|
|
|
await self.withdraw(amount, allow_overdraft, overdraft_limit)
|
|
|
|
await wallet.deposit(amount, balance_limit)
|