diff --git a/dibbler/models/Transaction.py b/dibbler/models/Transaction.py index 3cf2fd5..46b16e5 100644 --- a/dibbler/models/Transaction.py +++ b/dibbler/models/Transaction.py @@ -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, diff --git a/dibbler/models/TransactionType.py b/dibbler/models/TransactionType.py index ed1f7e9..f1ab4c2 100644 --- a/dibbler/models/TransactionType.py +++ b/dibbler/models/TransactionType.py @@ -14,6 +14,8 @@ class TransactionType(StrEnum): ADJUST_PENALTY = auto() ADJUST_STOCK = auto() BUY_PRODUCT = auto() + JOINT = auto() + JOINT_BUY_PRODUCT = auto() TRANSFER = auto() diff --git a/dibbler/queries/add_product.py b/dibbler/queries/add_product.py index e69de29..5e9e5a1 100644 --- a/dibbler/queries/add_product.py +++ b/dibbler/queries/add_product.py @@ -0,0 +1 @@ +# TODO: implement me diff --git a/dibbler/queries/add_user.py b/dibbler/queries/add_user.py new file mode 100644 index 0000000..5e9e5a1 --- /dev/null +++ b/dibbler/queries/add_user.py @@ -0,0 +1 @@ +# TODO: implement me diff --git a/dibbler/queries/joint_buy_product.py b/dibbler/queries/joint_buy_product.py new file mode 100644 index 0000000..9ece96c --- /dev/null +++ b/dibbler/queries/joint_buy_product.py @@ -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() diff --git a/dibbler/subcommands/repopulate_cache.py b/dibbler/subcommands/repopulate_cache.py index e69de29..5e9e5a1 100644 --- a/dibbler/subcommands/repopulate_cache.py +++ b/dibbler/subcommands/repopulate_cache.py @@ -0,0 +1 @@ +# TODO: implement me diff --git a/tests/queries/test_add_product.py b/tests/queries/test_add_product.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/queries/test_add_user.py b/tests/queries/test_add_user.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/queries/test_joint_buy_product.py b/tests/queries/test_joint_buy_product.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/queries/test_product_price.py b/tests/queries/test_product_price.py index 47c8892..16c804b 100644 --- a/tests/queries/test_product_price.py +++ b/tests/queries/test_product_price.py @@ -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) diff --git a/tests/queries/test_search_product.py b/tests/queries/test_search_product.py index 16e4b5a..e711413 100644 --- a/tests/queries/test_search_product.py +++ b/tests/queries/test_search_product.py @@ -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 diff --git a/tests/queries/test_user_balance.py b/tests/queries/test_user_balance.py index 99cab2c..3ae5f14 100644 --- a/tests/queries/test_user_balance.py +++ b/tests/queries/test_user_balance.py @@ -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 diff --git a/tests/queries/test_user_transactions.py b/tests/queries/test_user_transactions.py index 55e5265..d1029ec 100644 --- a/tests/queries/test_user_transactions.py +++ b/tests/queries/test_user_transactions.py @@ -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