@@ -47,6 +47,7 @@ DEFAULT_PENALTY_MULTIPLIER_PERCENTAGE = 200
|
|||||||
_DYNAMIC_FIELDS: set[str] = {
|
_DYNAMIC_FIELDS: set[str] = {
|
||||||
"amount",
|
"amount",
|
||||||
"interest_rate_percent",
|
"interest_rate_percent",
|
||||||
|
"joint_transaction_id",
|
||||||
"penalty_multiplier_percent",
|
"penalty_multiplier_percent",
|
||||||
"penalty_threshold",
|
"penalty_threshold",
|
||||||
"per_product",
|
"per_product",
|
||||||
@@ -62,6 +63,8 @@ _EXPECTED_FIELDS: dict[TransactionType, set[str]] = {
|
|||||||
TransactionType.ADJUST_PENALTY: {"penalty_multiplier_percent", "penalty_threshold"},
|
TransactionType.ADJUST_PENALTY: {"penalty_multiplier_percent", "penalty_threshold"},
|
||||||
TransactionType.ADJUST_STOCK: {"product_count", "product_id"},
|
TransactionType.ADJUST_STOCK: {"product_count", "product_id"},
|
||||||
TransactionType.BUY_PRODUCT: {"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"},
|
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.
|
- `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.
|
- `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
|
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.
|
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
|
# Receiving user when moving credit from one user to another
|
||||||
@@ -238,15 +254,16 @@ class Transaction(Base):
|
|||||||
type_: TransactionType,
|
type_: TransactionType,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
amount: int | None = None,
|
amount: int | None = None,
|
||||||
time: datetime | None = None,
|
interest_rate_percent: int | None = None,
|
||||||
|
joint_transaction_id: int | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
product_id: int | None = None,
|
penalty_multiplier_percent: int | None = None,
|
||||||
transfer_user_id: int | None = None,
|
penalty_threshold: int | None = None,
|
||||||
per_product: int | None = None,
|
per_product: int | None = None,
|
||||||
product_count: int | None = None,
|
product_count: int | None = None,
|
||||||
penalty_threshold: int | None = None,
|
product_id: int | None = None,
|
||||||
penalty_multiplier_percent: int | None = None,
|
time: datetime | None = None,
|
||||||
interest_rate_percent: int | None = None,
|
transfer_user_id: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Please do not call this constructor directly, use the factory methods instead.
|
Please do not call this constructor directly, use the factory methods instead.
|
||||||
@@ -254,18 +271,19 @@ class Transaction(Base):
|
|||||||
if time is None:
|
if time is None:
|
||||||
time = datetime.now()
|
time = datetime.now()
|
||||||
|
|
||||||
self.time = time
|
|
||||||
self.message = message
|
|
||||||
self.type_ = type_
|
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.user_id = user_id
|
self.interest_rate_percent = interest_rate_percent
|
||||||
self.product_id = product_id
|
self.joint_transaction_id = joint_transaction_id
|
||||||
self.transfer_user_id = transfer_user_id
|
self.message = message
|
||||||
|
self.penalty_multiplier_percent = penalty_multiplier_percent
|
||||||
|
self.penalty_threshold = penalty_threshold
|
||||||
self.per_product = per_product
|
self.per_product = per_product
|
||||||
self.product_count = product_count
|
self.product_count = product_count
|
||||||
self.penalty_threshold = penalty_threshold
|
self.product_id = product_id
|
||||||
self.penalty_multiplier_percent = penalty_multiplier_percent
|
self.time = time
|
||||||
self.interest_rate_percent = interest_rate_percent
|
self.transfer_user_id = transfer_user_id
|
||||||
|
self.type_ = type_
|
||||||
|
self.user_id = user_id
|
||||||
|
|
||||||
self._validate_by_transaction_type()
|
self._validate_by_transaction_type()
|
||||||
|
|
||||||
@@ -343,7 +361,7 @@ class Transaction(Base):
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.ADJUST_BALANCE,
|
type_=TransactionType.ADJUST_BALANCE,
|
||||||
@@ -359,7 +377,7 @@ class Transaction(Base):
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.ADJUST_INTEREST,
|
type_=TransactionType.ADJUST_INTEREST,
|
||||||
@@ -376,7 +394,7 @@ class Transaction(Base):
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.ADJUST_PENALTY,
|
type_=TransactionType.ADJUST_PENALTY,
|
||||||
@@ -394,7 +412,7 @@ class Transaction(Base):
|
|||||||
product_count: int,
|
product_count: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.ADJUST_STOCK,
|
type_=TransactionType.ADJUST_STOCK,
|
||||||
@@ -414,7 +432,7 @@ class Transaction(Base):
|
|||||||
product_count: int,
|
product_count: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.ADD_PRODUCT,
|
type_=TransactionType.ADD_PRODUCT,
|
||||||
@@ -434,7 +452,7 @@ class Transaction(Base):
|
|||||||
product_count: int,
|
product_count: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.BUY_PRODUCT,
|
type_=TransactionType.BUY_PRODUCT,
|
||||||
@@ -444,6 +462,40 @@ class Transaction(Base):
|
|||||||
message=message,
|
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
|
@classmethod
|
||||||
def transfer(
|
def transfer(
|
||||||
cls: type[Self],
|
cls: type[Self],
|
||||||
@@ -452,7 +504,7 @@ class Transaction(Base):
|
|||||||
transfer_user_id: int,
|
transfer_user_id: int,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
) -> Transaction:
|
) -> Self:
|
||||||
return cls(
|
return cls(
|
||||||
time=time,
|
time=time,
|
||||||
type_=TransactionType.TRANSFER,
|
type_=TransactionType.TRANSFER,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class TransactionType(StrEnum):
|
|||||||
ADJUST_PENALTY = auto()
|
ADJUST_PENALTY = auto()
|
||||||
ADJUST_STOCK = auto()
|
ADJUST_STOCK = auto()
|
||||||
BUY_PRODUCT = auto()
|
BUY_PRODUCT = auto()
|
||||||
|
JOINT = auto()
|
||||||
|
JOINT_BUY_PRODUCT = auto()
|
||||||
TRANSFER = auto()
|
TRANSFER = auto()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# TODO: implement me
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# TODO: implement me
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
# Stock went subzero, price should be the ceiled average of the last added products
|
||||||
product1_price = product_price(sql_session, product)
|
product1_price = product_price(sql_session, product)
|
||||||
assert product1_price == math.ceil((22 + 29 * 2) / (1 + 2))
|
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:
|
def test_search_product_barcode_exact_match(sql_session: Session) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Should not be able to find hidden products
|
||||||
def test_search_product_hidden_products(sql_session: Session) -> None:
|
def test_search_product_hidden_products(sql_session: Session) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Should be able to find hidden products if specified
|
||||||
def test_search_product_find_hidden_products(sql_session: Session) -> None:
|
def test_search_product_find_hidden_products(sql_session: Session) -> None:
|
||||||
pass
|
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))
|
pprint(user_balance_log(sql_session, user))
|
||||||
|
|
||||||
assert user_balance(sql_session, user) == (27 - 200 - math.ceil(27 * 2 * 1.1))
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_transactions_no_transactions(sql_session: Session):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_user_transactions(sql_session: Session):
|
def test_user_transactions(sql_session: Session):
|
||||||
user = insert_test_data(sql_session)
|
user = insert_test_data(sql_session)
|
||||||
|
|
||||||
@@ -145,3 +149,7 @@ def test_filtered_user_transactions(sql_session: Session):
|
|||||||
)
|
)
|
||||||
== 1
|
== 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_transactions_joint_transactions(sql_session: Session):
|
||||||
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user