fixup! WIP
This commit is contained in:
@@ -27,6 +27,13 @@ if TYPE_CHECKING:
|
|||||||
from .Product import Product
|
from .Product import Product
|
||||||
from .User import User
|
from .User import User
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: these only matter when there are no adjustments made in the database.
|
||||||
|
|
||||||
|
DEFAULT_INTEREST_RATE_PERCENTAGE = 100
|
||||||
|
DEFAULT_PENALTY_THRESHOLD = -100
|
||||||
|
DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE = 200
|
||||||
|
|
||||||
# TODO: allow for joint transactions?
|
# TODO: allow for joint transactions?
|
||||||
# dibbler allows joint transactions (e.g. buying more than one product at once, several people buying the same product, etc.)
|
# dibbler allows joint transactions (e.g. buying more than one product at once, several people buying the same product, etc.)
|
||||||
# instead of having the software split the transactions up, making them hard to reconnect,
|
# instead of having the software split the transactions up, making them hard to reconnect,
|
||||||
@@ -51,7 +58,7 @@ _EXPECTED_FIELDS: dict[TransactionType, set[str]] = {
|
|||||||
TransactionType.ADJUST_STOCK: {"product_count", "product_id"},
|
TransactionType.ADJUST_STOCK: {"product_count", "product_id"},
|
||||||
# TODO: remove amount from BUY_PRODUCT
|
# TODO: remove amount from BUY_PRODUCT
|
||||||
# this requires modifications to user credit calculations
|
# this requires modifications to user credit calculations
|
||||||
TransactionType.BUY_PRODUCT: {"amount", "product_count", "product_id"},
|
TransactionType.BUY_PRODUCT: {"product_count", "product_id"},
|
||||||
TransactionType.TRANSFER: {"amount", "transfer_user_id"},
|
TransactionType.TRANSFER: {"amount", "transfer_user_id"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,7 +386,6 @@ class Transaction(Base):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def buy_product(
|
def buy_product(
|
||||||
cls: type[Self],
|
cls: type[Self],
|
||||||
amount: int,
|
|
||||||
user_id: int,
|
user_id: int,
|
||||||
product_id: int,
|
product_id: int,
|
||||||
product_count: int,
|
product_count: int,
|
||||||
@@ -389,7 +395,6 @@ class Transaction(Base):
|
|||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.BUY_PRODUCT,
|
type_=TransactionType.BUY_PRODUCT,
|
||||||
amount=amount,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
product_id=product_id,
|
product_id=product_id,
|
||||||
product_count=product_count,
|
product_count=product_count,
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import (
|
from dibbler.models import (
|
||||||
Transaction,
|
|
||||||
TransactionType,
|
|
||||||
User,
|
|
||||||
Product,
|
Product,
|
||||||
|
Transaction,
|
||||||
|
User,
|
||||||
)
|
)
|
||||||
|
from dibbler.queries.current_interest import current_interest
|
||||||
|
from dibbler.queries.current_penalty import current_penalty
|
||||||
|
from dibbler.queries.user_balance import user_balance
|
||||||
|
|
||||||
from .product_price import product_price
|
from .product_price import product_price
|
||||||
|
|
||||||
@@ -24,12 +27,26 @@ def buy_product(
|
|||||||
Creates a BUY_PRODUCT transaction with the amount automatically calculated based on the product's current price.
|
Creates a BUY_PRODUCT transaction with the amount automatically calculated based on the product's current price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
price = product_price(sql_session, product)
|
# balance = user_balance(sql_session, user)
|
||||||
|
|
||||||
return Transaction(
|
# price = product_price(sql_session, product)
|
||||||
|
|
||||||
|
# interest_rate = current_interest(sql_session)
|
||||||
|
|
||||||
|
# penalty_threshold, penalty_multiplier_percent = current_penalty(sql_session)
|
||||||
|
|
||||||
|
# price *= product_count
|
||||||
|
|
||||||
|
# price *= 1 + interest_rate / 100
|
||||||
|
|
||||||
|
# if balance < penalty_threshold:
|
||||||
|
# price *= 1 + penalty_multiplier_percent / 100
|
||||||
|
|
||||||
|
# price = math.ceil(price)
|
||||||
|
|
||||||
|
return Transaction.buy_product(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.BUY_PRODUCT,
|
# amount=price,
|
||||||
amount=price * product_count,
|
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
product_id=product.id,
|
product_id=product.id,
|
||||||
product_count=product_count,
|
product_count=product_count,
|
||||||
|
21
dibbler/queries/current_interest.py
Normal file
21
dibbler/queries/current_interest.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from dibbler.models import Transaction, TransactionType
|
||||||
|
from dibbler.models.Transaction import DEFAULT_INTEREST_RATE_PERCENTAGE
|
||||||
|
|
||||||
|
|
||||||
|
def current_interest(sql_session: Session) -> int:
|
||||||
|
result = sql_session.scalars(
|
||||||
|
select(Transaction)
|
||||||
|
.where(Transaction.type_ == TransactionType.ADJUST_INTEREST)
|
||||||
|
.order_by(Transaction.time.desc())
|
||||||
|
.limit(1)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return DEFAULT_INTEREST_RATE_PERCENTAGE
|
||||||
|
|
||||||
|
assert result.interest_rate_percent is not None, "Interest rate percent must be set"
|
||||||
|
|
||||||
|
return result.interest_rate_percent
|
25
dibbler/queries/current_penalty.py
Normal file
25
dibbler/queries/current_penalty.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from dibbler.models import Transaction, TransactionType
|
||||||
|
from dibbler.models.Transaction import (
|
||||||
|
DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE,
|
||||||
|
DEFAULT_PENALTY_THRESHOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def current_penalty(sql_session: Session) -> tuple[int, int]:
|
||||||
|
result = sql_session.scalars(
|
||||||
|
select(Transaction)
|
||||||
|
.where(Transaction.type_ == TransactionType.ADJUST_PENALTY)
|
||||||
|
.order_by(Transaction.time.desc())
|
||||||
|
.limit(1)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return DEFAULT_PENALTY_THRESHOLD, DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE
|
||||||
|
|
||||||
|
assert result.penalty_threshold is not None, "Penalty threshold must be set"
|
||||||
|
assert result.penalty_multiplier_percent is not None, "Penalty multiplier percent must be set"
|
||||||
|
|
||||||
|
return result.penalty_threshold, result.penalty_multiplier_percent
|
@@ -9,7 +9,6 @@ from sqlalchemy import (
|
|||||||
literal,
|
literal,
|
||||||
select,
|
select,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import (
|
from dibbler.models import (
|
||||||
@@ -18,14 +17,20 @@ from dibbler.models import (
|
|||||||
TransactionType,
|
TransactionType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _product_price_query(
|
def _product_price_query(
|
||||||
product: Product,
|
product_id: int,
|
||||||
# use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
# until: datetime | None = None,
|
until: datetime | None = None,
|
||||||
|
cte_name: str = "rec_cte",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
The inner query for calculating the product price.
|
The inner query for calculating the product price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if use_cache:
|
||||||
|
print("WARNING: Using cache for product price query is not implemented yet.")
|
||||||
|
|
||||||
initial_element = select(
|
initial_element = select(
|
||||||
literal(0).label("i"),
|
literal(0).label("i"),
|
||||||
literal(0).label("time"),
|
literal(0).label("time"),
|
||||||
@@ -33,7 +38,7 @@ def _product_price_query(
|
|||||||
literal(0).label("product_count"),
|
literal(0).label("product_count"),
|
||||||
)
|
)
|
||||||
|
|
||||||
recursive_cte = initial_element.cte(name="rec_cte", recursive=True)
|
recursive_cte = initial_element.cte(name=cte_name, recursive=True)
|
||||||
|
|
||||||
# Subset of transactions that we'll want to iterate over.
|
# Subset of transactions that we'll want to iterate over.
|
||||||
trx_subset = (
|
trx_subset = (
|
||||||
@@ -52,11 +57,8 @@ def _product_price_query(
|
|||||||
TransactionType.ADJUST_STOCK,
|
TransactionType.ADJUST_STOCK,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
Transaction.product_id == product.id,
|
Transaction.product_id == product_id,
|
||||||
# TODO:
|
Transaction.time <= until if until is not None else 1 == 1,
|
||||||
# If we have a transaction to limit the price calculation to, use it.
|
|
||||||
# If not, use all transactions for the product.
|
|
||||||
# (Transaction.time <= until.time) if until else True,
|
|
||||||
)
|
)
|
||||||
.order_by(Transaction.time.asc())
|
.order_by(Transaction.time.asc())
|
||||||
.alias("trx_subset")
|
.alias("trx_subset")
|
||||||
@@ -122,15 +124,18 @@ def _product_price_query(
|
|||||||
def product_price_log(
|
def product_price_log(
|
||||||
sql_session: Session,
|
sql_session: Session,
|
||||||
product: Product,
|
product: Product,
|
||||||
# use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
# Optional: calculate the price until a certain transaction.
|
until: Transaction | None = None,
|
||||||
# until: Transaction | None = None,
|
|
||||||
) -> list[tuple[int, datetime, int, int]]:
|
) -> list[tuple[int, datetime, int, int]]:
|
||||||
"""
|
"""
|
||||||
Calculates the price of a product and returns a log of the price changes.
|
Calculates the price of a product and returns a log of the price changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
recursive_cte = _product_price_query(product)
|
recursive_cte = _product_price_query(
|
||||||
|
product.id,
|
||||||
|
use_cache=use_cache,
|
||||||
|
until=until.time if until else None,
|
||||||
|
)
|
||||||
|
|
||||||
result = sql_session.execute(
|
result = sql_session.execute(
|
||||||
select(
|
select(
|
||||||
@@ -154,15 +159,18 @@ def product_price_log(
|
|||||||
def product_price(
|
def product_price(
|
||||||
sql_session: Session,
|
sql_session: Session,
|
||||||
product: Product,
|
product: Product,
|
||||||
# use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
# Optional: calculate the price until a certain transaction.
|
until: Transaction | None = None,
|
||||||
# until: Transaction | None = None,
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Calculates the price of a product.
|
Calculates the price of a product.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
recursive_cte = _product_price_query(product) # , until=until)
|
recursive_cte = _product_price_query(
|
||||||
|
product.id,
|
||||||
|
use_cache=use_cache,
|
||||||
|
until=until.time if until else None,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: optionally verify subresults:
|
# TODO: optionally verify subresults:
|
||||||
# - product_count should never be negative (but this happens sometimes, so just a warning)
|
# - product_count should never be negative (but this happens sometimes, so just a warning)
|
||||||
|
@@ -1,4 +1,17 @@
|
|||||||
from sqlalchemy import func, select
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Integer,
|
||||||
|
and_,
|
||||||
|
asc,
|
||||||
|
case,
|
||||||
|
cast,
|
||||||
|
column,
|
||||||
|
func,
|
||||||
|
literal,
|
||||||
|
or_,
|
||||||
|
select,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import (
|
from dibbler.models import (
|
||||||
@@ -6,92 +19,236 @@ from dibbler.models import (
|
|||||||
TransactionType,
|
TransactionType,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from dibbler.models.Transaction import (
|
||||||
|
DEFAULT_INTEREST_RATE_PERCENTAGE,
|
||||||
|
DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE,
|
||||||
|
DEFAULT_PENALTY_THRESHOLD,
|
||||||
|
)
|
||||||
|
from dibbler.queries.product_price import _product_price_query
|
||||||
|
|
||||||
# TODO: rename to 'balance' everywhere
|
|
||||||
|
|
||||||
def _user_balance_query(
|
def _user_balance_query(
|
||||||
user: User,
|
user: User,
|
||||||
# use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
# until: datetime | None = None,
|
until: datetime | None = None,
|
||||||
|
cte_name: str = "rec_cte",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
The inner query for calculating the user's balance.
|
The inner query for calculating the user's balance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
balance_adjustments = (
|
if use_cache:
|
||||||
select(func.coalesce(func.sum(Transaction.amount).label("balance_adjustments"), 0))
|
print("WARNING: Using cache for user balance query is not implemented yet.")
|
||||||
.where(
|
|
||||||
Transaction.user_id == user.id,
|
initial_element = select(
|
||||||
Transaction.type_ == TransactionType.ADJUST_BALANCE,
|
literal(0).label("i"),
|
||||||
|
literal(0).label("time"),
|
||||||
|
literal(0).label("balance"),
|
||||||
|
literal(DEFAULT_INTEREST_RATE_PERCENTAGE).label("interest_rate_percent"),
|
||||||
|
literal(DEFAULT_PENALTY_THRESHOLD).label("penalty_threshold"),
|
||||||
|
literal(DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE).label("penalty_multiplier_percent"),
|
||||||
|
)
|
||||||
|
|
||||||
|
recursive_cte = initial_element.cte(name=cte_name, recursive=True)
|
||||||
|
|
||||||
|
# Subset of transactions that we'll want to iterate over.
|
||||||
|
trx_subset = (
|
||||||
|
select(
|
||||||
|
func.row_number().over(order_by=asc(Transaction.time)).label("i"),
|
||||||
|
Transaction.time,
|
||||||
|
Transaction.type_,
|
||||||
|
Transaction.amount,
|
||||||
|
Transaction.product_count,
|
||||||
|
Transaction.product_id,
|
||||||
|
Transaction.transfer_user_id,
|
||||||
|
Transaction.interest_rate_percent,
|
||||||
|
Transaction.penalty_multiplier_percent,
|
||||||
|
Transaction.penalty_threshold,
|
||||||
)
|
)
|
||||||
.scalar_subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
transfers_to_other_users = (
|
|
||||||
select(func.coalesce(func.sum(Transaction.amount).label("transfers_to_other_users"), 0))
|
|
||||||
.where(
|
.where(
|
||||||
Transaction.user_id == user.id,
|
or_(
|
||||||
Transaction.type_ == TransactionType.TRANSFER,
|
and_(
|
||||||
|
Transaction.user_id == user.id,
|
||||||
|
Transaction.type_.in_(
|
||||||
|
[
|
||||||
|
TransactionType.ADD_PRODUCT,
|
||||||
|
TransactionType.ADJUST_BALANCE,
|
||||||
|
TransactionType.BUY_PRODUCT,
|
||||||
|
TransactionType.TRANSFER,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
and_(
|
||||||
|
Transaction.type_ == TransactionType.TRANSFER,
|
||||||
|
Transaction.transfer_user_id == user.id,
|
||||||
|
),
|
||||||
|
Transaction.type_.in_(
|
||||||
|
[
|
||||||
|
TransactionType.ADJUST_INTEREST,
|
||||||
|
TransactionType.ADJUST_PENALTY,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Transaction.time <= until if until is not None else 1 == 1,
|
||||||
)
|
)
|
||||||
.scalar_subquery()
|
.order_by(Transaction.time.asc())
|
||||||
|
.alias("trx_subset")
|
||||||
)
|
)
|
||||||
|
|
||||||
transfers_to_self = (
|
recursive_elements = (
|
||||||
select(func.coalesce(func.sum(Transaction.amount).label("transfers_to_self"), 0))
|
select(
|
||||||
.where(
|
trx_subset.c.i,
|
||||||
Transaction.transfer_user_id == user.id,
|
trx_subset.c.time,
|
||||||
Transaction.type_ == TransactionType.TRANSFER,
|
case(
|
||||||
|
# Adjusts balance -> balance gets adjusted
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.ADJUST_BALANCE,
|
||||||
|
recursive_cte.c.balance + trx_subset.c.amount,
|
||||||
|
),
|
||||||
|
# Adds a product -> balance increases
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.ADD_PRODUCT,
|
||||||
|
recursive_cte.c.balance + trx_subset.c.amount,
|
||||||
|
),
|
||||||
|
# Buys a product -> balance decreases
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.BUY_PRODUCT,
|
||||||
|
recursive_cte.c.balance
|
||||||
|
- (
|
||||||
|
trx_subset.c.product_count
|
||||||
|
# Price of a single product, accounted for penalties and interest.
|
||||||
|
* cast(
|
||||||
|
func.ceil(
|
||||||
|
# TODO: This can get quite expensive real quick, so we should do some caching of the
|
||||||
|
# product prices somehow.
|
||||||
|
# Base price
|
||||||
|
(
|
||||||
|
select(column("price"))
|
||||||
|
.select_from(
|
||||||
|
_product_price_query(
|
||||||
|
trx_subset.c.product_id,
|
||||||
|
use_cache=use_cache,
|
||||||
|
until=trx_subset.c.time,
|
||||||
|
cte_name="product_price_cte",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(column("i").desc())
|
||||||
|
.limit(1)
|
||||||
|
).scalar_subquery()
|
||||||
|
# TODO: should interest be applied before or after the penalty multiplier?
|
||||||
|
# at the moment of writing, after sound right, but maybe ask someone?
|
||||||
|
# Interest
|
||||||
|
* (recursive_cte.c.interest_rate_percent / 100)
|
||||||
|
# Penalty
|
||||||
|
* case(
|
||||||
|
(
|
||||||
|
recursive_cte.c.balance < recursive_cte.c.penalty_threshold,
|
||||||
|
(recursive_cte.c.penalty_multiplier_percent / 100),
|
||||||
|
),
|
||||||
|
else_=1.0,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Integer,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Transfers money to self -> balance increases
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.TRANSFER
|
||||||
|
and trx_subset.c.transfer_user_id == user.id,
|
||||||
|
recursive_cte.c.balance + trx_subset.c.amount,
|
||||||
|
),
|
||||||
|
# Transfers money from self -> balance decreases
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.TRANSFER
|
||||||
|
and trx_subset.c.transfer_user_id != user.id,
|
||||||
|
recursive_cte.c.balance - trx_subset.c.amount,
|
||||||
|
),
|
||||||
|
# Interest adjustment -> balance stays the same
|
||||||
|
# Penalty adjustment -> balance stays the same
|
||||||
|
else_=recursive_cte.c.balance,
|
||||||
|
).label("balance"),
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.ADJUST_INTEREST,
|
||||||
|
trx_subset.c.interest_rate_percent,
|
||||||
|
),
|
||||||
|
else_=recursive_cte.c.interest_rate_percent,
|
||||||
|
).label("interest_rate_percent"),
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.ADJUST_PENALTY,
|
||||||
|
trx_subset.c.penalty_threshold,
|
||||||
|
),
|
||||||
|
else_=recursive_cte.c.penalty_threshold,
|
||||||
|
).label("penalty_threshold"),
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
trx_subset.c.type_ == TransactionType.ADJUST_PENALTY,
|
||||||
|
trx_subset.c.penalty_multiplier_percent,
|
||||||
|
),
|
||||||
|
else_=recursive_cte.c.penalty_multiplier_percent,
|
||||||
|
).label("penalty_multiplier_percent"),
|
||||||
)
|
)
|
||||||
.scalar_subquery()
|
.select_from(trx_subset)
|
||||||
|
.where(trx_subset.c.i == recursive_cte.c.i + 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
add_products = (
|
return recursive_cte.union_all(recursive_elements)
|
||||||
select(func.coalesce(func.sum(Transaction.amount).label("add_products"), 0))
|
|
||||||
.where(
|
|
||||||
Transaction.user_id == user.id,
|
|
||||||
Transaction.type_ == TransactionType.ADD_PRODUCT,
|
|
||||||
)
|
|
||||||
.scalar_subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
buy_products = (
|
|
||||||
select(func.coalesce(func.sum(Transaction.amount).label("buy_products"), 0))
|
|
||||||
.where(
|
|
||||||
Transaction.user_id == user.id,
|
|
||||||
Transaction.type_ == TransactionType.BUY_PRODUCT,
|
|
||||||
)
|
|
||||||
.scalar_subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
query = select(
|
|
||||||
# TODO: clearly define and fix the sign of the amount
|
|
||||||
(
|
|
||||||
0
|
|
||||||
+ balance_adjustments
|
|
||||||
- transfers_to_other_users
|
|
||||||
+ transfers_to_self
|
|
||||||
+ add_products
|
|
||||||
- buy_products
|
|
||||||
).label("balance")
|
|
||||||
)
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def user_balance(
|
def user_balance_log(
|
||||||
sql_session: Session,
|
sql_session: Session,
|
||||||
user: User,
|
user: User,
|
||||||
# use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
# Optional: calculate the balance until a certain transaction.
|
until: Transaction | None = None,
|
||||||
# until: Transaction | None = None,
|
) -> list[tuple[int, datetime, int, int, int, int]]:
|
||||||
) -> int:
|
recursive_cte = _user_balance_query(
|
||||||
"""
|
user,
|
||||||
Calculates the balance of a user.
|
use_cache=use_cache,
|
||||||
"""
|
until=until.time if until else None,
|
||||||
|
)
|
||||||
|
|
||||||
query = _user_balance_query(user) # , until=until)
|
result = sql_session.execute(
|
||||||
|
select(
|
||||||
result = sql_session.scalar(query)
|
recursive_cte.c.i,
|
||||||
|
recursive_cte.c.time,
|
||||||
|
recursive_cte.c.balance,
|
||||||
|
recursive_cte.c.interest_rate_percent,
|
||||||
|
recursive_cte.c.penalty_threshold,
|
||||||
|
recursive_cte.c.penalty_multiplier_percent,
|
||||||
|
).order_by(recursive_cte.c.i.asc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
# If there are no transactions for this user, the query should return 0, not None.
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Something went wrong while calculating the balance for user {user.name} (ID: {user.id})."
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def user_balance(
|
||||||
|
sql_session: Session,
|
||||||
|
user: User,
|
||||||
|
use_cache: bool = True,
|
||||||
|
# Optional: calculate the balance until a certain transaction.
|
||||||
|
until: Transaction | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Calculates the balance of a user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
recursive_cte = _user_balance_query(
|
||||||
|
user,
|
||||||
|
use_cache=use_cache,
|
||||||
|
until=until.time if until else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = sql_session.scalar(
|
||||||
|
select(recursive_cte.c.balance).order_by(recursive_cte.c.i.desc()).limit(1)
|
||||||
|
)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
# If there are no transactions for this user, the query should return 0, not None.
|
# If there are no transactions for this user, the query should return 0, not None.
|
||||||
|
@@ -40,34 +40,3 @@ def test_transaction_no_duplicate_timestamps(sql_session: Session):
|
|||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
sql_session.commit()
|
sql_session.commit()
|
||||||
|
|
||||||
|
|
||||||
def test_transaction_buy_product_wrong_amount(sql_session: Session) -> None:
|
|
||||||
user, product = insert_test_data(sql_session)
|
|
||||||
|
|
||||||
# Set price by adding a product
|
|
||||||
transaction = Transaction.add_product(
|
|
||||||
time=datetime(2023, 10, 1, 12, 0, 0),
|
|
||||||
user_id=user.id,
|
|
||||||
product_id=product.id,
|
|
||||||
amount=27,
|
|
||||||
per_product=27,
|
|
||||||
product_count=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
sql_session.add(transaction)
|
|
||||||
sql_session.commit()
|
|
||||||
|
|
||||||
# Attempt to buy product with wrong amount
|
|
||||||
transaction2 = Transaction.buy_product(
|
|
||||||
time=datetime(2023, 10, 1, 12, 0, 1),
|
|
||||||
user_id=user.id,
|
|
||||||
product_id=product.id,
|
|
||||||
amount=(27 * 2) + 1, # Wrong amount
|
|
||||||
product_count=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
sql_session.add(transaction2)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
sql_session.commit()
|
|
||||||
|
@@ -59,7 +59,6 @@ def test_user_transactions(sql_session: Session):
|
|||||||
),
|
),
|
||||||
Transaction.buy_product(
|
Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 12, 0, 1),
|
time=datetime(2023, 10, 1, 12, 0, 1),
|
||||||
amount=27,
|
|
||||||
product_count=1,
|
product_count=1,
|
||||||
user_id=user2.id,
|
user_id=user2.id,
|
||||||
product_id=product.id,
|
product_id=product.id,
|
||||||
|
@@ -4,7 +4,6 @@ from datetime import datetime
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import Product, Transaction, User
|
from dibbler.models import Product, Transaction, User
|
||||||
from dibbler.queries.buy_product import buy_product
|
|
||||||
from dibbler.queries.product_stock import product_stock
|
from dibbler.queries.product_stock import product_stock
|
||||||
from dibbler.queries.user_balance import user_balance
|
from dibbler.queries.user_balance import user_balance
|
||||||
|
|
||||||
@@ -48,11 +47,10 @@ def insert_test_data(sql_session: Session) -> tuple[User, Product]:
|
|||||||
def test_buy_product_basic(sql_session: Session) -> None:
|
def test_buy_product_basic(sql_session: Session) -> None:
|
||||||
user, product = insert_test_data(sql_session)
|
user, product = insert_test_data(sql_session)
|
||||||
|
|
||||||
transaction = buy_product(
|
transaction = Transaction.buy_product(
|
||||||
sql_session=sql_session,
|
|
||||||
time=datetime(2023, 10, 1, 12, 0, 0),
|
time=datetime(2023, 10, 1, 12, 0, 0),
|
||||||
user=user,
|
user_id=user.id,
|
||||||
product=product,
|
product_id=product.id,
|
||||||
product_count=1,
|
product_count=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,11 +71,10 @@ def test_buy_product_with_penalty(sql_session: Session) -> None:
|
|||||||
sql_session.add_all(transactions)
|
sql_session.add_all(transactions)
|
||||||
sql_session.commit()
|
sql_session.commit()
|
||||||
|
|
||||||
transaction = buy_product(
|
transaction = Transaction.buy_product(
|
||||||
sql_session=sql_session,
|
|
||||||
time=datetime(2023, 10, 1, 12, 0, 0),
|
time=datetime(2023, 10, 1, 12, 0, 0),
|
||||||
user=user,
|
user_id=user.id,
|
||||||
product=product,
|
product_id=product.id,
|
||||||
product_count=1,
|
product_count=1,
|
||||||
)
|
)
|
||||||
sql_session.add(transaction)
|
sql_session.add(transaction)
|
||||||
@@ -99,11 +96,10 @@ def test_buy_product_with_interest(sql_session: Session) -> None:
|
|||||||
sql_session.add_all(transactions)
|
sql_session.add_all(transactions)
|
||||||
sql_session.commit()
|
sql_session.commit()
|
||||||
|
|
||||||
transaction = buy_product(
|
transaction = Transaction.buy_product(
|
||||||
sql_session=sql_session,
|
|
||||||
time=datetime(2023, 10, 1, 12, 0, 0),
|
time=datetime(2023, 10, 1, 12, 0, 0),
|
||||||
user=user,
|
user_id=user.id,
|
||||||
product=product,
|
product_id=product.id,
|
||||||
product_count=1,
|
product_count=1,
|
||||||
)
|
)
|
||||||
sql_session.add(transaction)
|
sql_session.add(transaction)
|
||||||
@@ -125,11 +121,10 @@ def test_buy_product_with_changing_penalty(sql_session: Session) -> None:
|
|||||||
sql_session.add_all(transactions)
|
sql_session.add_all(transactions)
|
||||||
sql_session.commit()
|
sql_session.commit()
|
||||||
|
|
||||||
transaction = buy_product(
|
transaction = Transaction.buy_product(
|
||||||
sql_session=sql_session,
|
|
||||||
time=datetime(2023, 10, 1, 12, 0, 0),
|
time=datetime(2023, 10, 1, 12, 0, 0),
|
||||||
user=user,
|
user_id=user.id,
|
||||||
product=product,
|
product_id=product.id,
|
||||||
product_count=1,
|
product_count=1,
|
||||||
)
|
)
|
||||||
sql_session.add(transaction)
|
sql_session.add(transaction)
|
||||||
@@ -146,11 +141,10 @@ def test_buy_product_with_changing_penalty(sql_session: Session) -> None:
|
|||||||
sql_session.add(adjust_penalty)
|
sql_session.add(adjust_penalty)
|
||||||
sql_session.commit()
|
sql_session.commit()
|
||||||
|
|
||||||
transaction = buy_product(
|
transaction = Transaction.buy_product(
|
||||||
sql_session=sql_session,
|
|
||||||
time=datetime(2023, 10, 1, 14, 0, 0),
|
time=datetime(2023, 10, 1, 14, 0, 0),
|
||||||
user=user,
|
user_id=user.id,
|
||||||
product=product,
|
product_id=product.id,
|
||||||
product_count=1,
|
product_count=1,
|
||||||
)
|
)
|
||||||
sql_session.add(transaction)
|
sql_session.add(transaction)
|
||||||
@@ -170,12 +164,11 @@ def test_buy_product_with_penalty_interest_combined(sql_session: Session) -> Non
|
|||||||
def test_buy_product_more_than_stock(sql_session: Session) -> None:
|
def test_buy_product_more_than_stock(sql_session: Session) -> None:
|
||||||
user, product = insert_test_data(sql_session)
|
user, product = insert_test_data(sql_session)
|
||||||
|
|
||||||
transaction = buy_product(
|
transaction = Transaction.buy_product(
|
||||||
sql_session=sql_session,
|
|
||||||
time=datetime(2023, 10, 1, 13, 0, 0),
|
time=datetime(2023, 10, 1, 13, 0, 0),
|
||||||
product_count=10,
|
product_count=10,
|
||||||
user=user,
|
user_id=user.id,
|
||||||
product=product,
|
product_id=product.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
sql_session.add(transaction)
|
sql_session.add(transaction)
|
||||||
|
@@ -49,7 +49,6 @@ def insert_test_data(sql_session: Session) -> None:
|
|||||||
),
|
),
|
||||||
Transaction.buy_product(
|
Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 12, 0, 1),
|
time=datetime(2023, 10, 1, 12, 0, 1),
|
||||||
amount=27,
|
|
||||||
product_count=1,
|
product_count=1,
|
||||||
user_id=user2.id,
|
user_id=user2.id,
|
||||||
product_id=product1.id,
|
product_id=product1.id,
|
||||||
@@ -76,7 +75,6 @@ def insert_test_data(sql_session: Session) -> None:
|
|||||||
),
|
),
|
||||||
Transaction.buy_product(
|
Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 12, 0, 5),
|
time=datetime(2023, 10, 1, 12, 0, 5),
|
||||||
amount=50,
|
|
||||||
product_count=1,
|
product_count=1,
|
||||||
user_id=user1.id,
|
user_id=user1.id,
|
||||||
product_id=product3.id,
|
product_id=product3.id,
|
||||||
@@ -139,7 +137,6 @@ def test_product_price_with_negative_stock_single_addition(sql_session: Session)
|
|||||||
|
|
||||||
transaction = Transaction.buy_product(
|
transaction = Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 13, 0, 0),
|
time=datetime(2023, 10, 1, 13, 0, 0),
|
||||||
amount=27 * 5,
|
|
||||||
product_count=10,
|
product_count=10,
|
||||||
user_id=user1.id,
|
user_id=user1.id,
|
||||||
product_id=product1.id,
|
product_id=product1.id,
|
||||||
|
@@ -60,7 +60,6 @@ def test_product_stock_complex_history(sql_session: Session) -> None:
|
|||||||
),
|
),
|
||||||
Transaction.buy_product(
|
Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 13, 0, 1),
|
time=datetime(2023, 10, 1, 13, 0, 1),
|
||||||
amount=27 * 3,
|
|
||||||
user_id=user1.id,
|
user_id=user1.id,
|
||||||
product_id=product.id,
|
product_id=product.id,
|
||||||
product_count=3,
|
product_count=3,
|
||||||
@@ -123,7 +122,6 @@ def test_negative_product_stock(sql_session: Session) -> None:
|
|||||||
),
|
),
|
||||||
Transaction.buy_product(
|
Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 14, 0, 1),
|
time=datetime(2023, 10, 1, 14, 0, 1),
|
||||||
amount=50,
|
|
||||||
user_id=user1.id,
|
user_id=user1.id,
|
||||||
product_id=product.id,
|
product_id=product.id,
|
||||||
product_count=2,
|
product_count=2,
|
||||||
|
@@ -4,7 +4,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import Product, Transaction, User
|
from dibbler.models import Product, Transaction, User
|
||||||
from dibbler.queries.user_balance import user_balance
|
from dibbler.queries.user_balance import user_balance, user_balance_log
|
||||||
|
|
||||||
|
|
||||||
def insert_test_data(sql_session: Session) -> None:
|
def insert_test_data(sql_session: Session) -> None:
|
||||||
@@ -50,7 +50,6 @@ def insert_test_data(sql_session: Session) -> None:
|
|||||||
),
|
),
|
||||||
Transaction.buy_product(
|
Transaction.buy_product(
|
||||||
time=datetime(2023, 10, 1, 12, 0, 1),
|
time=datetime(2023, 10, 1, 12, 0, 1),
|
||||||
amount=27,
|
|
||||||
product_count=1,
|
product_count=1,
|
||||||
user_id=user2.id,
|
user_id=user2.id,
|
||||||
product_id=product1.id,
|
product_id=product1.id,
|
||||||
|
Reference in New Issue
Block a user