fixup! WIP
All checks were successful
Run tests / run-tests (push) Successful in 41s

This commit is contained in:
2025-12-09 11:55:13 +09:00
parent 0a2fc799dd
commit f39e649b3d
13 changed files with 227 additions and 28 deletions

View File

@@ -47,6 +47,7 @@ DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE = 200
_DYNAMIC_FIELDS: set[str] = {
"amount",
"interest_rate_percent",
"joint_transaction_id",
"penalty_multiplier_percent",
"penalty_threshold",
"per_product",
@@ -62,6 +63,8 @@ _EXPECTED_FIELDS: dict[TransactionType, set[str]] = {
TransactionType.ADJUST_PENALTY: {"penalty_multiplier_percent", "penalty_threshold"},
TransactionType.ADJUST_STOCK: {"product_count", "product_id"},
TransactionType.BUY_PRODUCT: {"product_count", "product_id"},
TransactionType.JOINT: {"product_count", "product_id"},
TransactionType.JOINT_BUY_PRODUCT: {"joint_transaction_id"},
TransactionType.TRANSFER: {"amount", "transfer_user_id"},
}
@@ -147,10 +150,6 @@ class Transaction(Base):
- `ADJUST_BALANCE`: The amount of credit to add or subtract from the user's balance.
- `BUY_PRODUCT`: The amount of credit spent on the product.
Note that this includes any penalties and interest that the user
had to pay as well.
- `TRANSFER`: The amount of balance to transfer to another user.
"""
@@ -177,6 +176,23 @@ class Transaction(Base):
For others, like `ADJUST_PENALTY` and `ADJUST_STOCK`, this is just a record of who
performed the transaction, and does not affect any state calculations.
In the case of `JOINT` transactions, this is the user who initiated the joint transaction.
"""
joint_transaction_id: Mapped[int | None] = mapped_column(ForeignKey("transaction.id"))
"""
An optional ID to group multiple transactions together as part of a joint transaction.
This is used for `JOINT` and `JOINT_BUY_PRODUCT` transactions, where multiple users
are involved in a single transaction.
"""
joint_transaction: Mapped[Transaction | None] = relationship(
lazy="joined",
foreign_keys=[joint_transaction_id],
)
"""
The joint transaction that this transaction is part of, if any.
"""
# Receiving user when moving credit from one user to another
@@ -238,15 +254,16 @@ class Transaction(Base):
type_: TransactionType,
user_id: int,
amount: int | None = None,
time: datetime | None = None,
interest_rate_percent: int | None = None,
joint_transaction_id: int | None = None,
message: str | None = None,
product_id: int | None = None,
transfer_user_id: int | None = None,
penalty_multiplier_percent: int | None = None,
penalty_threshold: int | None = None,
per_product: int | None = None,
product_count: int | None = None,
penalty_threshold: int | None = None,
penalty_multiplier_percent: int | None = None,
interest_rate_percent: int | None = None,
product_id: int | None = None,
time: datetime | None = None,
transfer_user_id: int | None = None,
) -> None:
"""
Please do not call this constructor directly, use the factory methods instead.
@@ -254,18 +271,19 @@ class Transaction(Base):
if time is None:
time = datetime.now()
self.time = time
self.message = message
self.type_ = type_
self.amount = amount
self.user_id = user_id
self.product_id = product_id
self.transfer_user_id = transfer_user_id
self.interest_rate_percent = interest_rate_percent
self.joint_transaction_id = joint_transaction_id
self.message = message
self.penalty_multiplier_percent = penalty_multiplier_percent
self.penalty_threshold = penalty_threshold
self.per_product = per_product
self.product_count = product_count
self.penalty_threshold = penalty_threshold
self.penalty_multiplier_percent = penalty_multiplier_percent
self.interest_rate_percent = interest_rate_percent
self.product_id = product_id
self.time = time
self.transfer_user_id = transfer_user_id
self.type_ = type_
self.user_id = user_id
self._validate_by_transaction_type()
@@ -343,7 +361,7 @@ class Transaction(Base):
user_id: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.ADJUST_BALANCE,
@@ -359,7 +377,7 @@ class Transaction(Base):
user_id: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.ADJUST_INTEREST,
@@ -376,7 +394,7 @@ class Transaction(Base):
user_id: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.ADJUST_PENALTY,
@@ -394,7 +412,7 @@ class Transaction(Base):
product_count: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.ADJUST_STOCK,
@@ -414,7 +432,7 @@ class Transaction(Base):
product_count: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.ADD_PRODUCT,
@@ -434,7 +452,7 @@ class Transaction(Base):
product_count: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.BUY_PRODUCT,
@@ -444,6 +462,40 @@ class Transaction(Base):
message=message,
)
@classmethod
def joint(
cls: type[Self],
user_id: int,
product_id: int,
product_count: int,
time: datetime | None = None,
message: str | None = None,
) -> Self:
return cls(
time=time,
type_=TransactionType.JOINT,
user_id=user_id,
product_id=product_id,
product_count=product_count,
message=message,
)
@classmethod
def joint_buy_product(
cls: type[Self],
joint_transaction_id: int,
user_id: int,
time: datetime | None = None,
message: str | None = None,
) -> Self:
return cls(
time=time,
type_=TransactionType.JOINT_BUY_PRODUCT,
joint_transaction_id=joint_transaction_id,
user_id=user_id,
message=message,
)
@classmethod
def transfer(
cls: type[Self],
@@ -452,7 +504,7 @@ class Transaction(Base):
transfer_user_id: int,
time: datetime | None = None,
message: str | None = None,
) -> Transaction:
) -> Self:
return cls(
time=time,
type_=TransactionType.TRANSFER,

View File

@@ -14,6 +14,8 @@ class TransactionType(StrEnum):
ADJUST_PENALTY = auto()
ADJUST_STOCK = auto()
BUY_PRODUCT = auto()
JOINT = auto()
JOINT_BUY_PRODUCT = auto()
TRANSFER = auto()

View File

@@ -0,0 +1 @@
# TODO: implement me

View File

@@ -0,0 +1 @@
# TODO: implement me

View File

@@ -0,0 +1,43 @@
from datetime import datetime
from sqlalchemy.orm import Session
from dibbler.models import (
Product,
Transaction,
User,
)
def joint_buy_product(
sql_session: Session,
product: Product,
product_count: int,
instigator: User,
users: list[User],
time: datetime | None = None,
message: str | None = None,
) -> None:
"""
Create buy product transactions for multiple users at once.
"""
joint_transaction = Transaction.joint(
user_id=instigator.id,
product_id=product.id,
product_count=product_count,
time=time,
message=message,
)
sql_session.add(joint_transaction)
sql_session.flush() # Ensure joint_transaction gets an ID
for user in users:
buy_transaction = Transaction.joint_buy_product(
user_id=user.id,
joint_transaction_id=joint_transaction.id,
time=time,
message=message,
)
sql_session.add(buy_transaction)
sql_session.commit()

View File

@@ -0,0 +1 @@
# TODO: implement me

View File

View File

View File

View File

@@ -340,3 +340,66 @@ def test_product_price_with_negative_stock_multiple_additions(sql_session: Sessi
# Stock went subzero, price should be the ceiled average of the last added products
product1_price = product_price(sql_session, product)
assert product1_price == math.ceil((22 + 29 * 2) / (1 + 2))
def test_product_price_joint_transactions(sql_session: Session) -> None:
user1, product = insert_test_data(sql_session)
user2 = User("Test User 2")
sql_session.add(user2)
sql_session.commit()
transactions = [
Transaction.add_product(
time=datetime(2023, 10, 1, 12, 0, 0),
amount=30 * 3,
per_product=30,
product_count=3,
user_id=user1.id,
product_id=product.id,
),
Transaction.add_product(
time=datetime(2023, 10, 1, 12, 0, 1),
amount=20 * 2,
per_product=20,
product_count=2,
user_id=user2.id,
product_id=product.id,
),
]
transactions += Transaction.buy_joint_product(
time=datetime(2023, 10, 1, 12, 0, 2),
product_count=2,
user_ids=[user1.id, user2.id],
product_id=product.id,
)
sql_session.add_all(transactions)
sql_session.commit()
pprint(product_price_log(sql_session, product))
product_price_ = product_price(sql_session, product)
assert product_price_ == math.ceil((30 * 3 + 20 * 2) / (3 + 2))
transactions = [
Transaction.add_product(
time=datetime(2023, 10, 1, 12, 0, 3),
amount=25 * 4,
per_product=25,
product_count=4,
user_id=user1.id,
product_id=product.id,
),
]
sql_session.add_all(transactions)
sql_session.commit()
pprint(product_price_log(sql_session, product))
product_price_ = product_price(sql_session, product)
expected_product_price = (30 * 3 + 20 * 2) / (3 + 2)
expected_product_price = (expected_product_price * (3 + 2) + 25 * 4) / (3 + 4)
assert product_price_ == math.ceil(expected_product_price)

View File

@@ -25,10 +25,14 @@ def test_search_product_name_no_match(sql_session: Session) -> None:
def test_search_product_barcode_exact_match(sql_session: Session) -> None:
pass
# Should not be able to find hidden products
def test_search_product_hidden_products(sql_session: Session) -> None:
pass
# Should be able to find hidden products if specified
def test_search_product_find_hidden_products(sql_session: Session) -> None:
pass
# Should be able to find hidden products by barcode despite not specified
def test_search_product_hidden_products_by_barcode(sql_session: Session) -> None:
pass

View File

@@ -300,3 +300,27 @@ def test_user_balance_penalty_interest_combined(sql_session: Session) -> None:
pprint(user_balance_log(sql_session, user))
assert user_balance(sql_session, user) == (27 - 200 - math.ceil(27 * 2 * 1.1))
def test_user_balance_joint_transactions(sql_session: Session):
pass
def test_user_balance_joint_transactions_interest(sql_session: Session):
pass
def test_user_balance_joint_transactions_changing_interest(sql_session: Session):
pass
def test_user_balance_joint_transactions_penalty(sql_session: Session):
pass
def test_user_balance_joint_transactions_changing_penalty(sql_session: Session):
pass
def test_user_balance_joint_transactions_penalty_interest_combined(sql_session: Session):
pass

View File

@@ -15,6 +15,10 @@ def insert_test_data(sql_session: Session) -> User:
return user
def test_user_transactions_no_transactions(sql_session: Session):
pass
def test_user_transactions(sql_session: Session):
user = insert_test_data(sql_session)
@@ -145,3 +149,7 @@ def test_filtered_user_transactions(sql_session: Session):
)
== 1
)
def test_user_transactions_joint_transactions(sql_session: Session):
pass