This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -14,6 +14,8 @@ class TransactionType(StrEnum):
|
||||
ADJUST_PENALTY = auto()
|
||||
ADJUST_STOCK = auto()
|
||||
BUY_PRODUCT = auto()
|
||||
JOINT = auto()
|
||||
JOINT_BUY_PRODUCT = auto()
|
||||
TRANSFER = auto()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# TODO: implement me
|
||||
|
||||
1
dibbler/queries/add_user.py
Normal file
1
dibbler/queries/add_user.py
Normal file
@@ -0,0 +1 @@
|
||||
# TODO: implement me
|
||||
43
dibbler/queries/joint_buy_product.py
Normal file
43
dibbler/queries/joint_buy_product.py
Normal 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()
|
||||
@@ -0,0 +1 @@
|
||||
# TODO: implement me
|
||||
|
||||
0
tests/queries/test_add_product.py
Normal file
0
tests/queries/test_add_product.py
Normal file
0
tests/queries/test_add_user.py
Normal file
0
tests/queries/test_add_user.py
Normal file
0
tests/queries/test_joint_buy_product.py
Normal file
0
tests/queries/test_joint_buy_product.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user